agentxchain 2.110.0 → 2.112.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 +70 -0
- package/dashboard/app.js +6 -0
- package/dashboard/components/chain.js +200 -0
- package/dashboard/components/mission.js +177 -0
- package/dashboard/index.html +2 -0
- package/package.json +2 -1
- package/scripts/check-release-alignment.mjs +66 -0
- package/scripts/release-bump.sh +8 -59
- package/scripts/release-preflight.sh +23 -8
- package/src/commands/chain.js +252 -0
- package/src/commands/diff.js +19 -0
- package/src/commands/mission.js +252 -0
- package/src/commands/run.js +112 -97
- package/src/lib/chain-reports.js +54 -0
- package/src/lib/dashboard/bridge-server.js +16 -0
- package/src/lib/dashboard/chain-report-reader.js +15 -0
- package/src/lib/dashboard/file-watcher.js +13 -11
- package/src/lib/dashboard/mission-reader.js +14 -0
- package/src/lib/dashboard/state-reader.js +15 -1
- package/src/lib/export-diff.js +10 -1
- package/src/lib/missions.js +195 -0
- package/src/lib/release-alignment.js +336 -0
- package/src/lib/run-chain.js +296 -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,16 @@ 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
|
+
const chainParts = [`max ${chainOpts.maxChains} continuations`, `on: ${chainOpts.chainOn.join(',')}`, `cooldown: ${chainOpts.cooldownSeconds}s`];
|
|
60
|
+
if (chainOpts.mission) chainParts.push(`mission: ${chainOpts.mission}`);
|
|
61
|
+
console.log(chalk.dim(` Chain mode: enabled (${chainParts.join(', ')})`));
|
|
62
|
+
const { exitCode } = await executeChainedRun(context, opts, chainOpts, executeGovernedRun);
|
|
63
|
+
process.exit(exitCode);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
const execution = await executeGovernedRun(context, opts);
|
|
56
67
|
process.exit(execution.exitCode);
|
|
57
68
|
}
|
|
@@ -167,8 +178,7 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
167
178
|
let aborted = false;
|
|
168
179
|
let sigintCount = 0;
|
|
169
180
|
const controller = new AbortController();
|
|
170
|
-
|
|
171
|
-
process.on('SIGINT', () => {
|
|
181
|
+
const onSigint = () => {
|
|
172
182
|
sigintCount++;
|
|
173
183
|
if (sigintCount >= 2) {
|
|
174
184
|
process.exit(130);
|
|
@@ -176,37 +186,39 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
176
186
|
aborted = true;
|
|
177
187
|
controller.abort();
|
|
178
188
|
log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
|
|
179
|
-
}
|
|
189
|
+
};
|
|
190
|
+
process.on('SIGINT', onSigint);
|
|
180
191
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
try {
|
|
193
|
+
// ── Run header ──────────────────────────────────────────────────────────
|
|
194
|
+
log(chalk.cyan.bold('agentxchain run'));
|
|
195
|
+
log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
|
|
196
|
+
if (provenance) {
|
|
197
|
+
const provenanceSummary = summarizeRunProvenance(provenance);
|
|
198
|
+
if (provenanceSummary) {
|
|
199
|
+
log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
200
|
+
}
|
|
188
201
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
log('');
|
|
202
|
+
if (inheritedContext) {
|
|
203
|
+
const ic = inheritedContext;
|
|
204
|
+
const phasesCount = ic.parent_phases_completed?.length || 0;
|
|
205
|
+
const decisionsCount = ic.recent_decisions?.length || 0;
|
|
206
|
+
const turnsCount = ic.recent_accepted_turns?.length || 0;
|
|
207
|
+
const parts = [];
|
|
208
|
+
if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
|
|
209
|
+
if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
|
|
210
|
+
if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
|
|
211
|
+
const detail = parts.length ? ` — ${parts.join(', ')}` : '';
|
|
212
|
+
log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
|
|
213
|
+
}
|
|
214
|
+
log('');
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
// ── Track first-call for --role override ────────────────────────────────
|
|
217
|
+
let firstSelectRole = true;
|
|
218
|
+
let qaMissingCredentialsFallback = null;
|
|
207
219
|
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
221
|
+
const callbacks = {
|
|
210
222
|
selectRole(state, cfg) {
|
|
211
223
|
if (aborted) return null;
|
|
212
224
|
|
|
@@ -407,84 +419,87 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
407
419
|
break;
|
|
408
420
|
}
|
|
409
421
|
},
|
|
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);
|
|
422
|
+
};
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
424
|
+
// ── Execute ─────────────────────────────────────────────────────────────
|
|
425
|
+
const runLoopOpts = {
|
|
426
|
+
maxTurns,
|
|
427
|
+
startNewRunFromCompleted: true,
|
|
428
|
+
startNewRunFromBlocked: opts.allowBlockedRestart ?? Boolean(provenance),
|
|
429
|
+
};
|
|
430
|
+
if (provenance) runLoopOpts.provenance = provenance;
|
|
431
|
+
if (inheritedContext) runLoopOpts.inheritedContext = inheritedContext;
|
|
432
|
+
const result = await runLoop(root, config, callbacks, runLoopOpts);
|
|
433
|
+
|
|
434
|
+
// ── Summary ─────────────────────────────────────────────────────────────
|
|
435
|
+
log('');
|
|
436
|
+
log(chalk.dim('─── Run Summary ───'));
|
|
437
|
+
log(` Status: ${result.ok ? chalk.green('completed') : chalk.yellow(result.stop_reason)}`);
|
|
438
|
+
log(` Turns: ${result.turns_executed}`);
|
|
439
|
+
log(` Gates: ${result.gates_approved} approved`);
|
|
440
|
+
log(` Errors: ${result.errors.length ? chalk.red(result.errors.length) : 'none'}`);
|
|
441
|
+
|
|
442
|
+
if (result.errors.length) {
|
|
443
|
+
for (const err of result.errors) {
|
|
444
|
+
log(chalk.red(` ${err}`));
|
|
445
|
+
}
|
|
433
446
|
}
|
|
434
|
-
}
|
|
435
447
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
if (qaMissingCredentialsFallback) {
|
|
449
|
+
printManualQaFallback(log);
|
|
450
|
+
}
|
|
439
451
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
// Recovery guidance for blocked/rejected states
|
|
453
|
+
if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
|
|
454
|
+
const recovery = deriveRecoveryDescriptor(result.state, config);
|
|
455
|
+
if (recovery) {
|
|
456
|
+
log('');
|
|
457
|
+
log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
|
|
458
|
+
log(chalk.dim(` Action: ${recovery.recovery_action}`));
|
|
459
|
+
if (recovery.detail) {
|
|
460
|
+
log(chalk.dim(` Detail: ${recovery.detail}`));
|
|
461
|
+
}
|
|
449
462
|
}
|
|
450
463
|
}
|
|
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
464
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
465
|
+
// ── Auto governance report ──────────────────────────────────────────────
|
|
466
|
+
if (opts.report !== false && result.state) {
|
|
467
|
+
try {
|
|
468
|
+
const reportsDir = join(root, '.agentxchain', 'reports');
|
|
469
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
470
|
+
|
|
471
|
+
const exportResult = buildRunExport(root);
|
|
472
|
+
if (exportResult.ok) {
|
|
473
|
+
const runId = result.state.run_id || 'unknown';
|
|
474
|
+
const exportPath = join(reportsDir, `export-${runId}.json`);
|
|
475
|
+
writeFileSync(exportPath, JSON.stringify(exportResult.export, null, 2));
|
|
476
|
+
|
|
477
|
+
const reportResult = buildGovernanceReport(exportResult.export, { input: exportPath });
|
|
478
|
+
const reportPath = join(reportsDir, `report-${runId}.md`);
|
|
479
|
+
writeFileSync(reportPath, formatGovernanceReportMarkdown(reportResult.report));
|
|
480
|
+
|
|
481
|
+
log('');
|
|
482
|
+
log(chalk.dim(` Governance report: .agentxchain/reports/report-${runId}.md`));
|
|
483
|
+
} else {
|
|
484
|
+
log(chalk.dim(` Governance report skipped: ${exportResult.error}`));
|
|
485
|
+
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
log(chalk.dim(` Governance report failed: ${err.message}`));
|
|
473
488
|
}
|
|
474
|
-
} catch (err) {
|
|
475
|
-
log(chalk.dim(` Governance report failed: ${err.message}`));
|
|
476
489
|
}
|
|
477
|
-
}
|
|
478
490
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
491
|
+
// ── Exit code ───────────────────────────────────────────────────────────
|
|
492
|
+
const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
|
|
493
|
+
return {
|
|
494
|
+
exitCode: result.ok || successReasons.has(result.stop_reason) ? 0 : 1,
|
|
495
|
+
result,
|
|
496
|
+
skipped: false,
|
|
497
|
+
skipReason: null,
|
|
498
|
+
provenance: provenance || null,
|
|
499
|
+
};
|
|
500
|
+
} finally {
|
|
501
|
+
process.removeListener('SIGINT', onSigint);
|
|
502
|
+
}
|
|
488
503
|
}
|
|
489
504
|
|
|
490
505
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
@@ -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,8 @@ 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';
|
|
33
|
+
import { readMissionSnapshot } from './mission-reader.js';
|
|
32
34
|
|
|
33
35
|
const MIME_TYPES = {
|
|
34
36
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -462,6 +464,20 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
|
|
|
462
464
|
return;
|
|
463
465
|
}
|
|
464
466
|
|
|
467
|
+
if (pathname === '/api/chain-reports') {
|
|
468
|
+
const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
|
|
469
|
+
const result = readChainReportSnapshot(workspacePath, { limit });
|
|
470
|
+
writeJson(res, result.status, result.body);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (pathname === '/api/missions') {
|
|
475
|
+
const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
|
|
476
|
+
const result = readMissionSnapshot(workspacePath, { limit });
|
|
477
|
+
writeJson(res, result.status, result.body);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
465
481
|
if (pathname === '/api/gate-actions') {
|
|
466
482
|
const result = readGateActionSnapshot(workspacePath);
|
|
467
483
|
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
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { watch, existsSync } from 'fs';
|
|
9
9
|
import { basename, join } from 'path';
|
|
10
10
|
import { EventEmitter } from 'events';
|
|
11
|
-
import { WATCH_DIRECTORIES,
|
|
11
|
+
import { WATCH_DIRECTORIES, resourcesForRelativePath } from './state-reader.js';
|
|
12
12
|
|
|
13
13
|
const DEBOUNCE_MS = 100;
|
|
14
14
|
|
|
@@ -40,24 +40,26 @@ export class FileWatcher extends EventEmitter {
|
|
|
40
40
|
if (!filename || this.#closed) return;
|
|
41
41
|
const base = basename(filename);
|
|
42
42
|
const relativePath = relativeDir ? `${relativeDir}/${base}` : base;
|
|
43
|
-
const
|
|
43
|
+
const resources = resourcesForRelativePath(relativePath);
|
|
44
44
|
|
|
45
|
-
if (
|
|
45
|
+
if (resources.length === 0) {
|
|
46
46
|
if (!relativeDir && base === 'multirepo') {
|
|
47
47
|
this.#watchPath('multirepo');
|
|
48
48
|
}
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.#debounceTimers.set(resource, setTimeout(() => {
|
|
56
|
-
this.#debounceTimers.delete(resource);
|
|
57
|
-
if (!this.#closed) {
|
|
58
|
-
this.emit('invalidate', { resource });
|
|
52
|
+
for (const resource of resources) {
|
|
53
|
+
if (this.#debounceTimers.has(resource)) {
|
|
54
|
+
clearTimeout(this.#debounceTimers.get(resource));
|
|
59
55
|
}
|
|
60
|
-
|
|
56
|
+
this.#debounceTimers.set(resource, setTimeout(() => {
|
|
57
|
+
this.#debounceTimers.delete(resource);
|
|
58
|
+
if (!this.#closed) {
|
|
59
|
+
this.emit('invalidate', { resource });
|
|
60
|
+
}
|
|
61
|
+
}, DEBOUNCE_MS));
|
|
62
|
+
}
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
watcher.on('error', (err) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildMissionListSummary, loadLatestMissionSnapshot } from '../missions.js';
|
|
2
|
+
|
|
3
|
+
export function readMissionSnapshot(workspacePath, { limit } = {}) {
|
|
4
|
+
const missions = buildMissionListSummary(workspacePath, limit);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
body: {
|
|
10
|
+
latest: loadLatestMissionSnapshot(workspacePath),
|
|
11
|
+
missions,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -62,14 +62,28 @@ FILE_TO_RESOURCE[normalizeRelativePath(REPO_DECISIONS_FILE)] = '/api/repo-decisi
|
|
|
62
62
|
export const WATCH_DIRECTORIES = [
|
|
63
63
|
'',
|
|
64
64
|
MULTIREPO_DIR,
|
|
65
|
+
'missions',
|
|
66
|
+
'reports',
|
|
65
67
|
];
|
|
66
68
|
|
|
67
69
|
export function normalizeRelativePath(filePath) {
|
|
68
70
|
return normalize(filePath).replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
export function resourcesForRelativePath(filePath) {
|
|
74
|
+
const normalized = normalizeRelativePath(filePath);
|
|
75
|
+
if (normalized.startsWith('missions/') && normalized.endsWith('.json')) {
|
|
76
|
+
return ['/api/missions'];
|
|
77
|
+
}
|
|
78
|
+
if (normalized.startsWith('reports/chain-') && normalized.endsWith('.json')) {
|
|
79
|
+
return ['/api/chain-reports', '/api/missions'];
|
|
80
|
+
}
|
|
81
|
+
return FILE_TO_RESOURCE[normalized] ? [FILE_TO_RESOURCE[normalized]] : [];
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
export function resourceForRelativePath(filePath) {
|
|
72
|
-
|
|
85
|
+
const resources = resourcesForRelativePath(filePath);
|
|
86
|
+
return resources[0] || null;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
/**
|
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,
|