mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.']
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user