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.
@@ -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 markdown', 'text')
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 markdown', 'text')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.92.0",
3
+ "version": "2.94.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 "markdown".`);
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
+ }
@@ -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 "markdown".`);
38
+ console.error(`Unsupported report format "${format}". Use "text", "json", "markdown", or "html".`);
33
39
  process.exit(1);
34
40
  }
35
41
 
@@ -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 };