@open-agent-toolkit/cli 0.1.69 → 0.1.72

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 (43) hide show
  1. package/assets/docs/cli-utilities/configuration.md +27 -0
  2. package/assets/docs/cli-utilities/index.md +1 -1
  3. package/assets/docs/cli-utilities/workflow-gates.md +126 -3
  4. package/assets/docs/reference/cli-reference.md +5 -1
  5. package/assets/docs/workflows/projects/dispatch-ceiling.md +14 -5
  6. package/assets/docs/workflows/projects/orchestration-model.md +21 -0
  7. package/assets/docs/workflows/projects/review-flavors.md +9 -0
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/oat-dispatch-subagents/SKILL.md +21 -1
  10. package/assets/skills/oat-dispatch-subagents/references/provider-claude.md +21 -0
  11. package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +16 -0
  12. package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +21 -0
  13. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -0
  14. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -0
  15. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -0
  16. package/assets/skills/oat-project-plan-writing/SKILL.md +23 -33
  17. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -0
  18. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -0
  19. package/assets/skills/oat-project-review-provide/SKILL.md +62 -1
  20. package/dist/commands/config/index.d.ts.map +1 -1
  21. package/dist/commands/config/index.js +65 -1
  22. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts +3 -0
  23. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts.map +1 -0
  24. package/dist/commands/gate/__fixtures__/fake-runtime.mjs +167 -0
  25. package/dist/commands/gate/activity-probes.d.ts +35 -0
  26. package/dist/commands/gate/activity-probes.d.ts.map +1 -0
  27. package/dist/commands/gate/activity-probes.js +135 -0
  28. package/dist/commands/gate/branch-local-cli.d.ts +31 -0
  29. package/dist/commands/gate/branch-local-cli.d.ts.map +1 -0
  30. package/dist/commands/gate/branch-local-cli.js +116 -0
  31. package/dist/commands/gate/index.d.ts +29 -0
  32. package/dist/commands/gate/index.d.ts.map +1 -1
  33. package/dist/commands/gate/index.js +438 -32
  34. package/dist/commands/gate/route.d.ts +22 -0
  35. package/dist/commands/gate/route.d.ts.map +1 -0
  36. package/dist/commands/gate/route.js +109 -0
  37. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  38. package/dist/commands/project/dispatch-ceiling/index.js +100 -16
  39. package/dist/config/oat-config.d.ts +9 -0
  40. package/dist/config/oat-config.d.ts.map +1 -1
  41. package/dist/config/oat-config.js +23 -0
  42. package/dist/config/resolve.js +7 -0
  43. package/package.json +2 -2
@@ -1,11 +1,13 @@
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';
6
7
  import { getFrontmatterBlock, parseFrontmatterScalarFields, parseGeneratedTime, } from '../shared/frontmatter.js';
7
8
  import { readGlobalOptions } from '../shared/shared.utils.js';
8
- import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
9
+ import { parseJsonConfig } from '../../config/json.js';
10
+ 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
11
  import { resolveEffectiveConfig, resolveExecTargetViews, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
10
12
  import { dirExists, fileExists } from '../../fs/io.js';
11
13
  import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope, } from '../../fs/paths.js';
@@ -15,7 +17,10 @@ import { resolveIdentityConfidence, } from '../../providers/identity/provenance.
15
17
  import { parseDispatchStamps } from '../../providers/identity/stamp.js';
16
18
  import { Command } from 'commander';
17
19
  import YAML from 'yaml';
20
+ import { createGateActivityProbe, } from './activity-probes.js';
21
+ import { createBranchLocalGateCli, currentGateCliLaunch, readGateRouteReceipt, removeBranchLocalGateCli, } from './branch-local-cli.js';
18
22
  import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
