agentxchain 2.46.2 → 2.47.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 +4 -1
- package/package.json +1 -1
- package/src/commands/history.js +41 -1
- package/src/commands/run.js +32 -1
- package/src/commands/status.js +6 -0
- package/src/lib/export.js +2 -0
- package/src/lib/governed-state.js +55 -5
- package/src/lib/report.js +8 -0
- package/src/lib/run-history.js +111 -0
- package/src/lib/run-loop.js +9 -3
- package/src/lib/run-provenance.js +90 -0
package/bin/agentxchain.js
CHANGED
|
@@ -262,7 +262,8 @@ program
|
|
|
262
262
|
.description('Show cross-run history of governed runs in this project')
|
|
263
263
|
.option('-j, --json', 'Output as JSON')
|
|
264
264
|
.option('-l, --limit <n>', 'Number of recent runs to show (default: 20)')
|
|
265
|
-
.option('-s, --status <status>', 'Filter by status: completed
|
|
265
|
+
.option('-s, --status <status>', 'Filter by status: completed or blocked')
|
|
266
|
+
.option('--lineage <run_id>', 'Show lineage chain for a specific run')
|
|
266
267
|
.option('-d, --dir <path>', 'Project directory')
|
|
267
268
|
.action(historyCommand);
|
|
268
269
|
|
|
@@ -355,6 +356,8 @@ program
|
|
|
355
356
|
.option('--verbose', 'Stream adapter subprocess output')
|
|
356
357
|
.option('--dry-run', 'Print what would be dispatched without executing')
|
|
357
358
|
.option('--no-report', 'Suppress automatic governance report after run completes')
|
|
359
|
+
.option('--continue-from <run_id>', 'Continue from a prior terminal run (sets trigger=continuation)')
|
|
360
|
+
.option('--recover-from <run_id>', 'Recover from a prior blocked run (sets trigger=recovery)')
|
|
358
361
|
.action(runCommand);
|
|
359
362
|
|
|
360
363
|
program
|
package/package.json
CHANGED
package/src/commands/history.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
8
|
import { existsSync, readFileSync } from 'fs';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { queryRunHistory } from '../lib/run-history.js';
|
|
10
|
+
import { queryRunHistory, queryRunLineage } from '../lib/run-history.js';
|
|
11
|
+
import { getRunTriggerLabel, summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @param {object} opts - { json?: boolean, limit?: number, status?: string, dir?: string }
|
|
@@ -19,6 +20,42 @@ export async function historyCommand(opts) {
|
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// ── Lineage mode ─────────────────────────────────────────────────────────
|
|
24
|
+
if (opts.lineage) {
|
|
25
|
+
const result = queryRunLineage(root, opts.lineage);
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
console.error(chalk.red(result.error));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log(JSON.stringify(result.chain, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.bold(`Run Lineage for ${opts.lineage}:`));
|
|
37
|
+
result.chain.forEach((entry, i) => {
|
|
38
|
+
if (entry.broken_link) {
|
|
39
|
+
const prefix = i === 0 ? ' ' : ' └─ ';
|
|
40
|
+
console.log(chalk.red(`${prefix}[broken link: ${entry.missing_run_id}]`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const runId = (entry.run_id || '—').slice(0, 12);
|
|
44
|
+
const status = formatStatus(entry.status);
|
|
45
|
+
const phases = (entry.phases_completed || []).join(',') || '—';
|
|
46
|
+
const turns = `${entry.total_turns || 0} turns`;
|
|
47
|
+
const cost = entry.total_cost_usd != null ? `$${entry.total_cost_usd.toFixed(2)}` : '';
|
|
48
|
+
const trigger = getRunTriggerLabel(entry.provenance);
|
|
49
|
+
const parentNote = entry.provenance?.parent_run_id
|
|
50
|
+
? ` from ${entry.provenance.parent_run_id.slice(0, 12)}`
|
|
51
|
+
: '';
|
|
52
|
+
const prefix = i === 0 ? ' ' : ' └─ ';
|
|
53
|
+
console.log(`${prefix}${runId} ${status} ${pad(phases, 20)} ${pad(turns, 10)} ${pad(cost, 8)} (${trigger}${parentNote})`);
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Standard history view ────────────────────────────────────────────────
|
|
22
59
|
const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
|
|
23
60
|
const entries = queryRunHistory(root, {
|
|
24
61
|
limit,
|
|
@@ -43,6 +80,7 @@ export async function historyCommand(opts) {
|
|
|
43
80
|
pad('#', 4),
|
|
44
81
|
pad('Run ID', 14),
|
|
45
82
|
pad('Status', 11),
|
|
83
|
+
pad('Trigger', 14),
|
|
46
84
|
pad('Phases', 8),
|
|
47
85
|
pad('Turns', 6),
|
|
48
86
|
pad('Cost', 10),
|
|
@@ -57,6 +95,7 @@ export async function historyCommand(opts) {
|
|
|
57
95
|
const idx = String(i + 1);
|
|
58
96
|
const runId = (entry.run_id || '—').slice(0, 12);
|
|
59
97
|
const status = formatStatus(entry.status);
|
|
98
|
+
const trigger = getRunTriggerLabel(entry.provenance);
|
|
60
99
|
const phases = String(entry.phases_completed?.length || 0);
|
|
61
100
|
const turns = String(entry.total_turns || 0);
|
|
62
101
|
const cost = entry.total_cost_usd != null
|
|
@@ -73,6 +112,7 @@ export async function historyCommand(opts) {
|
|
|
73
112
|
pad(idx, 4),
|
|
74
113
|
pad(runId, 14),
|
|
75
114
|
pad(status, 11),
|
|
115
|
+
pad(trigger, 14),
|
|
76
116
|
pad(phases, 8),
|
|
77
117
|
pad(turns, 6),
|
|
78
118
|
pad(cost, 10),
|
package/src/commands/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
|
20
20
|
import { runLoop } from '../lib/run-loop.js';
|
|
21
21
|
import { buildRunExport } from '../lib/export.js';
|
|
22
22
|
import { buildGovernanceReport, formatGovernanceReportMarkdown } from '../lib/report.js';
|
|
23
|
+
import { validateParentRun } from '../lib/run-history.js';
|
|
23
24
|
import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
|
|
24
25
|
import {
|
|
25
26
|
dispatchLocalCli,
|
|
@@ -56,6 +57,30 @@ export async function runCommand(opts) {
|
|
|
56
57
|
process.exit(1);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// ── Provenance flag validation ──────────────────────────────────────────
|
|
61
|
+
const continueFrom = opts.continueFrom;
|
|
62
|
+
const recoverFrom = opts.recoverFrom;
|
|
63
|
+
|
|
64
|
+
if (continueFrom && recoverFrom) {
|
|
65
|
+
console.log(chalk.red('Cannot specify both --continue-from and --recover-from'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let provenance = undefined;
|
|
70
|
+
if (continueFrom || recoverFrom) {
|
|
71
|
+
const parentId = continueFrom || recoverFrom;
|
|
72
|
+
const validation = validateParentRun(root, parentId);
|
|
73
|
+
if (!validation.ok) {
|
|
74
|
+
console.log(chalk.red(validation.error));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
provenance = {
|
|
78
|
+
trigger: continueFrom ? 'continuation' : 'recovery',
|
|
79
|
+
parent_run_id: parentId,
|
|
80
|
+
created_by: 'operator',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
const maxTurns = opts.maxTurns || 50;
|
|
60
85
|
const autoApprove = !!opts.autoApprove;
|
|
61
86
|
const verbose = !!opts.verbose;
|
|
@@ -319,7 +344,13 @@ export async function runCommand(opts) {
|
|
|
319
344
|
};
|
|
320
345
|
|
|
321
346
|
// ── Execute ─────────────────────────────────────────────────────────────
|
|
322
|
-
const
|
|
347
|
+
const runLoopOpts = {
|
|
348
|
+
maxTurns,
|
|
349
|
+
startNewRunFromCompleted: true,
|
|
350
|
+
startNewRunFromBlocked: Boolean(provenance),
|
|
351
|
+
};
|
|
352
|
+
if (provenance) runLoopOpts.provenance = provenance;
|
|
353
|
+
const result = await runLoop(root, config, callbacks, runLoopOpts);
|
|
323
354
|
|
|
324
355
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
325
356
|
console.log('');
|
package/src/commands/status.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getContinuityStatus } from '../lib/continuity-status.js';
|
|
|
6
6
|
import { getConnectorHealth } from '../lib/connector-health.js';
|
|
7
7
|
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
8
8
|
import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
|
|
9
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
9
10
|
|
|
10
11
|
export async function statusCommand(opts) {
|
|
11
12
|
const context = loadProjectContext();
|
|
@@ -90,6 +91,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
90
91
|
template: config.template || 'generic',
|
|
91
92
|
config,
|
|
92
93
|
state,
|
|
94
|
+
provenance: state?.provenance || null,
|
|
93
95
|
continuity,
|
|
94
96
|
connector_health: connectorHealth,
|
|
95
97
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
@@ -107,6 +109,10 @@ function renderGovernedStatus(context, opts) {
|
|
|
107
109
|
console.log(` ${chalk.dim('Template:')} ${config.template || 'generic'}`);
|
|
108
110
|
console.log(` ${chalk.dim('Phase:')} ${state?.phase ? formatGovernedPhase(state.phase) : chalk.dim('unknown')}`);
|
|
109
111
|
console.log(` ${chalk.dim('Run:')} ${formatRunStatus(state?.status)}`);
|
|
112
|
+
const provenanceSummary = summarizeRunProvenance(state?.provenance);
|
|
113
|
+
if (provenanceSummary) {
|
|
114
|
+
console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
115
|
+
}
|
|
110
116
|
if (state?.accepted_integration_ref) {
|
|
111
117
|
console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
|
|
112
118
|
}
|
package/src/lib/export.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, relative, resolve } from 'node:path';
|
|
|
6
6
|
import { loadProjectContext, loadProjectState } from './config.js';
|
|
7
7
|
import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-config.js';
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
|
+
import { normalizeRunProvenance } from './run-provenance.js';
|
|
9
10
|
|
|
10
11
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
11
12
|
|
|
@@ -296,6 +297,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
296
297
|
run_id: state?.run_id || null,
|
|
297
298
|
status: state?.status || null,
|
|
298
299
|
phase: state?.phase || null,
|
|
300
|
+
provenance: normalizeRunProvenance(state?.provenance),
|
|
299
301
|
active_turn_ids: activeTurns,
|
|
300
302
|
retained_turn_ids: retainedTurns,
|
|
301
303
|
history_entries: countJsonl(files, '.agentxchain/history.jsonl'),
|
|
@@ -42,6 +42,7 @@ import { runHooks } from './hook-runner.js';
|
|
|
42
42
|
import { emitNotifications } from './notification-runner.js';
|
|
43
43
|
import { writeSessionCheckpoint } from './session-checkpoint.js';
|
|
44
44
|
import { recordRunHistory } from './run-history.js';
|
|
45
|
+
import { buildDefaultRunProvenance } from './run-provenance.js';
|
|
45
46
|
|
|
46
47
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
47
48
|
|
|
@@ -61,6 +62,48 @@ function generateId(prefix) {
|
|
|
61
62
|
return `${prefix}_${randomBytes(8).toString('hex')}`;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function getInitialPhase(config) {
|
|
66
|
+
return Object.keys(config?.routing || {})[0] || 'planning';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildInitialPhaseGateStatus(config) {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
[...new Set(
|
|
72
|
+
Object.values(config?.routing || {})
|
|
73
|
+
.map((route) => route?.exit_gate)
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
)].map((gateId) => [gateId, 'pending'])
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildFreshIdleStateForNewRun(state, config) {
|
|
80
|
+
return {
|
|
81
|
+
schema_version: state?.schema_version || GOVERNED_SCHEMA_VERSION,
|
|
82
|
+
run_id: null,
|
|
83
|
+
project_id: state?.project_id || config?.project?.id || null,
|
|
84
|
+
status: 'idle',
|
|
85
|
+
phase: getInitialPhase(config),
|
|
86
|
+
accepted_integration_ref: null,
|
|
87
|
+
active_turns: {},
|
|
88
|
+
turn_sequence: 0,
|
|
89
|
+
last_completed_turn_id: null,
|
|
90
|
+
blocked_on: null,
|
|
91
|
+
blocked_reason: null,
|
|
92
|
+
escalation: null,
|
|
93
|
+
pending_phase_transition: null,
|
|
94
|
+
pending_run_completion: null,
|
|
95
|
+
queued_phase_transition: null,
|
|
96
|
+
queued_run_completion: null,
|
|
97
|
+
last_gate_failure: null,
|
|
98
|
+
phase_gate_status: buildInitialPhaseGateStatus(config),
|
|
99
|
+
budget_reservations: {},
|
|
100
|
+
budget_status: {
|
|
101
|
+
spent_usd: 0,
|
|
102
|
+
remaining_usd: config?.budget?.per_run_max_usd ?? null,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
64
107
|
function normalizeGateFailure(value) {
|
|
65
108
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
109
|
return null;
|
|
@@ -1766,21 +1809,27 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1766
1809
|
* @param {object} config - normalized config
|
|
1767
1810
|
* @returns {{ ok: boolean, error?: string, state?: object }}
|
|
1768
1811
|
*/
|
|
1769
|
-
export function initializeGovernedRun(root, config) {
|
|
1770
|
-
|
|
1812
|
+
export function initializeGovernedRun(root, config, options = {}) {
|
|
1813
|
+
let state = readState(root);
|
|
1771
1814
|
if (!state) {
|
|
1772
1815
|
return { ok: false, error: 'No governed state.json found' };
|
|
1773
1816
|
}
|
|
1774
|
-
|
|
1817
|
+
const allowTerminalRestart = options.allow_terminal_restart === true
|
|
1818
|
+
&& (state.status === 'completed' || state.status === 'blocked');
|
|
1819
|
+
if (state.status === 'completed' && !allowTerminalRestart) {
|
|
1775
1820
|
return { ok: false, error: 'Cannot initialize run: this run is already completed. Start a new project or reset state.' };
|
|
1776
1821
|
}
|
|
1777
1822
|
const allowBlockedBootstrap = state.status === 'blocked' && state.run_id === null && getActiveTurnCount(state) === 0;
|
|
1778
|
-
if (state.status !== 'idle' && state.status !== 'paused' && !allowBlockedBootstrap) {
|
|
1823
|
+
if (state.status !== 'idle' && state.status !== 'paused' && !allowBlockedBootstrap && !allowTerminalRestart) {
|
|
1779
1824
|
return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle", "paused", or pre-run "blocked"` };
|
|
1780
1825
|
}
|
|
1826
|
+
if (allowTerminalRestart) {
|
|
1827
|
+
state = buildFreshIdleStateForNewRun(state, config);
|
|
1828
|
+
}
|
|
1781
1829
|
|
|
1782
1830
|
const runId = generateId('run');
|
|
1783
1831
|
const now = new Date().toISOString();
|
|
1832
|
+
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
1784
1833
|
const updatedState = {
|
|
1785
1834
|
...state,
|
|
1786
1835
|
run_id: runId,
|
|
@@ -1792,7 +1841,8 @@ export function initializeGovernedRun(root, config) {
|
|
|
1792
1841
|
budget_status: {
|
|
1793
1842
|
spent_usd: 0,
|
|
1794
1843
|
remaining_usd: config.budget?.per_run_max_usd ?? null
|
|
1795
|
-
}
|
|
1844
|
+
},
|
|
1845
|
+
provenance,
|
|
1796
1846
|
};
|
|
1797
1847
|
|
|
1798
1848
|
writeState(root, updatedState);
|
package/src/lib/report.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { verifyExportArtifact } from './export-verifier.js';
|
|
2
|
+
import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance.js';
|
|
2
3
|
|
|
3
4
|
export const GOVERNANCE_REPORT_VERSION = '0.1';
|
|
4
5
|
|
|
@@ -639,6 +640,7 @@ function buildRunSubject(artifact) {
|
|
|
639
640
|
phase: artifact.summary?.phase || null,
|
|
640
641
|
blocked_on: artifact.state?.blocked_on || null,
|
|
641
642
|
blocked_reason: artifact.state?.blocked_reason || null,
|
|
643
|
+
provenance: normalizeRunProvenance(artifact.summary?.provenance || artifact.state?.provenance),
|
|
642
644
|
active_turn_count: activeTurns.length,
|
|
643
645
|
retained_turn_count: retainedTurns.length,
|
|
644
646
|
active_turn_ids: activeTurns,
|
|
@@ -899,6 +901,9 @@ export function formatGovernanceReportText(report) {
|
|
|
899
901
|
if (run.duration_seconds != null) {
|
|
900
902
|
lines.push(`Duration: ${run.duration_seconds}s`);
|
|
901
903
|
}
|
|
904
|
+
if (summarizeRunProvenance(run.provenance)) {
|
|
905
|
+
lines.push(`Provenance: ${summarizeRunProvenance(run.provenance)}`);
|
|
906
|
+
}
|
|
902
907
|
|
|
903
908
|
lines.push(
|
|
904
909
|
`History entries: ${artifacts.history_entries}`,
|
|
@@ -1300,6 +1305,9 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1300
1305
|
if (run.duration_seconds != null) {
|
|
1301
1306
|
lines.push(`- Duration: \`${run.duration_seconds}s\``);
|
|
1302
1307
|
}
|
|
1308
|
+
if (summarizeRunProvenance(run.provenance)) {
|
|
1309
|
+
lines.push(`- Provenance: \`${summarizeRunProvenance(run.provenance)}\``);
|
|
1310
|
+
}
|
|
1303
1311
|
|
|
1304
1312
|
lines.push(
|
|
1305
1313
|
`- History entries: ${artifacts.history_entries}`,
|
package/src/lib/run-history.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
11
11
|
import { join, dirname } from 'path';
|
|
12
|
+
import { normalizeRunProvenance } from './run-provenance.js';
|
|
12
13
|
|
|
13
14
|
const RUN_HISTORY_PATH = '.agentxchain/run-history.jsonl';
|
|
14
15
|
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
@@ -79,6 +80,7 @@ export function recordRunHistory(root, state, config, status) {
|
|
|
79
80
|
gate_results: state?.phase_gate_status || {},
|
|
80
81
|
connector_used: connectorUsed,
|
|
81
82
|
model_used: modelUsed,
|
|
83
|
+
provenance: normalizeRunProvenance(state?.provenance),
|
|
82
84
|
recorded_at: new Date().toISOString(),
|
|
83
85
|
};
|
|
84
86
|
|
|
@@ -132,6 +134,115 @@ export function queryRunHistory(root, opts = {}) {
|
|
|
132
134
|
return entries;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Walk run lineage backwards from a given run_id via parent_run_id links.
|
|
139
|
+
*
|
|
140
|
+
* Returns an ordered array (oldest ancestor first) of history entries
|
|
141
|
+
* forming the lineage chain. If a parent_run_id references a run not
|
|
142
|
+
* found in history, the chain terminates with a broken_link sentinel.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} root - project root directory
|
|
145
|
+
* @param {string} runId - the run to trace lineage for
|
|
146
|
+
* @returns {{ ok: boolean, chain?: Array<object>, error?: string }}
|
|
147
|
+
*/
|
|
148
|
+
export function queryRunLineage(root, runId) {
|
|
149
|
+
const filePath = join(root, RUN_HISTORY_PATH);
|
|
150
|
+
if (!existsSync(filePath)) {
|
|
151
|
+
return { ok: false, error: 'No run history found. Run at least one governed run first.' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let content;
|
|
155
|
+
try {
|
|
156
|
+
content = readFileSync(filePath, 'utf8').trim();
|
|
157
|
+
} catch {
|
|
158
|
+
return { ok: false, error: 'Failed to read run history file.' };
|
|
159
|
+
}
|
|
160
|
+
if (!content) {
|
|
161
|
+
return { ok: false, error: 'No run history found. Run at least one governed run first.' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const entries = content
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
|
|
170
|
+
// Build lookup by run_id
|
|
171
|
+
const byId = new Map();
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (entry.run_id) byId.set(entry.run_id, entry);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Find the target entry
|
|
177
|
+
const target = byId.get(runId);
|
|
178
|
+
if (!target) {
|
|
179
|
+
return { ok: false, error: `Run ${runId} not found in run history.` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Walk backwards collecting ancestors
|
|
183
|
+
const chain = [target];
|
|
184
|
+
let current = target;
|
|
185
|
+
const visited = new Set([runId]);
|
|
186
|
+
|
|
187
|
+
while (current.provenance?.parent_run_id) {
|
|
188
|
+
const parentId = current.provenance.parent_run_id;
|
|
189
|
+
if (visited.has(parentId)) break; // safety: prevent cycles
|
|
190
|
+
visited.add(parentId);
|
|
191
|
+
|
|
192
|
+
const parent = byId.get(parentId);
|
|
193
|
+
if (!parent) {
|
|
194
|
+
chain.unshift({ broken_link: true, missing_run_id: parentId });
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
chain.unshift(parent);
|
|
198
|
+
current = parent;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { ok: true, chain };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate that a run_id exists in history and is in a terminal state.
|
|
206
|
+
* Used by --continue-from and --recover-from flag validation.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} root - project root directory
|
|
209
|
+
* @param {string} runId - the run_id to validate
|
|
210
|
+
* @returns {{ ok: boolean, entry?: object, error?: string }}
|
|
211
|
+
*/
|
|
212
|
+
export function validateParentRun(root, runId) {
|
|
213
|
+
const filePath = join(root, RUN_HISTORY_PATH);
|
|
214
|
+
if (!existsSync(filePath)) {
|
|
215
|
+
return { ok: false, error: `Run ${runId} not found in run history` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let content;
|
|
219
|
+
try {
|
|
220
|
+
content = readFileSync(filePath, 'utf8').trim();
|
|
221
|
+
} catch {
|
|
222
|
+
return { ok: false, error: `Run ${runId} not found in run history` };
|
|
223
|
+
}
|
|
224
|
+
if (!content) {
|
|
225
|
+
return { ok: false, error: `Run ${runId} not found in run history` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const entries = content
|
|
229
|
+
.split('\n')
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
|
|
234
|
+
const entry = entries.find(e => e.run_id === runId);
|
|
235
|
+
if (!entry) {
|
|
236
|
+
return { ok: false, error: `Run ${runId} not found in run history` };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!WRITABLE_TERMINAL_STATUSES.has(entry.status)) {
|
|
240
|
+
return { ok: false, error: `Run ${runId} is still active (status: ${entry.status}). Cannot chain from a non-terminal run.` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { ok: true, entry };
|
|
244
|
+
}
|
|
245
|
+
|
|
135
246
|
/**
|
|
136
247
|
* Get the path to the run-history file.
|
|
137
248
|
*/
|
package/src/lib/run-loop.js
CHANGED
|
@@ -38,7 +38,7 @@ const DEFAULT_MAX_TURNS = 50;
|
|
|
38
38
|
* @param {string} root - project root directory
|
|
39
39
|
* @param {object} config - normalized governed config
|
|
40
40
|
* @param {object} callbacks - { selectRole, dispatch, approveGate, onEvent? }
|
|
41
|
-
* @param {object} [options] - { maxTurns?: number }
|
|
41
|
+
* @param {object} [options] - { maxTurns?: number, provenance?: object, startNewRunFromCompleted?: boolean, startNewRunFromBlocked?: boolean }
|
|
42
42
|
* @returns {Promise<RunLoopResult>}
|
|
43
43
|
*/
|
|
44
44
|
export async function runLoop(root, config, callbacks, options = {}) {
|
|
@@ -58,8 +58,14 @@ export async function runLoop(root, config, callbacks, options = {}) {
|
|
|
58
58
|
|
|
59
59
|
// ── Initialize if idle ──────────────────────────────────────────────────
|
|
60
60
|
let state = loadState(root, config);
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const shouldRestartCompleted = state?.status === 'completed' && options.startNewRunFromCompleted === true;
|
|
62
|
+
const shouldRestartBlocked = state?.status === 'blocked' && options.startNewRunFromBlocked === true;
|
|
63
|
+
if (!state || state.status === 'idle' || shouldRestartCompleted || shouldRestartBlocked) {
|
|
64
|
+
const initOpts = options.provenance ? { provenance: options.provenance } : {};
|
|
65
|
+
if (shouldRestartCompleted || shouldRestartBlocked) {
|
|
66
|
+
initOpts.allow_terminal_restart = true;
|
|
67
|
+
}
|
|
68
|
+
const initResult = initRun(root, config, initOpts);
|
|
63
69
|
if (!initResult.ok) {
|
|
64
70
|
return makeResult(false, 'init_failed', loadState(root, config), turnsExecuted, turnHistory, gatesApproved, [initResult.error]);
|
|
65
71
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const VALID_TRIGGERS = new Set([
|
|
2
|
+
'manual',
|
|
3
|
+
'continuation',
|
|
4
|
+
'recovery',
|
|
5
|
+
'intake',
|
|
6
|
+
'schedule',
|
|
7
|
+
'coordinator',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const VALID_CREATORS = new Set([
|
|
11
|
+
'operator',
|
|
12
|
+
'coordinator',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function normalizeString(value) {
|
|
16
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeRunProvenance(value, { fallbackManual = false } = {}) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
return fallbackManual ? buildDefaultRunProvenance() : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trigger = normalizeString(value.trigger);
|
|
25
|
+
const createdBy = normalizeString(value.created_by);
|
|
26
|
+
const normalized = {
|
|
27
|
+
trigger: VALID_TRIGGERS.has(trigger) ? trigger : (fallbackManual ? 'manual' : null),
|
|
28
|
+
parent_run_id: normalizeString(value.parent_run_id),
|
|
29
|
+
trigger_reason: normalizeString(value.trigger_reason),
|
|
30
|
+
intake_intent_id: normalizeString(value.intake_intent_id),
|
|
31
|
+
created_by: VALID_CREATORS.has(createdBy) ? createdBy : 'operator',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!normalized.trigger) {
|
|
35
|
+
return fallbackManual ? buildDefaultRunProvenance() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildDefaultRunProvenance(overrides = {}) {
|
|
42
|
+
return normalizeRunProvenance({
|
|
43
|
+
trigger: 'manual',
|
|
44
|
+
parent_run_id: null,
|
|
45
|
+
trigger_reason: null,
|
|
46
|
+
intake_intent_id: null,
|
|
47
|
+
created_by: 'operator',
|
|
48
|
+
...overrides,
|
|
49
|
+
}, { fallbackManual: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getRunTriggerLabel(provenance) {
|
|
53
|
+
const normalized = normalizeRunProvenance(provenance);
|
|
54
|
+
return normalized?.trigger || 'legacy';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function summarizeRunProvenance(provenance) {
|
|
58
|
+
const normalized = normalizeRunProvenance(provenance);
|
|
59
|
+
if (!normalized) return null;
|
|
60
|
+
|
|
61
|
+
const details = [];
|
|
62
|
+
if (normalized.parent_run_id) {
|
|
63
|
+
details.push(`from ${normalized.parent_run_id}`);
|
|
64
|
+
}
|
|
65
|
+
if (normalized.intake_intent_id) {
|
|
66
|
+
details.push(`intent ${normalized.intake_intent_id}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const base = details.length > 0
|
|
70
|
+
? `${normalized.trigger} ${details.join(' ')}`
|
|
71
|
+
: normalized.trigger;
|
|
72
|
+
const creatorSuffix = normalized.created_by === 'coordinator'
|
|
73
|
+
? ' (created by coordinator)'
|
|
74
|
+
: '';
|
|
75
|
+
const reasonSuffix = normalized.trigger_reason
|
|
76
|
+
? ` ("${normalized.trigger_reason}")`
|
|
77
|
+
: '';
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
normalized.trigger === 'manual'
|
|
81
|
+
&& !normalized.parent_run_id
|
|
82
|
+
&& !normalized.intake_intent_id
|
|
83
|
+
&& !normalized.trigger_reason
|
|
84
|
+
&& normalized.created_by !== 'coordinator'
|
|
85
|
+
) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `${base}${creatorSuffix}${reasonSuffix}`;
|
|
90
|
+
}
|