agentxchain 2.109.0 → 2.111.0

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.
@@ -44,6 +44,7 @@ import {
44
44
  getDispatchTurnDir,
45
45
  getTurnStagingResultPath,
46
46
  } from '../lib/turn-paths.js';
47
+ import { resolveChainOptions, executeChainedRun } from '../lib/run-chain.js';
47
48
 
48
49
  export async function runCommand(opts) {
49
50
  const context = loadProjectContext();
@@ -52,6 +53,14 @@ export async function runCommand(opts) {
52
53
  process.exit(1);
53
54
  }
54
55
 
56
+ const chainOpts = resolveChainOptions(opts, context.config);
57
+ if (chainOpts.enabled) {
58
+ console.log(chalk.cyan.bold('agentxchain run --chain'));
59
+ console.log(chalk.dim(` Chain mode: enabled (max ${chainOpts.maxChains} continuations, on: ${chainOpts.chainOn.join(',')}, cooldown: ${chainOpts.cooldownSeconds}s)`));
60
+ const { exitCode } = await executeChainedRun(context, opts, chainOpts, executeGovernedRun);
61
+ process.exit(exitCode);
62
+ }
63
+
55
64
  const execution = await executeGovernedRun(context, opts);
56
65
  process.exit(execution.exitCode);
57
66
  }
