Skip to content
7 changes: 4 additions & 3 deletions src/harness/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};

use super::summary::AWAITING_APPROVAL;
use super::{
CAPTURE_ENV, CapturePaths, Harness, Invocation, SpawnPlan, is_uuid, last_hint, pin_plan,
shell_quote, within_window, within_window_ms,
Expand Down Expand Up @@ -115,7 +116,7 @@ struct SessionRecord {
/// remain verbatim. A missing reason falls back to `awaiting input`.
fn waiting_preview(reason: Option<&str>) -> (String, &'static str) {
match reason.filter(|r| !r.is_empty()) {
Some("permission prompt") => ("awaiting approval".to_string(), "claude:registry-approval"),
Some("permission prompt") => (AWAITING_APPROVAL.to_string(), "claude:registry-approval"),
Some(other) => (other.to_string(), "claude:registry-waiting"),
None => ("awaiting input".to_string(), "claude:registry-waiting"),
}
Expand Down Expand Up @@ -155,8 +156,8 @@ fn record_for_pid(
let text = fs::read_to_string(dir.join(format!("{pid}.json"))).ok()?;
let rec = parse_record(&text)?;
let spawned_ms = spawned.duration_since(UNIX_EPOCH).ok()?.as_millis();
// Accept the task's literal path or its canonical form; reject a failed
// canonicalization unless the literal paths already match.
// Test literal equality before canonicalizing the task path: identical
// nonexistent paths remain eligible.
let same_cwd = rec.cwd == cwd || cwd.canonicalize().is_ok_and(|c| rec.cwd == c);
(rec.pid == pid && same_cwd && within_window_ms(rec.started_at, spawned_ms)).then_some(rec)
}
Expand Down
15 changes: 2 additions & 13 deletions src/harness/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{

use super::{
CAPTURE_ENV, CapturePaths, Harness, Invocation, NOTIFY_CHAIN_ENV, SpawnPlan, is_uuid,
last_hint, leading_uuid, shell_quote, within_window_ms,
last_hint, leading_uuid, shell_quote, v7_millis, within_window_ms,
};

pub struct Codex;
Expand Down Expand Up @@ -295,15 +295,6 @@ fn toml_escape(s: &str) -> String {
out
}

/// Extract the millisecond timestamp from a validated v7 UUID. Other versions
/// return `None`.
fn v7_millis(id: &str) -> Option<u64> {
if id.as_bytes()[14] != b'7' {
return None;
}
u64::from_str_radix(&format!("{}{}", &id[..8], &id[9..13]), 16).ok()
}

/// Check the rollout's first record for a matching `cwd` and no explicit
/// spawned-thread provenance. Missing and unrecognized `thread_source` values
/// remain eligible; `"subagent"` or any `parent_thread_id` rejects the record.
Expand Down Expand Up @@ -524,10 +515,8 @@ mod tests {
assert_eq!(toml_escape("a\tb"), "a\\u0009b");
// Execute the suffix through a shell and inspect the resulting words.
let paths = CapturePaths {
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"),
..paths()
};
let inv = Codex.detect("codex").unwrap();
let plan = Codex.instrument(&inv, &paths, Some(&no_config_home()));
Expand Down
13 changes: 13 additions & 0 deletions src/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ fn within_window_ms(a: u128, b: u128) -> bool {
a.abs_diff(b) <= CORRELATE_WINDOW.as_millis()
}

/// 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<u64> {
if id.as_bytes()[14] != b'7' {
return None;
}
u64::from_str_radix(&format!("{}{}", &id[..8], &id[9..13]), 16).ok()
}

/// Single-quote `s` for `$SHELL -c`, encoding embedded `'` as `'\''`.
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
Expand Down Expand Up @@ -649,6 +659,9 @@ mod tests {
let (h, inv) = detect("grok").unwrap();
assert_eq!(h.home_dot_dir(), ".grok");
assert_eq!(inv, Invocation::Bare);
let (h, inv) = detect("omp").unwrap();
assert_eq!(h.home_dot_dir(), ".omp/agent/sessions");
assert_eq!(inv, Invocation::Bare);
assert!(detect("vim").is_none());
assert!(detect("").is_none());
}
Expand Down
12 changes: 1 addition & 11 deletions src/harness/omp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use std::{

use super::{
CAPTURE_ENV, CapturePaths, Harness, Invocation, SpawnPlan, is_uuid, leading_uuid, shell_quote,
within_window_ms,
v7_millis, within_window_ms,
};

/// Command fragment shared by ordinary exit and recovery hints.
Expand Down Expand Up @@ -222,16 +222,6 @@ impl Harness for Omp {
}
}

/// 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<u64> {
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
Expand Down
35 changes: 19 additions & 16 deletions src/harness/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
//! 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.
//! 3. preserves CLI-generated ellipsis truncation. omp also requires its
//! trailing interrupt hint; wrapped rows fail that structural check.
//!
//! Normalization removes spinner glyphs, elapsed counters, throughput data,
//! and key hints while preserving the CLI's status text. The only synthesized
Expand All @@ -42,6 +42,10 @@ pub fn select(command: &str) -> Option<&'static dyn SummaryAdapter> {
.map(|a| a.summary)
}

/// Preview text shared by approval-menu matchers and Claude's registry
/// permission prompt.
pub(crate) const AWAITING_APPROVAL: &str = "awaiting approval";

/// Whether `row` is a full-width horizontal rule: nothing but `─`, long
/// enough that box borders and inline list rules never qualify. claude's
/// input box is fenced by two such rows.
Expand All @@ -56,6 +60,12 @@ fn is_rule_row(row: &str) -> bool {
n >= 40
}

/// Whether `c` is a Unicode Braille Patterns code point used as a spinner
/// frame by the supported CLIs.
fn braille_frame(c: char) -> bool {
('\u{2800}'..='\u{28FF}').contains(&c)
}

/// The status phrase of a spinner row: a frame char accepted by `is_frame`,
/// a space, then text through the first `…` inclusive. The phrase must open
/// alphanumeric; past that it is task-derived and unconstrained. Trailing
Expand Down Expand Up @@ -126,7 +136,7 @@ impl SummaryAdapter for ClaudeSummary {
let frame = chars.next()?;
// Normalize the entire quadrant-circle block as one animation set.
let framed = CLAUDE_SPINNER.contains(&frame)
|| ('\u{2800}'..='\u{28FF}').contains(&frame)
|| braille_frame(frame)
|| ('\u{25D0}'..='\u{25D3}').contains(&frame);
(framed && chars.next()? == ' ').then(|| format!("✻ {}", chars.as_str()))
}
Expand Down Expand Up @@ -267,7 +277,7 @@ fn claude_approval(rows: &[String]) -> Option<(String, &'static str)> {
let next = rows[i + 1..].iter().find(|r| !r.is_empty())?;
next.trim_start()
.starts_with("2. ")
.then(|| ("awaiting approval".to_string(), "claude:approval-menu"))
.then(|| (AWAITING_APPROVAL.to_string(), "claude:approval-menu"))
}

/// Complete effort values accepted before a welcome-box ellipsis.
Expand Down Expand Up @@ -362,8 +372,7 @@ impl SummaryAdapter for CodexSummary {
}
let mut chars = title.chars();
let frame = chars.next()?;
(('\u{2800}'..='\u{28FF}').contains(&frame) && chars.next()? == ' ')
.then(|| format!("⠋ {}", chars.as_str()))
(braille_frame(frame) && chars.next()? == ' ').then(|| format!("⠋ {}", chars.as_str()))
}
}

Expand Down Expand Up @@ -397,7 +406,7 @@ fn codex_approval(rows: &[String]) -> Option<(String, &'static str)> {
rows[i + 1..]
.iter()
.all(|r| !r.starts_with(CODEX_PROMPT) || codex_menu_head(r))
.then(|| ("awaiting approval".to_string(), "codex:approval-menu"))
.then(|| (AWAITING_APPROVAL.to_string(), "codex:approval-menu"))
}

/// Return the first ` · `-separated item from the bottom-most qualifying row
Expand Down Expand Up @@ -587,7 +596,7 @@ impl SummaryAdapter for GrokSummary {
let t = probe.trim_start();
// Keep the label through its first ellipsis. Wrapped tail rows have no
// spinner prefix, so they fail the frame check and fall through.
if let Some(text) = spinner_text(t, |c| ('\u{2800}'..='\u{28FF}').contains(&c)) {
if let Some(text) = spinner_text(t, braille_frame) {
return Some((text, "grok:spinner"));
}
// Still-running is the same probe, never a scan: a closer spinner
Expand Down Expand Up @@ -678,12 +687,6 @@ fn grok_border_label(row: &str) -> Option<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⟩"];

Expand Down Expand Up @@ -736,7 +739,7 @@ fn omp_input_box(rows: &[String]) -> Option<usize> {
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()? != ' ' {
if !braille_frame(chars.next()?) || chars.next()? != ' ' {
return None;
}
let rest = chars.as_str();
Expand Down Expand Up @@ -764,7 +767,7 @@ fn omp_approval(rows: &[String]) -> Option<(String, &'static str)> {
rows[i.saturating_sub(6)..i]
.iter()
.any(|r| omp_allow_head(r))
.then(|| ("awaiting approval".to_string(), "omp:approval-menu"))
.then(|| (AWAITING_APPROVAL.to_string(), "omp:approval-menu"))
}

/// The selector's chosen row: a cursor spelling, a space, then `Approve` and
Expand Down
Loading