mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* refactor(agents): extract the title name-anchoring vocabulary Pure move, no behaviour change. The name tokens, display labels and the bare-name/whole-title resolvers come out of agent-title-evidence.ts into agent-title-name-anchoring.ts so more than one predicate can resolve a name to an agent through the same implementation rather than by convention. Nothing is added or reworded on the way out; the extracted functions are unchanged and simply exported. Verified by the existing suites: the title, identity and sidebar tests all pass untouched. The identity ratchet moves with it — titleHasAgentName's two parser-implementation callsites now live in the new module — so this commit is green on its own rather than leaving the ratchet red until the next one. * fix(agents): stop a foreign agent name in task text from taking a pane A Claude pane's WORKING title is a bare braille spinner plus task text, so any agent name inside that text resolved the title to a committed foreign identity. The guard that fixed the mirror-image bug was written for the Claude label only, so every other name short-circuited above it and took the pane from its owner — the tab icon in use-tab-agent.ts, and the sidebar/dashboard row in worktree-title-derived-agent-rows.ts, which never consulted the owner at all. Make the rule agent-neutral in both directions: a title may only take a pane from a known owner when it PRESENTS that agent, never when the name merely occurs in free-form task text. titlePresentsAgent sits beside collectAgentTitleEvidence and resolves names through the vocabulary they now share. It differs from the parser in one respect: the parser asks who a title names given no owner, so it must refuse anything a task sentence could forge; this predicate is told which agent to check, so it also accepts that one name in an identity POSITION — the whole undecorated remainder, or the head of a `Name:` frame. The colon is what separates `Codex: fix cursor offsets` from `Claude Code compare Opencode`. resolveTabAgentFromSignals exists twice, already drifted in owner handling. useTabAgent calls the copy in use-tab-agent.ts, which is what paints the tab icon; every existing test imports the copy in tab-agent-from-signals.ts. Both are fixed, and the new suite runs its whole contract through both so a future divergence fails instead of hiding. * fix(agents): let the title label map admit a missing label Narrowing the map's value type to TuiAgent surfaced an inaccuracy that was always there: noUncheckedIndexedAccess is off, so indexing Record<string, _> types every lookup as a hit, and `label` is arbitrary text parsed off a terminal title. The compiler concluded the `?? 'unknown'` fallback was unreachable and the unknown-label comparison impossible. The runtime disagrees — the map carries a fixed set of ~10 labels and misses everything else. Deleting the branch would return undefined where the function returns 'unknown' today, so the type is what is wrong. Partial<Record<...>> makes absence expressible and leaves behaviour unchanged. Pinned by a test, since nothing else forced the map to be honest about this. * fix(agents): let an agent's own sigil still present it against an owner titlePresentsAgent read only anchoredNames, so it missed the evidence class the parser ranks highest: a vendor marker, the sigil an agent emits itself and task text cannot forge. Gemini's four glyphs and Cursor's native literal are markers, not anchored names, so every Gemini working, idle and permission title — and Cursor's literal — failed the gate and the pane stayed on its previous owner. Run Claude, let it finish, run gemini in the same pane: tab and sidebar both stayed Claude. The bug this change exists to fix, mirrored. Admitting the whole vendor class would have reintroduced #8940 the other way, because Claude's '. ', '* ' and idle sigil are generic — OpenCode emits them too — so they prove activity, not identity. hasGenericClaudeStatusPrefix already modelled that distinction inside terminal-title-agent-type.ts; it moves to agent-title-core.ts so both callers share the one definition. Not isGenericClaudeStatusClaim, which is that model plus a clause for a different question: it treats a 'claude' token as upgrading a generic prefix into a real claim, which is right when asking whether a title may MINT Claude identity and wrong when asking whether it PRESENTS Claude. Applied here it would have let ". port the claude prompt" reclaim an OpenCode pane. The predicate is now one memoized set per title rather than a memoized first line and an uncached scan, so the common path — a title presenting nobody — no longer re-runs the name matchers over every wrapper segment on each render. * fix(agents): gate on one identity-frame grammar, ratcheted against the corpus Two review rounds found the same class of defect: the gate was a second model of "does this title present an agent", poorer than the model its callers use. Round 1 missed an evidence class, round 2 missed four whole ROUTES in a different function. A third patch would have bought the fifth route. getAgentLabel mints identity through routes the evidence parser cannot express, because titleHasAgentName asks whether a name occurs ANYWHERE and never where. One Codex branch therefore serves both 'codex working' and the mid-sentence mention this change exists to reject. Route identity cannot separate them; the gate genuinely needs position information the resolver discards. So the models cannot be collapsed. What they can be is one GRAMMAR plus an enforced agreement: agentForIdentityFrame generalizes what isClaudeIdentityFrameSegment has always described — a name, an optional status word, an optional '- action required' — which was agent-neutral in shape and applied only to Claude. That was the same one-way-guard mistake as the original bug, one level down. Generalized, it covers every minting route: bare names, 'Codex: ...', em-dash frames, name plus status, Windows launcher forms, '|'-headed titles and Pi's native format. The positional and vendor-marker rules fold into it rather than stacking beside it. terminal-title-pane-claim-corpus.test.ts is the enforcement. It sweeps the whole classification corpus, and any title getAgentLabel still mints an identity from must remain able to claim a pane. Shared code cannot enforce this because the two models legitimately differ; the corpus can, and it names the offending title when a future route is added. Corpus: 14 lost claims before this commit, 0 after. One intended gain, 'claude.bat working', listed and explained in the ratchet: the old gate accepted 'aider.ps1 ready' for aider and refused the identical form for Claude purely because the guard was Claude-only. * fix(agents): stop reading a wrapper prefix as an identity frame The head grammar treated '|' as a separator an agent puts after its own name. Wrappers PREFIX the pane's title and the innermost title comes LAST (terminal-title-wrapper-segments.ts), so a name before a '|' is a tmux window or an ssh host, never the pane. `claude | zsh` on a Codex-owned pane therefore handed it to Claude through all three consumers — this change's own bug, inverted. Wrapper-joined titles already reach the real pane title through getEvidenceTitleSegments' suffix splitting, which is what actually resolves `ssh host | opencode ready`; the separator was not carrying that case. '>' goes too. Pi's native `π > session` is matched before the head grammar runs, so the only titles it affected were `name > text` breadcrumbs, which are closer to task text than to an identity frame. That narrows `⠋ gemini > next steps` and `⠋ codex > plan` relative to the previous gate; both are pinned as intended rejections, since refusing leaves the pane with its known owner, which is the safe direction on ambiguous evidence. The corpus gain assertion is renamed to say it is corpus-scoped. Over every string literal in the repo the change gains more than the one title this 67-title corpus can see, mostly the same Claude symmetry, and the test should not read as a global guarantee. Near-misses the grammar deliberately accepts are pinned too, so the boundary is visible and moving it requires editing a list. * fix(agents): admit identity frames before trusting terminal titles * fix(agents): separate title identity from readiness policy * Preserve title frame boundaries and normalization identity * Preserve activity token whitespace at title wrapper boundaries * test: expose typed mailbox idle evidence source * test: scope title projection work contract to renderer * test: distinguish task activity from agent identity --------- Co-authored-by: Merge Sim <sim@local>