diff --git a/README.md b/README.md index 6a3a677..db38370 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Organize related tasks into named groups, even when they run in different direct ### Resume agent sessions -Start `claude`, `codex`, or `grok` normally. When you rerun the task or reload a saved session, `fleetcom` resumes the same conversation automatically. +Start `claude`, `codex`, `grok`, or `omp` normally. When you rerun the task or reload a saved session, `fleetcom` resumes the same conversation automatically. ## Operational model @@ -40,7 +40,7 @@ Running several long-lived commands is pesky once they span terminal panes or ne - Delegates tasks to a daemon, so a disconnecting client stops nothing. - Saves and reloads task recipes: directories, commands, group assignments, and display names. - Reruns a completed task in place, keeping its identity, group, and name. -- Preserves `claude`, `codex`, and `grok` conversations, so saved or rerun tasks resume instead of starting fresh. +- Preserves `claude`, `codex`, `grok`, and `omp` conversations, so saved or rerun tasks resume instead of starting fresh. - Automatically snapshots the current task set for recovery. ## Documentation @@ -102,7 +102,7 @@ Every task runs in its own pseudo-terminal, emulated with `alacritty_terminal`. - Several long-lived commands need one place for observation, tagging, and attachment. - Jobs must survive a terminal closing and remain available for reattachment. - The same command set is launched often enough to justify a saved session. -- Agent sessions (`claude`, `codex`, `grok`) must resume their conversations on rerun rather than start new ones. +- Agent sessions (`claude`, `codex`, `grok`, `omp`) must resume their conversations on rerun rather than start new ones. ### When to avoid `fleetcom` diff --git a/docs/README.md b/docs/README.md index 001feae..b1e30a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ - [Commands](commands.md): every key and launch flag, including the routing mechanics - [How it works](how-it-works.md): the PTY emulation, input routing, and activity grouping - [Sessions](sessions.md): the task recipe format and where it lives -- [Agent session resume](agent-resume.md): how `fleetcom` captures and resumes supported `claude`, `codex`, and `grok` sessions +- [Agent session resume](agent-resume.md): how `fleetcom` captures and resumes supported `claude`, `codex`, `grok`, and `omp` sessions - [Storage paths](#storage-paths): runtime and session paths - [First-run walkthrough](#first-run-walkthrough): a first run, start to finish - [Security](#security): the trust boundary, on-disk state, and what is not protected @@ -153,7 +153,7 @@ The attached status bar shows both: `[attached] api tests · cargo watch -x test | `/.json` | `0600` | directories, commands, groups, display names | | `/recovery/` | `0700` | [automatic snapshots](sessions.md#recovery) | | `/recovery/.json` | `0600` | one automatic session recipe | -| `/-/` | `0700` | [agent hook and notifier assets plus per-run capture payloads](agent-resume.md#capture-state-and-isolation) | +| `/-/` | `0700` | [agent hook, notifier, and extension-module assets plus per-run capture payloads](agent-resume.md#capture-state-and-isolation) | Saves are atomic: `fleetcom` writes a mode-`0600` temporary file in the destination directory, syncs it, then renames it over the target. This does not expose a partial or world-readable recipe. New session and recovery directories use mode `0700`; each save also removes group and other permissions from the destination directory. diff --git a/docs/agent-resume.md b/docs/agent-resume.md index dd614d1..438ee10 100644 --- a/docs/agent-resume.md +++ b/docs/agent-resume.md @@ -1,13 +1,13 @@ # Agent session resume -Session files preserve launch commands, not process state. Relaunching a bare `claude`, `codex`, or `grok` command ordinarily starts another conversation. For accepted commands, `fleetcom` captures a validated conversation ID when available and builds a canonical resume command when saving a session or rerunning a finished task (`r`). +Session files preserve launch commands, not process state. Relaunching a bare `claude`, `codex`, `grok`, or `omp` command ordinarily starts another conversation. For accepted commands, `fleetcom` captures a validated conversation ID when available and builds a canonical resume command when saving a session or rerunning a finished task (`r`). ## Workflow Start a supported agent without flags: -1. Press `n` and run `claude`, `codex`, or `grok`. The task appears in the dashboard under the command you typed. Instrumentation changes only the string executed through `$SHELL -c`, so a direct spawn still displays the requested command. -2. Work in it. `Enter` attaches; `Ctrl-\` returns to the dashboard. Depending on the agent, `fleetcom` pins an ID at launch and may update it from a hook or notifier while the task runs or from terminal output after it exits. +1. Press `n` and run `claude`, `codex`, `grok`, or `omp`. The task appears in the dashboard under the command you typed. Instrumentation changes only the string executed through `$SHELL -c`, so a direct spawn still displays the requested command. +2. Work in it. `Enter` attaches; `Ctrl-\` returns to the dashboard. Depending on the agent, `fleetcom` pins an ID at launch and may update it from a hook, notifier, or extension while the task runs or from terminal output after it exits. 3. Press `w`, enter a session name, and press `Enter`. If the earlier sources produced no ID, the save also checks the agent's on-disk session store. A captured bare command becomes its canonical resume form, such as `claude --resume ''`. 4. Run `fleetcom `, or press `o` in the dashboard, to start new processes from the saved commands. A stored resume command reopens its captured conversation. @@ -19,10 +19,11 @@ Capture is best-effort and narrow by design. A command carrying a prompt, extra The capture boundary is intentionally narrow. Only these forms participate: -- `claude`, `codex`, or `grok` +- `claude`, `codex`, `grok`, or `omp` - `claude --resume ` - `codex resume ` - `grok --resume ` +- `omp --resume ` The program word may be a path such as `/usr/local/bin/claude` when its basename matches and the token contains no shell syntax. A resume UUID may be bare or single-quoted, but it must be the final argument. @@ -30,12 +31,13 @@ Everything else remains opaque and runs, displays, and saves verbatim. This incl ## Capture state and isolation -Hooks and notifiers run outside the supervisor, so they need stable paths. The supervisor installs those assets once for each runtime root. An explicit `FLEETCOM_RUNTIME_DIR` becomes that root. Otherwise, `fleetcom` uses the platform runtime or cache directory and partitions it by session directory. +Hooks, notifiers, and extension modules are loaded by the agent rather than the supervisor, so they need stable paths. The supervisor installs those assets once for each runtime root. An explicit `FLEETCOM_RUNTIME_DIR` becomes that root. Otherwise, `fleetcom` uses the platform runtime or cache directory and partitions it by session directory. Each supervisor installation creates a private mode-`0700` `/-` namespace containing: - `claude-settings.json`, mode `0600` - `codex-notify.sh`, mode `0700` +- `omp-capture.js`, mode `0600`: omp imports the module rather than executing it, so it needs no executable bit - `task--.json` capture paths The random nonce separates concurrent supervisors and prevents PID reuse from selecting an existing namespace. The run number gives each rerun a distinct capture file, so a displaced process cannot overwrite the replacement run's session state. Installation leaves every other root entry unchanged. @@ -78,6 +80,24 @@ Grok accepts a launch-time ID but exposes no injectable live-capture channel. A After exit, the harness scans retained terminal text for the last `grok -r ` or `grok --resume ` hint. Save-time filesystem correlation checks `/sessions///`, percent-encoding the canonical working directory, falling back to a group whose `.cwd` file names that path when the encoded name is too long, and ignoring `session_kind: subagent` directories. +### `omp` + +omp cannot pin an ID at launch: it has no `--session-id`, and `--resume` requires an existing session. Both accepted forms therefore receive the same injection and no pinned ID: + +```text +-e '/omp-capture.js' +``` + +`-e` loads the JavaScript module into the agent process and appends it to the user's extensions. Its `session_start` and `session_switch` handlers write `sessionId` as JSON to `FLEETCOM_CAPTURE_FILE`. The second handler follows in-TUI `/resume` changes. Capture writes are best-effort: the module returns when the capture path is empty and ignores write errors. + +After exit, the harness scans retained terminal text for the last trusted `omp --resume ` hint. It accepts ordinary exit hints and `Main:` entries in `[Recovery]` blocks. Other labels identify subagent sessions that `omp --resume` cannot open, so they contribute no exit evidence. The aliases `-r`, `--session`, and `-c` remain opaque because `fleetcom` rewrites only the canonical form it detects exactly. + +Save-time filesystem correlation reads `//_.jsonl`, where the sessions root comes from omp's own variable chain rather than one home override; the [environment-variable table](#environment-variables) lists it. `PI_CODING_AGENT_SESSION_DIR` is the exception: omp passes that path straight through as the session file's parent and never computes a bucket, so the store is flat under it. The scan covers the root and one level below without inferring which layout is in play. + +Correlation enumerates buckets instead of deriving their names, then verifies each session header's `cwd` against the task path and its canonical target. A candidate must be the sole UUIDv7 session created within 30 seconds of task spawn. UUIDs found in multiple buckets count once. + +An empty sessions root or bucket contributes no candidate. + ## ID precedence Several channels can identify different conversations during one task. To make the result deterministic, `fleetcom` chooses the first available ID in this order: @@ -96,6 +116,7 @@ Saving and rerunning rewrite accepted commands to one of these forms: claude --resume '' codex resume '' grok --resume '' +omp --resume '' ``` The program word is preserved as typed. If no valid ID is available, the original command remains unchanged. A rerun increments the run number before spawning its replacement, so capture data from the displaced run cannot affect the new run. @@ -110,21 +131,28 @@ Each tool implements the `Harness` trait in [`src/harness/mod.rs`](../src/harnes - `shape` supplies the program word and resume selector. The default `detect` and `resume_command` methods derive the accepted and canonical forms from that pair. - `instrument` returns spawn-time arguments, environment entries, and an optional pinned ID. -- `parse_capture` reads an ID from hook or notify JSON. +- `parse_capture` reads an ID from hook, notify, or extension JSON. - `scrape_exit` reads an ID from retained terminal text. - `live_session_id` reads the ID a live session publishes on disk. It defaults to `None` for tools that publish no registry. - `live_blocked_status` reads that same registry for one display fact: whether the tool says it is blocked on the user. It returns preview text, never an ID, and defaults to `None`. - `correlate_fs` finds one matching on-disk session. +- `resolve_home` turns the task's launch environment into the tool's store root. -The supervisor resolves each harness home from the task's launch environment: the tool-specific variable first, then `$HOME` plus the tool's dot directory. That resolved path remains attached to the task for later filesystem correlation. +The supervisor supplies the launch environment and delegates the decision to `resolve_home`. Its default is the two-step rule three of the four tools follow: the tool-specific variable first, then `$HOME` plus the tool's dot directory. omp overrides it, because its store root comes from a chain of variables and a filesystem-conditional XDG branch that no single override can express. Either way, the resolved path remains attached to the task for later filesystem correlation. ## Environment variables | Variable | Meaning | | -- | -- | | `FLEETCOM_RUNTIME_DIR` | Explicit capture-asset root as well as the daemon runtime override. | -| `FLEETCOM_CAPTURE_FILE` | Per-run capture file used by the injected hook or notifier. | +| `FLEETCOM_CAPTURE_FILE` | Per-run capture file used by the injected hook, notifier, or extension module. | | `FLEETCOM_NOTIFY_CHAIN` | Newline-joined argv for the configured Codex notifier; empty when none is active. | | `CLAUDE_CONFIG_DIR` | Claude home holding the `sessions/.json` registry and the transcripts used for correlation; defaults to `$HOME/.claude`. | | `CODEX_HOME` | Codex home used for notify routing and rollout correlation; defaults to `$HOME/.codex`. | | `GROK_HOME` | Grok home used for session-directory correlation; defaults to `$HOME/.grok`. | +| `PI_CODING_AGENT_SESSION_DIR` | omp sessions root, used verbatim for correlation. The rest of omp's chain builds that path instead of naming it. | +| `PI_CODING_AGENT_DIR` | omp agent directory, whose `sessions` subdirectory is the store. A selected profile ignores it. | +| `PI_CONFIG_DIR` | omp config directory under `$HOME`; defaults to `.omp`. Under `fleetcom`, an absolute value replaces `$HOME`. | +| `OMP_PROFILE` | omp profile, read by presence: it selects a profile when non-empty and suppresses `PI_PROFILE` when empty. | +| `PI_PROFILE` | omp profile used only when `OMP_PROFILE` is absent. A profile inserts `profiles/` under the config directory. | +| `XDG_DATA_HOME` | Redirects the still-default omp agent directory to `/omp`, flattening the `agent/` level, when that target already exists. | diff --git a/docs/commands.md b/docs/commands.md index 0131cee..e30d40a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -104,7 +104,7 @@ Destroy is Shift-gated: only uppercase `X` acts. It kills a running task or remo `r` acts only on a finished task. A running task remains untouched because rerunning it would first require a destructive kill. -The replacement starts in the same directory using the requesting client's environment. Most tasks reuse their stored command. A supported `claude`, `codex`, or `grok` task instead uses its captured resume command when a valid conversation ID is available. +The replacement starts in the same directory using the requesting client's environment. Most tasks reuse their stored command. A supported `claude`, `codex`, `grok`, or `omp` task instead uses its captured resume command when a valid conversation ID is available. Rerunning preserves the task's ID, `◆` tag, group, name, and spawn order; its clock and screen reset. Since lifecycle affects sorting, the task may move to another section when it starts. The same key works inside peek, which remains open while the replacement starts. diff --git a/docs/sessions.md b/docs/sessions.md index 6fa467d..0e4e4b9 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -51,7 +51,7 @@ The file is plain JSON and practical to edit by hand. Editing the `name` field c Commands with neither a group nor a name use the string form. String and object entries can appear in the same directory array. -A bare agent command does not identify its conversation, so saving it verbatim would start another one on load. When `fleetcom` captures an ID for `claude`, `codex`, or `grok`, it stores the resume form instead. The result remains an ordinary command string that can run directly in a shell: +A bare agent command does not identify its conversation, so saving it verbatim would start another one on load. When `fleetcom` captures an ID for `claude`, `codex`, `grok`, or `omp`, it stores the resume form instead. The result remains an ordinary command string that can run directly in a shell: ```json { diff --git a/src/harness/assets.rs b/src/harness/assets.rs index bd37513..4d9bd42 100644 --- a/src/harness/assets.rs +++ b/src/harness/assets.rs @@ -21,6 +21,11 @@ //! newline-joined argv with the payload appended, so the displaced notifier //! receives the same final argument `codex` would have passed; otherwise //! exit 0. +//! - `omp`: `-e ` loads an extension module inside the agent's +//! own process, appending to the user's extensions rather than replacing +//! them. Its `session_start` and `session_switch` handlers write the session +//! id as JSON over `$FLEETCOM_CAPTURE_FILE`, skip the write when that +//! variable is unset or empty, and swallow every error. use std::{ fs, io, @@ -53,6 +58,35 @@ set -- $FLEETCOM_NOTIFY_CHAIN "$1" exec "$@" "#; +/// Extension module loaded by `omp -e`. Its default export registers handlers +/// for initial sessions and in-TUI session changes. omp imports the module, so +/// the asset needs no executable bit. +const OMP_CAPTURE_MODULE: &str = r#"import * as fs from "node:fs"; + +function write(ctx, reason) { + const path = process.env.FLEETCOM_CAPTURE_FILE; + if (!path) return; + try { + fs.writeFileSync( + path, + JSON.stringify({ + reason, + sessionId: ctx.sessionManager.getSessionId(), + sessionFile: ctx.sessionManager.getSessionFile(), + cwd: ctx.cwd, + }), + ); + } catch { + // Best effort: a capture failure must never take the session down. + } +} + +export default function (pi) { + pi.on("session_start", (_e, ctx) => write(ctx, "session_start")); + pi.on("session_switch", (_e, ctx) => write(ctx, "session_switch")); +} +"#; + /// Build the `claude` settings overlay containing the `SessionStart` hook. fn claude_settings_json() -> String { jzon::object! { @@ -130,13 +164,15 @@ pub struct CaptureAssets { dir: PathBuf, claude_settings: PathBuf, codex_notify: PathBuf, + omp_capture: PathBuf, } impl CaptureAssets { /// Create `root` and a private `/-` namespace. The - /// namespace uses mode `0700`; its Claude settings use `0600`, and its - /// executable Codex notifier uses `0700`. Dead-owner namespaces are reaped - /// before the new namespace is created; other root entries remain. + /// namespace uses mode `0700`; its Claude settings and omp module use + /// `0600`, and its executable Codex notifier uses `0700`. Dead-owner + /// namespaces are reaped before the new namespace is created; other root + /// entries remain. pub fn install(root: &Path, pid: u32) -> io::Result { fs::DirBuilder::new() .recursive(true) @@ -168,10 +204,16 @@ impl CaptureAssets { fs::write(&codex_notify, CODEX_NOTIFY_SCRIPT)?; fs::set_permissions(&codex_notify, fs::Permissions::from_mode(0o700))?; + // The module is imported, not executed. + let omp_capture = dir.join("omp-capture.js"); + fs::write(&omp_capture, OMP_CAPTURE_MODULE)?; + fs::set_permissions(&omp_capture, fs::Permissions::from_mode(0o600))?; + Ok(Self { dir, claude_settings, codex_notify, + omp_capture, }) } @@ -182,6 +224,7 @@ impl CaptureAssets { capture_file: self.dir.join(format!("task-{task_id}-{run}.json")), claude_settings: self.claude_settings.clone(), codex_notify: self.codex_notify.clone(), + omp_capture: self.omp_capture.clone(), } } } @@ -252,8 +295,11 @@ mod tests { assert_eq!(mode(&ns), 0o700); assert_eq!(assets.claude_settings, ns.join("claude-settings.json")); assert_eq!(assets.codex_notify, ns.join("codex-notify.sh")); + assert_eq!(assets.omp_capture, ns.join("omp-capture.js")); assert_eq!(mode(&assets.claude_settings), 0o600); assert_eq!(mode(&assets.codex_notify), 0o700); + // omp imports the module rather than executing it. + assert_eq!(mode(&assets.omp_capture), 0o600); } /// Installation creates a distinct namespace, reapplies the root mode, and @@ -279,6 +325,10 @@ mod tests { fs::read_to_string(&second.codex_notify).unwrap(), CODEX_NOTIFY_SCRIPT ); + assert_eq!( + fs::read_to_string(&second.omp_capture).unwrap(), + OMP_CAPTURE_MODULE + ); assert_eq!( fs::read_to_string(&first.claude_settings).unwrap(), "garbage", @@ -287,6 +337,7 @@ mod tests { assert_eq!(mode(&root), 0o700); assert_eq!(mode(&second.claude_settings), 0o600); assert_eq!(mode(&second.codex_notify), 0o700); + assert_eq!(mode(&second.omp_capture), 0o600); } /// Installation retains live-owner namespaces and non-namespace entries. @@ -331,6 +382,7 @@ mod tests { fs::write(stale.join("task-1-0.json"), "predecessor").unwrap(); fs::write(stale.join("claude-settings.json"), "old settings").unwrap(); fs::write(stale.join("codex-notify.sh"), "old script").unwrap(); + fs::write(stale.join("omp-capture.js"), "old module").unwrap(); let assets = CaptureAssets::install(&root, std::process::id()).unwrap(); let ns = namespace(&assets, &root); @@ -349,6 +401,10 @@ mod tests { fs::read_to_string(stale.join("codex-notify.sh")).unwrap(), "old script" ); + assert_eq!( + fs::read_to_string(stale.join("omp-capture.js")).unwrap(), + "old module" + ); assert_eq!( fs::read_to_string(&assets.claude_settings).unwrap(), claude_settings_json() @@ -357,6 +413,10 @@ mod tests { fs::read_to_string(&assets.codex_notify).unwrap(), CODEX_NOTIFY_SCRIPT ); + assert_eq!( + fs::read_to_string(&assets.omp_capture).unwrap(), + OMP_CAPTURE_MODULE + ); assert_eq!(mode(&ns), 0o700); } @@ -597,5 +657,6 @@ mod tests { ); assert_eq!(paths.claude_settings, ns.join("claude-settings.json")); assert_eq!(paths.codex_notify, ns.join("codex-notify.sh")); + assert_eq!(paths.omp_capture, ns.join("omp-capture.js")); } } diff --git a/src/harness/codex.rs b/src/harness/codex.rs index da8d6f8..0566f82 100644 --- a/src/harness/codex.rs +++ b/src/harness/codex.rs @@ -527,6 +527,7 @@ mod tests { capture_file: PathBuf::from("/c"), claude_settings: PathBuf::from("/s"), codex_notify: PathBuf::from(r#"/Odd Path/it's "here"\now"#), + omp_capture: PathBuf::from("/e.js"), }; let inv = Codex.detect("codex").unwrap(); let plan = Codex.instrument(&inv, &paths, Some(&no_config_home())); diff --git a/src/harness/mod.rs b/src/harness/mod.rs index a6689e1..b7f8249 100644 --- a/src/harness/mod.rs +++ b/src/harness/mod.rs @@ -18,6 +18,7 @@ pub mod assets; mod claude; mod codex; mod grok; +mod omp; pub mod summary; use std::{ @@ -33,6 +34,7 @@ pub use codex::Codex; pub use grok::Grok; #[cfg(test)] pub(crate) use grok::encode_cwd; +pub use omp::Omp; /// Environment variable naming the capture file used by injected assets. pub const CAPTURE_ENV: &str = "FLEETCOM_CAPTURE_FILE"; @@ -51,9 +53,16 @@ pub trait Harness: Sync { /// resolves it from the launch context used for instrumentation or save. fn home_env_var(&self) -> &'static str; - /// The tool's directory name under the launched process's `$HOME`. + /// Default store path relative to the launched process's `$HOME`. fn home_dot_dir(&self) -> &'static str; + /// Resolve the store root from the launch environment. The default uses + /// the tool-specific override, then `$HOME` plus [`Self::home_dot_dir`]. + /// Returning `None` delegates to [`Self::home_root`]'s platform fallback. + fn resolve_home(&self, env: &dyn Fn(&str) -> Option) -> Option { + env(self.home_env_var()).or_else(|| Some(env("HOME")?.join(self.home_dot_dir()))) + } + /// Resolve the tool's home root. `home` follows the `instrument` contract: /// falling back to this process's home happens only when the launch /// environment supplied neither the tool-specific override nor `HOME`. @@ -149,6 +158,10 @@ static AGENTS: &[Agent] = &[ harness: &Grok, summary: &summary::GrokSummary, }, + Agent { + harness: &Omp, + summary: &summary::OmpSummary, + }, ]; /// Return the first harness that recognizes `cmd`. @@ -181,12 +194,15 @@ impl Invocation { /// Capture paths allocated by [`assets::CaptureAssets::paths_for`]. #[derive(Debug, Clone)] pub struct CapturePaths { - /// Per-run path available to an injected hook or notifier. + /// Per-run path available to an injected capture asset. pub capture_file: PathBuf, /// Additive settings file passed to `claude --settings`. pub claude_settings: PathBuf, /// Program installed through `codex`'s `notify` config override. pub codex_notify: PathBuf, + /// Extension module loaded by `omp -e`, which appends to the user's own + /// extensions rather than replacing them. + pub omp_capture: PathBuf, } /// Spawn-time additions for one instrumented launch. @@ -354,13 +370,14 @@ pub(crate) mod fixtures { /// A second distinct ID for last-hint, requote, and ambiguity cases. pub(crate) const OTHER: &str = "11111111-2222-4333-8444-555555555555"; - /// Capture-path fixture. The spaced `claude_settings` and `codex_notify` - /// paths keep the shell- and TOML-quoting assertions honest. + /// Capture-path fixture. The spaced asset paths keep the shell- and + /// TOML-quoting assertions honest. pub(super) fn paths() -> CapturePaths { CapturePaths { capture_file: PathBuf::from("/tmp/cap/session.json"), claude_settings: PathBuf::from("/tmp/Application Support/fleetcom.json"), codex_notify: PathBuf::from("/tmp/Application Support/notify.sh"), + omp_capture: PathBuf::from("/tmp/Application Support/omp-capture.js"), } } @@ -614,9 +631,11 @@ mod tests { assert_eq!(Claude.home_env_var(), "CLAUDE_CONFIG_DIR"); assert_eq!(Codex.home_env_var(), "CODEX_HOME"); assert_eq!(Grok.home_env_var(), "GROK_HOME"); + assert_eq!(Omp.home_env_var(), "PI_CODING_AGENT_SESSION_DIR"); assert_eq!(Claude.home_dot_dir(), ".claude"); assert_eq!(Codex.home_dot_dir(), ".codex"); assert_eq!(Grok.home_dot_dir(), ".grok"); + assert_eq!(Omp.home_dot_dir(), ".omp/agent/sessions"); } #[test] diff --git a/src/harness/omp.rs b/src/harness/omp.rs new file mode 100644 index 0000000..da7312a --- /dev/null +++ b/src/harness/omp.rs @@ -0,0 +1,747 @@ +//! omp cannot pin a session ID at launch: it has no `--session-id` flag, and +//! `--resume` requires an existing session. Live capture therefore loads an +//! extension whose `session_start` and `session_switch` handlers write the +//! current ID. Exit capture reads `omp --resume ` hints from ordinary +//! exit output and `[Recovery]` blocks. +//! +//! omp's IDs are UUIDv7. [`is_uuid`](super::is_uuid) validates the 8-4-4-4-12 +//! lowercase-hex shape and not the version field, so they pass unchanged. +//! +//! `-r`, `--session`, and `-c` resume as well, but detection stays on the +//! canonical pair: a command fleetcom cannot rewrite exactly is left verbatim. +//! +//! # The store +//! +//! Sessions live at `//_.jsonl`. +//! The harness home *is* the sessions root: `PI_CODING_AGENT_SESSION_DIR` +//! names a sessions directory outright, so no agent-dir value can express it. +//! That override also flattens the store — it is passed straight through as the +//! session file's parent and the bucket level is never computed — so +//! correlation scans the root and one level below it. +//! +//! Correlation does not derive bucket names. It enumerates the root and its +//! immediate subdirectories, then verifies the working directory from each +//! session header. + +use std::{ + fs, + io::{BufRead, BufReader, Read}, + path::{Path, PathBuf}, + time::SystemTime, +}; + +use super::{ + CAPTURE_ENV, CapturePaths, Harness, Invocation, SpawnPlan, is_uuid, leading_uuid, shell_quote, + within_window_ms, +}; + +/// Command fragment shared by ordinary exit and recovery hints. +const RESUME_HINT: &str = "omp --resume "; + +/// Label identifying the resumable session in a recovery block. +const MAIN_LABEL: &str = "Main"; + +pub struct Omp; + +impl Harness for Omp { + /// The only variable that names the sessions root directly. + fn home_env_var(&self) -> &'static str { + "PI_CODING_AGENT_SESSION_DIR" + } + + /// Default sessions path relative to `$HOME`. + fn home_dot_dir(&self) -> &'static str { + ".omp/agent/sessions" + } + + /// Resolve omp's sessions root, not its agent directory. + /// `PI_CODING_AGENT_SESSION_DIR` names the root directly; other inputs name + /// or construct its parent directories. + /// + /// Precedence: `PI_CODING_AGENT_SESSION_DIR`; an unprofiled + /// `PI_CODING_AGENT_DIR`; an existing XDG store; then the config path under + /// `$HOME`. `OMP_PROFILE` is selected by presence, so an empty value still + /// suppresses `PI_PROFILE`. Empty directory overrides are treated as unset. + fn resolve_home(&self, env: &dyn Fn(&str) -> Option) -> Option { + let set = |key: &str| env(key).filter(|p| !p.as_os_str().is_empty()); + if let Some(sessions) = set(self.home_env_var()) { + return Some(sessions); + } + + // Presence of `OMP_PROFILE` decides; an empty value selects no profile + // and still shadows `PI_PROFILE`. + let profile = match env("OMP_PROFILE") { + Some(p) => p, + None => env("PI_PROFILE").unwrap_or_default(), + }; + // Trim profile names; empty and `default` select the unprofiled store. + let profile = profile + .to_str() + .map(str::trim) + .filter(|p| !p.is_empty() && *p != "default") + .map(PathBuf::from); + + // Named profiles ignore `PI_CODING_AGENT_DIR`. + if let (None, Some(agent)) = (&profile, set("PI_CODING_AGENT_DIR")) { + return Some(agent.join("sessions")); + } + + // XDG redirects only when the target path already exists. + if let Some(xdg) = set("XDG_DATA_HOME") { + let data = match &profile { + Some(p) => xdg.join("omp").join("profiles").join(p), + None => xdg.join("omp"), + }; + if data.exists() { + return Some(data.join("sessions")); + } + } + + // Only the config-path branch requires `$HOME`; earlier overrides are + // complete paths. An absolute `PI_CONFIG_DIR` replaces `$HOME` under + // `Path::join`. + let config = env("HOME")?.join(set("PI_CONFIG_DIR").unwrap_or_else(|| ".omp".into())); + let root = match &profile { + Some(p) => config.join("profiles").join(p), + None => config, + }; + Some(root.join("agent").join("sessions")) + } + + fn shape(&self) -> (&'static str, &'static str) { + ("omp", "--resume") + } + + /// Append the capture extension with `-e` for both accepted command shapes. + fn instrument( + &self, + _inv: &Invocation, + capture: &CapturePaths, + _home: Option<&Path>, + ) -> SpawnPlan { + SpawnPlan { + args_suffix: format!( + " -e {}", + shell_quote(&capture.omp_capture.to_string_lossy()) + ), + env: vec![( + CAPTURE_ENV.into(), + capture.capture_file.clone().into_os_string(), + )], + injected_id: None, + } + } + + /// Return `sessionId` from a valid extension payload. + fn parse_capture(&self, payload: &str) -> Option { + let v = jzon::parse(payload).ok()?; + let id = v["sessionId"].as_str()?; + is_uuid(id).then(|| id.to_string()) + } + + /// Return the last trusted exit hint. Unlabelled hints are ordinary exit + /// lines. Labelled recovery hints count only when their label is `Main`; + /// other labels identify subagent sessions that `omp --resume` cannot open. + fn scrape_exit(&self, text: &str) -> Option { + let mut last: Option = None; + for (i, _) in text.match_indices(RESUME_HINT) { + let Some(id) = leading_uuid(&text[i + RESUME_HINT.len()..]) else { + continue; + }; + let head = &text[..i]; + let head = &head[head.rfind(['\n', '\r']).map_or(0, |n| n + 1)..]; + // A trailing `": "` marks a labelled recovery entry. Reject + // unknown labels because exit evidence outranks the capture file. + if let Some(label) = head.strip_suffix(": ") + && label.trim() != MAIN_LABEL + { + continue; + } + last = Some(id.to_string()); + } + last + } + + /// Return the sole in-window session whose header names `cwd`. Scan the + /// sessions root and each immediate subdirectory to cover flat and bucketed + /// stores. The ID follows the last `_` in the filename. + fn correlate_fs(&self, cwd: &Path, spawned: SystemTime, home: Option<&Path>) -> Option { + let sessions = self.home_root(home)?; + let spawn_ms = spawned + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_millis(); + // Session headers may record either the supplied or canonical path. + let canon = cwd.canonicalize().ok(); + + // The default store is bucketed; `PI_CODING_AGENT_SESSION_DIR` is flat. + let mut dirs = vec![sessions.clone()]; + dirs.extend( + fs::read_dir(&sessions) + .ok()? + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_dir())) + .map(|e| e.path()), + ); + + let mut survivors: Vec = Vec::new(); + for dir in dirs { + let Ok(entries) = fs::read_dir(dir) else { + continue; + }; + for entry in entries.flatten() { + let name = entry.file_name(); + let Some(id) = name + .to_str() + .and_then(|n| n.strip_suffix(".jsonl")) + .and_then(|stem| stem.rsplit_once('_')) + .map(|(_, id)| id) + .filter(|id| is_uuid(id)) + else { + continue; + }; + // Only UUIDv7 provides the creation instant used for matching. + let Some(ms) = v7_millis(id) else { continue }; + if !within_window_ms(u128::from(ms), spawn_ms) { + continue; + } + if !header_cwd_matches(&entry.path(), cwd, canon.as_deref()) { + continue; + } + // The same session may appear in multiple buckets; count its + // UUID once. + if !survivors.iter().any(|s| s == id) { + survivors.push(id.to_string()); + } + } + } + match survivors.as_slice() { + [only] => Some(only.clone()), + _ => None, + } + } +} + +/// Milliseconds embedded in the first 48 bits of a UUIDv7: the session's +/// creation instant. `None` when `id` is not v7. `id` must already satisfy +/// [`is_uuid`], which fixes its length and alphabet. +fn v7_millis(id: &str) -> Option { + if id.as_bytes()[14] != b'7' { + return None; + } + u64::from_str_radix(&format!("{}{}", &id[..8], &id[9..13]), 16).ok() +} + +/// Whether either of the first two records is a session header naming `cwd`, +/// its canonical form, or a path with the same canonical target. The optional +/// first record is a fixed-width title slot. Nothing later can affect +/// correlation, so transcripts are not read beyond the header. +fn header_cwd_matches(path: &Path, cwd: &Path, canon: Option<&Path>) -> bool { + let Ok(file) = fs::File::open(path) else { + return false; + }; + let mut reader = BufReader::new(file.take(64 * 1024)); + let mut line = String::new(); + for _ in 0..2 { + line.clear(); + if !matches!(reader.read_line(&mut line), Ok(n) if n > 0) { + return false; + } + let Ok(record) = jzon::parse(&line) else { + continue; + }; + if record["type"].as_str() != Some("session") { + continue; + } + return record["cwd"].as_str().is_some_and(|c| { + let header = Path::new(c); + header == cwd + || Some(header) == canon + || canon.is_some_and(|canon| header.canonicalize().is_ok_and(|h| h == canon)) + }); + } + false +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + use crate::{ + harness::fixtures::{ID, OTHER, assert_all_opaque, assert_corpus_scrape, paths}, + testutil::{temp, v7_at}, + }; + + /// Spawn instant shared by the correlation tests: 2026-08-15T22:13:20Z. + const SPAWN_MS: u64 = 1_786_000_000_000; + /// Working directory recorded in the generated headers. + const CWD: &str = "/work/proj"; + /// Valid UUIDv7 used in capture payloads. + const CAPTURED: &str = "01a0077c-e18e-7000-ae0b-016f4834b6e9"; + + fn spawned() -> SystemTime { + SystemTime::UNIX_EPOCH + Duration::from_millis(SPAWN_MS) + } + + /// Fixed-width title record preceding a session header. + fn title_slot() -> String { + let head = concat!( + r#"{"type":"title","v":1,"title":"Run ls -la","source":"auto","#, + r#""updatedAt":"2026-08-15T22:19:51.048Z","pad":""# + ); + let tail = r#""}"#; + format!("{head}{}{tail}", " ".repeat(256 - head.len() - tail.len())) + } + + /// Write a session file under `//`, optionally + /// behind the title slot. + fn write_named(sessions: &Path, bucket: &str, file: &str, id: &str, cwd: &str, slot: bool) { + let dir = sessions.join(bucket); + fs::create_dir_all(&dir).unwrap(); + let mut body = String::new(); + if slot { + body.push_str(&title_slot()); + body.push('\n'); + } + body.push_str(&format!( + r#"{{"type":"session","version":3,"id":"{id}","timestamp":"2026-08-15T22:19:51.048Z","cwd":"{cwd}","title":"Run ls -la"}}"# + )); + // Transcript content after the header does not participate. + body.push_str("\n{\"type\":\"message\",\"role\":\"assistant\"}\n"); + fs::write(dir.join(file), body).unwrap(); + } + + /// Write a session under `_.jsonl`. + fn write_session(sessions: &Path, bucket: &str, id: &str, cwd: &str, slot: bool) { + let file = format!("2026-08-15T22-19-51-048Z_{id}.jsonl"); + write_named(sessions, bucket, &file, id, cwd, slot); + } + + /// Resolve omp's sessions root against a synthetic launch environment. + fn home(env: &[(&str, &str)]) -> Option { + Omp.resolve_home(&|key| { + env.iter() + .find(|(name, _)| *name == key) + .map(|(_, value)| PathBuf::from(value)) + }) + } + + /// omp-specific aliases, shortcuts, prompts, and malformed resume forms + /// remain opaque. + #[test] + fn everything_else_is_opaque_and_never_rewritten() { + let opaque: Vec = ["omp -c", "omp --continue", "omp --resume", "ompx"] + .iter() + .map(|s| s.to_string()) + .chain([ + format!("omp -r {ID}"), + format!("omp --session {ID}"), + format!("omp --resume {}", &ID[..8]), + "omp -p 'fix the tests'".to_string(), + ]) + .collect(); + assert_all_opaque(&Omp, ID, &opaque); + } + + /// Both accepted shapes load the extension and name the capture file, and + /// neither gains an ID: omp has no `--session-id`, so a pinned UUID would + /// name a session `--resume` cannot reach. The fixture's asset path + /// carries a space, so the quoting has to hold it to one word. + #[test] + fn instrument_loads_the_extension_for_either_accepted_shape() { + for cmd in ["omp".to_string(), format!("omp --resume {ID}")] { + let inv = Omp.detect(&cmd).unwrap(); + let plan = Omp.instrument(&inv, &paths(), None); + assert_eq!(plan.injected_id, None, "{cmd}"); + assert_eq!( + plan.args_suffix, " -e '/tmp/Application Support/omp-capture.js'", + "{cmd}" + ); + assert_eq!( + plan.env, + vec![( + CAPTURE_ENV.into(), + PathBuf::from("/tmp/cap/session.json").into_os_string() + )], + "{cmd}" + ); + } + } + + /// The extension payload yields only a validated ID. + #[test] + fn parse_capture_returns_only_strict_ids() { + for reason in ["session_start", "session_switch"] { + let payload = format!( + r#"{{"reason":"{reason}","sessionId":"{CAPTURED}","sessionFile":"/s/2026-08-15T22-13-39-854Z_{CAPTURED}.jsonl","cwd":"/work/proj"}}"# + ); + assert_eq!(Omp.parse_capture(&payload).as_deref(), Some(CAPTURED)); + } + + assert_eq!(Omp.parse_capture("not json"), None); + assert_eq!(Omp.parse_capture("{}"), None); + assert_eq!(Omp.parse_capture(r#"{"sessionId":"my session"}"#), None); + assert_eq!(Omp.parse_capture(r#"{"sessionId":"x'; rm -rf ~'"}"#), None); + // UUID validation rejects uppercase hex. + assert_eq!( + Omp.parse_capture(&format!(r#"{{"sessionId":"{}"}}"#, CAPTURED.to_uppercase())), + None + ); + assert_eq!(Omp.parse_capture(""), None); + } + + #[test] + fn scrape_exit_reads_the_hint_and_takes_the_last() { + let hint = format!("Resume this session with omp --resume {ID}"); + assert_eq!(Omp.scrape_exit(&hint).as_deref(), Some(ID)); + + // The last hint by position wins. + let both = format!( + "Resume this session with omp --resume {OTHER}\n...\n\ + Resume this session with omp --resume {ID}\n" + ); + assert_eq!(Omp.scrape_exit(&both).as_deref(), Some(ID)); + + // A labelled recovery hint names the main session. + let crash = format!("[Recovery]\n Main: omp --resume {ID}\n"); + assert_eq!(Omp.scrape_exit(&crash).as_deref(), Some(ID)); + + // Subagent labels do not displace the main session. + let sub_a = "22222222-3333-4444-8555-666666666666"; + let sub_b = "33333333-4444-4555-8666-777777777777"; + let subagents = + format!(" agent-1: omp --resume {sub_a}\n agent-2: omp --resume {sub_b}\n"); + let swarm = format!("[Recovery]\n Main: omp --resume {ID}\n{subagents}"); + assert_eq!(Omp.scrape_exit(&swarm).as_deref(), Some(ID)); + + // A recovery block containing only subagents yields no exit evidence. + let orphans = format!("[Recovery]\n{subagents}"); + assert_eq!(Omp.scrape_exit(&orphans), None); + + // A later unlabelled exit hint remains eligible. + let recovered = format!("{orphans}...\nResume this session with omp --resume {ID}\n"); + assert_eq!(Omp.scrape_exit(&recovered).as_deref(), Some(ID)); + + assert_eq!(Omp.scrape_exit("no hint here"), None); + // A hint whose ID fails validation returns nothing. + assert_eq!(Omp.scrape_exit("omp --resume NOT-A-UUID"), None); + // A longer hexadecimal run is not an ID. + assert_eq!(Omp.scrape_exit(&format!("omp --resume {ID}ff")), None); + } + + /// One in-window session whose header names the task's cwd correlates; + /// another directory, another window, and a second candidate do not. + #[test] + fn correlate_fs_requires_a_unique_in_window_session_for_the_cwd() { + let sessions = temp("omp_correlate"); + let id = v7_at(SPAWN_MS + 4_000, 1); + write_session(&sessions, "bucket", &id, CWD, true); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)) + .as_deref(), + Some(id.as_str()) + ); + + // The header decides the directory, so another cwd matches nothing. + assert_eq!( + Omp.correlate_fs(Path::new("/elsewhere"), spawned(), Some(&sessions)), + None + ); + + // A session minted 90 s later falls outside the window. + let late = v7_at(SPAWN_MS + 90_000, 2); + write_session(&sessions, "late", &late, "/late/proj", true); + assert_eq!( + Omp.correlate_fs(Path::new("/late/proj"), spawned(), Some(&sessions)), + None + ); + + // Two in-window sessions for one directory cannot be told apart. + write_session(&sessions, "bucket", &v7_at(SPAWN_MS + 8_000, 3), CWD, true); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)), + None + ); + } + + /// `PI_CODING_AGENT_SESSION_DIR` becomes the session file's parent + /// directly, so its store carries no bucket level. One scan covers both + /// layouts: a file in the root and a file one level down, in the same + /// root, each correlating for its own header cwd. + #[test] + fn correlate_fs_reads_a_flat_store_and_a_bucketed_one() { + let sessions = temp("omp_layouts"); + // An empty bucket name writes into the root itself. + let flat = v7_at(SPAWN_MS + 3_000, 8); + write_session(&sessions, "", &flat, CWD, true); + let nested = v7_at(SPAWN_MS + 5_000, 9); + write_session(&sessions, "bucket", &nested, "/work/other", true); + + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)) + .as_deref(), + Some(flat.as_str()) + ); + assert_eq!( + Omp.correlate_fs(Path::new("/work/other"), spawned(), Some(&sessions)) + .as_deref(), + Some(nested.as_str()) + ); + // The header still decides the directory in either layout. + assert_eq!( + Omp.correlate_fs(Path::new("/elsewhere"), spawned(), Some(&sessions)), + None + ); + } + + /// The same session ID in two buckets remains one candidate. + #[test] + fn correlate_fs_collapses_one_session_seen_in_two_buckets() { + let sessions = temp("omp_dupe"); + let id = v7_at(SPAWN_MS + 6_000, 10); + write_session(&sessions, "encoded", &id, CWD, true); + write_session(&sessions, "--legacy--", &id, CWD, true); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)) + .as_deref(), + Some(id.as_str()) + ); + } + + /// A header path alias matches the task's canonical working directory. + #[test] + fn correlate_fs_matches_a_header_holding_an_alias_of_the_cwd() { + let tmp = temp("omp_alias"); + let (real, alias) = (tmp.join("real"), tmp.join("alias")); + fs::create_dir_all(&real).unwrap(); + std::os::unix::fs::symlink(&real, &alias).unwrap(); + let physical = real.canonicalize().unwrap(); + let sessions = tmp.join("sessions"); + let id = v7_at(SPAWN_MS + 7_000, 11); + write_session(&sessions, "bucket", &id, alias.to_str().unwrap(), true); + assert_eq!( + Omp.correlate_fs(&physical, spawned(), Some(&sessions)) + .as_deref(), + Some(id.as_str()) + ); + } + + /// A session header may occupy the first line when no title record exists. + #[test] + fn correlate_fs_reads_a_legacy_file_whose_header_is_line_one() { + let sessions = temp("omp_legacy"); + let id = v7_at(SPAWN_MS + 1_000, 5); + write_session(&sessions, "bucket", &id, CWD, false); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)) + .as_deref(), + Some(id.as_str()) + ); + } + + /// Malformed filenames, non-v7 IDs, and empty buckets contribute nothing. + #[test] + fn correlate_fs_refuses_names_and_buckets_it_cannot_read() { + let sessions = temp("omp_names"); + // UUIDv4 embeds no creation instant. + let v4 = format!("2026-08-15T22-19-51-048Z_{ID}.jsonl"); + write_named(&sessions, "v4", &v4, ID, CWD, true); + // Without a `_` nothing marks where the id starts. + let good = v7_at(SPAWN_MS + 1_000, 6); + write_named( + &sessions, + "nosep", + &format!("{good}.jsonl"), + &good, + CWD, + true, + ); + // An empty bucket contributes no candidate. + fs::create_dir_all(sessions.join("empty")).unwrap(); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)), + None + ); + + // A valid filename remains the sole candidate. + write_session(&sessions, "good", &good, CWD, true); + assert_eq!( + Omp.correlate_fs(Path::new(CWD), spawned(), Some(&sessions)) + .as_deref(), + Some(good.as_str()) + ); + } + + /// Canonical cwd comparison lets a task launched through a symlink match. + #[test] + fn correlate_fs_matches_a_symlinked_cwd_through_its_canonical_form() { + let tmp = temp("omp_canon"); + let (real, link) = (tmp.join("real"), tmp.join("link")); + fs::create_dir_all(&real).unwrap(); + std::os::unix::fs::symlink(&real, &link).unwrap(); + let canonical = real.canonicalize().unwrap(); + let sessions = tmp.join("sessions"); + let id = v7_at(SPAWN_MS + 2_000, 4); + write_session(&sessions, "bucket", &id, canonical.to_str().unwrap(), true); + assert_eq!( + Omp.correlate_fs(&link, spawned(), Some(&sessions)) + .as_deref(), + Some(id.as_str()) + ); + } + + /// Every branch of the chain resolves to a sessions root. + #[test] + fn resolve_home_walks_the_store_root_chain() { + assert_eq!( + home(&[("HOME", "/h")]), + Some("/h/.omp/agent/sessions".into()) + ); + + // The session dir names the store outright and bypasses the rest. + assert_eq!( + home(&[ + ("HOME", "/h"), + ("PI_CONFIG_DIR", ".alt"), + ("PI_CODING_AGENT_DIR", "/a"), + ("PI_CODING_AGENT_SESSION_DIR", "/s"), + ]), + Some("/s".into()) + ); + // Set but empty is unset. + assert_eq!( + home(&[("HOME", "/h"), ("PI_CODING_AGENT_SESSION_DIR", "")]), + Some("/h/.omp/agent/sessions".into()) + ); + + // The agent dir replaces `/agent` whole. + assert_eq!( + home(&[("HOME", "/h"), ("PI_CODING_AGENT_DIR", "/a")]), + Some("/a/sessions".into()) + ); + // A selected profile ignores it. + assert_eq!( + home(&[ + ("HOME", "/h"), + ("PI_CODING_AGENT_DIR", "/a"), + ("PI_PROFILE", "work"), + ]), + Some("/h/.omp/profiles/work/agent/sessions".into()) + ); + // Trimmed `default` selects the unprofiled store. + assert_eq!( + home(&[("HOME", "/h"), ("OMP_PROFILE", "default")]), + Some("/h/.omp/agent/sessions".into()) + ); + assert_eq!( + home(&[ + ("HOME", "/h"), + ("PI_PROFILE", " default "), + ("PI_CODING_AGENT_DIR", "/a"), + ]), + Some("/a/sessions".into()) + ); + assert_eq!( + home(&[("HOME", "/h"), ("OMP_PROFILE", " work ")]), + Some("/h/.omp/profiles/work/agent/sessions".into()) + ); + // Whitespace alone trims to nothing, which is no profile. + assert_eq!( + home(&[("HOME", "/h"), ("OMP_PROFILE", " ")]), + Some("/h/.omp/agent/sessions".into()) + ); + + // `OMP_PROFILE` wins by presence: it selects when non-empty and + // suppresses `PI_PROFILE` when empty. + assert_eq!( + home(&[("HOME", "/h"), ("OMP_PROFILE", "a"), ("PI_PROFILE", "b")]), + Some("/h/.omp/profiles/a/agent/sessions".into()) + ); + assert_eq!( + home(&[ + ("HOME", "/h"), + ("OMP_PROFILE", ""), + ("PI_PROFILE", "b"), + ("PI_CODING_AGENT_DIR", "/a"), + ]), + Some("/a/sessions".into()) + ); + + // `PI_CONFIG_DIR` replaces the default config-directory name. + assert_eq!( + home(&[("HOME", "/h"), ("PI_CONFIG_DIR", ".alt")]), + Some("/h/.alt/agent/sessions".into()) + ); + // An absolute value replaces `$HOME` under `Path::join`. + assert_eq!( + home(&[("HOME", "/h"), ("PI_CONFIG_DIR", "/abs")]), + Some("/abs/agent/sessions".into()) + ); + + // A complete agent-directory override does not require `HOME`. + assert_eq!( + home(&[("PI_CODING_AGENT_DIR", "/a")]), + Some("/a/sessions".into()) + ); + + // `home_root` applies the platform fallback when no path resolves. + assert_eq!(home(&[]), None); + } + + /// The XDG redirect requires an existing target and drops `agent/`. + #[test] + fn resolve_home_redirects_to_xdg_only_when_that_directory_exists() { + let dir = temp("omp_xdg"); + let xdg = dir.to_str().unwrap(); + + assert_eq!( + home(&[("HOME", "/h"), ("XDG_DATA_HOME", xdg)]), + Some("/h/.omp/agent/sessions".into()) + ); + fs::create_dir_all(dir.join("omp")).unwrap(); + assert_eq!( + home(&[("HOME", "/h"), ("XDG_DATA_HOME", xdg)]), + Some(dir.join("omp/sessions")) + ); + + // With a profile, the profile target must exist. + let profile = [ + ("HOME", "/h"), + ("XDG_DATA_HOME", xdg), + ("OMP_PROFILE", "work"), + ]; + assert_eq!( + home(&profile), + Some("/h/.omp/profiles/work/agent/sessions".into()) + ); + fs::create_dir_all(dir.join("omp/profiles/work")).unwrap(); + assert_eq!(home(&profile), Some(dir.join("omp/profiles/work/sessions"))); + + // An agent dir named outright is never redirected. + assert_eq!( + home(&[ + ("HOME", "/h"), + ("XDG_DATA_HOME", xdg), + ("PI_CODING_AGENT_DIR", "/a"), + ]), + Some("/a/sessions".into()) + ); + + // The redirect target is absolute, so it too answers without `HOME`. + assert_eq!( + home(&[("XDG_DATA_HOME", xdg)]), + Some(dir.join("omp/sessions")) + ); + } + + /// The scraper recovers the exit-hint ID from the corpus terminal bytes. + #[test] + fn corpus_scrape_recovers_the_exit_hint_id() { + assert_corpus_scrape( + &Omp, + include_bytes!("../../tests/corpus/omp_resume.bin"), + "01a0078a-7714-7000-9927-f167df9b6476", + ); + } +} diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 2be7d7e..41b4495 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -13,17 +13,17 @@ //! To avoid treating it as live status, every matcher: //! //! 1. locates the chrome region structurally (claude's separator-pair input -//! box, codex's composer, grok's bordered input box) and limits status -//! candidates relative to it; +//! box, codex's composer, grok's bordered input box, omp's two-row input +//! box) and limits status candidates relative to it; //! 2. returns `None` when the expected structure is absent or inconsistent; //! 3. matches row prefixes so status rows truncated with an ellipsis at narrow //! widths remain recognizable. A wrapped row fails the structural check. //! //! Normalization removes spinner glyphs, elapsed counters, throughput data, //! and key hints while preserving the CLI's status text. The only synthesized -//! status is `awaiting approval`, for approval menus: claude's dialog and -//! codex's modal. Corpus fixtures in `tests/corpus` pin the supported screen -//! structures. +//! status is `awaiting approval`, for approval menus: claude's dialog, +//! codex's modal, and omp's selector. Corpus fixtures in `tests/corpus` pin +//! the supported screen structures. use std::path::Path; @@ -676,6 +676,115 @@ fn grok_border_label(row: &str) -> Option { (!label.is_empty()).then(|| label.to_string()) } +// ------------------------------------------------------------------- omp -- + +/// Accept any braille code point as a spinner frame. The row still requires an +/// interrupt hint, and ASCII punctuation is too weak to anchor safely. +fn omp_frame(c: char) -> bool { + ('\u{2800}'..='\u{28FF}').contains(&c) +} + +/// Interrupt-hint suffixes accepted on an anchored status row. +const OMP_HINTS: &[&str] = &["⟦esc⟧", "⟨esc⟩"]; + +/// Selector cursors accepted by [`omp_approve_row`]. The exact remainder check +/// prevents the ASCII `>` cursor from matching quoted prose. +const OMP_CURSORS: &[&str] = &["❯", "\u{f054}", ">"]; + +/// omp inline-UI adapter. A two-row `╭…╮`/`╰…╯` input box anchors the nearest +/// painted status row above it. The approval selector replaces the box, so its +/// absence selects approval matching. +/// +/// Unicode box corners anchor status. ASCII `+` and `-` do not: transcript +/// tables and rules use the same glyphs. The plain-text approval selector still +/// matches under ASCII. +pub struct OmpSummary; + +impl SummaryAdapter for OmpSummary { + fn live_preview(&self, rows: &[String]) -> Option<(String, &'static str)> { + match omp_input_box(rows) { + Some(top) => omp_spinner_status(rows, top), + // Consider the approval selector only with the input box gone. + None => omp_approval(rows), + } + } + + /// Model text is user-configurable status-line content, not a stable label. + fn model_label(&self, _rows: &[String]) -> Option { + None + } +} + +/// Inspect the bottom-most `╰…╯` row and return its predecessor only when that +/// row is a `╭…╮` border. Adjacency rejects preview boxes containing a command. +fn omp_input_box(rows: &[String]) -> Option { + let bottom = rows.iter().rposition(|r| { + let t = r.trim(); + t.starts_with('╰') && t.ends_with('╯') + })?; + let t = rows[..bottom].last()?.trim(); + (t.starts_with('╭') && t.ends_with('╮')).then(|| bottom - 1) +} + +/// The status row: the first painted row above the input box, shaped +/// `{frame} {phrase} {hint}` one column in. Everything between the frame and +/// the hint is the model's own streamed intent phrase (`Listing directory +/// contents`; `Working…` when the model streams nothing) and is returned +/// verbatim, the CLI's own truncating `…` included. A wrapped row left its +/// hint on the next line and fails the suffix check rather than yielding half +/// a phrase. +fn omp_spinner_status(rows: &[String], top: usize) -> Option<(String, &'static str)> { + let probe = rows[..top].iter().rev().find(|r| !r.is_empty())?; + let mut chars = probe.trim_start().chars(); + if !omp_frame(chars.next()?) || chars.next()? != ' ' { + return None; + } + let rest = chars.as_str(); + let text = OMP_HINTS + .iter() + .find_map(|h| rest.strip_suffix(h))? + .strip_suffix(' ')?; + text.chars() + .next()? + .is_alphanumeric() + .then(|| (text.to_string(), "omp:spinner")) +} + +/// omp's approval selector, reached only with the input box gone: an +/// `Allow tool: {name}` head within six rows above the selected `Approve` row, +/// and `Deny` as the next painted row below it. The selection must occupy one +/// of the final nine rows. Prose quoted above a live input box never reaches +/// this matcher. +fn omp_approval(rows: &[String]) -> Option<(String, &'static str)> { + let last = rows.iter().rposition(|r| !r.is_empty())?; + let i = (last.saturating_sub(8)..=last).find(|&i| omp_approve_row(&rows[i]))?; + if rows[i + 1..].iter().find(|r| !r.is_empty())?.trim() != "Deny" { + return None; + } + rows[i.saturating_sub(6)..i] + .iter() + .any(|r| omp_allow_head(r)) + .then(|| ("awaiting approval".to_string(), "omp:approval-menu")) +} + +/// The selector's chosen row: a cursor spelling, a space, then `Approve` and +/// nothing more. Equality after the cursor is the whole check — the ascii +/// cursor `>` also opens a quoted line, so the row's remainder has to be +/// exact. +fn omp_approve_row(row: &str) -> bool { + let t = row.trim(); + OMP_CURSORS + .iter() + .any(|c| t.strip_prefix(c) == Some(" Approve")) +} + +/// The selector's head row: `Allow tool: {name}`. The prefix's trailing +/// space carries the name requirement — a trimmed row cannot end in one — so +/// a bare `Allow tool:` fails. +fn omp_allow_head(row: &str) -> bool { + row.trim().starts_with("Allow tool: ") +} + #[cfg(test)] #[path = "summary_tests.rs"] mod tests; diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 1df184a..c9f5e10 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -1473,3 +1473,224 @@ fn corpus_non_agent_tuis_keep_their_tiers() { assert_eq!(with.source, PreviewSource::Marker, "{name}"); } } + +// -------------------------------------------------------------------- omp -- + +/// Place the provided rows above an adjacent two-row omp input box. +fn omp_screen>(above: &[S]) -> Vec { + let mut rows: Vec = above.iter().map(|s| s.as_ref().to_string()).collect(); + rows.push("╭── π > ⬢ model · ◒ high > ◫ 12.2%/131K ▶──╮".to_string()); + rows.push("╰─ ─╯".to_string()); + rows +} + +/// omp's approval screen: the selector replaces the input box while the +/// status row keeps animating above it. `head` is the row that names the +/// tool. +fn omp_selector(head: &str) -> Vec { + omp_selector_row(" ❯ Approve", head) +} + +/// Build an approval selector with a configurable selected row. +fn omp_selector_row(approve: &str, head: &str) -> Vec { + let rule = "─".repeat(120); + rs(&[ + " ⠴ Listing directory contents ⟦esc⟧", + "", + &rule, + "", + head, + " Command: ls -la", + "", + approve, + " Deny", + "", + " up/down navigate enter select esc cancel", + "", + &rule, + ]) +} + +/// The intent phrase survives verbatim across both anchoring bracket themes, +/// the CLI's own truncating ellipsis included. +#[test] +fn omp_status_row_extracts_the_intent_phrase() { + let probe = |row: &str| OmpSummary.live_preview(&omp_screen(&[row, ""])); + for hint in ["⟦esc⟧", "⟨esc⟩"] { + assert_eq!( + probe(&format!(" ⠴ Listing directory contents {hint}")), + Some(("Listing directory contents".to_string(), "omp:spinner")), + "{hint:?}" + ); + } + // omp's default phrase, used when the model streams no intent of its own. + assert_eq!( + probe(" ⠹ Working… ⟦esc⟧"), + Some(("Working…".to_string(), "omp:spinner")) + ); + // ASCII frames and hints do not satisfy the anchored status grammar. + assert_eq!(probe(" - Working… [esc]"), None); + assert_eq!(probe(" ⠹ Working… [esc]"), None); + assert_eq!( + probe(" ⠋ Reading the fixture corpus rea… ⟦esc⟧"), + Some(("Reading the fixture corpus rea…".to_string(), "omp:spinner")), + "a phrase the CLI truncated keeps its own ellipsis" + ); + // The model text is a user-configured status-line segment: never read. + assert_eq!( + OmpSummary.model_label(&omp_screen(&[" ⠴ Listing directory contents ⟦esc⟧", ""])), + None + ); +} + +/// The status row needs its frame, its separating space, a phrase, and the +/// interrupt hint. A row whose hint wrapped onto the next line fails here +/// rather than surfacing half a phrase. +#[test] +fn omp_status_row_requires_the_whole_skeleton() { + let probe = |row: &str| OmpSummary.live_preview(&omp_screen(&[row, ""])); + for row in [ + " ⠴ Listing directory contents", + " ⠴Listing directory contents ⟦esc⟧", + " ⠴ ⟦esc⟧", + " ⠴ · queued ⟦esc⟧", + " Listing directory contents ⟦esc⟧", + " Press ⟦esc⟧ to interrupt", + ] { + assert_eq!(probe(row), None, "{row:?}"); + } +} + +/// The pin is the row above the input box, not a substring search: a +/// status-shaped row parked in the transcript never anchors, and the +/// tool-call preview box's matching corners are not the input box. +#[test] +fn omp_pins_the_status_row_to_the_input_box() { + let quoted = omp_screen(&[ + " ⠴ Listing directory contents ⟦esc⟧", + "", + " That row is chrome, not transcript.", + "", + ]); + assert_eq!(OmpSummary.live_preview("ed), None); + + // The preview box fences a `│`-headed command row between the same + // corners, so the pair is not adjacent and the pin fails. + let preview_box = rs(&[ + " ⠴ Listing directory contents ⟦esc⟧", + "", + "╭──────────────────╮", + "│ $ ls -la │", + "╰──────────────────╯", + ]); + assert_eq!(OmpSummary.live_preview(&preview_box), None); +} + +/// The selector synthesizes its label from the `Allow tool:` head, the +/// selection, and the `Deny` sibling below it. The status row keeps +/// painting throughout and never wins. +#[test] +fn omp_approval_requires_the_selector_shape() { + assert_eq!( + OmpSummary.live_preview(&omp_selector(" Allow tool: bash")), + Some(("awaiting approval".to_string(), "omp:approval-menu")) + ); + for head in [" Allow tool: ", " Reviewing the plan"] { + assert_eq!( + OmpSummary.live_preview(&omp_selector(head)), + None, + "{head:?}" + ); + } + + // `Deny` must be the next painted row below the selection. + let lone = rs(&[" Allow tool: bash", "", " ❯ Approve", "", " esc cancel"]); + assert_eq!(OmpSummary.live_preview(&lone), None); + + // The selection must occupy one of the final nine rows. + let mut buried = omp_selector(" Allow tool: bash"); + buried.extend(std::iter::repeat_n(" tool output".to_string(), 6)); + assert_eq!(OmpSummary.live_preview(&buried), None); + + // The same block quoted in the transcript keeps the live input box + // below it; the box routes to the status probe, which sees prose. + let mut quoted = omp_selector(" Allow tool: bash"); + quoted.push(String::new()); + assert_eq!(OmpSummary.live_preview(&omp_screen("ed)), None); +} + +/// Every supported selector cursor reports a blocked task. +#[test] +fn omp_approval_accepts_every_cursor_preset() { + for cursor in ['❯', '\u{f054}', '>'] { + assert_eq!( + OmpSummary.live_preview(&omp_selector_row( + &format!(" {cursor} Approve"), + " Allow tool: bash" + )), + Some(("awaiting approval".to_string(), "omp:approval-menu")), + "{cursor:?}" + ); + } + // The row after the cursor must be `Approve` exactly: `>` also opens a + // quoted line, and the selector is the one place a bare `>` is trusted. + for approve in [" > Approve now", " >Approve", " > approve", " * Approve"] { + assert_eq!( + OmpSummary.live_preview(&omp_selector_row(approve, " Allow tool: bash")), + None, + "{approve:?}" + ); + } +} + +/// ASCII box glyphs are indistinguishable from transcript tables and rules, so +/// they do not anchor status and the preview falls through to the floor tier. +#[test] +fn omp_ascii_box_glyphs_do_not_anchor() { + let ascii_box = rs(&[ + " - Listing directory contents [esc]", + "", + "+-- pi > model . high > 12.2%/131K --+", + "+- -+", + ]); + assert_eq!(OmpSummary.live_preview(&ascii_box), None); +} + +/// omp corpus replay at capture geometry (40×120): exact status text, Anchor +/// provenance, and matcher ID. +#[test] +fn corpus_omp_states_anchor_exactly() { + for (name, bytes, want) in [ + ( + "preview_omp_working", + &include_bytes!("../../tests/corpus/preview_omp_working.bin")[..], + anchor("Listing directory contents", "omp:spinner"), + ), + ( + "preview_omp_approval", + &include_bytes!("../../tests/corpus/preview_omp_approval.bin")[..], + anchor("awaiting approval", "omp:approval-menu"), + ), + ] { + assert_eq!(corpus(bytes, &OmpSummary, 120), want, "{name}"); + } +} + +/// omp is an inline UI, so a screen with no anchor falls through to the +/// floor tier — its input row — never the alternate-screen marker. +#[test] +fn corpus_omp_states_fall_through_to_the_floor() { + let input_row = floor(&format!("╰─{}─╯", " ".repeat(116))); + for (name, bytes) in [ + ( + "preview_omp_idle", + &include_bytes!("../../tests/corpus/preview_omp_idle.bin")[..], + ), + ( + "preview_omp_body_hint", + &include_bytes!("../../tests/corpus/preview_omp_body_hint.bin")[..], + ), + ] { + assert_eq!(corpus(bytes, &OmpSummary, 120), input_row, "{name}"); + } +} diff --git a/src/supervisor.rs b/src/supervisor.rs index 1ac22eb..f92ed3a 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -168,12 +168,9 @@ fn scrape_now(t: &mut Task) { t.scrape_exit_hint(); } -/// Resolve the harness home from the launch environment. The tool-specific -/// override wins, followed by `$HOME/`; neither yields -/// `None` so the harness can apply its platform-home fallback. +/// Resolve the harness store root from the task's launch environment. fn harness_home(env: &[(OsString, OsString)], h: &dyn harness::Harness) -> Option { - let val = |key: &str| env_get(env, key).map(PathBuf::from); - val(h.home_env_var()).or_else(|| Some(val("HOME")?.join(h.home_dot_dir()))) + h.resolve_home(&|key| env_get(env, key).map(PathBuf::from)) } /// State for automatic recovery snapshots. Write failures do not interrupt diff --git a/src/supervisor_capture_tests.rs b/src/supervisor_capture_tests.rs index 7212cd8..490a3a5 100644 --- a/src/supervisor_capture_tests.rs +++ b/src/supervisor_capture_tests.rs @@ -1444,6 +1444,48 @@ fn recovery_cadence_rewrites_on_capture_drift_and_skips_when_static() { assert_eq!(names.len(), 1, "one incarnation owns one snapshot file"); } +/// An `omp` spawn receives `-e ` and the capture environment. omp +/// cannot pin an ID at launch, so the task carries none. +#[test] +fn spawn_omp_loads_the_capture_extension() { + let dir = scratch("cap_omp"); + let (bin, runtime) = (dir.join("bin"), dir.join("run")); + install_stub(&bin, "omp", &dir); + let mut s = sup_ctx(agent_ctx(&bin, &runtime, dir.to_path_buf())); + spawn(&mut s, "omp", dir.to_path_buf()); + + let argv = wait_argv(&mut s, &dir.join("argv")); + let ei = argv + .iter() + .position(|a| a == "-e") + .expect("the stub must receive -e"); + let module = PathBuf::from(&argv[ei + 1]); + assert!(module.is_file(), "the extension module must exist"); + let text = std::fs::read_to_string(&module).unwrap(); + assert!( + text.contains("FLEETCOM_CAPTURE_FILE"), + "the module must write to the capture env: {text:?}" + ); + let t = &s.tasks[0]; + let cap = t.capture_file.clone().expect("capture file set"); + assert_eq!( + module.parent(), + cap.parent(), + "assets and captures must share the namespace" + ); + assert_eq!( + std::fs::read_to_string(dir.join("capenv")).unwrap(), + cap.display().to_string(), + "the child env must name this run's capture file" + ); + assert_eq!( + t.command, "omp", + "instrumentation must never leak into the stored command" + ); + assert!(t.harness.is_some()); + assert!(t.resume_id.is_none(), "omp cannot pin an id at launch"); +} + // --- live registry blocked status -------------------------------------- /// Tick until the sole task's preview satisfies `pred` or the budget expires, diff --git a/tests/corpus/README.md b/tests/corpus/README.md index d8c7449..88b2d52 100644 --- a/tests/corpus/README.md +++ b/tests/corpus/README.md @@ -18,6 +18,7 @@ feed the bytes to the emulator verbatim. | `claude_resume.bin` | `claude --session-id`: one prompt, reply, `/exit` | alternate-screen exit followed by the primary-screen resume hint (`claude --resume `); the scrape target for harness exit capture | | `codex_resume.bin` | `codex resume` | top-anchored DECSTBM scroll regions (`CSI 1;N r`), reverse index, inline-TUI history insertion, and an SGR-split resume hint for harness exit capture | | `grok_resume.bin` | `grok --session-id`: one prompt, reply, `/exit` | primary-screen exit followed by the resume hint (`grok --resume `); the scrape target for harness exit capture | +| `omp_resume.bin` | `omp`: one launch, `/exit` | primary-screen exit followed by the resume hint (`omp --resume `); the scrape target for harness exit capture | | `tmux_split.bin` | `tmux` session with two splits and one command per pane | scroll regions, pane borders, full redraws | | `vim_session.bin` | `vim -u NONE`: insert, navigate, `:set number`, `:q!` | alternate screen, cursor addressing, line editing | | `less_altscreen.bin` | `less` over `/usr/share/dict/words`: page, `G`, `g`, `q` | alternate-screen entry and exit, full-screen paging | @@ -33,9 +34,9 @@ feed the bytes to the emulator verbatim. The `preview_*.bin` fixtures pin summary-adapter extraction, normalization, and fallback behavior. Each fixture is a constructed repaint stream: optional alternate-screen entry, clear, home, then sanitized screen rows joined with -CRLF. Claude and Grok use the alternate screen; Codex is inline. Identifying -and user-configured text is replaced with alignment-preserving synthetic -values. Geometry is 40×120 unless noted. +CRLF. Claude and Grok use the alternate screen; Codex and omp are inline. +Identifying and user-configured text is replaced with alignment-preserving +synthetic values. Geometry is 40×120 unless noted. `preview_codex_reasoning.bin` uses 40×80 geometry, and `preview_codex_queued.bin` uses 40×36. Both are bottom-anchored on a 40-row @@ -47,7 +48,10 @@ omits the welcome box and includes agent-roster rows below the input box. The Claude task-list fixtures use generic phase names in a task-list layout; both omit the welcome box. The Claude workflow-wait fixture uses generic wording, omits the welcome box, and includes a long blank gap above the input box and a -roster below it. +roster below it. The omp fixtures replace the local model path and the working +directory in the status line with same-length synthetic values, and their +status rows carry a streamed intent phrase rather than omp's default +`Working…`. | Fixture | Scenario | Coverage | | --- | --- | --- | @@ -78,6 +82,10 @@ roster below it. | `preview_grok_subagent_scrollback.bin` | grok idle with `Subagent running:` in the body, no `◎` row | fall-through; body-shaped text is not status | | `preview_grok_idle.bin` | grok idle session | fall-through to the marker | | `preview_grok_splash.bin` | grok launch splash with resume hint above the box | fall-through; distinct views never anchor | +| `preview_omp_working.bin` | omp status row carrying the model's streamed intent phrase above the input box | `omp:spinner`; padding, spinner frame, and interrupt hint stripped | +| `preview_omp_approval.bin` | omp approval selector, input box replaced, tool-call preview box and a live status row still above it | `omp:approval-menu` synthesizes `awaiting approval` while the status row keeps animating | +| `preview_omp_idle.bin` | omp idle with the welcome box and tip above the input box | fall-through to the floor tier; an inline UI reaches no marker | +| `preview_omp_body_hint.bin` | status-shaped row quoted in the transcript, prose between it and an idle input box | negative: the pin is the row above the box, not a substring search | | `preview_trunc_claude.bin` | synthetic 40×80: spinner row truncated inside its parenthetical | head match still extracts `Hashing…` | | `preview_trunc_codex.bin` | synthetic 40×80: working row truncated inside the `/ps` hint | the head still matches and the key-hint suffix is omitted | | `preview_trunc_grok.bin` | synthetic 40×80: spinner label truncated with the CLI's ellipsis | extraction keeps the CLI's own `…` verbatim | @@ -93,10 +101,10 @@ The fixtures provide evidence for three distinct boundaries: - `codex_resume`, `wide_emoji`, `dec_scrollregion`, and `topregion_scroll` pin parser semantics: scrollback retention, intensity stacking, charset translation, and VS16 width. -- `claude_resume`, `codex_resume`, and `grok_resume` verify that retained - terminal text preserves the exit hints consumed by their harnesses. +- `claude_resume`, `codex_resume`, `grok_resume`, and `omp_resume` verify that + retained terminal text preserves the exit hints consumed by their harnesses. `src/golden.rs` contains the absolute display and parser expectations. -`src/harness/claude.rs`, `src/harness/codex.rs`, and `src/harness/grok.rs` -contain the agent-resume scrape expectations. `src/harness/summary.rs` -contains the preview-fixture expectations. +`src/harness/claude.rs`, `src/harness/codex.rs`, `src/harness/grok.rs`, and +`src/harness/omp.rs` contain the agent-resume scrape expectations. +`src/harness/summary.rs` contains the preview-fixture expectations. diff --git a/tests/corpus/omp_resume.bin b/tests/corpus/omp_resume.bin new file mode 100644 index 0000000..a376c1f --- /dev/null +++ b/tests/corpus/omp_resume.bin @@ -0,0 +1,372 @@ +[?2004h[?1l>[?u]11;?[?2031h[?2026$p[?2048$p[?2031$p[?1010$p[?1011$p[?5522h[?25l]0;π >]0;π > work3[?25l[?2026h[?7l +╭─── omp v17.3.4 ──────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Welcome back! │ +│ │ +│ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/chris/Documents/Models/Qwen/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: `/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last +  shell/python command + + Connecting to MCP servers: node_repl…  + +╭── π  > ⬢ /Users/chris/Documents/Models/Qwen/Qwen3.8-27B-UD-Q8_K_XL.gguf · ◒ high > 🗑 ]8;id=8327217c;file:///tmp/claude-501/-Users-chris-Documents-Code-Rust-fleetcom/195801f2-9949-4130-addc-4289c1cde424/scratchpad/work3\…hpad/work3]8;;\ > ◫ 11.8%/131K ⟲ ▶──╮]8;; +╰─   ─╯ + + + + + + + + + + + + + + + + + + +[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[>4;2m[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/chris/Documents/Models/Qwen/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: `/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last +  shell/python command + + xdev: xd://: mounted mcp__node_repl_js, mcp__node_repl_js_add_node_module_dir, mcp__node_repl_js_reset  + +╭── π  > ⬢ /Users/chris/Documents/Models/Qwen/Qwen3.8-27B-UD-Q8_K_XL.gguf · ◒ high > 🗑 ]8;id=8327217c;file:///tmp/claude-501/-Users-chris-Documents-Code-Rust-fleetcom/195801f2-9949-4130-addc-4289c1cde424/scratchpad/work3\…hpad/work3]8;;\ > ◫ 12.2%/131K ⟲ ▶──╮]8;;[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[?25l[?2026h[?7l │ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │[?25h[?7h[?2026l[>4;0m[?25l[?2026h[?7l  Closing session… [?25h[?7h[?2026l [?25h[?2026l[?7h[?1l>[?2004l[?5522l[?1006l[?1003l[?1000l[?2031l +Resume this session with omp --resume 01a0078a-7714-7000-9927-f167df9b6476 +[?2026l[?7h[?1l>[?2004l[?2031l[?2048l[?5522l[?1006l[?1003l[?1000l[?25h \ No newline at end of file diff --git a/tests/corpus/preview_omp_approval.bin b/tests/corpus/preview_omp_approval.bin new file mode 100644 index 0000000..7a89106 --- /dev/null +++ b/tests/corpus/preview_omp_approval.bin @@ -0,0 +1,40 @@ +│ │ +│ Welcome back! │ +│ │ +│ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: Log in to several accounts of the same provider — `/login` again — and omp load-balances + across them automatically + + xdev: xd://: mounted mcp__fixture_js, mcp__fixture_js_add_module_dir, mcp__fixture_js_reset + + + Use the bash tool to run: ls -la + + + I'll use the bash tool to run the requested command. + +╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ $ ls -la │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + ⠴ Listing directory contents ⟦esc⟧ + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + Allow tool: bash + Command: ls -la + + ❯ Approve + Deny + + up/down navigate enter select esc cancel + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/tests/corpus/preview_omp_body_hint.bin b/tests/corpus/preview_omp_body_hint.bin new file mode 100644 index 0000000..db37c18 --- /dev/null +++ b/tests/corpus/preview_omp_body_hint.bin @@ -0,0 +1,39 @@ + +╭─── omp v17.3.4 ──────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Welcome back! │ +│ │ +│ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh + keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any + OpenAI-compatible client can hit + + Use the bash tool to run: ls -la + + While a turn runs, omp paints its status row as: + + ⠴ Listing directory contents ⟦esc⟧ + + That row is chrome, not transcript: quoting it must not anchor. + +╭── π > ⬢ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf · ◒ high > 🗑 …tures/proj > ◫ 12.2%/131K ⟲ ▶──╮ +╰─ ─╯ + + + + + + + + + + + diff --git a/tests/corpus/preview_omp_idle.bin b/tests/corpus/preview_omp_idle.bin new file mode 100644 index 0000000..0264947 --- /dev/null +++ b/tests/corpus/preview_omp_idle.bin @@ -0,0 +1,39 @@ + +╭─── omp v17.3.4 ──────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Welcome back! │ +│ │ +│ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh + keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any + OpenAI-compatible client can hit + + + +╭── π > ⬢ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf · ◒ high > 🗑 …tures/proj > ◫ 12.2%/131K ⟲ ▶──╮ +╰─ ─╯ + + + + + + + + + + + + + + + + + diff --git a/tests/corpus/preview_omp_working.bin b/tests/corpus/preview_omp_working.bin new file mode 100644 index 0000000..c156ab0 --- /dev/null +++ b/tests/corpus/preview_omp_working.bin @@ -0,0 +1,39 @@ + +╭─── omp v17.3.4 ──────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Welcome back! │ +│ │ +│ ▀██████████▀ │ +│ ╘██ ██ │ +│ ██ ██ │ +│ ██ ██ │ +│ ▄██▄ ▄██▄ │ +│ │ +│ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf │ +│ llama.cpp │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + Tip: Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow + rainbow as you type + + xdev: xd://: mounted mcp__fixture_js, mcp__fixture_js_add_module_dir, mcp__fixture_js_reset + + + Use the bash tool to run: ls -la + + + ⠴ Listing directory contents ⟦esc⟧ + +╭── π > ⬢ /Users/dev/Projects/fixtures/qwen3/Qwen3.8-27B-UD-Q8_K_XL.gguf · ◒ high > 🗑 …tures/proj > ◫ 12.2%/131K ⟲ ▶──╮ +╰─ ─╯ + + + + + + + + + + + +