agentxchain 2.92.0 → 2.94.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 +12 -2
- package/package.json +1 -1
- package/src/commands/audit.js +7 -1
- package/src/commands/decisions.js +94 -0
- package/src/commands/report.js +7 -1
- package/src/commands/status.js +4 -0
- package/src/lib/dispatch-bundle.js +20 -1
- package/src/lib/export.js +20 -0
- package/src/lib/governed-state.js +66 -1
- package/src/lib/repo-decisions.js +100 -0
- package/src/lib/report.js +592 -2
- package/src/lib/schemas/turn-result.schema.json +20 -0
- package/src/lib/turn-result-validator.js +92 -1
package/bin/agentxchain.js
CHANGED
|
@@ -113,6 +113,7 @@ import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
|
|
|
113
113
|
import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
114
114
|
import { demoCommand } from '../src/commands/demo.js';
|
|
115
115
|
import { historyCommand } from '../src/commands/history.js';
|
|
116
|
+
import { decisionsCommand } from '../src/commands/decisions.js';
|
|
116
117
|
import { diffCommand } from '../src/commands/diff.js';
|
|
117
118
|
import { eventsCommand } from '../src/commands/events.js';
|
|
118
119
|
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
@@ -157,7 +158,7 @@ program
|
|
|
157
158
|
program
|
|
158
159
|
.command('audit')
|
|
159
160
|
.description('Render a governance audit directly from the current governed project or coordinator workspace')
|
|
160
|
-
.option('--format <format>', 'Output format: text, json, or
|
|
161
|
+
.option('--format <format>', 'Output format: text, json, markdown, or html', 'text')
|
|
161
162
|
.action(auditCommand);
|
|
162
163
|
|
|
163
164
|
program
|
|
@@ -176,7 +177,7 @@ program
|
|
|
176
177
|
.command('report')
|
|
177
178
|
.description('Render a human-readable governance summary from an export artifact')
|
|
178
179
|
.option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
|
|
179
|
-
.option('--format <format>', 'Output format: text, json, or
|
|
180
|
+
.option('--format <format>', 'Output format: text, json, markdown, or html', 'text')
|
|
180
181
|
.action(reportCommand);
|
|
181
182
|
|
|
182
183
|
program
|
|
@@ -333,6 +334,15 @@ program
|
|
|
333
334
|
.option('-d, --dir <path>', 'Project directory')
|
|
334
335
|
.action(historyCommand);
|
|
335
336
|
|
|
337
|
+
program
|
|
338
|
+
.command('decisions')
|
|
339
|
+
.description('Show repo-level decisions that persist across governed runs')
|
|
340
|
+
.option('-j, --json', 'Output as JSON')
|
|
341
|
+
.option('-a, --all', 'Include overridden decisions')
|
|
342
|
+
.option('-s, --show <id>', 'Show details for a specific decision (e.g. DEC-042)')
|
|
343
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
344
|
+
.action(decisionsCommand);
|
|
345
|
+
|
|
336
346
|
program
|
|
337
347
|
.command('diff <left_run_id> <right_run_id>')
|
|
338
348
|
.description('Compare two recorded governed runs from run-history')
|
package/package.json
CHANGED
package/src/commands/audit.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { buildCoordinatorExport, buildRunExport } from '../lib/export.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGovernanceReport,
|
|
6
|
+
formatGovernanceReportHtml,
|
|
6
7
|
formatGovernanceReportMarkdown,
|
|
7
8
|
formatGovernanceReportText,
|
|
8
9
|
} from '../lib/report.js';
|
|
@@ -43,6 +44,11 @@ function printAndExit(report, format, exitCode) {
|
|
|
43
44
|
process.exit(exitCode);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (format === 'html') {
|
|
48
|
+
console.log(formatGovernanceReportHtml(report));
|
|
49
|
+
process.exit(exitCode);
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
if (format === 'text') {
|
|
47
53
|
if (report.overall === 'error' || report.overall === 'fail') {
|
|
48
54
|
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
@@ -52,7 +58,7 @@ function printAndExit(report, format, exitCode) {
|
|
|
52
58
|
process.exit(exitCode);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
console.error(`Unsupported audit format "${format}". Use "text", "json", or "
|
|
61
|
+
console.error(`Unsupported audit format "${format}". Use "text", "json", "markdown", or "html".`);
|
|
56
62
|
process.exit(2);
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain decisions — cross-run decision carryover surface.
|
|
3
|
+
*
|
|
4
|
+
* Shows repo-level decisions that persist across governed runs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById } from '../lib/repo-decisions.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} opts - { json?: boolean, all?: boolean, show?: string, dir?: string }
|
|
14
|
+
*/
|
|
15
|
+
export async function decisionsCommand(opts) {
|
|
16
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
17
|
+
if (!root) {
|
|
18
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Show single decision ───────────────────────────────────────────────
|
|
23
|
+
if (opts.show) {
|
|
24
|
+
const dec = getRepoDecisionById(root, opts.show);
|
|
25
|
+
if (!dec) {
|
|
26
|
+
console.error(chalk.red(`Decision ${opts.show} not found in repo decisions.`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
console.log(JSON.stringify(dec, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(chalk.bold(`Decision ${dec.id}`));
|
|
34
|
+
console.log(` Category: ${dec.category}`);
|
|
35
|
+
console.log(` Statement: ${dec.statement}`);
|
|
36
|
+
console.log(` Rationale: ${dec.rationale}`);
|
|
37
|
+
console.log(` Status: ${formatStatus(dec.status)}`);
|
|
38
|
+
console.log(` Role: ${dec.role || '—'}`);
|
|
39
|
+
console.log(` Phase: ${dec.phase || '—'}`);
|
|
40
|
+
console.log(` Run: ${(dec.run_id || '—').slice(0, 16)}`);
|
|
41
|
+
console.log(` Turn: ${(dec.turn_id || '—').slice(0, 16)}`);
|
|
42
|
+
console.log(` Created: ${dec.created_at || '—'}`);
|
|
43
|
+
if (dec.overridden_by) {
|
|
44
|
+
console.log(` Overridden: ${chalk.yellow(dec.overridden_by)}`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── List decisions ─────────────────────────────────────────────────────
|
|
50
|
+
const decisions = opts.all ? readRepoDecisions(root) : getActiveRepoDecisions(root);
|
|
51
|
+
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify(decisions, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (decisions.length === 0) {
|
|
58
|
+
console.log(chalk.dim('No repo-level decisions found.'));
|
|
59
|
+
if (!opts.all) {
|
|
60
|
+
console.log(chalk.dim('Use --all to include overridden decisions.'));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const label = opts.all ? 'Repo Decisions (all)' : 'Active Repo Decisions';
|
|
66
|
+
console.log(chalk.bold(`${label}: ${decisions.length}`));
|
|
67
|
+
console.log('');
|
|
68
|
+
|
|
69
|
+
for (const dec of decisions) {
|
|
70
|
+
const status = formatStatus(dec.status);
|
|
71
|
+
const runShort = (dec.run_id || '').slice(0, 12);
|
|
72
|
+
const override = dec.overridden_by ? chalk.dim(` → ${dec.overridden_by}`) : '';
|
|
73
|
+
console.log(` ${chalk.cyan(dec.id)} ${status} ${chalk.dim(dec.category)} ${dec.statement}${override}`);
|
|
74
|
+
console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}`)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatStatus(status) {
|
|
79
|
+
if (status === 'active') return chalk.green('active');
|
|
80
|
+
if (status === 'overridden') return chalk.yellow('overridden');
|
|
81
|
+
return chalk.dim(status || '—');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findProjectRoot(dir) {
|
|
85
|
+
let current = resolve(dir);
|
|
86
|
+
while (current !== '/') {
|
|
87
|
+
if (existsSync(resolve(current, 'agentxchain.json'))) return current;
|
|
88
|
+
if (existsSync(resolve(current, '.agentxchain'))) return current;
|
|
89
|
+
const parent = resolve(current, '..');
|
|
90
|
+
if (parent === current) break;
|
|
91
|
+
current = parent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
package/src/commands/report.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { loadExportArtifact } from '../lib/export-verifier.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGovernanceReport,
|
|
6
|
+
formatGovernanceReportHtml,
|
|
6
7
|
formatGovernanceReportMarkdown,
|
|
7
8
|
formatGovernanceReportText,
|
|
8
9
|
} from '../lib/report.js';
|
|
@@ -18,6 +19,11 @@ function printAndExit(report, format, exitCode) {
|
|
|
18
19
|
process.exit(exitCode);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if (format === 'html') {
|
|
23
|
+
console.log(formatGovernanceReportHtml(report));
|
|
24
|
+
process.exit(exitCode);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
if (format === 'text') {
|
|
22
28
|
if (report.overall === 'error') {
|
|
23
29
|
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
@@ -29,7 +35,7 @@ function printAndExit(report, format, exitCode) {
|
|
|
29
35
|
process.exit(exitCode);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
console.error(`Unsupported report format "${format}". Use "text", "json", or "
|
|
38
|
+
console.error(`Unsupported report format "${format}". Use "text", "json", "markdown", or "html".`);
|
|
33
39
|
process.exit(1);
|
|
34
40
|
}
|
|
35
41
|
|
package/src/commands/status.js
CHANGED
|
@@ -105,6 +105,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
105
105
|
state,
|
|
106
106
|
provenance: state?.provenance || null,
|
|
107
107
|
inherited_context: state?.inherited_context || null,
|
|
108
|
+
repo_decisions: state?.repo_decisions || null,
|
|
108
109
|
continuity,
|
|
109
110
|
connector_health: connectorHealth,
|
|
110
111
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
@@ -133,6 +134,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
133
134
|
if (state?.inherited_context?.parent_run_id) {
|
|
134
135
|
console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
|
|
135
136
|
}
|
|
137
|
+
if (state?.repo_decisions?.length > 0) {
|
|
138
|
+
console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(`${state.repo_decisions.length} active`)}`);
|
|
139
|
+
}
|
|
136
140
|
if (state?.accepted_integration_ref) {
|
|
137
141
|
console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
|
|
138
142
|
}
|
|
@@ -18,6 +18,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync
|
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { getActiveTurn, getActiveTurns } from './governed-state.js';
|
|
20
20
|
import { renderInheritedContextMarkdown } from './run-context-inheritance.js';
|
|
21
|
+
import { renderRepoDecisionsMarkdown } from './repo-decisions.js';
|
|
21
22
|
import {
|
|
22
23
|
DISPATCH_INDEX_PATH,
|
|
23
24
|
getDispatchAssignmentPath,
|
|
@@ -574,6 +575,11 @@ function renderContext(state, config, root, turn, role) {
|
|
|
574
575
|
lines.push(` - ${req}`);
|
|
575
576
|
}
|
|
576
577
|
}
|
|
578
|
+
if (Array.isArray(dc.required_decision_ids) && dc.required_decision_ids.length > 0) {
|
|
579
|
+
lines.push(`- **Required decisions:** ${dc.required_decision_ids.join(', ')}`);
|
|
580
|
+
lines.push('');
|
|
581
|
+
lines.push('Your accepted turn must emit these decision IDs in `decisions[]` before the parent review may advance the phase or complete the run.');
|
|
582
|
+
}
|
|
577
583
|
lines.push('');
|
|
578
584
|
lines.push('Focus exclusively on the charter above. Do not expand scope beyond the delegation.');
|
|
579
585
|
lines.push('');
|
|
@@ -598,13 +604,26 @@ function renderContext(state, config, root, turn, role) {
|
|
|
598
604
|
if (result.verification?.status) {
|
|
599
605
|
lines.push(`- **Verification:** ${result.verification.status}`);
|
|
600
606
|
}
|
|
607
|
+
if (Array.isArray(result.required_decision_ids) && result.required_decision_ids.length > 0) {
|
|
608
|
+
lines.push(`- **Required decisions:** ${result.required_decision_ids.join(', ')}`);
|
|
609
|
+
lines.push(`- **Satisfied decisions:** ${(result.satisfied_decision_ids || []).join(', ') || 'none'}`);
|
|
610
|
+
lines.push(`- **Missing decisions:** ${(result.missing_decision_ids || []).join(', ') || 'none'}`);
|
|
611
|
+
}
|
|
601
612
|
lines.push('');
|
|
602
613
|
}
|
|
603
|
-
lines.push('Evaluate whether each delegation met its acceptance contract.');
|
|
614
|
+
lines.push('Evaluate whether each delegation met its acceptance contract and returned any required named decisions.');
|
|
604
615
|
lines.push('Your turn result should assess the delegation outcomes and decide next steps.');
|
|
605
616
|
lines.push('');
|
|
606
617
|
}
|
|
607
618
|
|
|
619
|
+
// Repo-level decisions that persist across runs
|
|
620
|
+
if (state.repo_decisions && state.repo_decisions.length > 0) {
|
|
621
|
+
const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions);
|
|
622
|
+
if (repoDecMd) {
|
|
623
|
+
lines.push(repoDecMd);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
608
627
|
// Inherited context from parent run (when --inherit-context was used)
|
|
609
628
|
if (state.inherited_context) {
|
|
610
629
|
// First turn gets the full rendering; subsequent turns get compact
|
package/src/lib/export.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-co
|
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
9
|
import { normalizeRunProvenance } from './run-provenance.js';
|
|
10
10
|
import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
|
|
11
|
+
import { readRepoDecisions } from './repo-decisions.js';
|
|
11
12
|
|
|
12
13
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
13
14
|
|
|
@@ -30,6 +31,7 @@ export const RUN_EXPORT_INCLUDED_ROOTS = [
|
|
|
30
31
|
'.agentxchain/session.json',
|
|
31
32
|
'.agentxchain/history.jsonl',
|
|
32
33
|
'.agentxchain/decision-ledger.jsonl',
|
|
34
|
+
'.agentxchain/repo-decisions.jsonl',
|
|
33
35
|
'.agentxchain/hook-audit.jsonl',
|
|
34
36
|
'.agentxchain/hook-annotations.jsonl',
|
|
35
37
|
'.agentxchain/notification-audit.jsonl',
|
|
@@ -208,6 +210,20 @@ function buildDashboardSessionSummary(root) {
|
|
|
208
210
|
};
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
export function buildRepoDecisionsSummary(root) {
|
|
214
|
+
const all = readRepoDecisions(root);
|
|
215
|
+
if (!all || all.length === 0) return null;
|
|
216
|
+
const active = all.filter(d => d.status === 'active');
|
|
217
|
+
const overridden = all.filter(d => d.status === 'overridden');
|
|
218
|
+
return {
|
|
219
|
+
total: all.length,
|
|
220
|
+
active_count: active.length,
|
|
221
|
+
overridden_count: overridden.length,
|
|
222
|
+
active: active.map(d => ({ id: d.id, category: d.category, statement: d.statement, role: d.role, run_id: d.run_id })),
|
|
223
|
+
overridden: overridden.map(d => ({ id: d.id, overridden_by: d.overridden_by, statement: d.statement })),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
export function buildDelegationSummary(files) {
|
|
212
228
|
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
213
229
|
if (!Array.isArray(historyData)) {
|
|
@@ -263,6 +279,9 @@ export function buildDelegationSummary(files) {
|
|
|
263
279
|
delegation_id: del.id,
|
|
264
280
|
to_role: del.to_role,
|
|
265
281
|
charter: del.charter,
|
|
282
|
+
required_decision_ids: Array.isArray(del.required_decision_ids) ? del.required_decision_ids : [],
|
|
283
|
+
satisfied_decision_ids: Array.isArray(reviewResult?.satisfied_decision_ids) ? reviewResult.satisfied_decision_ids : [],
|
|
284
|
+
missing_decision_ids: Array.isArray(reviewResult?.missing_decision_ids) ? reviewResult.missing_decision_ids : [],
|
|
266
285
|
status: reviewResult?.status || child?.status || 'pending',
|
|
267
286
|
child_turn_id: child?.turn_id || null,
|
|
268
287
|
};
|
|
@@ -448,6 +467,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
448
467
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
449
468
|
dashboard_session: buildDashboardSessionSummary(root),
|
|
450
469
|
delegation_summary: buildDelegationSummary(files),
|
|
470
|
+
repo_decisions: buildRepoDecisionsSummary(root),
|
|
451
471
|
},
|
|
452
472
|
workspace: buildRunWorkspaceMetadata(root),
|
|
453
473
|
files,
|
|
@@ -44,6 +44,12 @@ import { emitRunEvent } from './run-events.js';
|
|
|
44
44
|
import { writeSessionCheckpoint } from './session-checkpoint.js';
|
|
45
45
|
import { recordRunHistory } from './run-history.js';
|
|
46
46
|
import { buildDefaultRunProvenance } from './run-provenance.js';
|
|
47
|
+
import {
|
|
48
|
+
getActiveRepoDecisions,
|
|
49
|
+
appendRepoDecision,
|
|
50
|
+
overrideRepoDecision,
|
|
51
|
+
validateOverride,
|
|
52
|
+
} from './repo-decisions.js';
|
|
47
53
|
import {
|
|
48
54
|
replayVerificationMachineEvidence,
|
|
49
55
|
summarizeVerificationReplay,
|
|
@@ -1908,6 +1914,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1908
1914
|
const runId = generateId('run');
|
|
1909
1915
|
const now = new Date().toISOString();
|
|
1910
1916
|
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
1917
|
+
const repoDecisions = getActiveRepoDecisions(root);
|
|
1911
1918
|
const updatedState = {
|
|
1912
1919
|
...state,
|
|
1913
1920
|
run_id: runId,
|
|
@@ -1922,6 +1929,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1922
1929
|
},
|
|
1923
1930
|
provenance,
|
|
1924
1931
|
inherited_context: options.inherited_context || null,
|
|
1932
|
+
repo_decisions: repoDecisions.length > 0 ? repoDecisions : null,
|
|
1925
1933
|
};
|
|
1926
1934
|
|
|
1927
1935
|
writeState(root, updatedState);
|
|
@@ -2144,6 +2152,7 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
2144
2152
|
parent_role: pendingDelegation.parent_role,
|
|
2145
2153
|
charter: pendingDelegation.charter,
|
|
2146
2154
|
acceptance_contract: pendingDelegation.acceptance_contract,
|
|
2155
|
+
required_decision_ids: pendingDelegation.required_decision_ids || [],
|
|
2147
2156
|
};
|
|
2148
2157
|
// Mark the delegation as active
|
|
2149
2158
|
pendingDelegation.status = 'active';
|
|
@@ -2423,6 +2432,23 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2423
2432
|
}
|
|
2424
2433
|
|
|
2425
2434
|
const turnResult = validation.turnResult;
|
|
2435
|
+
|
|
2436
|
+
// Validate cross-run decision overrides against repo-decisions.jsonl
|
|
2437
|
+
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
2438
|
+
for (const dec of turnResult.decisions) {
|
|
2439
|
+
if (dec.overrides) {
|
|
2440
|
+
const overrideCheck = validateOverride(root, dec);
|
|
2441
|
+
if (!overrideCheck.ok) {
|
|
2442
|
+
return {
|
|
2443
|
+
ok: false,
|
|
2444
|
+
error: `Override validation failed: ${overrideCheck.error}`,
|
|
2445
|
+
error_code: 'override_validation_failed',
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2426
2452
|
const stagingFile = join(root, resolvedStagingPath);
|
|
2427
2453
|
const now = new Date().toISOString();
|
|
2428
2454
|
const baseline = currentTurn.baseline || null;
|
|
@@ -2717,6 +2743,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2717
2743
|
to_role: delegation.to_role,
|
|
2718
2744
|
charter: delegation.charter,
|
|
2719
2745
|
acceptance_contract: delegation.acceptance_contract,
|
|
2746
|
+
required_decision_ids: delegation.required_decision_ids || [],
|
|
2720
2747
|
})),
|
|
2721
2748
|
}
|
|
2722
2749
|
: {}),
|
|
@@ -2728,6 +2755,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2728
2755
|
parent_role: currentTurn.delegation_context.parent_role,
|
|
2729
2756
|
charter: currentTurn.delegation_context.charter,
|
|
2730
2757
|
acceptance_contract: currentTurn.delegation_context.acceptance_contract,
|
|
2758
|
+
required_decision_ids: currentTurn.delegation_context.required_decision_ids || [],
|
|
2731
2759
|
},
|
|
2732
2760
|
}
|
|
2733
2761
|
: {}),
|
|
@@ -2808,6 +2836,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2808
2836
|
to_role: del.to_role,
|
|
2809
2837
|
charter: del.charter,
|
|
2810
2838
|
acceptance_contract: del.acceptance_contract,
|
|
2839
|
+
required_decision_ids: del.required_decision_ids || [],
|
|
2811
2840
|
status: 'pending',
|
|
2812
2841
|
child_turn_id: null,
|
|
2813
2842
|
created_at: now,
|
|
@@ -2834,12 +2863,23 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2834
2863
|
// Build delegation review context
|
|
2835
2864
|
const delegationResults = parentDelegations.map(d => {
|
|
2836
2865
|
const childHistory = nextHistoryEntries.find(h => h.turn_id === d.child_turn_id);
|
|
2866
|
+
const childDecisionIds = Array.isArray(childHistory?.decisions)
|
|
2867
|
+
? childHistory.decisions
|
|
2868
|
+
.map((decision) => decision?.id)
|
|
2869
|
+
.filter((id) => typeof id === 'string')
|
|
2870
|
+
: [];
|
|
2871
|
+
const requiredDecisionIds = Array.isArray(d.required_decision_ids) ? d.required_decision_ids : [];
|
|
2872
|
+
const satisfiedDecisionIds = requiredDecisionIds.filter((id) => childDecisionIds.includes(id));
|
|
2873
|
+
const missingDecisionIds = requiredDecisionIds.filter((id) => !childDecisionIds.includes(id));
|
|
2837
2874
|
return {
|
|
2838
2875
|
delegation_id: d.delegation_id,
|
|
2839
2876
|
child_turn_id: d.child_turn_id,
|
|
2840
2877
|
to_role: d.to_role,
|
|
2841
2878
|
charter: d.charter,
|
|
2842
2879
|
acceptance_contract: d.acceptance_contract,
|
|
2880
|
+
required_decision_ids: requiredDecisionIds,
|
|
2881
|
+
satisfied_decision_ids: satisfiedDecisionIds,
|
|
2882
|
+
missing_decision_ids: missingDecisionIds,
|
|
2843
2883
|
summary: childHistory?.summary || '(no summary)',
|
|
2844
2884
|
status: d.status,
|
|
2845
2885
|
files_changed: childHistory?.files_changed || [],
|
|
@@ -3296,11 +3336,36 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3296
3336
|
};
|
|
3297
3337
|
writeAcceptanceJournal(root, journal);
|
|
3298
3338
|
|
|
3299
|
-
// ── Commit order: history → ledger → talk → state → cleanup → journal ─
|
|
3339
|
+
// ── Commit order: history → ledger → repo-decisions → talk → state → cleanup → journal ─
|
|
3300
3340
|
appendJsonl(root, HISTORY_PATH, historyEntry);
|
|
3301
3341
|
for (const entry of ledgerEntries) {
|
|
3302
3342
|
appendJsonl(root, LEDGER_PATH, entry);
|
|
3303
3343
|
}
|
|
3344
|
+
// Persist repo-durable decisions and process overrides
|
|
3345
|
+
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
3346
|
+
for (const dec of turnResult.decisions) {
|
|
3347
|
+
// Process override first (marks target as overridden in repo-decisions.jsonl)
|
|
3348
|
+
if (dec.overrides) {
|
|
3349
|
+
overrideRepoDecision(root, dec.overrides, dec.id);
|
|
3350
|
+
}
|
|
3351
|
+
// Write to repo-decisions.jsonl if repo-durable or overriding a repo decision
|
|
3352
|
+
if ((dec.durability || 'run') === 'repo' || dec.overrides) {
|
|
3353
|
+
appendRepoDecision(root, {
|
|
3354
|
+
id: dec.id,
|
|
3355
|
+
run_id: state.run_id,
|
|
3356
|
+
turn_id: turnResult.turn_id,
|
|
3357
|
+
role: turnResult.role,
|
|
3358
|
+
phase: state.phase,
|
|
3359
|
+
category: dec.category,
|
|
3360
|
+
statement: dec.statement,
|
|
3361
|
+
rationale: dec.rationale,
|
|
3362
|
+
status: 'active',
|
|
3363
|
+
overridden_by: null,
|
|
3364
|
+
created_at: now,
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3304
3369
|
appendTalk(root, talkSection);
|
|
3305
3370
|
writeState(root, updatedState);
|
|
3306
3371
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Decisions — cross-run decision carryover.
|
|
3
|
+
*
|
|
4
|
+
* Decisions with durability: "repo" persist in `.agentxchain/repo-decisions.jsonl`
|
|
5
|
+
* across governed runs. They act as binding constraints: agents in future runs
|
|
6
|
+
* must comply with active repo decisions or explicitly override them.
|
|
7
|
+
*
|
|
8
|
+
* DEC-SPEC: .planning/CROSS_RUN_DECISION_CARRYOVER_SPEC.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, appendFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
const REPO_DECISIONS_PATH = '.agentxchain/repo-decisions.jsonl';
|
|
15
|
+
|
|
16
|
+
// ── Read ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export function readRepoDecisions(root) {
|
|
19
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
20
|
+
if (!existsSync(filePath)) return [];
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(filePath, 'utf8').trim();
|
|
23
|
+
if (!content) return [];
|
|
24
|
+
return content.split('\n').filter(Boolean).map(line => {
|
|
25
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
26
|
+
}).filter(Boolean);
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getActiveRepoDecisions(root) {
|
|
33
|
+
return readRepoDecisions(root).filter(d => d.status === 'active');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getRepoDecisionById(root, decisionId) {
|
|
37
|
+
return readRepoDecisions(root).find(d => d.id === decisionId) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Write ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function appendRepoDecision(root, entry) {
|
|
43
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
44
|
+
const dir = dirname(filePath);
|
|
45
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
46
|
+
appendFileSync(filePath, JSON.stringify(entry) + '\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function overrideRepoDecision(root, targetId, overridingId) {
|
|
50
|
+
const all = readRepoDecisions(root);
|
|
51
|
+
const updated = all.map(d => {
|
|
52
|
+
if (d.id === targetId) {
|
|
53
|
+
return { ...d, status: 'overridden', overridden_by: overridingId };
|
|
54
|
+
}
|
|
55
|
+
return d;
|
|
56
|
+
});
|
|
57
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
58
|
+
const dir = dirname(filePath);
|
|
59
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(filePath, updated.map(d => JSON.stringify(d)).join('\n') + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Validate Override ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function validateOverride(root, decision) {
|
|
66
|
+
if (!decision.overrides) return { ok: true };
|
|
67
|
+
const targetId = decision.overrides;
|
|
68
|
+
const target = getRepoDecisionById(root, targetId);
|
|
69
|
+
if (!target) {
|
|
70
|
+
return { ok: false, error: `decisions: overrides references ${targetId} which does not exist in repo decisions.` };
|
|
71
|
+
}
|
|
72
|
+
if (target.status === 'overridden') {
|
|
73
|
+
return { ok: false, error: `decisions: ${targetId} is already overridden by ${target.overridden_by}.` };
|
|
74
|
+
}
|
|
75
|
+
if (target.status !== 'active') {
|
|
76
|
+
return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export function renderRepoDecisionsMarkdown(activeDecisions) {
|
|
84
|
+
if (!activeDecisions || activeDecisions.length === 0) return '';
|
|
85
|
+
const lines = [
|
|
86
|
+
'## Active Repo Decisions',
|
|
87
|
+
'',
|
|
88
|
+
'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
|
|
89
|
+
'',
|
|
90
|
+
];
|
|
91
|
+
for (const d of activeDecisions) {
|
|
92
|
+
lines.push(`- **${d.id}** (${d.category}): ${d.statement}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export { REPO_DECISIONS_PATH };
|