@open-agent-toolkit/cli 0.1.66 → 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 (65) hide show
  1. package/assets/agents/oat-reviewer.md +0 -1
  2. package/assets/docs/cli-utilities/configuration.md +27 -0
  3. package/assets/docs/cli-utilities/index.md +1 -1
  4. package/assets/docs/cli-utilities/workflow-gates.md +147 -5
  5. package/assets/docs/reference/cli-reference.md +5 -1
  6. package/assets/docs/workflows/projects/dispatch-ceiling.md +25 -8
  7. package/assets/docs/workflows/projects/orchestration-model.md +21 -0
  8. package/assets/docs/workflows/projects/pr-flow.md +12 -1
  9. package/assets/docs/workflows/projects/review-flavors.md +9 -0
  10. package/assets/docs/workflows/projects/reviews.md +21 -1
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/oat-dispatch-subagents/SKILL.md +21 -1
  13. package/assets/skills/oat-dispatch-subagents/references/provider-claude.md +21 -0
  14. package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +16 -0
  15. package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +21 -0
  16. package/assets/skills/oat-project-autonomous/SKILL.md +1 -1
  17. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -2
  18. package/assets/skills/oat-project-complete/SKILL.md +20 -2
  19. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -2
  20. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  21. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +12 -1
  22. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +24 -9
  23. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -2
  24. package/assets/skills/oat-project-implement/references/phase-execution.md +5 -2
  25. package/assets/skills/oat-project-next/SKILL.md +21 -6
  26. package/assets/skills/oat-project-plan/SKILL.md +6 -1
  27. package/assets/skills/oat-project-plan-writing/SKILL.md +41 -34
  28. package/assets/skills/oat-project-pr-final/SKILL.md +20 -5
  29. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -2
  30. package/assets/skills/oat-project-pr-progress/SKILL.md +18 -3
  31. package/assets/skills/oat-project-progress/SKILL.md +4 -1
  32. package/assets/skills/oat-project-quick-start/SKILL.md +6 -1
  33. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -2
  34. package/assets/skills/oat-project-review-provide/SKILL.md +68 -2
  35. package/assets/skills/oat-project-review-receive/SKILL.md +46 -18
  36. package/assets/skills/oat-project-review-receive-remote/SKILL.md +41 -8
  37. package/dist/commands/cleanup/project/project.utils.d.ts.map +1 -1
  38. package/dist/commands/cleanup/project/project.utils.js +19 -1
  39. package/dist/commands/config/index.d.ts.map +1 -1
  40. package/dist/commands/config/index.js +65 -1
  41. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts +3 -0
  42. package/dist/commands/gate/__fixtures__/fake-runtime.d.mts.map +1 -0
  43. package/dist/commands/gate/__fixtures__/fake-runtime.mjs +167 -0
  44. package/dist/commands/gate/activity-probes.d.ts +35 -0
  45. package/dist/commands/gate/activity-probes.d.ts.map +1 -0
  46. package/dist/commands/gate/activity-probes.js +135 -0
  47. package/dist/commands/gate/branch-local-cli.d.ts +31 -0
  48. package/dist/commands/gate/branch-local-cli.d.ts.map +1 -0
  49. package/dist/commands/gate/branch-local-cli.js +116 -0
  50. package/dist/commands/gate/index.d.ts +31 -0
  51. package/dist/commands/gate/index.d.ts.map +1 -1
  52. package/dist/commands/gate/index.js +465 -31
  53. package/dist/commands/gate/route.d.ts +22 -0
  54. package/dist/commands/gate/route.d.ts.map +1 -0
  55. package/dist/commands/gate/route.js +109 -0
  56. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  57. package/dist/commands/project/dispatch-ceiling/index.js +100 -16
  58. package/dist/commands/review/latest.d.ts +1 -0
  59. package/dist/commands/review/latest.d.ts.map +1 -1
  60. package/dist/commands/review/latest.js +96 -22
  61. package/dist/config/oat-config.d.ts +9 -0
  62. package/dist/config/oat-config.d.ts.map +1 -1
  63. package/dist/config/oat-config.js +23 -0
  64. package/dist/config/resolve.js +7 -0
  65. 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,
