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.
- package/bin/agentxchain.js +31 -0
- package/dashboard/app.js +3 -0
- package/dashboard/components/blocked.js +4 -2
- package/dashboard/components/chain.js +200 -0
- package/dashboard/components/gate.js +6 -3
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/src/commands/approve-completion.js +9 -4
- package/src/commands/approve-transition.js +9 -4
- package/src/commands/chain.js +252 -0
- package/src/commands/diff.js +19 -0
- package/src/commands/run.js +110 -97
- package/src/commands/status.js +2 -2
- package/src/lib/chain-reports.js +54 -0
- package/src/lib/dashboard/bridge-server.js +8 -0
- package/src/lib/dashboard/chain-report-reader.js +15 -0
- package/src/lib/dashboard/state-reader.js +6 -1
- package/src/lib/export-diff.js +10 -1
- package/src/lib/gate-actions.js +33 -2
- package/src/lib/governed-state.js +6 -2
- package/src/lib/report.js +6 -3
- package/src/lib/run-chain.js +262 -0
package/src/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
// ── Track first-call for --role override ────────────────────────────────
|
|
215
|
+
let firstSelectRole = true;
|
|
216
|
+
let qaMissingCredentialsFallback = null;
|
|
207
217
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
446
|
+
if (qaMissingCredentialsFallback) {
|
|
447
|
+
printManualQaFallback(log);
|
|
448
|
+
}
|
|
439
449
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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 ───────────────────────────────────────────────────────────────
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/lib/export-diff.js
CHANGED
|
@@ -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,
|
package/src/lib/gate-actions.js
CHANGED
|
@@ -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:
|
|
98
|
-
spawn_error:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|