refactor(orchestration): declare which flag names the caller on each spec and refuse at the CLI entry

Each handler hand-classified its --from/--terminal as the caller or a target, and the refusal of a
conflicting caller flag ran inside the caller resolver plus two standalone calls for --run listings,
so a new verb that read its flag raw would pass a sibling's handle to a pre-session host. Specs now
declare identityFlagRoles, the CLI entry refuses a conflicting caller flag once from the spec, the
resolver only applies the id-wins rule, and a test fails any orchestration verb that accepts --from
or --terminal without classifying it.
This commit is contained in:
Brennan Benson
2026-09-25 18:41:25 -07:00
parent 6314fc57de
commit 1729a904cf
11 changed files with 206 additions and 95 deletions
+5
View File
@@ -15,8 +15,13 @@ export type CommandSpec = {
positionalArgs?: string[]
examples?: string[]
notes?: string[]
// Why: `--from`/`--terminal` names either the acting caller or a target, and only the spec can
// say which. An agent session refuses a caller flag naming anyone else before dispatch.
identityFlagRoles?: Partial<Record<IdentityFlag, 'caller' | 'target'>>
}
export type IdentityFlag = 'from' | 'terminal'
export function specPaths(spec: CommandSpec): string[][] {
return spec.aliases ? [spec.path, ...spec.aliases] : [spec.path]
}
@@ -5,7 +5,8 @@ import { RuntimeClientError } from '../../runtime-client'
import { orchestrationMigrationData } from '../../../shared/orchestration-rpc-contract'
import { callOrchestrationMutation } from './mutation-request'
import { isDevCliInvocation } from './runtime-compatibility'
import { injectedSessionAddress, resolveCoordinatorTerminalHandle } from './terminal-identity'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { injectedSessionAddress } from '../../session-caller-flags'
export const ORCHESTRATION_DISPATCH_HANDLER: Record<string, CommandHandler> = {
'orchestration dispatch': async ({ flags, client, cwd, json }) => {
@@ -2,10 +2,7 @@ import type { CommandHandler } from '../../dispatch'
import { printResult } from '../../format'
import { getOptionalJsonFlag, getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { callOrchestrationMutation } from './mutation-request'
import {
refuseConflictingSessionCaller,
resolveCoordinatorTerminalHandle
} from './terminal-identity'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
'orchestration gate-create': async ({ flags, client, cwd, json }) => {
@@ -40,9 +37,7 @@ export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
'orchestration gate-list': async ({ flags, client, cwd, json }) => {
const run = getOptionalStringFlag(flags, 'run')
// Why: named runs remain inspectable without a pane; only implicit runs resolve identity.
const from = run
? refuseConflictingSessionCaller(flags, 'from')
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const from = run ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
gates: { id: string; task_id: string; question: string; status: string }[]
count: number
@@ -4,10 +4,7 @@ import { getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { abbreviateOrchestrationTasks } from '../../../shared/orchestration-task-summary'
import { callOrchestrationMutation } from './mutation-request'
import {
refuseConflictingSessionCaller,
resolveCoordinatorTerminalHandle
} from './terminal-identity'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
const TASK_STATUS_VALUES = [
'pending',
@@ -42,7 +39,7 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
const brief = flags.has('brief')
const run = getOptionalStringFlag(flags, 'run')
const callerTerminalHandle = run
? refuseConflictingSessionCaller(flags, 'from')
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
tasks: {
@@ -4,8 +4,7 @@ import { RuntimeClientError } from '../../runtime-client'
import { getTerminalHandle } from '../../selectors'
import { hasStructuredSessionMarker } from '../../../shared/structured-session-marker'
import { readInjectedAgentSessionId } from '../../../shared/agent-session-caller-env'
import { normalizeOrchestrationActor } from '../../../shared/orchestration-actor'
import { isStructuredWorkerHandle } from '../../../shared/structured-worker-handle'
import { injectedSessionAddress } from '../../session-caller-flags'
/**
* The caller's terminal handle, or `undefined` when an injected agent session id names the caller:
@@ -19,7 +18,8 @@ export async function resolveOrchestrationTerminalHandle(
flagName: 'from' | 'terminal',
options: { validateEnvHandle?: boolean } = {}
): Promise<string | undefined> {
if (resolveInjectedSessionCaller(flags, flagName)) {
// A caller flag naming anyone else was already refused at the CLI entry, from the command's spec.
if (readInjectedAgentSessionId()) {
return undefined
}
const explicit = getOptionalStringFlag(flags, flagName)
@@ -168,61 +168,6 @@ function getClientErrorMessage(err: unknown): string | undefined {
return typeof message === 'string' ? message : undefined
}
/**
* The injected session id when this command runs as an agent session. The id wins over every other
* identity this process carries; a caller flag may restate that same session but never name
* another, and a conflicting one is refused here, before any request is sent.
*/
function resolveInjectedSessionCaller(
flags: Map<string, string | boolean>,
flagName: 'from' | 'terminal'
): string | undefined {
const sessionId = readInjectedAgentSessionId()
if (!sessionId) {
return undefined
}
const declared = getOptionalStringFlag(flags, flagName)
if (declared !== undefined && !namesInjectedSession(declared, sessionId)) {
throw new RuntimeClientError(
'consumer_fenced',
`This command runs as agent session ${sessionId}, so --${flagName} ${declared} would act as a ` +
`different caller. Drop --${flagName}: this session's orchestration commands already act as ` +
`session:${sessionId}. No request was sent.`
)
}
return sessionId
}
/**
* For a listing scoped by `--run`, which needs no caller: a session still refuses a caller flag
* naming someone else rather than dropping it.
*/
export function refuseConflictingSessionCaller(
flags: Map<string, string | boolean>,
flagName: 'from' | 'terminal'
): undefined {
resolveInjectedSessionCaller(flags, flagName)
return undefined
}
/** The session's own spellings, plus the handle a structured worker session was minted. */
function namesInjectedSession(value: string, sessionId: string): boolean {
return normalizeOrchestrationActor(value)?.id === sessionId || value === injectedSessionAddress()
}
/**
* The address the host gives this session: a structured worker keeps the handle it was minted, any
* other session is `session:<id>`. Only for text that must match what the host writes.
*/
export function injectedSessionAddress(): string | undefined {
const sessionId = readInjectedAgentSessionId()
if (!sessionId) {
return undefined
}
const ownHandle = process.env.ORCA_TERMINAL_HANDLE
return isStructuredWorkerHandle(ownHandle) ? ownHandle : `session:${sessionId}`
}
/** How check output names its caller: the handle, or the session's address. */
export function orchestrationCallerLabel(handle: string | undefined): string {
return handle ?? injectedSessionAddress() ?? 'unknown'
+19
View File
@@ -85,6 +85,25 @@ describe('orca cli worktree awareness', () => {
expect(logSpy).toHaveBeenCalledWith('Sent 2 messages to 2 recipients')
})
it("refuses an agent session's caller flag naming another actor before any request", async () => {
// One chokepoint for every verb: the spec says which flag names the caller.
process.env.ORCA_AGENT_SESSION_ID = 'f7a1c0de-1111-4222-8333-444455556666'
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
try {
await main(['orchestration', 'check', '--terminal', 'term_sibling', '--json'], '/tmp/repo')
} finally {
delete process.env.ORCA_AGENT_SESSION_ID
}
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: { code: 'consumer_fenced' }
})
process.exitCode = undefined
})
it('rejects no-flag orchestration reset before calling the runtime', async () => {
await main(['orchestration', 'reset'], '/tmp/repo')
+5
View File
@@ -21,6 +21,7 @@ import type { RuntimeClient } from './runtime-client'
import { COMMAND_SPECS } from './specs'
import { resolveOrchestrationCliExecutable } from './runtime/orchestration-recovery-command'
import { runAsSessionCli } from './session-cli-reexec'
import { refuseConflictingSessionCallerFlags } from './session-caller-flags'
export { COMMAND_SPECS } from './specs'
export { buildCurrentWorktreeSelector, normalizeWorktreeSelector } from './selectors'
@@ -111,6 +112,10 @@ export async function main(
// lookup so users do not get misleading "Orca is not running" failures for
// simple command typos or unsupported flags.
validateCommandAndFlags(COMMAND_SPECS, parsed)
refuseConflictingSessionCallerFlags(
findCommandSpec(COMMAND_SPECS, parsed.commandPath),
parsed.flags
)
const RuntimeClientClass = await loadRuntimeClientClass()
const ignoreRemoteSelection = shouldIgnoreRemoteSelection(parsed.commandPath)
const pairingCode = ignoreRemoteSelection ? null : parsed.flags.get('pairing-code')
@@ -19,6 +19,9 @@ vi.mock('./format', () => ({ printResult: vi.fn() }))
vi.mock('./selectors', () => ({ getTerminalHandle: getTerminalHandleMock }))
import { ORCHESTRATION_HANDLERS } from './handlers/orchestration'
import { findCommandSpec } from './args'
import { COMMAND_SPECS } from './specs'
import { refuseConflictingSessionCallerFlags } from './session-caller-flags'
import { createOrchestrationCompatibilityEnvelope } from './runtime/orchestration-compatibility-envelope'
import { formatCliError, reportCliError } from './cli-error'
import { RuntimeRpcFailureError } from './runtime/types'
@@ -171,10 +174,27 @@ const CALLER_VERBS: Verb[] = [
{ command: 'worker-list', flags: {}, method: 'runCurrent', callerParam: 'from' }
]
/** Enough flags for any verb to get past its own validation to identity resolution. */
const EVERY_REQUIRED_FLAG = {
objective: 'o',
id: 'id_1',
task: 'task_1',
spec: 's',
question: 'q',
resolution: 'r',
subject: 's',
body: 'b',
to: 'term_worker',
status: 'completed',
preamble: true,
request: 'req_1'
} as const
function flagMap(flags: Record<string, string | true>): Map<string, string | boolean> {
return new Map(Object.entries(flags))
}
/** What `main()` does between parsing and dispatch: the spec-driven caller check, then the handler. */
async function invoke(
command: string,
flags: Map<string, string | boolean>,
@@ -184,6 +204,10 @@ async function invoke(
if (!handler) {
throw new Error(`no handler for ${command}`)
}
refuseConflictingSessionCallerFlags(
findCommandSpec(COMMAND_SPECS, ['orchestration', command]),
flags
)
await handler({
flags,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers read only `call`; RuntimeClient is a class, so a structural double cannot satisfy it without the cast.
@@ -488,23 +512,62 @@ describe('the orchestration envelope', () => {
})
})
describe('every orchestration verb, enumerated', () => {
/** Enough flags for any verb to get past its own validation to identity resolution. */
const EVERY_REQUIRED_FLAG = {
objective: 'o',
id: 'id_1',
task: 'task_1',
spec: 's',
question: 'q',
resolution: 'r',
subject: 's',
body: 'b',
to: 'term_worker',
status: 'completed',
preamble: true,
request: 'req_1'
} as const
describe('which flag names the caller, declared on every spec', () => {
const ORCHESTRATION_SPECS = COMMAND_SPECS.filter((spec) => spec.path[0] === 'orchestration')
it('classifies every --from and --terminal an orchestration verb accepts', () => {
// A new verb cannot take either flag without saying whether it names the caller, so the entry
// check covers it by construction instead of each handler remembering to refuse.
const unclassified = ORCHESTRATION_SPECS.flatMap((spec) =>
(['from', 'terminal'] as const)
.filter((flag) => spec.allowedFlags.includes(flag) && !spec.identityFlagRoles?.[flag])
.map((flag) => `${spec.path.join(' ')} --${flag}`)
)
expect(unclassified).toEqual([])
})
const callerFlagVerbs = ORCHESTRATION_SPECS.flatMap((spec) =>
(['from', 'terminal'] as const)
.filter((flag) => spec.identityFlagRoles?.[flag] === 'caller')
.map((flag) => ({ command: spec.path[1] ?? '', flag }))
)
it('covers the verbs whose requests name a caller', () => {
expect(callerFlagVerbs.length).toBeGreaterThanOrEqual(CALLER_VERBS.length)
})
it.each(callerFlagVerbs)(
'$command refuses --$flag naming another actor, before any request',
async ({ command, flag }) => {
asSessionInTerminalView()
await expect(
invoke(command, flagMap({ ...EVERY_REQUIRED_FLAG, [flag]: 'term_sibling' }))
).rejects.toMatchObject({ code: 'consumer_fenced' })
expect(callMock).not.toHaveBeenCalled()
expect(getTerminalHandleMock).not.toHaveBeenCalled()
}
)
it.each(
ORCHESTRATION_SPECS.flatMap((spec) =>
(['from', 'terminal'] as const)
.filter((flag) => spec.identityFlagRoles?.[flag] === 'target')
.map((flag) => ({ command: spec.path[1] ?? '', flag }))
)
)('$command passes a --$flag target through unfenced', async ({ command, flag }) => {
asSessionInTerminalView()
await invoke(command, flagMap({ ...EVERY_REQUIRED_FLAG, [flag]: 'term_sibling' })).catch(
(error: unknown) => {
expect(error).not.toMatchObject({ code: 'consumer_fenced' })
}
)
expect(callMock.mock.calls.flatMap(([, params]) => Object.values(params ?? {}))).toContain(
'term_sibling'
)
})
})
describe('every orchestration verb, enumerated', () => {
/** Runs every verb once; returns the ones that guessed an implicit terminal. */
async function verbsThatGuess(): Promise<string[]> {
const guessed: string[] = []
+63
View File
@@ -0,0 +1,63 @@
/**
* An agent session is its own orchestration caller. A flag that names the caller may restate that
* session, but one naming anyone else is refused before any handler runs — never dropped, never
* allowed to win. Against a host that predates session callers this is the only guard: such a host
* would honor the flag as the caller, which is how a chat consumed a sibling's mail (#21097).
*
* Whether `--from`/`--terminal` names the caller or a target is declared on the command's spec
* (`identityFlagRoles`) and checked once, here, at the CLI entry, so a handler cannot forget it.
*/
import type { CommandSpec, IdentityFlag } from './command-spec'
import { RuntimeClientError } from './runtime/types'
import { readInjectedAgentSessionId } from '../shared/agent-session-caller-env'
import { normalizeOrchestrationActor } from '../shared/orchestration-actor'
import { isStructuredWorkerHandle } from '../shared/structured-worker-handle'
export function refuseConflictingSessionCallerFlags(
spec: CommandSpec | undefined,
flags: ReadonlyMap<string, string | boolean>,
env: NodeJS.ProcessEnv = process.env
): void {
const sessionId = readInjectedAgentSessionId(env)
if (!sessionId || !spec?.identityFlagRoles) {
return
}
for (const flagName of IDENTITY_FLAGS) {
const declared = flags.get(flagName)
if (
spec.identityFlagRoles[flagName] === 'caller' &&
typeof declared === 'string' &&
!namesInjectedSession(declared, sessionId, env)
) {
throw new RuntimeClientError(
'consumer_fenced',
`This command runs as agent session ${sessionId}, so --${flagName} ${declared} would act as a ` +
`different caller. Drop --${flagName}: this session's orchestration commands already act as ` +
`session:${sessionId}. No request was sent.`
)
}
}
}
const IDENTITY_FLAGS: readonly IdentityFlag[] = ['from', 'terminal']
/** The session's own spellings, plus the handle a structured worker session was minted. */
function namesInjectedSession(value: string, sessionId: string, env: NodeJS.ProcessEnv): boolean {
return (
normalizeOrchestrationActor(value)?.id === sessionId || value === injectedSessionAddress(env)
)
}
/**
* The address the host gives this session: a structured worker keeps the handle it was minted, any
* other session is `session:<id>`. Only for text that must match what the host writes.
*/
export function injectedSessionAddress(env: NodeJS.ProcessEnv = process.env): string | undefined {
const sessionId = readInjectedAgentSessionId(env)
if (!sessionId) {
return undefined
}
const ownHandle = env.ORCA_TERMINAL_HANDLE
return isStructuredWorkerHandle(ownHandle) ? ownHandle : `session:${sessionId}`
}
@@ -31,6 +31,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [
'from',
'retry-request'
],
identityFlagRoles: { from: 'caller', terminal: 'target' },
notes: [
'Current and existing worktrees never rerun setup; a fresh agent terminal is created unless --terminal is explicit.',
'When reusing --terminal, pass --worktree for that terminal; current means the coordinator worktree.',
+25 -8
View File
@@ -9,6 +9,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
usage:
'orca orchestration run-create --objective <text> [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'objective', 'from', 'retry-request'],
identityFlagRoles: { from: 'caller' },
notes: [
'A Run is a namespace and home inbox. It never schedules or places workers.',
'--retry-request is only for exact recovery after an unknown mutation result.'
@@ -20,6 +21,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
usage:
'orca orchestration run-use --id <run_id> [--from <handle>] [--takeover-legacy] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'from', 'takeover-legacy', 'retry-request'],
identityFlagRoles: { from: 'caller' },
notes: [
'--takeover-legacy must run in the live coordinator agent terminal it binds; it preserves existing worker assignments.'
]
@@ -28,7 +30,8 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
path: ['orchestration', 'run-current'],
summary: 'Show the Run bound to this coordinator terminal',
usage: 'orca orchestration run-current [--from <handle>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'from']
allowedFlags: [...GLOBAL_FLAGS, 'from'],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'run-list'],
@@ -67,6 +70,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'report-path',
'phase'
],
identityFlagRoles: { from: 'caller' },
notes: [
'Valid --type values: status, dispatch, worker_done, merge_ready, escalation, handoff, decision_gate, question, heartbeat.',
'To answer a worker question, use orchestration reply --id <msg_id> --body <text> with the same Orca CLI executable.',
@@ -109,6 +113,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'timeout-ms',
'retry-request'
],
identityFlagRoles: { terminal: 'caller' },
notes: [
'On Windows PowerShell, quote comma-separated type filters, e.g. --types "worker_done,escalation".',
'--types is the wake condition for --wait; a returned Delivery is always the whole FIFO batch, so it is never filtered by type. Without --wait it has no effect on consuming checks. Only --peek and --all filter their rows.',
@@ -121,13 +126,15 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
summary: 'Reply to a message',
usage:
'orca orchestration reply --id <msg_id> --body <text> [--run <run_id>] [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'body', 'run', 'from', 'retry-request']
allowedFlags: [...GLOBAL_FLAGS, 'id', 'body', 'run', 'from', 'retry-request'],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'inbox'],
summary: 'Show messages across (or for) recipients',
usage: 'orca orchestration inbox [--limit <n>] [--terminal <handle>] [--full] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'limit', 'terminal', 'full']
allowedFlags: [...GLOBAL_FLAGS, 'limit', 'terminal', 'full'],
identityFlagRoles: { terminal: 'target' }
},
{
path: ['orchestration', 'task-create'],
@@ -144,7 +151,8 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'run',
'from',
'retry-request'
]
],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'task-list'],
@@ -152,6 +160,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
usage:
'orca orchestration task-list [--status <status>] [--ready] [--brief] [--run <run_id>] [--from <handle>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'status', 'ready', 'brief', 'run', 'from'],
identityFlagRoles: { from: 'caller' },
notes: ['--brief collapses whitespace and caps each spec at 160 characters.']
},
{
@@ -160,6 +169,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
usage:
'orca orchestration task-update --id <task_id> --status <status> [--result <json>] [--run <run_id>] [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'status', 'result', 'run', 'from', 'retry-request'],
identityFlagRoles: { from: 'caller' },
notes: ['Valid --status values: pending, ready, dispatched, completed, failed, blocked.']
},
...ORCHESTRATION_WORKER_COMMAND_SPECS,
@@ -178,7 +188,8 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'dry-run',
'return-preamble',
'retry-request'
]
],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'request-show'],
@@ -196,7 +207,8 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
summary: 'Show dispatch context for a task',
usage:
'orca orchestration dispatch-show --task <task_id> [--preamble] [--from <handle>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'task', 'preamble', 'from']
allowedFlags: [...GLOBAL_FLAGS, 'task', 'preamble', 'from'],
identityFlagRoles: { from: 'target' }
},
{
path: ['orchestration', 'ask'],
@@ -215,6 +227,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'from',
'retry-request'
],
identityFlagRoles: { from: 'caller' },
notes: [
'From an active Dispatch, a new question defaults to its owning Run mailbox.',
'Timeout leaves the question pending; resume with the original message ID.'
@@ -234,6 +247,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'max-concurrent',
'worktree'
],
identityFlagRoles: { from: 'caller' },
notes: [
'This command performs no effects and returns the exact `skills get orchestration --full` recovery action.',
'Use the lightweight Run, Task, and worker-start primitives described by the current skill.'
@@ -254,14 +268,16 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
summary: 'Create a decision gate blocking a task',
usage:
'orca orchestration gate-create --task <task_id> --question <text> [--options <json_array>] [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'task', 'question', 'options', 'from', 'retry-request']
allowedFlags: [...GLOBAL_FLAGS, 'task', 'question', 'options', 'from', 'retry-request'],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'gate-resolve'],
summary: 'Resolve a pending decision gate',
usage:
'orca orchestration gate-resolve --id <gate_id> --resolution <text> [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'resolution', 'from', 'retry-request']
allowedFlags: [...GLOBAL_FLAGS, 'id', 'resolution', 'from', 'retry-request'],
identityFlagRoles: { from: 'caller' }
},
{
path: ['orchestration', 'gate-list'],
@@ -269,6 +285,7 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
usage:
'orca orchestration gate-list [--task <task_id>] [--status <status>] [--run <run_id>] [--from <handle>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'task', 'status', 'run', 'from'],
identityFlagRoles: { from: 'caller' },
notes: ['--run inspects a named Run without binding; otherwise gates are scoped to the caller.']
},
{