@@ -201,6 +241,14 @@ async function runChildProcess(command, args, options) {
201
241
  return new Promise((resolve, reject) => {
202
242
  let timedOut = false;
203
243
  let killTimeout = null;
244
+ let stderrBytes = 0;
245
+ let stdoutBytes = 0;
246
+ let refusal;
247
+ let stdoutLineBuffer = '';
248
+ let stderrLineBuffer = '';
249
+ let latestActivityEvidence;
250
+ let latestActivityProbeStatus;
251
+ let livenessProbePending = false;
204
252
  const startedAt = Date.now();
205
253
  let lastActivityAt = startedAt;
206
254
  const child = spawn(command, args, {
@@ -213,21 +261,71 @@ async function runChildProcess(command, args, options) {
213
261
  const recordActivity = () => {
214
262
  lastActivityAt = Date.now();
215
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
+ };
216
278
  child.stdout?.on('data', (chunk) => {
279
+ stdoutBytes += chunk.byteLength;
280
+ stdoutLineBuffer = scanChunk(stdoutLineBuffer, chunk);
217
281
  recordActivity();
218
282
  process.stdout.write(chunk);
219
283
  });
220
284
  child.stderr?.on('data', (chunk) => {
285
+ stderrBytes += chunk.byteLength;
286
+ stderrLineBuffer = scanChunk(stderrLineBuffer, chunk);
221
287
  recordActivity();
222
288
  process.stderr.write(chunk);
223
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
+ };
224
301
  const livenessInterval = options.onLiveness && options.livenessIntervalMs
225
302
  ? setInterval(() => {
303
+ if (livenessProbePending)
304
+ return;
305
+ livenessProbePending = true;
226
306
  const now = Date.now();
227
- options.onLiveness?.({
228
- elapsedMs: now - startedAt,
229
- hardBudgetMs: options.timeoutMs,
230
- 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;
231
329
  });
232
330
  }, options.livenessIntervalMs)
233
331
  : null;
@@ -259,13 +357,29 @@ async function runChildProcess(command, args, options) {
259
357
  if (killTimeout) {
260
358
  clearTimeout(killTimeout);
261
359
  }
360
+ refusal ??=
361
+ extractStructuredRefusal(stdoutLineBuffer.replace(/\r$/, '')) ??
362
+ extractStructuredRefusal(stderrLineBuffer.replace(/\r$/, ''));
262
363
  resolve({
364
+ ...(latestActivityEvidence
365
+ ? { activityEvidence: latestActivityEvidence }
366
+ : {}),
367
+ ...(latestActivityProbeStatus
368
+ ? { activityProbeStatus: latestActivityProbeStatus }
369
+ : {}),
263
370
  exitCode: timedOut ? 124 : (code ?? 1),
371
+ ...(refusal ? { refusal } : {}),
372
+ stderrBytes,
373
+ stdoutBytes,
264
374
  ...(timedOut ? { timedOut: true } : {}),
265
375
  });
266
376
  });
267
377
  });
268
378
  }
379
+ function extractStructuredRefusal(output) {
380
+ const match = output.match(/^OAT_GATE_REFUSAL: (.*)$/m);
381
+ return match?.[1]?.replace(/\r$/, '');
382
+ }
269
383
  function isGateWriteLayer(value) {
270
384
  return VALID_WRITE_LAYERS.includes(value);
271
385
  }
@@ -347,17 +461,131 @@ function parseNumericFlag(value, flag, defaultValue) {
347
461
  }
348
462
  return parsed;
349
463
  }
350
- function resolveGateExecTimeoutMs(env) {
351
- const rawValue = env.OAT_GATE_EXEC_TIMEOUT_MS?.trim();
352
- if (!rawValue) {
353
- return GATE_EXEC_TIMEOUT_MS;
354
- }
355
- const parsed = Number(rawValue);
356
- if (!Number.isInteger(parsed) || parsed < 1) {
357
- 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}.`);
358
468
  }
359
469
  return parsed;
360
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
+ }
361
589
  function resolveGateLivenessIntervalMs(env) {
362
590
  const rawValue = env.OAT_GATE_LIVENESS_INTERVAL_MS?.trim();
363
591
  if (!rawValue) {
@@ -425,6 +653,9 @@ function parseExecTargetConfig(options) {
425
653
  ...(options.priority !== undefined
426
654
  ? { priority: parseNumericFlag(options.priority, '--priority', 0) }
427
655
  : {}),
656
+ ...(options.timeoutMs !== undefined
657
+ ? { timeoutMs: parseGateTimeoutFlag(options.timeoutMs, '--timeout-ms') }
658
+ : {}),
428
659
  ...(options.invocationModel !== undefined ||
429
660
  options.invocationReasoningEffort !== undefined
430
661
  ? {
@@ -867,7 +1098,7 @@ async function resolveSelectedExecTarget(targets, options, producerIdentity, con
867
1098
  }
868
1099
  return attachDiversityMetadata(selected, avoid, producerIdentity);
869
1100
  }
870
- async function executeTarget(selected, prompt, context, dependencies) {
1101
+ async function executeTarget(selected, prompt, context, dependencies, timeout) {
871
1102
  const [command, baseArgs] = argvHead(selected.target.baseCommand);
872
1103
  if (!command) {
873
1104
  throw new Error(`Exec target "${selected.id}" has an empty base command.`);
@@ -875,21 +1106,39 @@ async function executeTarget(selected, prompt, context, dependencies) {
875
1106
  const modelArgs = selected.model && !findPinnedModelArg(selected.target.baseCommand)
876
1107
  ? ['--model', selected.model]
877
1108
  : [];
878
- const timeoutMs = resolveGateExecTimeoutMs(dependencies.processEnv);
879
1109
  const livenessIntervalMs = resolveGateLivenessIntervalMs(dependencies.processEnv);
880
- if (!context.json) {
881
- 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}).`);
882
1127
  }
883
1128
  try {
884
1129
  return await dependencies.runProcess(command, [...baseArgs, ...modelArgs, ...prompt], {
885
1130
  cwd: context.cwd,
886
1131
  env: dependencies.processEnv,
1132
+ ...(activityProbe ? { activityProbe } : {}),
887
1133
  livenessIntervalMs,
888
- onLiveness: ({ elapsedMs, hardBudgetMs, idleMs }) => {
1134
+ onLiveness: ({ elapsedMs, hardBudgetMs, idleMs, processAlive, activityProbeStatus, lastActivityEvidence, }) => {
889
1135
  const telemetry = {
890
1136
  elapsedMs,
891
1137
  hardBudgetMs,
892
1138
  idleMs,
1139
+ processAlive,
1140
+ ...(activityProbeStatus ? { activityProbeStatus } : {}),
1141
+ ...(lastActivityEvidence ? { lastActivityEvidence } : {}),
893
1142
  target: selected.id,
894
1143
  type: 'gate-liveness',
895
1144
  };
@@ -897,13 +1146,18 @@ async function executeTarget(selected, prompt, context, dependencies) {
897
1146
  dependencies.writeDiagnostic(`${JSON.stringify(telemetry)}\n`);
898
1147
  }
899
1148
  else {
900
- 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}.`);
901
1155
  }
902
1156
  },
903
1157
  purpose: 'execute',
904
1158
  stdin: 'ignore',
905
1159
  stdio: 'pipe',
906
- timeoutMs,
1160
+ timeoutMs: timeout.timeoutMs,
907
1161
  });
908
1162
  }
909
1163
  catch (error) {
@@ -1277,9 +1531,11 @@ function writeReviewGateResult(context, payload) {
1277
1531
  context.logger.info(payload.handoff);
1278
1532
  }
1279
1533
  function writeReviewGateExecutionFailure(context, payload) {
1280
- const message = payload.timedOut
1281
- ? `Review did not complete: target ${payload.target} timed out after ${payload.timeoutMs}ms.`
1282
- : `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}.`;
1283
1539
  if (context.json) {
1284
1540
  context.logger.json({
1285
1541
  status: 'review_failed',
@@ -1298,6 +1554,16 @@ function writeReviewGateExecutionFailure(context, payload) {
1298
1554
  ...(payload.timeoutMs !== undefined
1299
1555
  ? { timeoutMs: payload.timeoutMs }
1300
1556
  : {}),
1557
+ ...(payload.timeoutSource !== undefined
1558
+ ? { timeoutSource: payload.timeoutSource }
1559
+ : {}),
1560
+ ...(payload.noOutputProduced !== undefined
1561
+ ? { noOutputProduced: payload.noOutputProduced }
1562
+ : {}),
1563
+ ...(payload.refusal ? { refusal: payload.refusal } : {}),
1564
+ ...(payload.activityEvidence
1565
+ ? { activityEvidence: payload.activityEvidence }
1566
+ : {}),
1301
1567
  message,
1302
1568
  });
1303
1569
  return;
@@ -1517,7 +1783,21 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1517
1783
  const producerIdentity = parseProducerIdentityOption(options.producerIdentity);
1518
1784
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1519
1785
  logGateDiversity(selected, context);
1520
- 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);
1521
1801
  process.exitCode = result.exitCode;
1522
1802
  }
1523
1803
  catch (error) {
@@ -1526,6 +1806,9 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
1526
1806
  }
1527
1807
  async function runReviewGate(prompt, options, context, dependencies) {
1528
1808
  const runId = randomUUID();
1809
+ let runMarkerPath;
1810
+ let runMarkerWritten = false;
1811
+ let branchLocalGateCli;
1529
1812
  let postSelectionContext;
1530
1813
  try {
1531
1814
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
@@ -1546,6 +1829,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
1546
1829
  });
1547
1830
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1548
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
+ });
1549
1848
  const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
1550
1849
  postSelectionContext = {
1551
1850
  project: projectPath,
@@ -1572,9 +1871,61 @@ async function runReviewGate(prompt, options, context, dependencies) {
1572
1871
  : []),
1573
1872
  prompt.join(' '),
1574
1873
  ]);
1575
- 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`);
1576
1922
  const childExitCode = childResult.exitCode;
1577
- if (childExitCode !== 0) {
1923
+ const refusal = childResult.refusal ??
1924
+ extractStructuredRefusal(childResult.capturedOutput ?? '');
1925
+ const writeRefusalFailure = () => {
1926
+ if (!refusal) {
1927
+ return false;
1928
+ }
1578
1929
  writeReviewGateExecutionFailure(context, {
1579
1930
  runId,
1580
1931
  target: selected.id,
@@ -1582,13 +1933,18 @@ async function runReviewGate(prompt, options, context, dependencies) {
1582
1933
  projectResolutionSource: reviewProject.source,
1583
1934
  exitCode: childExitCode,
1584
1935
  timedOut: childResult.timedOut ?? false,
1585
- timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
1936
+ timeoutMs: timeout.timeoutMs,
1937
+ timeoutSource: timeout.source,
1938
+ refusal,
1939
+ ...(childResult.activityEvidence
1940
+ ? { activityEvidence: childResult.activityEvidence }
1941
+ : {}),
1586
1942
  gateInvocation,
1587
1943
  dispatchReport,
1588
1944
  });
1589
- process.exitCode = childExitCode;
1590
- return;
1591
- }
1945
+ process.exitCode = 1;
1946
+ return true;
1947
+ };
1592
1948
  const after = await listReviewGateArtifactCandidates({
1593
1949
  repoRoot,
1594
1950
  effective,
@@ -1599,6 +1955,52 @@ async function runReviewGate(prompt, options, context, dependencies) {
1599
1955
  before,
1600
1956
  after,
1601
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
+ }
1982
+ if (childResult.timedOut &&
1983
+ artifactResolution.matchingArtifactPaths.length === 0 &&
1984
+ !artifactResolution.diagnosticArtifact) {
1985
+ writeReviewGateExecutionFailure(context, {
1986
+ runId,
1987
+ target: selected.id,
1988
+ project: projectPath,
1989
+ projectResolutionSource: reviewProject.source,
1990
+ exitCode: childExitCode,
1991
+ timedOut: true,
1992
+ timeoutMs: timeout.timeoutMs,
1993
+ timeoutSource: timeout.source,
1994
+ noOutputProduced: childResult.stdoutBytes + childResult.stderrBytes === 0,
1995
+ ...(childResult.activityEvidence
1996
+ ? { activityEvidence: childResult.activityEvidence }
1997
+ : {}),
1998
+ gateInvocation,
1999
+ dispatchReport,
2000
+ });
2001
+ process.exitCode = childExitCode;
2002
+ return;
2003
+ }
1602
2004
  const initialTargetCorroboration = corroborateReviewTarget({
1603
2005
  repoRoot,
1604
2006
  reviewProject,
@@ -1633,6 +2035,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1633
2035
  if (producedArtifact.containingProject !== reviewProject.path ||
1634
2036
  (reviewProject.source === 'declared' &&
1635
2037
  initialTargetCorroboration.project !== 'matched')) {
2038
+ if (writeRefusalFailure()) {
2039
+ return;
2040
+ }
1636
2041
  writeReviewGateTargetingFailure(context, {
1637
2042
  runId,
1638
2043
  target: selected.id,
@@ -1654,6 +2059,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1654
2059
  }
1655
2060
  if (!producedArtifact.generatedAt ||
1656
2061
  !Number.isFinite(producedArtifact.generatedTime)) {
2062
+ if (writeRefusalFailure()) {
2063
+ return;
2064
+ }
1657
2065
  writeReviewGateArtifactValidationFailure(context, {
1658
2066
  runId,
1659
2067
  target: selected.id,
@@ -1681,6 +2089,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1681
2089
  });
1682
2090
  }
1683
2091
  catch (error) {
2092
+ if (writeRefusalFailure()) {
2093
+ return;
2094
+ }
1684
2095
  const detail = error instanceof Error ? error.message : String(error);
1685
2096
  writeReviewGateArtifactValidationFailure(context, {
1686
2097
  runId,
@@ -1702,6 +2113,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1702
2113
  const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
1703
2114
  if (corroboration.run !== 'matched' ||
1704
2115
  corroboration.invocation !== 'matched') {
2116
+ if (writeRefusalFailure()) {
2117
+ return;
2118
+ }
1705
2119
  const missing = corroboration.run === 'missing' ||
1706
2120
  corroboration.invocation === 'missing';
1707
2121
  writeReviewGateArtifactValidationFailure(context, {
@@ -1723,6 +2137,9 @@ async function runReviewGate(prompt, options, context, dependencies) {
1723
2137
  return;
1724
2138
  }
1725
2139
  if (verdict.invocation !== 'gate') {
2140
+ if (writeRefusalFailure()) {
2141
+ return;
2142
+ }
1726
2143
  writeReviewGateArtifactValidationFailure(context, {
1727
2144
  runId,
1728
2145
  target: selected.id,
@@ -1766,6 +2183,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1766
2183
  gateInvocation,
1767
2184
  dispatchReport,
1768
2185
  corroboration,
2186
+ ...(childResult.timedOut ? { lateCompletion: true } : {}),
1769
2187
  });
1770
2188
  process.exitCode = blocking ? 1 : 0;
1771
2189
  }
@@ -1781,6 +2199,14 @@ async function runReviewGate(prompt, options, context, dependencies) {
1781
2199
  writeError(context, error);
1782
2200
  }
1783
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
+ }
1784
2210
  }
1785
2211
  export function createGateCommand(overrides = {}) {
1786
2212
  const dependencies = {
@@ -1788,6 +2214,10 @@ export function createGateCommand(overrides = {}) {
1788
2214
  ...overrides,
1789
2215
  };
1790
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
+ }));
1791
2221
  cmd
1792
2222
  .command('resolve')
1793
2223
  .description('Print the resolved gate configuration for a skill')
@@ -1821,11 +2251,13 @@ export function createGateCommand(overrides = {}) {
1821
2251
  });
1822
2252
  cmd
1823
2253
  .command('cross-provider-exec')
2254
+ .alias('exec')
1824
2255
  .description('Run a prompt through an alternate configured runtime target')
1825
2256
  .option('--target <id>', 'Run this exact exec target')
1826
2257
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1827
2258
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1828
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})`)
1829
2261
  .argument('<prompt...>', 'Prompt arguments appended to the target command')
1830
2262
  .action(async (prompt, options, command) => {
1831
2263
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
@@ -1838,6 +2270,7 @@ export function createGateCommand(overrides = {}) {
1838
2270
  .option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
1839
2271
  .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
1840
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})`)
1841
2274
  .option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
1842
2275
  .option('--review-scope <scope>', 'Review scope hint for the provider')
1843
2276
  .option('--review-type <type>', 'Review type hint for the provider')
@@ -1859,6 +2292,7 @@ export function createGateCommand(overrides = {}) {
1859
2292
  .option('--invocation-model <model>', 'Configured invocation model or provider-default')
1860
2293
  .option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
1861
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})`)
1862
2296
  .option('--disable', 'Disable this exec target in the selected layer')
1863
2297
  .option('--layer <layer>', 'Config layer to write: shared, local, or user')
1864
2298
  .action(async (targetId, options, command) => {