@@ -167,8 +176,7 @@ export async function executeGovernedRun(context, opts = {}) {
167
176
  let aborted = false;
168
177
  let sigintCount = 0;
169
178
  const controller = new AbortController();
170
-
171
- process.on('SIGINT', () => {
179
+ const onSigint = () => {
172
180
  sigintCount++;
173
181
  if (sigintCount >= 2) {
174
182
  process.exit(130);
@@ -176,37 +184,39 @@ export async function executeGovernedRun(context, opts = {}) {
176
184
  aborted = true;
177
185
  controller.abort();
178
186
  log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
179
- });
187
+ };
188
+ process.on('SIGINT', onSigint);
180
189
 
181
- // ── Run header ──────────────────────────────────────────────────────────
182
- log(chalk.cyan.bold('agentxchain run'));
183
- log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
184
- if (provenance) {
185
- const provenanceSummary = summarizeRunProvenance(provenance);
186
- if (provenanceSummary) {
187
- log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
190
+ try {
191
+ // ── Run header ──────────────────────────────────────────────────────────
192
+ log(chalk.cyan.bold('agentxchain run'));
193
+ log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
194
+ if (provenance) {
195
+ const provenanceSummary = summarizeRunProvenance(provenance);
196
+ if (provenanceSummary) {
197
+ log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
198
+ }
188
199
  }
189
- }
190
- if (inheritedContext) {
191
- const ic = inheritedContext;
192
- const phasesCount = ic.parent_phases_completed?.length || 0;
193
- const decisionsCount = ic.recent_decisions?.length || 0;
194
- const turnsCount = ic.recent_accepted_turns?.length || 0;
195
- const parts = [];
196
- if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
197
- if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
198
- if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
199
- const detail = parts.length ? `${parts.join(', ')}` : '';
200
- log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
201
- }
202
- log('');
200
+ if (inheritedContext) {
201
+ const ic = inheritedContext;
202
+ const phasesCount = ic.parent_phases_completed?.length || 0;
203
+ const decisionsCount = ic.recent_decisions?.length || 0;
204
+ const turnsCount = ic.recent_accepted_turns?.length || 0;
205
+ const parts = [];
206
+ if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
207
+ if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
208
+ if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
209
+ const detail = parts.length ? ` ${parts.join(', ')}` : '';
210
+ log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
211
+ }
212
+ log('');
203
213
 
204
- // ── Track first-call for --role override ────────────────────────────────
205
- let firstSelectRole = true;
206
- let qaMissingCredentialsFallback = null;
214
+ // ── Track first-call for --role override ────────────────────────────────
215
+ let firstSelectRole = true;
216
+ let qaMissingCredentialsFallback = null;
207
217
 
208
- // ── Callbacks ───────────────────────────────────────────────────────────
209
- const callbacks = {
218
+ // ── Callbacks ───────────────────────────────────────────────────────────
219
+ const callbacks = {
210
220
  selectRole(state, cfg) {
211
221
  if (aborted) return null;
212
222
 
@@ -407,84 +417,87 @@ export async function executeGovernedRun(context, opts = {}) {
407
417
  break;
408
418
  }
409
419
  },
410
- };
411
-
412
- // ── Execute ─────────────────────────────────────────────────────────────
413
- const runLoopOpts = {
414
- maxTurns,
415
- startNewRunFromCompleted: true,
416
- startNewRunFromBlocked: opts.allowBlockedRestart ?? Boolean(provenance),
417
- };
418
- if (provenance) runLoopOpts.provenance = provenance;
419
- if (inheritedContext) runLoopOpts.inheritedContext = inheritedContext;
420
- const result = await runLoop(root, config, callbacks, runLoopOpts);
420
+ };
421
421
 
422
- // ── Summary ─────────────────────────────────────────────────────────────
423
- log('');
424
- log(chalk.dim('─── Run Summary ───'));
425
- log(` Status: ${result.ok ? chalk.green('completed') : chalk.yellow(result.stop_reason)}`);
426
- log(` Turns: ${result.turns_executed}`);
427
- log(` Gates: ${result.gates_approved} approved`);
428
- log(` Errors: ${result.errors.length ? chalk.red(result.errors.length) : 'none'}`);
429
-
430
- if (result.errors.length) {
431
- for (const err of result.errors) {
432
- log(chalk.red(` ${err}`));
422
+ // ── Execute ─────────────────────────────────────────────────────────────
423
+ const runLoopOpts = {
424
+ maxTurns,
425
+ startNewRunFromCompleted: true,
426
+ startNewRunFromBlocked: opts.allowBlockedRestart ?? Boolean(provenance),
427
+ };
428
+ if (provenance) runLoopOpts.provenance = provenance;
429
+ if (inheritedContext) runLoopOpts.inheritedContext = inheritedContext;
430
+ const result = await runLoop(root, config, callbacks, runLoopOpts);
431
+
432
+ // ── Summary ─────────────────────────────────────────────────────────────
433
+ log('');
434
+ log(chalk.dim('─── Run Summary ───'));
435
+ log(` Status: ${result.ok ? chalk.green('completed') : chalk.yellow(result.stop_reason)}`);
436
+ log(` Turns: ${result.turns_executed}`);
437
+ log(` Gates: ${result.gates_approved} approved`);
438
+ log(` Errors: ${result.errors.length ? chalk.red(result.errors.length) : 'none'}`);
439
+
440
+ if (result.errors.length) {
441
+ for (const err of result.errors) {
442
+ log(chalk.red(` ${err}`));
443
+ }
433
444
  }
434
- }
435
445
 
436
- if (qaMissingCredentialsFallback) {
437
- printManualQaFallback(log);
438
- }
446
+ if (qaMissingCredentialsFallback) {
447
+ printManualQaFallback(log);
448
+ }
439
449
 
440
- // Recovery guidance for blocked/rejected states
441
- if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
442
- const recovery = deriveRecoveryDescriptor(result.state, config);
443
- if (recovery) {
444
- log('');
445
- log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
446
- log(chalk.dim(` Action: ${recovery.recovery_action}`));
447
- if (recovery.detail) {
448
- log(chalk.dim(` Detail: ${recovery.detail}`));
450
+ // Recovery guidance for blocked/rejected states
451
+ if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
452
+ const recovery = deriveRecoveryDescriptor(result.state, config);
453
+ if (recovery) {
454
+ log('');
455
+ log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
456
+ log(chalk.dim(` Action: ${recovery.recovery_action}`));
457
+ if (recovery.detail) {
458
+ log(chalk.dim(` Detail: ${recovery.detail}`));
459
+ }
449
460
  }
450
461
  }
451
- }
452
-
453
- // ── Auto governance report ──────────────────────────────────────────────
454
- if (opts.report !== false && result.state) {
455
- try {
456
- const reportsDir = join(root, '.agentxchain', 'reports');
457
- mkdirSync(reportsDir, { recursive: true });
458
-
459
- const exportResult = buildRunExport(root);
460
- if (exportResult.ok) {
461
- const runId = result.state.run_id || 'unknown';
462
- const exportPath = join(reportsDir, `export-${runId}.json`);
463
- writeFileSync(exportPath, JSON.stringify(exportResult.export, null, 2));
464
-
465
- const reportResult = buildGovernanceReport(exportResult.export, { input: exportPath });
466
- const reportPath = join(reportsDir, `report-${runId}.md`);
467
- writeFileSync(reportPath, formatGovernanceReportMarkdown(reportResult.report));
468
462
 
469
- log('');
470
- log(chalk.dim(` Governance report: .agentxchain/reports/report-${runId}.md`));
471
- } else {
472
- log(chalk.dim(` Governance report skipped: ${exportResult.error}`));
463
+ // ── Auto governance report ──────────────────────────────────────────────
464
+ if (opts.report !== false && result.state) {
465
+ try {
466
+ const reportsDir = join(root, '.agentxchain', 'reports');
467
+ mkdirSync(reportsDir, { recursive: true });
468
+
469
+ const exportResult = buildRunExport(root);
470
+ if (exportResult.ok) {
471
+ const runId = result.state.run_id || 'unknown';
472
+ const exportPath = join(reportsDir, `export-${runId}.json`);
473
+ writeFileSync(exportPath, JSON.stringify(exportResult.export, null, 2));
474
+
475
+ const reportResult = buildGovernanceReport(exportResult.export, { input: exportPath });
476
+ const reportPath = join(reportsDir, `report-${runId}.md`);
477
+ writeFileSync(reportPath, formatGovernanceReportMarkdown(reportResult.report));
478
+
479
+ log('');
480
+ log(chalk.dim(` Governance report: .agentxchain/reports/report-${runId}.md`));
481
+ } else {
482
+ log(chalk.dim(` Governance report skipped: ${exportResult.error}`));
483
+ }
484
+ } catch (err) {
485
+ log(chalk.dim(` Governance report failed: ${err.message}`));
473
486
  }
474
- } catch (err) {
475
- log(chalk.dim(` Governance report failed: ${err.message}`));
476
487
  }
477
- }
478
488
 
479
- // ── Exit code ───────────────────────────────────────────────────────────
480
- const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
481
- return {
482
- exitCode: result.ok || successReasons.has(result.stop_reason) ? 0 : 1,
483
- result,
484
- skipped: false,
485
- skipReason: null,
486
- provenance: provenance || null,
487
- };
489
+ // ── Exit code ───────────────────────────────────────────────────────────
490
+ const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
491
+ return {
492
+ exitCode: result.ok || successReasons.has(result.stop_reason) ? 0 : 1,
493
+ result,
494
+ skipped: false,
495
+ skipReason: null,
496
+ provenance: provenance || null,
497
+ };
498
+ } finally {
499
+ process.removeListener('SIGINT', onSigint);
500
+ }
488
501
  }
489
502
 
490
503
  // ── Helpers ───────────────────────────────────────────────────────────────
@@ -358,9 +358,9 @@ function renderGovernedStatus(context, opts) {
358
358
  for (const action of gateActionAttempt.actions) {
359
359
  const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
360
360
  const outcome = action.status === 'failed'
361
- ? chalk.red('failed')
361
+ ? (action.timed_out ? chalk.red(`timed out after ${action.timeout_ms}ms`) : chalk.red('failed'))
362
362
  : chalk.green('succeeded');
363
- const exit = action.exit_code == null ? '' : ` (exit ${action.exit_code})`;
363
+ const exit = action.timed_out ? '' : (action.exit_code == null ? '' : ` (exit ${action.exit_code})`);
364
364
  console.log(` ${action.action_index || '?'}. ${label} — ${outcome}${exit}`);
365
365
  if (action.status === 'failed' && action.stderr_tail) {
366
366
  console.log(` ${chalk.dim(action.stderr_tail)}`);
@@ -0,0 +1,54 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function getChainReportsDir(root) {
5
+ return join(root, '.agentxchain', 'reports');
6
+ }
7
+
8
+ export function loadAllChainReports(root) {
9
+ const reportsDir = getChainReportsDir(root);
10
+ if (!existsSync(reportsDir)) return [];
11
+
12
+ const files = readdirSync(reportsDir)
13
+ .filter((file) => file.startsWith('chain-') && file.endsWith('.json'))
14
+ .sort()
15
+ .reverse();
16
+
17
+ const reports = [];
18
+ for (const file of files) {
19
+ try {
20
+ const content = readFileSync(join(reportsDir, file), 'utf8');
21
+ reports.push(JSON.parse(content));
22
+ } catch {
23
+ // Advisory artifact only. Skip malformed files instead of failing the surface.
24
+ }
25
+ }
26
+
27
+ reports.sort((a, b) => {
28
+ const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
29
+ const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
30
+ return bTime - aTime;
31
+ });
32
+
33
+ return reports;
34
+ }
35
+
36
+ export function loadLatestChainReport(root) {
37
+ const reports = loadAllChainReports(root);
38
+ return reports.length > 0 ? reports[0] : null;
39
+ }
40
+
41
+ export function loadChainReport(root, chainId) {
42
+ const reportsDir = getChainReportsDir(root);
43
+ const exactPath = join(reportsDir, `${chainId}.json`);
44
+ if (existsSync(exactPath)) {
45
+ try {
46
+ return JSON.parse(readFileSync(exactPath, 'utf8'));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ const reports = loadAllChainReports(root);
53
+ return reports.find((report) => report.chain_id === chainId) || null;
54
+ }
@@ -29,6 +29,7 @@ import { queryRunHistory } from '../run-history.js';
29
29
  import { loadProjectContext, loadProjectState } from '../config.js';
30
30
  import { evaluateApprovalSlaReminders } from '../notification-runner.js';
31
31
  import { readGateActionSnapshot } from './gate-action-reader.js';
32
+ import { readChainReportSnapshot } from './chain-report-reader.js';
32
33
 
33
34
  const MIME_TYPES = {
34
35
  '.html': 'text/html; charset=utf-8',
@@ -462,6 +463,13 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
462
463
  return;
463
464
  }
464
465
 
466
+ if (pathname === '/api/chain-reports') {
467
+ const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
468
+ const result = readChainReportSnapshot(workspacePath, { limit });
469
+ writeJson(res, result.status, result.body);
470
+ return;
471
+ }
472
+
465
473
  if (pathname === '/api/gate-actions') {
466
474
  const result = readGateActionSnapshot(workspacePath);
467
475
  writeJson(res, result.status, result.body);
@@ -0,0 +1,15 @@
1
+ import { loadAllChainReports, loadLatestChainReport } from '../chain-reports.js';
2
+
3
+ export function readChainReportSnapshot(workspacePath, { limit } = {}) {
4
+ const reports = loadAllChainReports(workspacePath);
5
+ const effectiveLimit = Number.isInteger(limit) && limit > 0 ? limit : reports.length;
6
+
7
+ return {
8
+ ok: true,
9
+ status: 200,
10
+ body: {
11
+ latest: loadLatestChainReport(workspacePath),
12
+ reports: reports.slice(0, effectiveLimit),
13
+ },
14
+ };
15
+ }
@@ -62,6 +62,7 @@ FILE_TO_RESOURCE[normalizeRelativePath(REPO_DECISIONS_FILE)] = '/api/repo-decisi
62
62
  export const WATCH_DIRECTORIES = [
63
63
  '',
64
64
  MULTIREPO_DIR,
65
+ 'reports',
65
66
  ];
66
67
 
67
68
  export function normalizeRelativePath(filePath) {
@@ -69,7 +70,11 @@ export function normalizeRelativePath(filePath) {
69
70
  }
70
71
 
71
72
  export function resourceForRelativePath(filePath) {
72
- return FILE_TO_RESOURCE[normalizeRelativePath(filePath)] || null;
73
+ const normalized = normalizeRelativePath(filePath);
74
+ if (normalized.startsWith('reports/chain-') && normalized.endsWith('.json')) {
75
+ return '/api/chain-reports';
76
+ }
77
+ return FILE_TO_RESOURCE[normalized] || null;
73
78
  }
74
79
 
75
80
  /**
@@ -216,6 +216,9 @@ function normalizeRunExport(artifact) {
216
216
  budget_warn_mode: budgetStatus.warn_mode === true,
217
217
  budget_exhausted: budgetStatus.exhausted === true,
218
218
  phase_gate_status: normalizeGateStatusMap(phaseGateStatus),
219
+ blocked_category: state.blocked_reason?.category || null,
220
+ blocked_gate_action_timed_out: state.blocked_reason?.gate_action?.timed_out === true,
221
+ blocked_gate_action_timeout_ms: typeof state.blocked_reason?.gate_action?.timeout_ms === 'number' ? state.blocked_reason.gate_action.timeout_ms : null,
219
222
  delegation_missing_decisions: normalizeDelegationMissingMap(summary.delegation_summary),
220
223
  };
221
224
  }
@@ -563,11 +566,17 @@ function detectRunRegressions(left, right) {
563
566
  const leftGate = (left.phase_gate_status || {})[gateId] || null;
564
567
  const rightGate = (right.phase_gate_status || {})[gateId] || null;
565
568
  if (leftGate && rightGate && GATE_PASSED_STATES.has(leftGate) && GATE_FAILED_STATES.has(rightGate)) {
569
+ let causeDetail = '';
570
+ if (right.blocked_category === 'gate_action_failed') {
571
+ causeDetail = right.blocked_gate_action_timed_out
572
+ ? ` (gate action timed out after ${right.blocked_gate_action_timeout_ms}ms)`
573
+ : ' (gate action failed)';
574
+ }
566
575
  regressions.push({
567
576
  id: `REG-GATE-${String(++counter).padStart(3, '0')}`,
568
577
  category: 'gate',
569
578
  severity: 'error',
570
- message: `Gate "${gateId}" regressed from ${leftGate} to ${rightGate}`,
579
+ message: `Gate "${gateId}" regressed from ${leftGate} to ${rightGate}${causeDetail}`,
571
580
  field: `phase_gate_status.${gateId}`,
572
581
  left: leftGate,
573
582
  right: rightGate,
@@ -5,6 +5,16 @@ import { spawnSync } from 'child_process';
5
5
 
6
6
  const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
7
7
  const MAX_OUTPUT_TAIL_CHARS = 1200;
8
+ export const DEFAULT_GATE_ACTION_TIMEOUT_MS = 15 * 60 * 1000;
9
+ const MIN_GATE_ACTION_TIMEOUT_MS = 1000;
10
+ const MAX_GATE_ACTION_TIMEOUT_MS = 60 * 60 * 1000;
11
+
12
+ function normalizeGateActionTimeoutMs(action) {
13
+ if (Number.isInteger(action?.timeout_ms)) {
14
+ return action.timeout_ms;
15
+ }
16
+ return DEFAULT_GATE_ACTION_TIMEOUT_MS;
17
+ }
8
18
 
9
19
  export function validateGateActionsConfig(gates, errors) {
10
20
  if (!gates || typeof gates !== 'object' || Array.isArray(gates)) {
@@ -44,6 +54,15 @@ export function validateGateActionsConfig(gates, errors) {
44
54
  if ('label' in action && (typeof action.label !== 'string' || !action.label.trim())) {
45
55
  errors.push(`${prefix}.label must be a non-empty string when provided`);
46
56
  }
57
+ if ('timeout_ms' in action) {
58
+ if (!Number.isInteger(action.timeout_ms)
59
+ || action.timeout_ms < MIN_GATE_ACTION_TIMEOUT_MS
60
+ || action.timeout_ms > MAX_GATE_ACTION_TIMEOUT_MS) {
61
+ errors.push(
62
+ `${prefix}.timeout_ms must be an integer between ${MIN_GATE_ACTION_TIMEOUT_MS} and ${MAX_GATE_ACTION_TIMEOUT_MS} when provided`,
63
+ );
64
+ }
65
+ }
47
66
  }
48
67
  }
49
68
  }
@@ -60,6 +79,7 @@ export function getGateActions(config, gateId) {
60
79
  index: index + 1,
61
80
  label: typeof action.label === 'string' && action.label.trim() ? action.label.trim() : null,
62
81
  run: action.run.trim(),
82
+ timeout_ms: normalizeGateActionTimeoutMs(action),
63
83
  }));
64
84
  }
65
85
 
@@ -78,6 +98,11 @@ function trimOutputTail(value) {
78
98
  }
79
99
 
80
100
  function buildGateActionEntry(action, meta, runtimeResult, status) {
101
+ const timedOut = runtimeResult?.error?.code === 'ETIMEDOUT';
102
+ const stderrTail = trimOutputTail(runtimeResult?.stderr) || (timedOut ? `Timed out after ${action.timeout_ms}ms` : null);
103
+ const spawnError = timedOut
104
+ ? `Timed out after ${action.timeout_ms}ms`
105
+ : runtimeResult?.error?.message || null;
81
106
  return {
82
107
  type: 'gate_action',
83
108
  timestamp: new Date().toISOString(),
@@ -90,12 +115,14 @@ function buildGateActionEntry(action, meta, runtimeResult, status) {
90
115
  action_index: action.index,
91
116
  action_label: action.label,
92
117
  command: action.run,
118
+ timeout_ms: action.timeout_ms,
93
119
  status,
94
120
  exit_code: Number.isInteger(runtimeResult?.status) ? runtimeResult.status : null,
95
121
  signal: runtimeResult?.signal || null,
96
122
  stdout_tail: trimOutputTail(runtimeResult?.stdout),
97
- stderr_tail: trimOutputTail(runtimeResult?.stderr),
98
- spawn_error: runtimeResult?.error?.message || null,
123
+ stderr_tail: stderrTail,
124
+ spawn_error: spawnError,
125
+ timed_out: timedOut,
99
126
  };
100
127
  }
101
128
 
@@ -125,6 +152,8 @@ export function executeGateActions(root, config, meta, opts = {}) {
125
152
  },
126
153
  encoding: 'utf8',
127
154
  maxBuffer: 10 * 1024 * 1024,
155
+ timeout: action.timeout_ms,
156
+ killSignal: 'SIGTERM',
128
157
  });
129
158
 
130
159
  const status = runtimeResult.error || runtimeResult.status !== 0 ? 'failed' : 'succeeded';
@@ -170,6 +199,8 @@ export function normalizeGateActionEntry(entry) {
170
199
  stdout_tail: entry.stdout_tail || null,
171
200
  stderr_tail: entry.stderr_tail || null,
172
201
  spawn_error: entry.spawn_error || null,
202
+ timeout_ms: Number.isInteger(entry.timeout_ms) ? entry.timeout_ms : null,
203
+ timed_out: entry.timed_out === true,
173
204
  timestamp: entry.timestamp || null,
174
205
  };
175
206
  }
@@ -1281,6 +1281,8 @@ function blockRunForGateActionFailure(root, state, gateFailure, config) {
1281
1281
  : 'agentxchain approve-transition';
1282
1282
  const actionLabel = gateFailure.action_label || gateFailure.command || gateFailure.gate_id || 'gate action';
1283
1283
  const blockedAt = gateFailure.timestamp || new Date().toISOString();
1284
+ const failureVerb = gateFailure.timed_out ? 'timed out' : 'failed';
1285
+ const failureDetail = `Gate action ${failureVerb} for "${gateFailure.gate_id || 'unknown'}": ${actionLabel}`;
1284
1286
  const blockedState = {
1285
1287
  ...state,
1286
1288
  status: 'blocked',
@@ -1289,13 +1291,13 @@ function blockRunForGateActionFailure(root, state, gateFailure, config) {
1289
1291
  category: 'gate_action_failed',
1290
1292
  blocked_at: blockedAt,
1291
1293
  turn_id: gateFailure.requested_by_turn || null,
1292
- detail: `Gate action failed for "${gateFailure.gate_id || 'unknown'}": ${actionLabel}`,
1294
+ detail: failureDetail,
1293
1295
  recovery: {
1294
1296
  typed_reason: 'gate_action_failed',
1295
1297
  owner: 'human',
1296
1298
  recovery_action: recoveryAction,
1297
1299
  turn_retained: false,
1298
- detail: `${gateFailure.gate_id || 'unknown'} action ${gateFailure.action_index || '?'} (${actionLabel})`,
1300
+ detail: `${gateFailure.gate_id || 'unknown'} action ${gateFailure.action_index || '?'} (${actionLabel})${gateFailure.timed_out ? ` timed out after ${gateFailure.timeout_ms}ms` : ''}`,
1299
1301
  },
1300
1302
  gate_action: {
1301
1303
  attempt_id: gateFailure.attempt_id || null,
@@ -1306,6 +1308,8 @@ function blockRunForGateActionFailure(root, state, gateFailure, config) {
1306
1308
  command: gateFailure.command || null,
1307
1309
  exit_code: gateFailure.exit_code ?? null,
1308
1310
  stderr_tail: gateFailure.stderr_tail || null,
1311
+ timeout_ms: gateFailure.timeout_ms ?? null,
1312
+ timed_out: gateFailure.timed_out === true,
1309
1313
  },
1310
1314
  },
1311
1315
  };
package/src/lib/report.js CHANGED
@@ -1444,7 +1444,8 @@ export function formatGovernanceReportText(report) {
1444
1444
  for (const action of run.gate_actions) {
1445
1445
  const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
1446
1446
  const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
1447
- lines.push(` - ${action.gate_id || 'unknown'} | ${action.gate_type || 'unknown'} | action ${action.action_index || '?'} | ${action.status} | ${label} | exit: ${exit} | at: ${action.timestamp || 'n/a'}`);
1447
+ const timeoutTag = action.timed_out ? ` | timed_out after ${action.timeout_ms}ms` : '';
1448
+ lines.push(` - ${action.gate_id || 'unknown'} | ${action.gate_type || 'unknown'} | action ${action.action_index || '?'} | ${action.status} | ${label} | exit: ${exit}${timeoutTag} | at: ${action.timestamp || 'n/a'}`);
1448
1449
  if (action.stderr_tail) {
1449
1450
  lines.push(` stderr: ${action.stderr_tail}`);
1450
1451
  }
@@ -2014,7 +2015,8 @@ export function formatGovernanceReportMarkdown(report) {
2014
2015
  for (const action of run.gate_actions) {
2015
2016
  const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
2016
2017
  const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
2017
- lines.push(`- \`${action.gate_id || 'unknown'}\` (${action.gate_type || 'unknown'}) action ${action.action_index || '?'} — **${action.status}** at \`${action.timestamp || 'n/a'}\`: ${label} (exit \`${exit}\`)`);
2018
+ const mdTimeout = action.timed_out ? ` timed out after ${action.timeout_ms}ms` : '';
2019
+ lines.push(`- \`${action.gate_id || 'unknown'}\` (${action.gate_type || 'unknown'}) action ${action.action_index || '?'} — **${action.status}** at \`${action.timestamp || 'n/a'}\`: ${label} (exit \`${exit}\`)${mdTimeout}`);
2018
2020
  if (action.stderr_tail) {
2019
2021
  lines.push(` - stderr: ${action.stderr_tail}`);
2020
2022
  }
@@ -2723,7 +2725,8 @@ function renderRunHtml(report) {
2723
2725
  for (const action of run.gate_actions) {
2724
2726
  const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
2725
2727
  const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
2726
- gaHtml += `<li><code>${esc(action.gate_id || 'unknown')}</code> (${esc(action.gate_type || 'unknown')}) action ${esc(String(action.action_index || '?'))} — <strong>${esc(action.status)}</strong> at <code>${esc(action.timestamp || 'n/a')}</code>: ${esc(label)} (exit <code>${esc(exit)}</code>)`;
2728
+ const htmlTimeout = action.timed_out ? ` <em>⏱ timed out after ${esc(String(action.timeout_ms))}ms</em>` : '';
2729
+ gaHtml += `<li><code>${esc(action.gate_id || 'unknown')}</code> (${esc(action.gate_type || 'unknown')}) action ${esc(String(action.action_index || '?'))} — <strong>${esc(action.status)}</strong> at <code>${esc(action.timestamp || 'n/a')}</code>: ${esc(label)} (exit <code>${esc(exit)}</code>)${htmlTimeout}`;
2727
2730
  if (action.stderr_tail) {
2728
2731
  gaHtml += `<br><code>${esc(action.stderr_tail)}</code>`;
2729
2732
  }