@open-agent-toolkit/cli 0.1.69 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +1 -0
  2. package/assets/docs/cli-utilities/configuration.md +34 -0
  3. package/assets/docs/cli-utilities/index.md +2 -1
  4. package/assets/docs/cli-utilities/project-log.md +183 -0
  5. package/assets/docs/cli-utilities/workflow-gates.md +126 -3
  6. package/assets/docs/reference/cli-reference.md +6 -1
  7. package/assets/docs/workflows/projects/dispatch-ceiling.md +14 -5
  8. package/assets/docs/workflows/projects/orchestration-model.md +21 -0
  9. package/assets/docs/workflows/projects/review-flavors.md +9 -0
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-dispatch-subagents/SKILL.md +21 -1
  12. package/assets/skills/oat-dispatch-subagents/references/provider-claude.md +21 -0
  13. package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +16 -0
  14. package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +21 -0
  15. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +6 -2
  16. package/assets/skills/oat-project-complete/SKILL.md +60 -1
  17. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +6 -2
  18. package/assets/skills/oat-project-implement/SKILL.md +18 -2
  19. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +6 -2
  20. package/assets/skills/oat-project-plan-writing/SKILL.md +23 -33
  21. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +6 -2
  22. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +6 -2
  23. package/assets/skills/oat-project-review-provide/SKILL.md +62 -1
  24. package/assets/skills/oat-project-summary/SKILL.md +122 -18
  25. package/assets/templates/project-log.md +47 -0
  26. package/dist/commands/config/index.d.ts.map +1 -1
  27. package/dist/commands/config/index.js +106 -1
  28. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts +3 -0
  29. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts.map +1 -0
  30. package/dist/commands/gate/__fixtures__/fake-runtime.mjs +167 -0
  31. package/dist/commands/gate/activity-probes.d.ts +35 -0
  32. package/dist/commands/gate/activity-probes.d.ts.map +1 -0
  33. package/dist/commands/gate/activity-probes.js +135 -0
  34. package/dist/commands/gate/branch-local-cli.d.ts +31 -0
  35. package/dist/commands/gate/branch-local-cli.d.ts.map +1 -0
  36. package/dist/commands/gate/branch-local-cli.js +116 -0
  37. package/dist/commands/gate/index.d.ts +31 -0
  38. package/dist/commands/gate/index.d.ts.map +1 -1
  39. package/dist/commands/gate/index.js +533 -32
  40. package/dist/commands/gate/route.d.ts +22 -0
  41. package/dist/commands/gate/route.d.ts.map +1 -0
  42. package/dist/commands/gate/route.js +109 -0
  43. package/dist/commands/init/tools/shared/skill-manifest.d.ts +1 -1
  44. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  45. package/dist/commands/init/tools/shared/skill-manifest.js +1 -0
  46. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  47. package/dist/commands/project/dispatch-ceiling/index.js +100 -16
  48. package/dist/commands/project/index.d.ts.map +1 -1
  49. package/dist/commands/project/index.js +2 -0
  50. package/dist/commands/project/log/append.d.ts +41 -0
  51. package/dist/commands/project/log/append.d.ts.map +1 -0
  52. package/dist/commands/project/log/append.js +241 -0
  53. package/dist/commands/project/log/check.d.ts +66 -0
  54. package/dist/commands/project/log/check.d.ts.map +1 -0
  55. package/dist/commands/project/log/check.js +194 -0
  56. package/dist/commands/project/log/grammar.d.ts +28 -0
  57. package/dist/commands/project/log/grammar.d.ts.map +1 -0
  58. package/dist/commands/project/log/grammar.js +31 -0
  59. package/dist/commands/project/log/index.d.ts +9 -0
  60. package/dist/commands/project/log/index.d.ts.map +1 -0
  61. package/dist/commands/project/log/index.js +13 -0
  62. package/dist/commands/project/log/rollup.d.ts +40 -0
  63. package/dist/commands/project/log/rollup.d.ts.map +1 -0
  64. package/dist/commands/project/log/rollup.js +203 -0
  65. package/dist/commands/project/log/synthesize.d.ts +23 -0
  66. package/dist/commands/project/log/synthesize.d.ts.map +1 -0
  67. package/dist/commands/project/log/synthesize.js +112 -0
  68. package/dist/commands/project/new/index.d.ts +1 -0
  69. package/dist/commands/project/new/index.d.ts.map +1 -1
  70. package/dist/commands/project/new/index.js +7 -0
  71. package/dist/commands/project/new/scaffold.d.ts +1 -0
  72. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  73. package/dist/commands/project/new/scaffold.js +24 -0
  74. package/dist/config/oat-config.d.ts +12 -0
  75. package/dist/config/oat-config.d.ts.map +1 -1
  76. package/dist/config/oat-config.js +30 -0
  77. package/dist/config/resolve.d.ts.map +1 -1
  78. package/dist/config/resolve.js +9 -0
  79. package/package.json +2 -2
@@ -1,11 +1,14 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { readdir, readFile } from 'node:fs/promises';
3
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
4
5
  import { basename, isAbsolute, join, relative } from 'node:path';
5
6
  import { buildCommandContext, } from '../../app/command-context.js';
7
+ import { appendProjectLog } from '../project/log/append.js';
6
8
  import { getFrontmatterBlock, parseFrontmatterScalarFields, parseGeneratedTime, } from '../shared/frontmatter.js';
7
9
  import { readGlobalOptions } from '../shared/shared.utils.js';
