mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
* feat(agent-status): add an order-independent title evidence parser
The chain this will replace is a first-match-wins scan of substring predicates, so its answer
is decided by list position rather than by how strong the evidence is. Four real recorded Grok
panes read as Codex today because Codex is checked first and their task text mentions it.
Reordering cannot fix that: whichever branch is hoisted, some other pair breaks.
collectAgentTitleEvidence collects every signal first and ranks by class afterwards:
vendor marker — a sigil or control sequence the agent itself emits; task text cannot forge it
anchored name — a name where a grammar reserves the position for identity (Orca's `- <agent>`
owner suffix, or the whole undecorated remainder)
free-text name — a name anywhere else
Anchored beats vendor marker, so `✳ agy` is Antigravity rather than Claude. Free text never
beats either, and never becomes identity on its own even as the only name present — an absent
icon is recoverable, a confidently wrong one is not. Conflicts within a class resolve to null.
No consumer imports this yet. Swapping getAgentLabel in place would move ~20 call sites at
once; each migrates behind the resolver, with its own evidence.
Measured against the live recorded-title corpus (747 distinct titles), 598 agree with the
current chain and 149 differ:
144 claude -> null spinner-only titles. Braille and quarter-circle frames are emitted by
many agents, so they prove the pane is busy and nothing about who it
is. These are answered by stronger signals once the resolver lands,
which is why the parser is not wired up on its own.
4 codex -> grok the misattributed Grok panes, fixed.
1 codex -> null a spinner-prefixed Claude pane whose task text names Codex.
Three parser defects were found and fixed by reviewing that delta rather than reasoning about
it: the OpenCode envelope was treated as a vetoable marker instead of an owning grammar, free
text was allowed to veto a vendor marker (which blinded 13 real Claude titles), and the owner
suffix matched the tail of a hyphenated worktree name (`review-14600-codex`).
* fix(agent-status): harden title evidence collection
* fix(agent-status): limit emitted display evidence
* fix(agent-status): anchor explicit title evidence
* test(agent-status): cover anchored token filtering
* fix(agent-status): complete explicit marker evidence
* fix(agent-status): recognize reserved owner ids
* fix(agent-status): reject cwd path titles
* fix(agent-status): avoid bare-name identity guesses
* fix(agent-status): require spinner for working labels
* fix(agent-status): honor opted-out synthetic profiles
* fix(agent-status): harden wrapper evidence boundaries
356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
GEMINI_IDLE,
|
|
GEMINI_PERMISSION,
|
|
GEMINI_SILENT_WORKING,
|
|
GEMINI_WORKING
|
|
} from './agent-title-core'
|
|
import { collectAgentTitleEvidence } from './agent-title-evidence'
|
|
|
|
const agentFor = (title: string) => collectAgentTitleEvidence(title).agent
|
|
const reasonFor = (title: string) => collectAgentTitleEvidence(title).reason
|
|
|
|
describe('collectAgentTitleEvidence', () => {
|
|
describe('an anchored name outranks a name in task text', () => {
|
|
// Minimized from real recorded titles that resolve to the wrong agent on the ordered chain:
|
|
// the pane owner is named by Orca's `- <agent>` suffix, the competitor only by task text.
|
|
it.each([
|
|
'Switch Claude and Codex off the load balancer… - grok',
|
|
'Codex structured chat revalidation… - grok',
|
|
'⠸ - Thinking - Codex native-chat work… - grok',
|
|
'Electron QA: check the Gemini label… - grok'
|
|
])('resolves %j to the suffix owner', (title) => {
|
|
expect(agentFor(title)).toBe('grok')
|
|
})
|
|
|
|
it('does not read a hyphenated worktree name as an owner suffix', () => {
|
|
// `review-14600-codex` is a directory, not an owner declaration. The suffix grammar
|
|
// requires whitespace before the dash precisely to keep these apart.
|
|
expect(agentFor('review-14600-codex')).toBeNull()
|
|
expect(agentFor('codex-split-core')).toBeNull()
|
|
})
|
|
|
|
it.each(['pi', 'omp', 'claude-agent-teams', 'qwen-code'] as const)(
|
|
'recognizes the reserved owner id %s',
|
|
(agent) => {
|
|
expect(agentFor(`Review another agent… - ${agent}`)).toBe(agent)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('order independence', () => {
|
|
// The defect this replaces is that chain position decides between two names. Swapping the
|
|
// two names in a title must not change the answer.
|
|
it.each([
|
|
['codex', 'grok'],
|
|
['gemini', 'antigravity'],
|
|
['copilot', 'devin'],
|
|
['claude', 'cursor']
|
|
])('gives %s + %s the same answer in both orders', (a, b) => {
|
|
const forward = collectAgentTitleEvidence(`${a} and ${b}`)
|
|
const reverse = collectAgentTitleEvidence(`${b} and ${a}`)
|
|
expect(forward.agent).toBe(reverse.agent)
|
|
expect(forward.agent).toBeNull()
|
|
expect([...forward.freeTextNames].sort()).toEqual([a, b].sort())
|
|
expect([...reverse.freeTextNames].sort()).toEqual([a, b].sort())
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['claude', 'claude'],
|
|
['openclaude', 'openclaude'],
|
|
['codex', 'codex'],
|
|
['copilot', 'copilot'],
|
|
['cursor', 'cursor'],
|
|
['gemini', 'gemini'],
|
|
['antigravity', 'antigravity'],
|
|
['opencode', 'opencode'],
|
|
['mimo', 'mimo-code'],
|
|
['openclaw', 'openclaw'],
|
|
['aider', 'aider'],
|
|
['grok', 'grok'],
|
|
['devin', 'devin']
|
|
] as const)('collects the free-text token %s without claiming identity', (token, agent) => {
|
|
expect(collectAgentTitleEvidence(`review the ${token} integration`)).toMatchObject({
|
|
agent: null,
|
|
reason: 'free-text-only',
|
|
freeTextNames: [agent]
|
|
})
|
|
})
|
|
|
|
describe('a vendor marker is evidence the agent emitted, not text a human typed', () => {
|
|
it('keeps a Claude pane Claude when its task text names another agent', () => {
|
|
// 13 recorded titles have this shape. The sigil is emitted by Claude; the name is typed.
|
|
expect(agentFor('✳ Fix Codex false attention notifications on Windows')).toBe('claude')
|
|
expect(reasonFor('✳ Consolidate Codex subagent sidebar rows')).toBe('vendor-marker')
|
|
})
|
|
|
|
it('lets an anchored name outrank a foreign vendor marker', () => {
|
|
expect(agentFor('✳ agy')).toBe('claude')
|
|
expect(reasonFor('✳ agy')).toBe('vendor-marker')
|
|
expect(agentFor('✳ codex')).toBe('claude')
|
|
expect(reasonFor('✳ codex')).toBe('vendor-marker')
|
|
})
|
|
|
|
it('keeps an OpenCode envelope OpenCode when its session text names another agent', () => {
|
|
expect(agentFor('OC | QA PR #14582 Cursor sidecar SSH arms')).toBe('opencode')
|
|
})
|
|
})
|
|
|
|
describe('Antigravity model names are metadata, not identity', () => {
|
|
it('reads an identity segment plus a model name as Antigravity', () => {
|
|
expect(agentFor('agy · Gemini 3.7 Flash')).toBe('antigravity')
|
|
expect(agentFor('Antigravity — Gemini 3.7 Flash')).toBe('antigravity')
|
|
})
|
|
|
|
it('declines a bare model name rather than guessing Gemini CLI', () => {
|
|
// No identity segment, no vendor glyph — only a name in free text. Antigravity and Gemini
|
|
// CLI are equally consistent with it, so the title cannot answer.
|
|
expect(agentFor('Gemini 3.7 Flash · high')).toBeNull()
|
|
})
|
|
|
|
it.each([GEMINI_WORKING, GEMINI_SILENT_WORKING, GEMINI_IDLE, GEMINI_PERMISSION])(
|
|
'still resolves the real Gemini marker %s',
|
|
(marker) => {
|
|
expect(agentFor(`${marker} Refactor the parser`)).toBe('gemini')
|
|
}
|
|
)
|
|
|
|
it('does not promote model names in task text', () => {
|
|
expect(agentFor('Compare Antigravity with Gemini 3.7 Flash')).toBeNull()
|
|
expect(agentFor('Compare Antigravity with Gemini 3.7 Flash… - grok')).toBe('grok')
|
|
})
|
|
|
|
it('does not treat a Gemini glyph inside task text as a vendor marker', () => {
|
|
const evidence = collectAgentTitleEvidence('Explain the ✦ marker… - grok')
|
|
expect(evidence.agent).toBe('grok')
|
|
expect(evidence.vendorMarkers).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('a name in free text alone is never identity', () => {
|
|
it.each([
|
|
'◐ DaemonConnectionLostError with 70 Codex agents',
|
|
'Fix the grok hook',
|
|
'Debug the cursor sidecar',
|
|
'grok',
|
|
'⠋ grok',
|
|
'⠋ codex'
|
|
])('declines %j', (title) => {
|
|
expect(agentFor(title)).toBeNull()
|
|
expect(reasonFor(title)).toBe('free-text-only')
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['Claude Code', 'claude'],
|
|
['Gemini CLI', 'gemini'],
|
|
['Claude Agent Teams', 'claude-agent-teams'],
|
|
['MiMo Code', 'mimo-code'],
|
|
['Prime Agent', 'prime-agent'],
|
|
['Command Code', 'command-code'],
|
|
['GitHub Copilot', 'copilot'],
|
|
['Agent Teams', 'claude-agent-teams']
|
|
] as const)('recognizes the emitted whole-title alias %s', (title, agent) => {
|
|
expect(agentFor(title)).toBe(agent)
|
|
expect(reasonFor(title)).toBe('anchored')
|
|
})
|
|
|
|
it.each(['Continue', 'Charm', 'Goose', 'Amp'])(
|
|
'does not treat the UI-only display label %s as identity',
|
|
(title) => {
|
|
expect(agentFor(title)).toBeNull()
|
|
expect(reasonFor(title)).toBe('no-evidence')
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
'~/codex',
|
|
'/grok',
|
|
'.\\openclaude',
|
|
'C:\\codex',
|
|
'C:/codex',
|
|
'~/Codex ready',
|
|
'.\\Cursor ready'
|
|
])('does not treat the cwd path %s as identity', (title) => {
|
|
expect(agentFor(title)).toBeNull()
|
|
})
|
|
|
|
it('does not duplicate an anchored token as free text', () => {
|
|
expect(collectAgentTitleEvidence('codex.exe').freeTextNames).toEqual([])
|
|
expect(collectAgentTitleEvidence('Claude Agent Teams').freeTextNames).toEqual([])
|
|
})
|
|
|
|
it.each([
|
|
['Codex ready', 'codex'],
|
|
['Codex - action required', 'codex'],
|
|
['Cursor ready', 'cursor'],
|
|
['Droid - action required', 'droid'],
|
|
['Hermes ready', 'hermes'],
|
|
['Devin - action required', 'devin'],
|
|
['Pi ready', 'pi'],
|
|
['OMP - action required', 'omp']
|
|
] as const)('recognizes Orca-controlled synthetic title %s', (title, agent) => {
|
|
expect(agentFor(title)).toBe(agent)
|
|
expect(reasonFor(title)).toBe('anchored')
|
|
})
|
|
|
|
it.each(['Droid', 'Hermes', 'Devin'])(
|
|
'does not treat a bare working label as synthetic identity: %s',
|
|
(title) => {
|
|
expect(agentFor(title)).toBeNull()
|
|
expect(reasonFor(title)).toBe('free-text-only')
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
'Claude Code ready',
|
|
'Claude thinking',
|
|
'. Claude Code working',
|
|
'zsh | ⠋ Claude Code - action required'
|
|
])('recognizes the explicit Claude identity frame %s', (title) => {
|
|
expect(agentFor(title)).toBe('claude')
|
|
expect(reasonFor(title)).toBe('anchored')
|
|
})
|
|
|
|
it('does not promote Claude status words in task text', () => {
|
|
expect(agentFor('Fix the Claude Code ready-state parser')).toBeNull()
|
|
expect(agentFor('Fix Claude Code ready behavior… - grok')).toBe('grok')
|
|
})
|
|
|
|
it.each(['. Review the parser', '* Waiting for input'])(
|
|
'recognizes the established Claude status prefix in %s',
|
|
(title) => {
|
|
expect(agentFor(title)).toBe('claude')
|
|
expect(reasonFor(title)).toBe('vendor-marker')
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
['⠋ Cursor Agent', 'cursor'],
|
|
['⠋ Pi idle', 'pi'],
|
|
['⠋ OMP done', 'omp'],
|
|
['⠋ Droid', 'droid'],
|
|
['⠋ Hermes', 'hermes'],
|
|
['⠋ Devin', 'devin']
|
|
] as const)('recognizes the decorated identity frame %s', (title, agent) => {
|
|
expect(agentFor(title)).toBe(agent)
|
|
expect(reasonFor(title)).toBe('anchored')
|
|
})
|
|
|
|
it('does not invent synthetic titles for an opted-out profile', () => {
|
|
expect(agentFor('OpenCode ready')).toBeNull()
|
|
expect(agentFor('⠋ OpenCode')).toBeNull()
|
|
expect(reasonFor('OpenCode ready')).toBe('free-text-only')
|
|
})
|
|
|
|
it('reads identity from the innermost wrapper segment', () => {
|
|
expect(agentFor('zsh | ⠋ Claude Code')).toBe('claude')
|
|
expect(agentFor('ssh | tmux | Cursor Agent')).toBe('cursor')
|
|
expect(agentFor('ssh | tmux | OC | review the parser')).toBe('opencode')
|
|
expect(agentFor('zsh | Fix the Codex parser')).toBeNull()
|
|
})
|
|
|
|
it('bounds wrapper inspection while preserving innermost identity', () => {
|
|
const wrappers = Array.from({ length: 200 }, (_, index) => `wrapper-${index}`).join(' | ')
|
|
expect(agentFor(`${wrappers} | ⠋ Cursor Agent`)).toBe('cursor')
|
|
expect(agentFor(`${wrappers} | OC | review the parser`)).toBe('opencode')
|
|
expect(agentFor(`outer-a | outer-b | OC | ${wrappers} | Cursor Agent`)).toBe('cursor')
|
|
})
|
|
|
|
it.each([
|
|
['codex.exe', 'codex'],
|
|
['openclaude.cmd', 'openclaude'],
|
|
['gemini.ps1', 'gemini'],
|
|
['droid.cmd', 'droid'],
|
|
['hermes.exe', 'hermes'],
|
|
['agy.bat', 'antigravity'],
|
|
['CODEX.EXE', 'codex'],
|
|
['DROID.CMD', 'droid']
|
|
] as const)('recognizes the bare Windows launcher %s', (title, agent) => {
|
|
expect(agentFor(title)).toBe(agent)
|
|
expect(reasonFor(title)).toBe('anchored')
|
|
})
|
|
|
|
it('produces no name evidence for an agent outside the token set', () => {
|
|
// The token set is deliberately narrower than the agent union: short names like `omp` would
|
|
// classify ordinary shell text. Such a title yields no evidence at all rather than a guess.
|
|
expect(reasonFor('Review PR for OMP transcript rendering')).toBe('no-evidence')
|
|
})
|
|
|
|
describe('activity is not identity', () => {
|
|
it.each(['◐ Rebase PR #14624 onto main', '⠂ Fix SSH fallback', '⠋ Thinking'])(
|
|
'declines the spinner-only title %j',
|
|
(title) => {
|
|
// Braille and quarter-circle spinners are emitted by many agents, so they prove the pane
|
|
// is busy and nothing about who it is. Callers that want busy-ness use activity parsing.
|
|
expect(agentFor(title)).toBeNull()
|
|
expect(reasonFor(title)).toBe('no-evidence')
|
|
}
|
|
)
|
|
})
|
|
|
|
it('does not treat an embedded Claude sigil as a vendor marker', () => {
|
|
expect(collectAgentTitleEvidence('task text ✳ decoration')).toEqual({
|
|
vendorMarkers: [],
|
|
anchoredNames: [],
|
|
freeTextNames: [],
|
|
agent: null,
|
|
reason: 'no-evidence'
|
|
})
|
|
})
|
|
|
|
it('recognizes a bare Claude sigil as a vendor marker', () => {
|
|
expect(collectAgentTitleEvidence('✳')).toEqual({
|
|
vendorMarkers: ['claude'],
|
|
anchoredNames: [],
|
|
freeTextNames: [],
|
|
agent: 'claude',
|
|
reason: 'vendor-marker'
|
|
})
|
|
})
|
|
|
|
describe('conflicting evidence of the same class resolves to nothing', () => {
|
|
it('declines two anchored names', () => {
|
|
const evidence = collectAgentTitleEvidence('OC | something… - grok')
|
|
expect(evidence.agent).toBeNull()
|
|
expect(evidence.reason).toBe('conflicting-anchored-names')
|
|
expect([...evidence.anchoredNames].sort()).toEqual(['grok', 'opencode'])
|
|
})
|
|
|
|
it('keeps an anchored conflict ahead of a vendor marker', () => {
|
|
const evidence = collectAgentTitleEvidence('✳ | OC | something… - grok')
|
|
expect(evidence.agent).toBeNull()
|
|
expect(evidence.reason).toBe('conflicting-anchored-names')
|
|
expect([...evidence.anchoredNames].sort()).toEqual(['grok', 'opencode'])
|
|
expect(evidence.vendorMarkers).toEqual(['claude'])
|
|
})
|
|
|
|
it('declines two vendor markers', () => {
|
|
const evidence = collectAgentTitleEvidence('✳ | ✦ two sigils')
|
|
expect(evidence.agent).toBeNull()
|
|
expect(evidence.reason).toBe('conflicting-vendor-markers')
|
|
expect([...evidence.vendorMarkers].sort()).toEqual(['claude', 'gemini'])
|
|
})
|
|
})
|
|
|
|
it('declines a Claude management screen', () => {
|
|
expect(collectAgentTitleEvidence('claude agents')).toEqual({
|
|
vendorMarkers: [],
|
|
anchoredNames: [],
|
|
freeTextNames: [],
|
|
agent: null,
|
|
reason: 'no-evidence'
|
|
})
|
|
})
|
|
|
|
it('requires whitespace before the owner suffix dash', () => {
|
|
expect(agentFor('task- codex')).toBeNull()
|
|
expect(reasonFor('task- codex')).toBe('free-text-only')
|
|
})
|
|
|
|
it('terminates when a wrapper title starts with a separator', () => {
|
|
expect(agentFor(' | ')).toBeNull()
|
|
})
|
|
})
|