Files
orca/src/cli/vocabulary-policy.ts
e2b4bc2c2c feat(cli): make the CLI self-correcting and self-describing for agents (#6303)
* feat(cli): make the CLI self-correcting and self-describing for agents

Agents build a generalized model of how CLIs work and apply it to every
tool. When orca diverged — `rm` where git uses `remove` — a reasonable
first guess (`orca worktree remove`) dead-ended on a bare "Unknown
command" with no path forward. This makes the CLI degrade gracefully when
the orca-cli skill isn't loaded in context.

- First-class CommandSpec.aliases, resolved to the canonical path before
  dispatch (no new handler registrations). `worktree remove`/`delete` now
  resolve to `rm`; the ad-hoc `terminal focus` duplicate spec/handler is
  migrated onto the mechanism.
- Did-you-mean suggestions on unknown commands and unknown flags, ranked
  by edit distance over the live registry, surfaced in both stderr and
  --json error.data (reusing the existing nextSteps channel).
- `orca agent-context [--json]`: a versioned, machine-readable dump of the
  command schema. Pure local read (no RPC), so it works over SSH and when
  the app isn't running.
- CI guards: specs<->handlers parity, and a vocabulary policy that fails
  on new off-policy deletion/read verbs (existing ones grandfathered).

* Address PR review feedback (#6303)

- agent-context now emits each command's effective flag set (globals +
  conditional --page), not just allowedFlags, so the schema no longer
  under-reports --json/--help. Shared as effectiveAllowedFlags() between
  validation and the schema.
- Collision check now covers alias paths too, so a duplicate alias that
  would silently shadow a real command fails the build.

* fix(cli): harden agent recovery and introspection

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-10 19:17:01 -07:00

74 lines
2.0 KiB
TypeScript

import type { CommandSpec } from './args'
// Why: predictable verbs prevent failed agent guesses; existing divergences stay
// grandfathered because renaming them would break compatibility.
type VerbFamily = {
name: string
offPolicyVerbs: Set<string>
canonical: string
allowlist: Set<string>
}
const FAMILIES: VerbFamily[] = [
{
name: 'deletion',
offPolicyVerbs: new Set(['remove', 'delete', 'destroy']),
canonical: 'rm',
allowlist: new Set([
'cookie delete',
'tab profile delete',
'automations remove',
'linear label remove'
])
},
{
name: 'single-item read',
offPolicyVerbs: new Set(['get']),
canonical: 'show',
allowlist: new Set(['get', 'cookie get', 'storage local get', 'storage session get'])
}
]
export type VocabularyViolation = {
command: string
verb: string
family: string
canonical: string
}
function terminalVerb(path: string[]): string {
return path.at(-1) ?? ''
}
function hasCanonicalAlias(spec: CommandSpec, canonical: string): boolean {
const expected = [...spec.path.slice(0, -1), canonical]
return (spec.aliases ?? []).some(
(alias) =>
alias.length === expected.length && alias.every((part, index) => part === expected[index])
)
}
export function findVocabularyViolations(specs: CommandSpec[]): VocabularyViolation[] {
const violations: VocabularyViolation[] = []
for (const spec of specs) {
const command = spec.path.join(' ')
const verb = terminalVerb(spec.path)
for (const family of FAMILIES) {
if (!family.offPolicyVerbs.has(verb)) {
continue
}
if (family.allowlist.has(command)) {
continue
}
// Why: an alias on the canonical verb makes the command reachable the
// canonical way, which satisfies the policy without a rename.
if (hasCanonicalAlias(spec, family.canonical)) {
continue
}
violations.push({ command, verb, family: family.name, canonical: family.canonical })
}
}
return violations
}