8
- import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
10
+ import { parseJsonConfig } from '../../config/json.js';
11
+ import { BUILTIN_EXEC_TARGETS, MAX_GATE_TIMEOUT_MS, MIN_GATE_TIMEOUT_MS, isValidGateTimeoutMs, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
9
12
  import { resolveEffectiveConfig, resolveExecTargetViews, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
10
13
  import { dirExists, fileExists } from '../../fs/io.js';
11
14
  import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope, } from '../../fs/paths.js';
@@ -15,7 +18,10 @@ import { resolveIdentityConfidence, } from '../../providers/identity/provenance.
15
18
  import { parseDispatchStamps } from '../../providers/identity/stamp.js';
16
19
  import { Command } from 'commander';
17
20
  import YAML from 'yaml';
21
+ import { createGateActivityProbe, } from './activity-probes.js';
22
+ import { createBranchLocalGateCli, currentGateCliLaunch, readGateRouteReceipt, removeBranchLocalGateCli, } from './branch-local-cli.js';
18
23
  import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
24
+ import { createGateRouteCommand } from './route.js';
19
25
  const DEFAULT_DEPENDENCIES = {
20
26
  buildCommandContext,
21
27
  resolveProjectRoot,
@@ -26,9 +32,17 @@ const DEFAULT_DEPENDENCIES = {
26
32
  readUserConfig,
27
33
  writeUserConfig,
28
34
  resolveEffectiveConfig,
35
+ createGateActivityProbe,
36
+ createBranchLocalGateCli,
37
+ currentGateCliLaunch,
38
+ removeBranchLocalGateCli,
39
+ readGateRouteReceipt,
29
40
  runProcess: runChildProcess,
30
41
  parseReviewGateVerdict,
42
+ appendProjectLog,
31
43
  processEnv: process.env,
44
+ writeGateRunMarker,
45
+ removeGateRunMarker,
32
46
  writeDiagnostic: (message) => process.stderr.write(message),
33
47
  };
34
48
  const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
@@ -61,6 +75,33 @@ const REVIEW_GATE_CONTEXT_NOTE = [
61
75
  const GATE_CHECK_TIMEOUT_MS = 5_000;
62
76
  const GATE_EXEC_TIMEOUT_MS = 15 * 60 * 1_000;
63
77
  const GATE_LIVENESS_INTERVAL_MS = 30_000;
78
+ async function writeGateRunMarker(path, marker, warn) {
79
+ try {
80
+ await mkdir(join(tmpdir(), 'oat-gate-runs'), { recursive: true });
81
+ await writeFile(path, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
82
+ return true;
83
+ }
84
+ catch (error) {
85
+ const detail = error instanceof Error ? error.message : String(error);
86
+ warn(`Unable to write gate run marker ${path}: ${detail}`);
87
+ return false;
88
+ }
89
+ }
90
+ async function removeGateRunMarker(path, warn) {
91
+ try {
92
+ await rm(path);
93
+ }
94
+ catch (error) {
95
+ if (error &&
96
+ typeof error === 'object' &&
97
+ 'code' in error &&
98
+ error.code === 'ENOENT') {
99
+ return;
100
+ }
101
+ const detail = error instanceof Error ? error.message : String(error);
102
+ warn(`Unable to remove gate run marker ${path}: ${detail}`);
103
+ }
104
+ }
64
105
  function reviewGateProjectContext(project) {
65
106
  return [
66
107
  `Resolved OAT project path: ${project.path}. Run the review for this project path.`,
@@ -158,6 +199,7 @@ function buildGateDispatchReport(invocation, scope) {
158
199
  }
159
200
  function gateInvocationPromptContext(invocation) {
160
201
  const frontmatter = YAML.stringify({
202
+ oat_gate_headless: true,
161
203
  oat_gate_run_id: invocation.runId,
162
204
  oat_gate_target: invocation.targetId,
163
205
  oat_gate_runtime: invocation.runtime,
@@ -203,6 +245,12 @@ async function runChildProcess(command, args, options) {
203
245
  let killTimeout = null;
204
246
  let stderrBytes = 0;
205
247
  let stdoutBytes = 0;
248
+ let refusal;
249
+ let stdoutLineBuffer = '';
250
+ let stderrLineBuffer = '';
251
+ let latestActivityEvidence;
252
+ let latestActivityProbeStatus;
253
+ let livenessProbePending = false;
206
254
  const startedAt = Date.now();
207
255
  let lastActivityAt = startedAt;
208
256
  const child = spawn(command, args, {
@@ -215,23 +263,71 @@ async function runChildProcess(command, args, options) {
215
263
  const recordActivity = () => {
216
264
  lastActivityAt = Date.now();
217
265
  };
266
+ const scanChunk = (buffer, chunk) => {
267
+ const combined = buffer + chunk.toString('utf8');
268
+ const lines = combined.split('\n');
269
+ const remainder = lines.pop() ?? '';
270
+ if (!refusal) {
271
+ for (const line of lines) {
272
+ refusal = extractStructuredRefusal(line.replace(/\r$/, ''));
273
+ if (refusal) {
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ return remainder;
279
+ };
218
280
  child.stdout?.on('data', (chunk) => {
219
281
  stdoutBytes += chunk.byteLength;
282
+ stdoutLineBuffer = scanChunk(stdoutLineBuffer, chunk);
220
283
  recordActivity();
221
284
  process.stdout.write(chunk);
222
285
  });
223
286
  child.stderr?.on('data', (chunk) => {
224
287
  stderrBytes += chunk.byteLength;
288
+ stderrLineBuffer = scanChunk(stderrLineBuffer, chunk);
225
289
  recordActivity();
226
290
  process.stderr.write(chunk);
227
291
  });
292
+ const processAlive = () => {
293
+ if (child.pid === undefined)
294
+ return false;
295
+ try {
296
+ process.kill(child.pid, 0);
297
+ return true;
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ };
228
303
  const livenessInterval = options.onLiveness && options.livenessIntervalMs
229
304
  ? setInterval(() => {
305
+ if (livenessProbePending)
306
+ return;
307
+ livenessProbePending = true;
230
308
  const now = Date.now();
231
- options.onLiveness?.({
232
- elapsedMs: now - startedAt,
233
- hardBudgetMs: options.timeoutMs,
234
- idleMs: now - lastActivityAt,
309
+ void (async () => {
310
+ const activityProbeStatus = await options.activityProbe?.observe(now);
311
+ const evidence = activityProbeStatus?.evidence;
312
+ if (activityProbeStatus) {
313
+ latestActivityProbeStatus = activityProbeStatus;
314
+ }
315
+ if (evidence)
316
+ latestActivityEvidence = evidence;
317
+ options.onLiveness?.({
318
+ elapsedMs: now - startedAt,
319
+ hardBudgetMs: options.timeoutMs,
320
+ idleMs: now - lastActivityAt,
321
+ processAlive: processAlive(),
322
+ ...(latestActivityProbeStatus
323
+ ? { activityProbeStatus: latestActivityProbeStatus }
324
+ : {}),
325
+ ...(latestActivityEvidence
326
+ ? { lastActivityEvidence: latestActivityEvidence }
327
+ : {}),
328
+ });
329
+ })().finally(() => {
330
+ livenessProbePending = false;
235
331
  });
236
332
  }, options.livenessIntervalMs)
237
333
  : null;
@@ -263,8 +359,18 @@ async function runChildProcess(command, args, options) {
263
359
  if (killTimeout) {
264
360
  clearTimeout(killTimeout);
265
361
  }
362
+ refusal ??=
363
+ extractStructuredRefusal(stdoutLineBuffer.replace(/\r$/, '')) ??
364
+ extractStructuredRefusal(stderrLineBuffer.replace(/\r$/, ''));
266
365
  resolve({
366
+ ...(latestActivityEvidence
367
+ ? { activityEvidence: latestActivityEvidence }
368
+ : {}),
369
+ ...(latestActivityProbeStatus
370
+ ? { activityProbeStatus: latestActivityProbeStatus }
371
+ : {}),
267
372
  exitCode: timedOut ? 124 : (code ?? 1),
373
+ ...(refusal ? { refusal } : {}),
268
374
  stderrBytes,
269
375
  stdoutBytes,
270
376
  ...(timedOut ? { timedOut: true } : {}),
@@ -272,6 +378,10 @@ async function runChildProcess(command, args, options) {
272
378
  });
273
379
  });
274
380
  }
381
+ function extractStructuredRefusal(output) {
382
+ const match = output.match(/^OAT_GATE_REFUSAL: (.*)$/m);
383
+ return match?.[1]?.replace(/\r$/, '');
384
+ }
275
385
  function isGateWriteLayer(value) {
276
386
  return VALID_WRITE_LAYERS.includes(value);
277
387
  }
@@ -353,17 +463,131 @@ function parseNumericFlag(value, flag, defaultValue) {
353
463
  }
354
464
  return parsed;
355
465
  }
356
- function resolveGateExecTimeoutMs(env) {
357
- const rawValue = env.OAT_GATE_EXEC_TIMEOUT_MS?.trim();
358
- if (!rawValue) {
359
- return GATE_EXEC_TIMEOUT_MS;
360
- }
361
- const parsed = Number(rawValue);
362
- if (!Number.isInteger(parsed) || parsed < 1) {
363
- return GATE_EXEC_TIMEOUT_MS;
466
+ function parseGateTimeoutFlag(value, flag) {
467
+ const parsed = Number(value);
468
+ if (!isValidGateTimeoutMs(parsed)) {
469
+ throw new Error(`${flag} must be an integer between ${MIN_GATE_TIMEOUT_MS} and ${MAX_GATE_TIMEOUT_MS}.`);
364
470
  }
365
471
  return parsed;
366
472
  }
473
+ function resolveGateExecTimeout(input) {
474
+ if (input.cliTimeoutMs !== undefined) {
475
+ return {
476
+ timeoutMs: parseGateTimeoutFlag(input.cliTimeoutMs, '--timeout-ms'),
477
+ source: 'cli',
478
+ };
479
+ }
480
+ const warned = new Set();
481
+ const warnOnce = (key, message) => {
482
+ if (!warned.has(key)) {
483
+ warned.add(key);
484
+ input.warn(message);
485
+ }
486
+ };
487
+ for (const persisted of input.rawPersisted?.target ?? []) {
488
+ if (isValidGateTimeoutMs(persisted.value)) {
489
+ return { timeoutMs: persisted.value, source: 'target' };
490
+ }
491
+ warnOnce(`target:${persisted.layer}`, `Ignoring invalid target.timeoutMs from ${persisted.layer} config; using the next timeout source.`);
492
+ }
493
+ if ((input.rawPersisted?.target.length ?? 0) === 0 &&
494
+ input.target.timeoutMs !== undefined) {
495
+ if (isValidGateTimeoutMs(input.target.timeoutMs)) {
496
+ return { timeoutMs: input.target.timeoutMs, source: 'target' };
497
+ }
498
+ input.warn('Ignoring invalid target.timeoutMs; using the next timeout source.');
499
+ }
500
+ const reviewType = input.reviewType?.trim().toLowerCase();
501
+ if (reviewType === 'code' || reviewType === 'artifact') {
502
+ const key = `workflow.gateTimeouts.${reviewType}`;
503
+ for (const persisted of input.rawPersisted?.workflow ?? []) {
504
+ if (isValidGateTimeoutMs(persisted.value)) {
505
+ return { timeoutMs: persisted.value, source: 'config' };
506
+ }
507
+ warnOnce(`${key}:${persisted.layer}`, `Ignoring invalid ${key} from ${persisted.layer} config; using the next timeout source.`);
508
+ }
509
+ const entry = input.effective.resolved[key];
510
+ if ((input.rawPersisted?.workflow.length ?? 0) === 0 &&
511
+ entry?.value !== null &&
512
+ entry?.value !== undefined) {
513
+ if (isValidGateTimeoutMs(entry.value)) {
514
+ return { timeoutMs: entry.value, source: 'config' };
515
+ }
516
+ warnOnce(`${key}:${entry.source}`, `Ignoring invalid ${key} from ${entry.source}; using the next timeout source.`);
517
+ }
518
+ }
519
+ const envValue = input.env.OAT_GATE_EXEC_TIMEOUT_MS?.trim();
520
+ if (envValue) {
521
+ const parsed = Number(envValue);
522
+ if (isValidGateTimeoutMs(parsed)) {
523
+ return { timeoutMs: parsed, source: 'env' };
524
+ }
525
+ warnOnce('env', 'Ignoring invalid OAT_GATE_EXEC_TIMEOUT_MS; using the next timeout source.');
526
+ }
527
+ const scope = input.reviewScope?.trim().toLowerCase() ?? '';
528
+ if (reviewType === 'artifact') {
529
+ return { timeoutMs: 900_000, source: 'scope-default' };
530
+ }
531
+ if (reviewType === 'code') {
532
+ if (/^p\d+-t\d+$/.test(scope)) {
533
+ return { timeoutMs: 900_000, source: 'scope-default' };
534
+ }
535
+ if (scope === 'final' ||
536
+ /^p\d+$/.test(scope) ||
537
+ /^p\d+-p\d+$/.test(scope)) {
538
+ return { timeoutMs: 1_800_000, source: 'scope-default' };
539
+ }
540
+ }
541
+ return { timeoutMs: GATE_EXEC_TIMEOUT_MS, source: 'default' };
542
+ }
543
+ function rawRecord(value) {
544
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
545
+ ? value
546
+ : null;
547
+ }
548
+ async function readRawConfig(path) {
549
+ try {
550
+ return rawRecord(parseJsonConfig(await readFile(path, 'utf8'), path)) ?? {};
551
+ }
552
+ catch (error) {
553
+ if (typeof error === 'object' &&
554
+ error !== null &&
555
+ 'code' in error &&
556
+ error.code === 'ENOENT') {
557
+ return {};
558
+ }
559
+ throw error;
560
+ }
561
+ }
562
+ async function readRawGateTimeoutLayers(input) {
563
+ const configs = await Promise.all([
564
+ readRawConfig(join(input.repoRoot, '.oat', 'config.local.json')),
565
+ readRawConfig(join(input.repoRoot, '.oat', 'config.json')),
566
+ readRawConfig(join(input.userConfigDir, 'config.json')),
567
+ ]);
568
+ const layers = ['local', 'shared', 'user'];
569
+ const target = [];
570
+ const workflow = [];
571
+ const reviewType = input.reviewType?.trim().toLowerCase();
572
+ for (const [index, config] of configs.entries()) {
573
+ const layer = layers[index];
574
+ const workflowConfig = rawRecord(config.workflow);
575
+ const gates = rawRecord(workflowConfig?.gates);
576
+ const execTargets = rawRecord(gates?.execTargets);
577
+ const rawTarget = rawRecord(execTargets?.[input.targetId]);
578
+ if (rawTarget &&
579
+ Object.prototype.hasOwnProperty.call(rawTarget, 'timeoutMs')) {
580
+ target.push({ layer, value: rawTarget.timeoutMs });
581
+ }
582
+ const gateTimeouts = rawRecord(workflowConfig?.gateTimeouts);
583
+ if ((reviewType === 'code' || reviewType === 'artifact') &&
584
+ gateTimeouts &&
585
+ Object.prototype.hasOwnProperty.call(gateTimeouts, reviewType)) {
586
+ workflow.push({ layer, value: gateTimeouts[reviewType] });
587
+ }
588
+ }
589
+ return { target, workflow };
590
+ }
367
591
  function resolveGateLivenessIntervalMs(env) {
368
592
  const rawValue = env.OAT_GATE_LIVENESS_INTERVAL_MS?.trim();
369
593
  if (!rawValue) {
@@ -431,6 +655,9 @@ function parseExecTargetConfig(options) {
431
655
  ...(options.priority !== undefined
432
656
  ? { priority: parseNumericFlag(options.priority, '--priority', 0) }
433
657
  : {}),
658
+ ...(options.timeoutMs !== undefined
659
+ ? { timeoutMs: parseGateTimeoutFlag(options.timeoutMs, '--timeout-ms') }
660
+ : {}),
434
661
  ...(options.invocationModel !== undefined ||
435
662
  options.invocationReasoningEffort !== undefined
436
663
  ? {
@@ -873,7 +1100,7 @@ async function resolveSelectedExecTarget(targets, options, producerIdentity, con
873
1100
  }
874
1101
  return attachDiversityMetadata(selected, avoid, producerIdentity);
875
1102
  }
876
- async function executeTarget(selected, prompt, context, dependencies) {
1103
+ async function executeTarget(selected, prompt, context, dependencies, timeout) {
877
1104
  const [command, baseArgs] = argvHead(selected.target.baseCommand);
878
1105
  if (!command) {
879
1106
  throw new Error(`Exec target "${selected.id}" has an empty base command.`);
@@ -881,21 +1108,39 @@ async function executeTarget(selected, prompt, context, dependencies) {
881
1108
  const modelArgs = selected.model && !findPinnedModelArg(selected.target.baseCommand)
882
1109
  ? ['--model', selected.model]
883
1110
  : [];
884
- const timeoutMs = resolveGateExecTimeoutMs(dependencies.processEnv);
885
1111
  const livenessIntervalMs = resolveGateLivenessIntervalMs(dependencies.processEnv);
886
- if (!context.json) {
887
- context.logger.info(`Running gate target ${selected.id} (${selected.target.runtime}); timeout=${timeoutMs}ms.`);
1112
+ const activityProbe = await dependencies.createGateActivityProbe({
1113
+ runtime: selected.target.runtime,
1114
+ cwd: context.cwd,
1115
+ home: context.home,
1116
+ spawnedAt: Date.now(),
1117
+ });
1118
+ if (context.json) {
1119
+ dependencies.writeDiagnostic(`${JSON.stringify({
1120
+ type: 'gate-start',
1121
+ target: selected.id,
1122
+ runtime: selected.target.runtime,
1123
+ timeoutMs: timeout.timeoutMs,
1124
+ timeoutSource: timeout.source,
1125
+ })}\n`);
1126
+ }
1127
+ else {
1128
+ context.logger.info(`Running gate target ${selected.id} (${selected.target.runtime}); timeout=${timeout.timeoutMs}ms (source=${timeout.source}).`);
888
1129
  }
889
1130
  try {
890
1131
  return await dependencies.runProcess(command, [...baseArgs, ...modelArgs, ...prompt], {
891
1132
  cwd: context.cwd,
892
1133
  env: dependencies.processEnv,
1134
+ ...(activityProbe ? { activityProbe } : {}),
893
1135
  livenessIntervalMs,
894
- onLiveness: ({ elapsedMs, hardBudgetMs, idleMs }) => {
1136
+ onLiveness: ({ elapsedMs, hardBudgetMs, idleMs, processAlive, activityProbeStatus, lastActivityEvidence, }) => {
895
1137
  const telemetry = {
896
1138
  elapsedMs,
897
1139
  hardBudgetMs,
898
1140
  idleMs,
1141
+ processAlive,
1142
+ ...(activityProbeStatus ? { activityProbeStatus } : {}),
1143
+ ...(lastActivityEvidence ? { lastActivityEvidence } : {}),
899
1144
  target: selected.id,
900
1145
  type: 'gate-liveness',
901
1146
  };
@@ -903,13 +1148,18 @@ async function executeTarget(selected, prompt, context, dependencies) {
903
1148
  dependencies.writeDiagnostic(`${JSON.stringify(telemetry)}\n`);
904
1149
  }
905
1150
  else {
906
- context.logger.info(`Gate liveness: target=${selected.id} elapsed_ms=${elapsedMs} idle_ms=${idleMs} hard_budget_ms=${hardBudgetMs}.`);
1151
+ const activityDescription = lastActivityEvidence
1152
+ ? lastActivityEvidence.scope === 'ambient-runtime'
1153
+ ? 'ambient runtime activity (not attributable to this gate child)'
1154
+ : 'project-directory activity'
1155
+ : `${activityProbeStatus?.status ?? 'unavailable'} (${activityProbeStatus?.attemptedPath ?? 'no path'})`;
1156
+ context.logger.info(`Gate liveness: target=${selected.id} elapsed_ms=${elapsedMs} idle_ms=${idleMs} hard_budget_ms=${hardBudgetMs} process_alive=${processAlive} activity_evidence=${activityDescription}.`);
907
1157
  }
908
1158
  },
909
1159
  purpose: 'execute',
910
1160
  stdin: 'ignore',
911
1161
  stdio: 'pipe',
912
- timeoutMs,
1162
+ timeoutMs: timeout.timeoutMs,
913
1163
  });
914
1164
  }
915
1165
  catch (error) {
@@ -1283,9 +1533,11 @@ function writeReviewGateResult(context, payload) {
1283
1533
  context.logger.info(payload.handoff);
1284
1534
  }
1285
1535
  function writeReviewGateExecutionFailure(context, payload) {
1286
- const message = payload.timedOut
1287
- ? `Review did not complete: target ${payload.target} timed out after ${payload.timeoutMs}ms.`
1288
- : `Review did not complete: target ${payload.target} exited with code ${payload.exitCode}.`;
1536
+ const message = payload.refusal
1537
+ ? `Review did not complete: reviewer refused the headless route (${payload.refusal}).`
1538
+ : payload.timedOut
1539
+ ? `Review did not complete: target ${payload.target} timed out after ${payload.timeoutMs}ms.`
1540
+ : `Review did not complete: target ${payload.target} exited with code ${payload.exitCode}.`;
1289
1541
  if (context.json) {
1290
1542
  context.logger.json({
1291
1543
  status: 'review_failed',
@@ -1304,9 +1556,16 @@ function writeReviewGateExecutionFailure(context, payload) {
1304
1556
  ...(payload.timeoutMs !== undefined
1305
1557
  ? { timeoutMs: payload.timeoutMs }
1306
1558
  : {}),
1559
+ ...(payload.timeoutSource !== undefined
1560
+ ? { timeoutSource: payload.timeoutSource }
1561
+ : {}),
1307
1562
  ...(payload.noOutputProduced !== undefined
1308
1563
  ? { noOutputProduced: payload.noOutputProduced }
1309
1564
  : {}),
1565
+ ...(payload.refusal ? { refusal: payload.refusal } : {}),
1566
+ ...(payload.activityEvidence
1567
+ ? { activityEvidence: payload.activityEvidence }
1568
+ : {}),
1310
1569
  message,
1311
1570
  });
1312
1571
  return;
@@ -1397,6 +1656,29 @@ function writeReviewGateTargetingFailure(context, payload) {
1397
1656
  context.logger.error(`Expected project=${payload.corroboration.expectedProject} run=${payload.gateInvocation.runId}; actual containing_project=${payload.corroboration.actual.containingProject ?? 'missing'} artifact_project=${payload.corroboration.actual.artifactProject ?? 'missing'} run_matches=${payload.corroboration.actual.matchingArtifactPaths.join(',') || 'none'}.`);
1398
1657
  context.logger.error('This targeting failure is not receive-eligible and must be corrected before severity or invocation remediation.');
1399
1658
  }
1659
+ async function finalizeReviewGateProjectLog(context, dependencies, finalization) {
1660
+ const findings = finalization.counts
1661
+ ? ` findings=critical:${finalization.counts.critical},important:${finalization.counts.important},medium:${finalization.counts.medium},minor:${finalization.counts.minor}`
1662
+ : '';
1663
+ const artifact = finalization.artifactPath
1664
+ ? ` artifact=${finalization.artifactPath}`
1665
+ : '';
1666
+ const body = `target=${finalization.target} threshold=${finalization.threshold}${findings} exit=${finalization.exitCode} status=${finalization.status}${artifact}`;
1667
+ try {
1668
+ await dependencies.appendProjectLog({
1669
+ repoRoot: finalization.repoRoot,
1670
+ home: finalization.home,
1671
+ project: finalization.project,
1672
+ structural: true,
1673
+ producer: 'oat gate review',
1674
+ ref: finalization.ref,
1675
+ body,
1676
+ });
1677
+ }
1678
+ catch (error) {
1679
+ context.logger.warn(`Warning: unable to append oat gate review result to the project log: ${error instanceof Error ? error.message : String(error)}. Gate result is unchanged.`);
1680
+ }
1681
+ }
1400
1682
  async function updateConfigLayer(context, layer, dependencies, mutate) {
1401
1683
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1402
1684
  const userConfigDir = join(context.home, '.oat');
@@ -1526,7 +1808,21 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1526
1808
  const producerIdentity = parseProducerIdentityOption(options.producerIdentity);
1527
1809
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1528
1810
  logGateDiversity(selected, context);
1529
- const result = await executeTarget(selected, prompt, context, dependencies);
1811
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1812
+ const rawPersisted = await readRawGateTimeoutLayers({
1813
+ repoRoot,
1814
+ userConfigDir: join(context.home, '.oat'),
1815
+ targetId: selected.id,
1816
+ });
1817
+ const timeout = resolveGateExecTimeout({
1818
+ cliTimeoutMs: options.timeoutMs,
1819
+ target: selected.target,
1820
+ effective,
1821
+ env: dependencies.processEnv,
1822
+ warn: context.logger.warn,
1823
+ rawPersisted,
1824
+ });
1825
+ const result = await executeTarget(selected, prompt, context, dependencies, timeout);
1530
1826
  process.exitCode = result.exitCode;
1531
1827
  }
1532
1828
  catch (error) {
@@ -1535,6 +1831,11 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1535
1831
  }
1536
1832
  async function runReviewGate(prompt, options, context, dependencies) {
1537
1833
  const runId = randomUUID();
1834
+ let runMarkerPath;
1835
+ let runMarkerWritten = false;
1836
+ let branchLocalGateCli;
1837
+ let projectLogFinalization;
1838
+ let projectLogFinalized = false;
1538
1839
  let postSelectionContext;
1539
1840
  try {
1540
1841
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
@@ -1555,6 +1856,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
1555
1856
  });
1556
1857
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1557
1858
  const gateInvocation = createGateInvocationMetadata(runId, selected);
1859
+ const rawPersisted = await readRawGateTimeoutLayers({
1860
+ repoRoot,
1861
+ userConfigDir,
1862
+ targetId: selected.id,
1863
+ reviewType: options.reviewType,
1864
+ });
1865
+ const timeout = resolveGateExecTimeout({
1866
+ cliTimeoutMs: options.timeoutMs,
1867
+ target: selected.target,
1868
+ effective,
1869
+ reviewType: options.reviewType,
1870
+ reviewScope: options.reviewScope,
1871
+ env: dependencies.processEnv,
1872
+ warn: context.logger.warn,
1873
+ rawPersisted,
1874
+ });
1558
1875
  const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
1559
1876
  postSelectionContext = {
1560
1877
  project: projectPath,
@@ -1564,6 +1881,16 @@ async function runReviewGate(prompt, options, context, dependencies) {
1564
1881
  dispatchReport,
1565
1882
  };
1566
1883
  const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
1884
+ projectLogFinalization = {
1885
+ repoRoot,
1886
+ home: context.home,
1887
+ project: projectPath,
1888
+ ref: options.reviewScope?.trim() || 'gate-review',
1889
+ target: selected.id,
1890
+ threshold,
1891
+ status: 'review_failed',
1892
+ exitCode: 1,
1893
+ };
1567
1894
  const before = await listReviewGateArtifactCandidates({
1568
1895
  repoRoot,
1569
1896
  effective,
@@ -1581,9 +1908,65 @@ async function runReviewGate(prompt, options, context, dependencies) {
1581
1908
  : []),
1582
1909
  prompt.join(' '),
1583
1910
  ]);
1584
- const childResult = await executeTarget(selected, [reviewPrompt], context, dependencies);
1911
+ runMarkerPath = join(tmpdir(), 'oat-gate-runs', `${runId}.json`);
1912
+ runMarkerWritten = await dependencies.writeGateRunMarker(runMarkerPath, {
1913
+ runId,
1914
+ targetId: selected.id,
1915
+ runtime: selected.target.runtime,
1916
+ reviewType: options.reviewType?.trim() || null,
1917
+ reviewScope: options.reviewScope?.trim() || null,
1918
+ project: projectPath,
1919
+ startedAt: new Date().toISOString(),
1920
+ budgetMs: timeout.timeoutMs,
1921
+ budgetSource: timeout.source,
1922
+ }, (message) => context.logger.warn(message));
1923
+ if (runMarkerWritten) {
1924
+ if (context.json) {
1925
+ dependencies.writeDiagnostic(`${JSON.stringify({
1926
+ type: 'gate-run-marker',
1927
+ runId,
1928
+ path: runMarkerPath,
1929
+ })}\n`);
1930
+ }
1931
+ else {
1932
+ context.logger.info(`Gate run marker: ${runMarkerPath}.`);
1933
+ }
1934
+ }
1935
+ branchLocalGateCli = await dependencies.createBranchLocalGateCli({
1936
+ runId,
1937
+ launch: dependencies.currentGateCliLaunch(),
1938
+ });
1939
+ const childResult = await executeTarget(selected, [reviewPrompt], context, {
1940
+ ...dependencies,
1941
+ processEnv: {
1942
+ ...dependencies.processEnv,
1943
+ OAT_GATE_HEADLESS: '1',
1944
+ OAT_NON_INTERACTIVE: '1',
1945
+ OAT_GATE_RUN_ID: runId,
1946
+ OAT_GATE_RUNTIME: gateInvocation.runtime,
1947
+ OAT_INVOCATION_MODEL: gateInvocation.model,
1948
+ OAT_GATE_CLI_PATH: branchLocalGateCli.cliPath,
1949
+ OAT_GATE_CLI_ROOT: branchLocalGateCli.cliRoot,
1950
+ OAT_GATE_ROUTE_RECEIPT_PATH: branchLocalGateCli.routeReceiptPath,
1951
+ },
1952
+ }, timeout);
1953
+ const routeReceipt = await dependencies.readGateRouteReceipt(branchLocalGateCli.routeReceiptPath, branchLocalGateCli.cliRoot, selected.target.runtime);
1954
+ dependencies.writeDiagnostic(`${JSON.stringify({
1955
+ type: 'gate-route',
1956
+ target: selected.id,
1957
+ ...routeReceipt,
1958
+ })}\n`);
1585
1959
  const childExitCode = childResult.exitCode;
1586
- if (childExitCode !== 0 && !childResult.timedOut) {
1960
+ const refusal = childResult.refusal ??
1961
+ extractStructuredRefusal(childResult.capturedOutput ?? '');
1962
+ const writeRefusalFailure = () => {
1963
+ if (!refusal) {
1964
+ return false;
1965
+ }
1966
+ if (projectLogFinalization) {
1967
+ projectLogFinalization.status = 'review_failed';
1968
+ projectLogFinalization.exitCode = 1;
1969
+ }
1587
1970
  writeReviewGateExecutionFailure(context, {
1588
1971
  runId,
1589
1972
  target: selected.id,
@@ -1591,13 +1974,18 @@ async function runReviewGate(prompt, options, context, dependencies) {
1591
1974
  projectResolutionSource: reviewProject.source,
1592
1975
  exitCode: childExitCode,
1593
1976
  timedOut: childResult.timedOut ?? false,
1594
- timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
1977
+ timeoutMs: timeout.timeoutMs,
1978
+ timeoutSource: timeout.source,
1979
+ refusal,
1980
+ ...(childResult.activityEvidence
1981
+ ? { activityEvidence: childResult.activityEvidence }
1982
+ : {}),
1595
1983
  gateInvocation,
1596
1984
  dispatchReport,
1597
1985
  });
1598
- process.exitCode = childExitCode;
1599
- return;
1600
- }
1986
+ process.exitCode = 1;
1987
+ return true;
1988
+ };
1601
1989
  const after = await listReviewGateArtifactCandidates({
1602
1990
  repoRoot,
1603
1991
  effective,
@@ -1608,9 +1996,46 @@ async function runReviewGate(prompt, options, context, dependencies) {
1608
1996
  before,
1609
1997
  after,
1610
1998
  });
1999
+ if (projectLogFinalization) {
2000
+ projectLogFinalization.artifactPath =
2001
+ artifactResolution.artifact?.path ??
2002
+ artifactResolution.diagnosticArtifact?.path;
2003
+ }
2004
+ if (!artifactResolution.artifact && writeRefusalFailure()) {
2005
+ return;
2006
+ }
2007
+ if (childExitCode !== 0 &&
2008
+ !childResult.timedOut &&
2009
+ !artifactResolution.artifact) {
2010
+ if (projectLogFinalization) {
2011
+ projectLogFinalization.status = 'review_failed';
2012
+ projectLogFinalization.exitCode = childExitCode;
2013
+ }
2014
+ writeReviewGateExecutionFailure(context, {
2015
+ runId,
2016
+ target: selected.id,
2017
+ project: projectPath,
2018
+ projectResolutionSource: reviewProject.source,
2019
+ exitCode: childExitCode,
2020
+ timedOut: childResult.timedOut ?? false,
2021
+ timeoutMs: timeout.timeoutMs,
2022
+ timeoutSource: timeout.source,
2023
+ ...(childResult.activityEvidence
2024
+ ? { activityEvidence: childResult.activityEvidence }
2025
+ : {}),
2026
+ gateInvocation,
2027
+ dispatchReport,
2028
+ });
2029
+ process.exitCode = childExitCode;
2030
+ return;
2031
+ }
1611
2032
  if (childResult.timedOut &&
1612
2033
  artifactResolution.matchingArtifactPaths.length === 0 &&
1613
2034
  !artifactResolution.diagnosticArtifact) {
2035
+ if (projectLogFinalization) {
2036
+ projectLogFinalization.status = 'review_failed';
2037
+ projectLogFinalization.exitCode = childExitCode;
2038
+ }
1614
2039
  writeReviewGateExecutionFailure(context, {
1615
2040
  runId,
1616
2041
  target: selected.id,
@@ -1618,8 +2043,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
1618
2043
  projectResolutionSource: reviewProject.source,
1619
2044
  exitCode: childExitCode,
1620
2045
  timedOut: true,
1621
- timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
2046
+ timeoutMs: timeout.timeoutMs,
2047
+ timeoutSource: timeout.source,
1622
2048
  noOutputProduced: childResult.stdoutBytes + childResult.stderrBytes === 0,
2049
+ ...(childResult.activityEvidence
2050
+ ? { activityEvidence: childResult.activityEvidence }
2051
+ : {}),
1623
2052
  gateInvocation,
1624
2053
  dispatchReport,
1625
2054
  });
@@ -1642,6 +2071,10 @@ async function runReviewGate(prompt, options, context, dependencies) {
1642
2071
  : initialTargetCorroboration.run === 'mismatched'
1643
2072
  ? `The changed review artifact did not carry the expected gate run ID ${runId}.`
1644
2073
  : `No direct active project review artifact carried gate run ID ${runId}.`;
2074
+ if (projectLogFinalization) {
2075
+ projectLogFinalization.status = 'targeting_correlation_failed';
2076
+ projectLogFinalization.exitCode = 1;
2077
+ }
1645
2078
  writeReviewGateTargetingFailure(context, {
1646
2079
  runId,
1647
2080
  target: selected.id,
@@ -1660,6 +2093,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
1660
2093
  if (producedArtifact.containingProject !== reviewProject.path ||
1661
2094
  (reviewProject.source === 'declared' &&
1662
2095
  initialTargetCorroboration.project !== 'matched')) {
2096
+ if (writeRefusalFailure()) {
2097
+ return;
2098
+ }
2099
+ if (projectLogFinalization) {
2100
+ projectLogFinalization.status = 'targeting_correlation_failed';
2101
+ projectLogFinalization.exitCode = 1;
2102
+ }
1663
2103
  writeReviewGateTargetingFailure(context, {
1664
2104
  runId,
1665
2105
  target: selected.id,
@@ -1681,6 +2121,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
1681
2121
  }
1682
2122
  if (!producedArtifact.generatedAt ||
1683
2123
  !Number.isFinite(producedArtifact.generatedTime)) {
2124
+ if (writeRefusalFailure()) {
2125
+ return;
2126
+ }
2127
+ if (projectLogFinalization) {
2128
+ projectLogFinalization.status = 'artifact_validation_failed';
2129
+ projectLogFinalization.exitCode = 1;
2130
+ }
1684
2131
  writeReviewGateArtifactValidationFailure(context, {
1685
2132
  runId,
1686
2133
  target: selected.id,
@@ -1708,7 +2155,14 @@ async function runReviewGate(prompt, options, context, dependencies) {
1708
2155
  });
1709
2156
  }
1710
2157
  catch (error) {
2158
+ if (writeRefusalFailure()) {
2159
+ return;
2160
+ }
1711
2161
  const detail = error instanceof Error ? error.message : String(error);
2162
+ if (projectLogFinalization) {
2163
+ projectLogFinalization.status = 'artifact_validation_failed';
2164
+ projectLogFinalization.exitCode = 1;
2165
+ }
1712
2166
  writeReviewGateArtifactValidationFailure(context, {
1713
2167
  runId,
1714
2168
  target: selected.id,
@@ -1725,12 +2179,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
1725
2179
  process.exitCode = 1;
1726
2180
  return;
1727
2181
  }
2182
+ if (projectLogFinalization) {
2183
+ projectLogFinalization.counts = verdict.counts;
2184
+ }
1728
2185
  const targetCorroboration = initialTargetCorroboration;
1729
2186
  const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
1730
2187
  if (corroboration.run !== 'matched' ||
1731
2188
  corroboration.invocation !== 'matched') {
2189
+ if (writeRefusalFailure()) {
2190
+ return;
2191
+ }
1732
2192
  const missing = corroboration.run === 'missing' ||
1733
2193
  corroboration.invocation === 'missing';
2194
+ if (projectLogFinalization) {
2195
+ projectLogFinalization.status = 'artifact_validation_failed';
2196
+ projectLogFinalization.exitCode = 1;
2197
+ }
1734
2198
  writeReviewGateArtifactValidationFailure(context, {
1735
2199
  runId,
1736
2200
  target: selected.id,
@@ -1750,6 +2214,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
1750
2214
  return;
1751
2215
  }
1752
2216
  if (verdict.invocation !== 'gate') {
2217
+ if (writeRefusalFailure()) {
2218
+ return;
2219
+ }
2220
+ if (projectLogFinalization) {
2221
+ projectLogFinalization.status = 'artifact_validation_failed';
2222
+ projectLogFinalization.exitCode = 1;
2223
+ }
1753
2224
  writeReviewGateArtifactValidationFailure(context, {
1754
2225
  runId,
1755
2226
  target: selected.id,
@@ -1773,6 +2244,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
1773
2244
  threshold,
1774
2245
  blocking,
1775
2246
  });
2247
+ if (projectLogFinalization) {
2248
+ projectLogFinalization.status = blocking ? 'blocked' : 'ok';
2249
+ projectLogFinalization.exitCode = blocking ? 1 : 0;
2250
+ projectLogFinalization.counts = verdict.counts;
2251
+ projectLogFinalization.artifactPath = producedArtifact.path;
2252
+ }
1776
2253
  writeReviewGateResult(context, {
1777
2254
  status: blocking ? 'blocked' : 'ok',
1778
2255
  runId,
@@ -1809,6 +2286,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
1809
2286
  writeError(context, error);
1810
2287
  }
1811
2288
  }
2289
+ finally {
2290
+ try {
2291
+ if (branchLocalGateCli) {
2292
+ await dependencies.removeBranchLocalGateCli(branchLocalGateCli);
2293
+ }
2294
+ if (runMarkerPath) {
2295
+ await dependencies.removeGateRunMarker(runMarkerPath, (message) => context.logger.warn(message));
2296
+ }
2297
+ }
2298
+ finally {
2299
+ if (projectLogFinalization && !projectLogFinalized) {
2300
+ projectLogFinalized = true;
2301
+ await finalizeReviewGateProjectLog(context, dependencies, projectLogFinalization);
2302
+ }
2303
+ }
2304
+ }
1812
2305
  }
1813
2306
  export function createGateCommand(overrides = {}) {
1814
2307
  const dependencies = {
@@ -1816,6 +2309,10 @@ export function createGateCommand(overrides = {}) {
1816
2309
  ...overrides,
1817
2310
  };
1818
2311
  const cmd = new Command('gate').description('Resolve and manage workflow gate configuration');
2312
+ cmd.addCommand(createGateRouteCommand({
2313
+ buildCommandContext: dependencies.buildCommandContext,
2314
+ processEnv: dependencies.processEnv,
2315
+ }));
1819
2316
  cmd
1820
2317
  .command('resolve')
1821
2318
  .description('Print the resolved gate configuration for a skill')
@@ -1849,11 +2346,13 @@ export function createGateCommand(overrides = {}) {
1849
2346
  });
1850
2347
  cmd
1851
2348
  .command('cross-provider-exec')
2349
+ .alias('exec')
1852
2350
  .description('Run a prompt through an alternate configured runtime target')
1853
2351
  .option('--target <id>', 'Run this exact exec target')
1854
2352
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1855
2353
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1856
2354
  .option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
2355
+ .option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1857
2356
  .argument('<prompt...>', 'Prompt arguments appended to the target command')
1858
2357
  .action(async (prompt, options, command) => {
1859
2358
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
@@ -1866,6 +2365,7 @@ export function createGateCommand(overrides = {}) {
1866
2365
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1867
2366
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1868
2367
  .option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
2368
+ .option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1869
2369
  .option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
1870
2370
  .option('--review-scope <scope>', 'Review scope hint for the provider')
1871
2371
  .option('--review-type <type>', 'Review type hint for the provider')
@@ -1887,6 +2387,7 @@ export function createGateCommand(overrides = {}) {
1887
2387
  .option('--invocation-model <model>', 'Configured invocation model or provider-default')
1888
2388
  .option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
1889
2389
  .option('--priority <number>', 'Target priority, higher wins')
2390
+ .option('--timeout-ms <milliseconds>', `Target gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1890
2391
  .option('--disable', 'Disable this exec target in the selected layer')
1891
2392
  .option('--layer <layer>', 'Config layer to write: shared, local, or user')
1892
2393
  .action(async (targetId, options, command) => {