23
+ import { createGateRouteCommand } from './route.js';
19
24
  const DEFAULT_DEPENDENCIES = {
20
25
  buildCommandContext,
21
26
  resolveProjectRoot,
@@ -26,9 +31,16 @@ const DEFAULT_DEPENDENCIES = {
26
31
  readUserConfig,
27
32
  writeUserConfig,
28
33
  resolveEffectiveConfig,
34
+ createGateActivityProbe,
35
+ createBranchLocalGateCli,
36
+ currentGateCliLaunch,
37
+ removeBranchLocalGateCli,
38
+ readGateRouteReceipt,
29
39
  runProcess: runChildProcess,
30
40
  parseReviewGateVerdict,
31
41
  processEnv: process.env,
42
+ writeGateRunMarker,
43
+ removeGateRunMarker,
32
44
  writeDiagnostic: (message) => process.stderr.write(message),
33
45
  };
34
46
  const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
@@ -61,6 +73,33 @@ const REVIEW_GATE_CONTEXT_NOTE = [
61
73
  const GATE_CHECK_TIMEOUT_MS = 5_000;
62
74
  const GATE_EXEC_TIMEOUT_MS = 15 * 60 * 1_000;
63
75
  const GATE_LIVENESS_INTERVAL_MS = 30_000;
76
+ async function writeGateRunMarker(path, marker, warn) {
77
+ try {
78
+ await mkdir(join(tmpdir(), 'oat-gate-runs'), { recursive: true });
79
+ await writeFile(path, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
80
+ return true;
81
+ }
82
+ catch (error) {
83
+ const detail = error instanceof Error ? error.message : String(error);
84
+ warn(`Unable to write gate run marker ${path}: ${detail}`);
85
+ return false;
86
+ }
87
+ }
88
+ async function removeGateRunMarker(path, warn) {
89
+ try {
90
+ await rm(path);
91
+ }
92
+ catch (error) {
93
+ if (error &&
94
+ typeof error === 'object' &&
95
+ 'code' in error &&
96
+ error.code === 'ENOENT') {
97
+ return;
98
+ }
99
+ const detail = error instanceof Error ? error.message : String(error);
100
+ warn(`Unable to remove gate run marker ${path}: ${detail}`);
101
+ }
102
+ }
64
103
  function reviewGateProjectContext(project) {
65
104
  return [
66
105
  `Resolved OAT project path: ${project.path}. Run the review for this project path.`,
@@ -158,6 +197,7 @@ function buildGateDispatchReport(invocation, scope) {
158
197
  }
159
198
  function gateInvocationPromptContext(invocation) {
160
199
  const frontmatter = YAML.stringify({
200
+ oat_gate_headless: true,
161
201
  oat_gate_run_id: invocation.runId,
162
202
  oat_gate_target: invocation.targetId,
163
203
  oat_gate_runtime: invocation.runtime,
@@ -203,6 +243,12 @@ async function runChildProcess(command, args, options) {
203
243
  let killTimeout = null;
204
244
  let stderrBytes = 0;
205
245
  let stdoutBytes = 0;
246
+ let refusal;
247
+ let stdoutLineBuffer = '';
248
+ let stderrLineBuffer = '';
249
+ let latestActivityEvidence;
250
+ let latestActivityProbeStatus;
251
+ let livenessProbePending = false;
206
252
  const startedAt = Date.now();
207
253
  let lastActivityAt = startedAt;
208
254
  const child = spawn(command, args, {
@@ -215,23 +261,71 @@ async function runChildProcess(command, args, options) {
215
261
  const recordActivity = () => {
216
262
  lastActivityAt = Date.now();
217
263
  };
264
+ const scanChunk = (buffer, chunk) => {
265
+ const combined = buffer + chunk.toString('utf8');
266
+ const lines = combined.split('\n');
267
+ const remainder = lines.pop() ?? '';
268
+ if (!refusal) {
269
+ for (const line of lines) {
270
+ refusal = extractStructuredRefusal(line.replace(/\r$/, ''));
271
+ if (refusal) {
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ return remainder;
277
+ };
218
278
  child.stdout?.on('data', (chunk) => {
219
279
  stdoutBytes += chunk.byteLength;
280
+ stdoutLineBuffer = scanChunk(stdoutLineBuffer, chunk);
220
281
  recordActivity();
221
282
  process.stdout.write(chunk);
222
283
  });
223
284
  child.stderr?.on('data', (chunk) => {
224
285
  stderrBytes += chunk.byteLength;
286
+ stderrLineBuffer = scanChunk(stderrLineBuffer, chunk);
225
287
  recordActivity();
226
288
  process.stderr.write(chunk);
227
289
  });
290
+ const processAlive = () => {
291
+ if (child.pid === undefined)
292
+ return false;
293
+ try {
294
+ process.kill(child.pid, 0);
295
+ return true;
296
+ }
297
+ catch {
298
+ return false;
299
+ }
300
+ };
228
301
  const livenessInterval = options.onLiveness && options.livenessIntervalMs
229
302
  ? setInterval(() => {
303
+ if (livenessProbePending)
304
+ return;
305
+ livenessProbePending = true;
230
306
  const now = Date.now();
231
- options.onLiveness?.({
232
- elapsedMs: now - startedAt,
233
- hardBudgetMs: options.timeoutMs,
234
- idleMs: now - lastActivityAt,
307
+ void (async () => {
308
+ const activityProbeStatus = await options.activityProbe?.observe(now);
309
+ const evidence = activityProbeStatus?.evidence;
310
+ if (activityProbeStatus) {
311
+ latestActivityProbeStatus = activityProbeStatus;
312
+ }
313
+ if (evidence)
314
+ latestActivityEvidence = evidence;
315
+ options.onLiveness?.({
316
+ elapsedMs: now - startedAt,
317
+ hardBudgetMs: options.timeoutMs,
318
+ idleMs: now - lastActivityAt,
319
+ processAlive: processAlive(),
320
+ ...(latestActivityProbeStatus
321
+ ? { activityProbeStatus: latestActivityProbeStatus }
322
+ : {}),
323
+ ...(latestActivityEvidence
324
+ ? { lastActivityEvidence: latestActivityEvidence }
325
+ : {}),
326
+ });
327
+ })().finally(() => {
328
+ livenessProbePending = false;
235
329
  });
236
330
  }, options.livenessIntervalMs)
237
331
  : null;
@@ -263,8 +357,18 @@ async function runChildProcess(command, args, options) {
263
357
  if (killTimeout) {
264
358
  clearTimeout(killTimeout);
265
359
  }
360
+ refusal ??=
361
+ extractStructuredRefusal(stdoutLineBuffer.replace(/\r$/, '')) ??
362
+ extractStructuredRefusal(stderrLineBuffer.replace(/\r$/, ''));
266
363
  resolve({
364
+ ...(latestActivityEvidence
365
+ ? { activityEvidence: latestActivityEvidence }
366
+ : {}),
367
+ ...(latestActivityProbeStatus
368
+ ? { activityProbeStatus: latestActivityProbeStatus }
369
+ : {}),
267
370
  exitCode: timedOut ? 124 : (code ?? 1),
371
+ ...(refusal ? { refusal } : {}),
268
372
  stderrBytes,
269
373
  stdoutBytes,
270
374
  ...(timedOut ? { timedOut: true } : {}),
@@ -272,6 +376,10 @@ async function runChildProcess(command, args, options) {
272
376
  });
273
377
  });
274
378
  }
379
+ function extractStructuredRefusal(output) {
380
+ const match = output.match(/^OAT_GATE_REFUSAL: (.*)$/m);
381
+ return match?.[1]?.replace(/\r$/, '');
382
+ }
275
383
  function isGateWriteLayer(value) {
276
384
  return VALID_WRITE_LAYERS.includes(value);
277
385
  }
@@ -353,17 +461,131 @@ function parseNumericFlag(value, flag, defaultValue) {
353
461
  }
354
462
  return parsed;
355
463
  }
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;
464
+ function parseGateTimeoutFlag(value, flag) {
465
+ const parsed = Number(value);
466
+ if (!isValidGateTimeoutMs(parsed)) {
467
+ throw new Error(`${flag} must be an integer between ${MIN_GATE_TIMEOUT_MS} and ${MAX_GATE_TIMEOUT_MS}.`);
364
468
  }
365
469
  return parsed;
366
470
  }
471
+ function resolveGateExecTimeout(input) {
472
+ if (input.cliTimeoutMs !== undefined) {
473
+ return {
474
+ timeoutMs: parseGateTimeoutFlag(input.cliTimeoutMs, '--timeout-ms'),
475
+ source: 'cli',
476
+ };
477
+ }
478
+ const warned = new Set();
479
+ const warnOnce = (key, message) => {
480
+ if (!warned.has(key)) {
481
+ warned.add(key);
482
+ input.warn(message);
483
+ }
484
+ };
485
+ for (const persisted of input.rawPersisted?.target ?? []) {
486
+ if (isValidGateTimeoutMs(persisted.value)) {
487
+ return { timeoutMs: persisted.value, source: 'target' };
488
+ }
489
+ warnOnce(`target:${persisted.layer}`, `Ignoring invalid target.timeoutMs from ${persisted.layer} config; using the next timeout source.`);
490
+ }
491
+ if ((input.rawPersisted?.target.length ?? 0) === 0 &&
492
+ input.target.timeoutMs !== undefined) {
493
+ if (isValidGateTimeoutMs(input.target.timeoutMs)) {
494
+ return { timeoutMs: input.target.timeoutMs, source: 'target' };
495
+ }
496
+ input.warn('Ignoring invalid target.timeoutMs; using the next timeout source.');
497
+ }
498
+ const reviewType = input.reviewType?.trim().toLowerCase();
499
+ if (reviewType === 'code' || reviewType === 'artifact') {
500
+ const key = `workflow.gateTimeouts.${reviewType}`;
501
+ for (const persisted of input.rawPersisted?.workflow ?? []) {
502
+ if (isValidGateTimeoutMs(persisted.value)) {
503
+ return { timeoutMs: persisted.value, source: 'config' };
504
+ }
505
+ warnOnce(`${key}:${persisted.layer}`, `Ignoring invalid ${key} from ${persisted.layer} config; using the next timeout source.`);
506
+ }
507
+ const entry = input.effective.resolved[key];
508
+ if ((input.rawPersisted?.workflow.length ?? 0) === 0 &&
509
+ entry?.value !== null &&
510
+ entry?.value !== undefined) {
511
+ if (isValidGateTimeoutMs(entry.value)) {
512
+ return { timeoutMs: entry.value, source: 'config' };
513
+ }
514
+ warnOnce(`${key}:${entry.source}`, `Ignoring invalid ${key} from ${entry.source}; using the next timeout source.`);
515
+ }
516
+ }
517
+ const envValue = input.env.OAT_GATE_EXEC_TIMEOUT_MS?.trim();
518
+ if (envValue) {
519
+ const parsed = Number(envValue);
520
+ if (isValidGateTimeoutMs(parsed)) {
521
+ return { timeoutMs: parsed, source: 'env' };
522
+ }
523
+ warnOnce('env', 'Ignoring invalid OAT_GATE_EXEC_TIMEOUT_MS; using the next timeout source.');
524
+ }
525
+ const scope = input.reviewScope?.trim().toLowerCase() ?? '';
526
+ if (reviewType === 'artifact') {
527
+ return { timeoutMs: 900_000, source: 'scope-default' };
528
+ }
529
+ if (reviewType === 'code') {
530
+ if (/^p\d+-t\d+$/.test(scope)) {
531
+ return { timeoutMs: 900_000, source: 'scope-default' };
532
+ }
533
+ if (scope === 'final' ||
534
+ /^p\d+$/.test(scope) ||
535
+ /^p\d+-p\d+$/.test(scope)) {
536
+ return { timeoutMs: 1_800_000, source: 'scope-default' };
537
+ }
538
+ }
539
+ return { timeoutMs: GATE_EXEC_TIMEOUT_MS, source: 'default' };
540
+ }
541
+ function rawRecord(value) {
542
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
543
+ ? value
544
+ : null;
545
+ }
546
+ async function readRawConfig(path) {
547
+ try {
548
+ return rawRecord(parseJsonConfig(await readFile(path, 'utf8'), path)) ?? {};
549
+ }
550
+ catch (error) {
551
+ if (typeof error === 'object' &&
552
+ error !== null &&
553
+ 'code' in error &&
554
+ error.code === 'ENOENT') {
555
+ return {};
556
+ }
557
+ throw error;
558
+ }
559
+ }
560
+ async function readRawGateTimeoutLayers(input) {
561
+ const configs = await Promise.all([
562
+ readRawConfig(join(input.repoRoot, '.oat', 'config.local.json')),
563
+ readRawConfig(join(input.repoRoot, '.oat', 'config.json')),
564
+ readRawConfig(join(input.userConfigDir, 'config.json')),
565
+ ]);
566
+ const layers = ['local', 'shared', 'user'];
567
+ const target = [];
568
+ const workflow = [];
569
+ const reviewType = input.reviewType?.trim().toLowerCase();
570
+ for (const [index, config] of configs.entries()) {
571
+ const layer = layers[index];
572
+ const workflowConfig = rawRecord(config.workflow);
573
+ const gates = rawRecord(workflowConfig?.gates);
574
+ const execTargets = rawRecord(gates?.execTargets);
575
+ const rawTarget = rawRecord(execTargets?.[input.targetId]);
576
+ if (rawTarget &&
577
+ Object.prototype.hasOwnProperty.call(rawTarget, 'timeoutMs')) {
578
+ target.push({ layer, value: rawTarget.timeoutMs });
579
+ }
580
+ const gateTimeouts = rawRecord(workflowConfig?.gateTimeouts);
581
+ if ((reviewType === 'code' || reviewType === 'artifact') &&
582
+ gateTimeouts &&
583
+ Object.prototype.hasOwnProperty.call(gateTimeouts, reviewType)) {
584
+ workflow.push({ layer, value: gateTimeouts[reviewType] });
585
+ }
586
+ }
587
+ return { target, workflow };
588
+ }
367
589
  function resolveGateLivenessIntervalMs(env) {
368
590
  const rawValue = env.OAT_GATE_LIVENESS_INTERVAL_MS?.trim();
369
591
  if (!rawValue) {
@@ -431,6 +653,9 @@ function parseExecTargetConfig(options) {
431
653
  ...(options.priority !== undefined
432
654
  ? { priority: parseNumericFlag(options.priority, '--priority', 0) }
433
655
  : {}),
656
+ ...(options.timeoutMs !== undefined
657
+ ? { timeoutMs: parseGateTimeoutFlag(options.timeoutMs, '--timeout-ms') }
658
+ : {}),
434
659
  ...(options.invocationModel !== undefined ||
435
660
  options.invocationReasoningEffort !== undefined
436
661
  ? {
@@ -873,7 +1098,7 @@ async function resolveSelectedExecTarget(targets, options, producerIdentity, con
873
1098
  }
874
1099
  return attachDiversityMetadata(selected, avoid, producerIdentity);
875
1100
  }
876
- async function executeTarget(selected, prompt, context, dependencies) {
1101
+ async function executeTarget(selected, prompt, context, dependencies, timeout) {
877
1102
  const [command, baseArgs] = argvHead(selected.target.baseCommand);
878
1103
  if (!command) {
879
1104
  throw new Error(`Exec target "${selected.id}" has an empty base command.`);
@@ -881,21 +1106,39 @@ async function executeTarget(selected, prompt, context, dependencies) {
881
1106
  const modelArgs = selected.model && !findPinnedModelArg(selected.target.baseCommand)
882
1107
  ? ['--model', selected.model]
883
1108
  : [];
884
- const timeoutMs = resolveGateExecTimeoutMs(dependencies.processEnv);
885
1109
  const livenessIntervalMs = resolveGateLivenessIntervalMs(dependencies.processEnv);
886
- if (!context.json) {
887
- context.logger.info(`Running gate target ${selected.id} (${selected.target.runtime}); timeout=${timeoutMs}ms.`);
1110
+ const activityProbe = await dependencies.createGateActivityProbe({
1111
+ runtime: selected.target.runtime,
1112
+ cwd: context.cwd,
1113
+ home: context.home,
1114
+ spawnedAt: Date.now(),
1115
+ });
1116
+ if (context.json) {
1117
+ dependencies.writeDiagnostic(`${JSON.stringify({
1118
+ type: 'gate-start',
1119
+ target: selected.id,
1120
+ runtime: selected.target.runtime,
1121
+ timeoutMs: timeout.timeoutMs,
1122
+ timeoutSource: timeout.source,
1123
+ })}\n`);
1124
+ }
1125
+ else {
1126
+ context.logger.info(`Running gate target ${selected.id} (${selected.target.runtime}); timeout=${timeout.timeoutMs}ms (source=${timeout.source}).`);
888
1127
  }
889
1128
  try {
890
1129
  return await dependencies.runProcess(command, [...baseArgs, ...modelArgs, ...prompt], {
891
1130
  cwd: context.cwd,
892
1131
  env: dependencies.processEnv,
1132
+ ...(activityProbe ? { activityProbe } : {}),
893
1133
  livenessIntervalMs,
894
- onLiveness: ({ elapsedMs, hardBudgetMs, idleMs }) => {
1134
+ onLiveness: ({ elapsedMs, hardBudgetMs, idleMs, processAlive, activityProbeStatus, lastActivityEvidence, }) => {
895
1135
  const telemetry = {
896
1136
  elapsedMs,
897
1137
  hardBudgetMs,
898
1138
  idleMs,
1139
+ processAlive,
1140
+ ...(activityProbeStatus ? { activityProbeStatus } : {}),
1141
+ ...(lastActivityEvidence ? { lastActivityEvidence } : {}),
899
1142
  target: selected.id,
900
1143
  type: 'gate-liveness',
901
1144
  };
@@ -903,13 +1146,18 @@ async function executeTarget(selected, prompt, context, dependencies) {
903
1146
  dependencies.writeDiagnostic(`${JSON.stringify(telemetry)}\n`);
904
1147
  }
905
1148
  else {
906
- context.logger.info(`Gate liveness: target=${selected.id} elapsed_ms=${elapsedMs} idle_ms=${idleMs} hard_budget_ms=${hardBudgetMs}.`);
1149
+ const activityDescription = lastActivityEvidence
1150
+ ? lastActivityEvidence.scope === 'ambient-runtime'
1151
+ ? 'ambient runtime activity (not attributable to this gate child)'
1152
+ : 'project-directory activity'
1153
+ : `${activityProbeStatus?.status ?? 'unavailable'} (${activityProbeStatus?.attemptedPath ?? 'no path'})`;
1154
+ 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
1155
  }
908
1156
  },
909
1157
  purpose: 'execute',
910
1158
  stdin: 'ignore',
911
1159
  stdio: 'pipe',
912
- timeoutMs,
1160
+ timeoutMs: timeout.timeoutMs,
913
1161
  });
914
1162
  }
915
1163
  catch (error) {
@@ -1283,9 +1531,11 @@ function writeReviewGateResult(context, payload) {
1283
1531
  context.logger.info(payload.handoff);
1284
1532
  }
1285
1533
  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}.`;
1534
+ const message = payload.refusal
1535
+ ? `Review did not complete: reviewer refused the headless route (${payload.refusal}).`
1536
+ : payload.timedOut
1537
+ ? `Review did not complete: target ${payload.target} timed out after ${payload.timeoutMs}ms.`
1538
+ : `Review did not complete: target ${payload.target} exited with code ${payload.exitCode}.`;
1289
1539
  if (context.json) {
1290
1540
  context.logger.json({
1291
1541
  status: 'review_failed',
@@ -1304,9 +1554,16 @@ function writeReviewGateExecutionFailure(context, payload) {
1304
1554
  ...(payload.timeoutMs !== undefined
1305
1555
  ? { timeoutMs: payload.timeoutMs }
1306
1556
  : {}),
1557
+ ...(payload.timeoutSource !== undefined
1558
+ ? { timeoutSource: payload.timeoutSource }
1559
+ : {}),
1307
1560
  ...(payload.noOutputProduced !== undefined
1308
1561
  ? { noOutputProduced: payload.noOutputProduced }
1309
1562
  : {}),
1563
+ ...(payload.refusal ? { refusal: payload.refusal } : {}),
1564
+ ...(payload.activityEvidence
1565
+ ? { activityEvidence: payload.activityEvidence }
1566
+ : {}),
1310
1567
  message,
1311
1568
  });
1312
1569
  return;
@@ -1526,7 +1783,21 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1526
1783
  const producerIdentity = parseProducerIdentityOption(options.producerIdentity);
1527
1784
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1528
1785
  logGateDiversity(selected, context);
1529
- const result = await executeTarget(selected, prompt, context, dependencies);
1786
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1787
+ const rawPersisted = await readRawGateTimeoutLayers({
1788
+ repoRoot,
1789
+ userConfigDir: join(context.home, '.oat'),
1790
+ targetId: selected.id,
1791
+ });
1792
+ const timeout = resolveGateExecTimeout({
1793
+ cliTimeoutMs: options.timeoutMs,
1794
+ target: selected.target,
1795
+ effective,
1796
+ env: dependencies.processEnv,
1797
+ warn: context.logger.warn,
1798
+ rawPersisted,
1799
+ });
1800
+ const result = await executeTarget(selected, prompt, context, dependencies, timeout);
1530
1801
  process.exitCode = result.exitCode;
1531
1802
  }
1532
1803
  catch (error) {
@@ -1535,6 +1806,9 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1535
1806
  }
1536
1807
  async function runReviewGate(prompt, options, context, dependencies) {
1537
1808
  const runId = randomUUID();
1809
+ let runMarkerPath;
1810
+ let runMarkerWritten = false;
1811
+ let branchLocalGateCli;
1538
1812
  let postSelectionContext;
1539
1813
  try {
1540
1814
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
@@ -1555,6 +1829,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
1555
1829
  });
1556
1830
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1557
1831
  const gateInvocation = createGateInvocationMetadata(runId, selected);
1832
+ const rawPersisted = await readRawGateTimeoutLayers({
1833
+ repoRoot,
1834
+ userConfigDir,
1835
+ targetId: selected.id,
1836
+ reviewType: options.reviewType,
1837
+ });
1838
+ const timeout = resolveGateExecTimeout({
1839
+ cliTimeoutMs: options.timeoutMs,
1840
+ target: selected.target,
1841
+ effective,
1842
+ reviewType: options.reviewType,
1843
+ reviewScope: options.reviewScope,
1844
+ env: dependencies.processEnv,
1845
+ warn: context.logger.warn,
1846
+ rawPersisted,
1847
+ });
1558
1848
  const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
1559
1849
  postSelectionContext = {
1560
1850
  project: projectPath,
@@ -1581,9 +1871,61 @@ async function runReviewGate(prompt, options, context, dependencies) {
1581
1871
  : []),
1582
1872
  prompt.join(' '),
1583
1873
  ]);
1584
- const childResult = await executeTarget(selected, [reviewPrompt], context, dependencies);
1874
+ runMarkerPath = join(tmpdir(), 'oat-gate-runs', `${runId}.json`);
1875
+ runMarkerWritten = await dependencies.writeGateRunMarker(runMarkerPath, {
1876
+ runId,
1877
+ targetId: selected.id,
1878
+ runtime: selected.target.runtime,
1879
+ reviewType: options.reviewType?.trim() || null,
1880
+ reviewScope: options.reviewScope?.trim() || null,
1881
+ project: projectPath,
1882
+ startedAt: new Date().toISOString(),
1883
+ budgetMs: timeout.timeoutMs,
1884
+ budgetSource: timeout.source,
1885
+ }, (message) => context.logger.warn(message));
1886
+ if (runMarkerWritten) {
1887
+ if (context.json) {
1888
+ dependencies.writeDiagnostic(`${JSON.stringify({
1889
+ type: 'gate-run-marker',
1890
+ runId,
1891
+ path: runMarkerPath,
1892
+ })}\n`);
1893
+ }
1894
+ else {
1895
+ context.logger.info(`Gate run marker: ${runMarkerPath}.`);
1896
+ }
1897
+ }
1898
+ branchLocalGateCli = await dependencies.createBranchLocalGateCli({
1899
+ runId,
1900
+ launch: dependencies.currentGateCliLaunch(),
1901
+ });
1902
+ const childResult = await executeTarget(selected, [reviewPrompt], context, {
1903
+ ...dependencies,
1904
+ processEnv: {
1905
+ ...dependencies.processEnv,
1906
+ OAT_GATE_HEADLESS: '1',
1907
+ OAT_NON_INTERACTIVE: '1',
1908
+ OAT_GATE_RUN_ID: runId,
1909
+ OAT_GATE_RUNTIME: gateInvocation.runtime,
1910
+ OAT_INVOCATION_MODEL: gateInvocation.model,
1911
+ OAT_GATE_CLI_PATH: branchLocalGateCli.cliPath,
1912
+ OAT_GATE_CLI_ROOT: branchLocalGateCli.cliRoot,
1913
+ OAT_GATE_ROUTE_RECEIPT_PATH: branchLocalGateCli.routeReceiptPath,
1914
+ },
1915
+ }, timeout);
1916
+ const routeReceipt = await dependencies.readGateRouteReceipt(branchLocalGateCli.routeReceiptPath, branchLocalGateCli.cliRoot, selected.target.runtime);
1917
+ dependencies.writeDiagnostic(`${JSON.stringify({
1918
+ type: 'gate-route',
1919
+ target: selected.id,
1920
+ ...routeReceipt,
1921
+ })}\n`);
1585
1922
  const childExitCode = childResult.exitCode;
1586
- if (childExitCode !== 0 && !childResult.timedOut) {
1923
+ const refusal = childResult.refusal ??
1924
+ extractStructuredRefusal(childResult.capturedOutput ?? '');
1925
+ const writeRefusalFailure = () => {
1926
+ if (!refusal) {
1927
+ return false;
1928
+ }
1587
1929
  writeReviewGateExecutionFailure(context, {
1588
1930
  runId,
1589
1931
  target: selected.id,
@@ -1591,13 +1933,18 @@ async function runReviewGate(prompt, options, context, dependencies) {
1591
1933
  projectResolutionSource: reviewProject.source,
1592
1934
  exitCode: childExitCode,
1593
1935
  timedOut: childResult.timedOut ?? false,
1594
- timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
1936
+ timeoutMs: timeout.timeoutMs,
1937
+ timeoutSource: timeout.source,
1938
+ refusal,
1939
+ ...(childResult.activityEvidence
1940
+ ? { activityEvidence: childResult.activityEvidence }
1941
+ : {}),
1595
1942
  gateInvocation,
1596
1943
  dispatchReport,
1597
1944
  });
1598
- process.exitCode = childExitCode;
1599
- return;
1600
- }
1945
+ process.exitCode = 1;
1946
+ return true;
1947
+ };
1601
1948
  const after = await listReviewGateArtifactCandidates({
1602
1949
  repoRoot,
1603
1950
  effective,
@@ -1608,6 +1955,30 @@ async function runReviewGate(prompt, options, context, dependencies) {
1608
1955
  before,
1609
1956
  after,
1610
1957
  });
1958
+ if (!artifactResolution.artifact && writeRefusalFailure()) {
1959
+ return;
1960
+ }
1961
+ if (childExitCode !== 0 &&
1962
+ !childResult.timedOut &&
1963
+ !artifactResolution.artifact) {
1964
+ writeReviewGateExecutionFailure(context, {
1965
+ runId,
1966
+ target: selected.id,
1967
+ project: projectPath,
1968
+ projectResolutionSource: reviewProject.source,
1969
+ exitCode: childExitCode,
1970
+ timedOut: childResult.timedOut ?? false,
1971
+ timeoutMs: timeout.timeoutMs,
1972
+ timeoutSource: timeout.source,
1973
+ ...(childResult.activityEvidence
1974
+ ? { activityEvidence: childResult.activityEvidence }
1975
+ : {}),
1976
+ gateInvocation,
1977
+ dispatchReport,
1978
+ });
1979
+ process.exitCode = childExitCode;
1980
+ return;
1981
+ }
1611
1982
  if (childResult.timedOut &&
1612
1983
  artifactResolution.matchingArtifactPaths.length === 0 &&
1613
1984
  !artifactResolution.diagnosticArtifact) {
@@ -1618,8 +1989,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
1618
1989
  projectResolutionSource: reviewProject.source,
1619
1990
  exitCode: childExitCode,
1620
1991
  timedOut: true,
1621
- timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
1992
+ timeoutMs: timeout.timeoutMs,
1993
+ timeoutSource: timeout.source,
1622
1994
  noOutputProduced: childResult.stdoutBytes + childResult.stderrBytes === 0,
1995
+ ...(childResult.activityEvidence
1996
+ ? { activityEvidence: childResult.activityEvidence }
1997
+ : {}),
1623
1998
  gateInvocation,
1624
1999
  dispatchReport,
1625
2000
  });
@@ -1660,6 +2035,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1660
2035
  if (producedArtifact.containingProject !== reviewProject.path ||
1661
2036
  (reviewProject.source === 'declared' &&
1662
2037
  initialTargetCorroboration.project !== 'matched')) {
2038
+ if (writeRefusalFailure()) {
2039
+ return;
2040
+ }
1663
2041
  writeReviewGateTargetingFailure(context, {
1664
2042
  runId,
1665
2043
  target: selected.id,
@@ -1681,6 +2059,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1681
2059
  }
1682
2060
  if (!producedArtifact.generatedAt ||
1683
2061
  !Number.isFinite(producedArtifact.generatedTime)) {
2062
+ if (writeRefusalFailure()) {
2063
+ return;
2064
+ }
1684
2065
  writeReviewGateArtifactValidationFailure(context, {
1685
2066
  runId,
1686
2067
  target: selected.id,
@@ -1708,6 +2089,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1708
2089
  });
1709
2090
  }
1710
2091
  catch (error) {
2092
+ if (writeRefusalFailure()) {
2093
+ return;
2094
+ }
1711
2095
  const detail = error instanceof Error ? error.message : String(error);
1712
2096
  writeReviewGateArtifactValidationFailure(context, {
1713
2097
  runId,
@@ -1729,6 +2113,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1729
2113
  const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
1730
2114
  if (corroboration.run !== 'matched' ||
1731
2115
  corroboration.invocation !== 'matched') {
2116
+ if (writeRefusalFailure()) {
2117
+ return;
2118
+ }
1732
2119
  const missing = corroboration.run === 'missing' ||
1733
2120
  corroboration.invocation === 'missing';
1734
2121
  writeReviewGateArtifactValidationFailure(context, {
@@ -1750,6 +2137,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1750
2137
  return;
1751
2138
  }
1752
2139
  if (verdict.invocation !== 'gate') {
2140
+ if (writeRefusalFailure()) {
2141
+ return;
2142
+ }
1753
2143
  writeReviewGateArtifactValidationFailure(context, {
1754
2144
  runId,
1755
2145
  target: selected.id,
@@ -1809,6 +2199,14 @@ async function runReviewGate(prompt, options, context, dependencies) {
1809
2199
  writeError(context, error);
1810
2200
  }
1811
2201
  }
2202
+ finally {
2203
+ if (branchLocalGateCli) {
2204
+ await dependencies.removeBranchLocalGateCli(branchLocalGateCli);
2205
+ }
2206
+ if (runMarkerPath) {
2207
+ await dependencies.removeGateRunMarker(runMarkerPath, (message) => context.logger.warn(message));
2208
+ }
2209
+ }
1812
2210
  }
1813
2211
  export function createGateCommand(overrides = {}) {
1814
2212
  const dependencies = {
@@ -1816,6 +2214,10 @@ export function createGateCommand(overrides = {}) {
1816
2214
  ...overrides,
1817
2215
  };
1818
2216
  const cmd = new Command('gate').description('Resolve and manage workflow gate configuration');
2217
+ cmd.addCommand(createGateRouteCommand({
2218
+ buildCommandContext: dependencies.buildCommandContext,
2219
+ processEnv: dependencies.processEnv,
2220
+ }));
1819
2221
  cmd
1820
2222
  .command('resolve')
1821
2223
  .description('Print the resolved gate configuration for a skill')
@@ -1849,11 +2251,13 @@ export function createGateCommand(overrides = {}) {
1849
2251
  });
1850
2252
  cmd
1851
2253
  .command('cross-provider-exec')
2254
+ .alias('exec')
1852
2255
  .description('Run a prompt through an alternate configured runtime target')
1853
2256
  .option('--target <id>', 'Run this exact exec target')
1854
2257
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1855
2258
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1856
2259
  .option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
2260
+ .option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1857
2261
  .argument('<prompt...>', 'Prompt arguments appended to the target command')
1858
2262
  .action(async (prompt, options, command) => {
1859
2263
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
@@ -1866,6 +2270,7 @@ export function createGateCommand(overrides = {}) {
1866
2270
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1867
2271
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1868
2272
  .option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
2273
+ .option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1869
2274
  .option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
1870
2275
  .option('--review-scope <scope>', 'Review scope hint for the provider')
1871
2276
  .option('--review-type <type>', 'Review type hint for the provider')
@@ -1887,6 +2292,7 @@ export function createGateCommand(overrides = {}) {
1887
2292
  .option('--invocation-model <model>', 'Configured invocation model or provider-default')
1888
2293
  .option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
1889
2294
  .option('--priority <number>', 'Target priority, higher wins')
2295
+ .option('--timeout-ms <milliseconds>', `Target gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
1890
2296
  .option('--disable', 'Disable this exec target in the selected layer')
1891
2297
  .option('--layer <layer>', 'Config layer to write: shared, local, or user')
1892
2298
  .action(async (targetId, options, command) => {