agentxchain 2.91.0 → 2.93.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.91.0",
3
+ "version": "2.93.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
+ }
@@ -5,6 +5,7 @@ import chalk from 'chalk';
5
5
  import { loadConfig, loadLock, findProjectRoot } from '../lib/config.js';
6
6
  import { validateProject } from '../lib/validation.js';
7
7
  import { getWatchPid } from './watch.js';
8
+ import { getDashboardPid, getDashboardSession } from './dashboard.js';
8
9
  import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
9
10
  import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
10
11
  import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
@@ -219,6 +220,21 @@ function governedDoctor(root, rawConfig, opts) {
219
220
  }
220
221
  }
221
222
 
223
+ // 9. Dashboard session health (unconditional — dashboard is a general operator surface)
224
+ {
225
+ const dashPid = getDashboardPid(root);
226
+ const dashSession = getDashboardSession(root);
227
+ if (dashPid && dashSession) {
228
+ checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'pass', detail: `Dashboard running at ${dashSession.url} (PID: ${dashPid})` });
229
+ } else if (dashPid && !dashSession) {
230
+ checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Dashboard PID ${dashPid} alive but session file missing` });
231
+ } else if (!dashPid && dashSession) {
232
+ checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Stale dashboard session files (PID ${dashSession.pid || '?'} not running)` });
233
+ } else {
234
+ checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'info', detail: 'No dashboard session' });
235
+ }
236
+ }
237
+
222
238
  // Compute summary
223
239
  const failCount = checks.filter(c => c.level === 'fail').length;
224
240
  const warnCount = checks.filter(c => c.level === 'warn').length;
@@ -253,7 +269,9 @@ function governedDoctor(root, rawConfig, opts) {
253
269
  ? chalk.green('PASS')
254
270
  : c.level === 'warn'
255
271
  ? chalk.yellow('WARN')
256
- : chalk.red('FAIL');
272
+ : c.level === 'info'
273
+ ? chalk.dim('INFO')
274
+ : chalk.red('FAIL');
257
275
  console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
258
276
  }
259
277
 
@@ -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
 
@@ -9,6 +9,7 @@ import { getConnectorHealth } from '../lib/connector-health.js';
9
9
  import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
10
10
  import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
11
11
  import { summarizeRunProvenance } from '../lib/run-provenance.js';
12
+ import { getDashboardPid, getDashboardSession } from './dashboard.js';
12
13
 
13
14
  export async function statusCommand(opts) {
14
15
  const context = loadProjectContext();
@@ -87,6 +88,14 @@ function renderGovernedStatus(context, opts) {
87
88
  const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
88
89
 
89
90
  if (opts.json) {
91
+ const dashPid = getDashboardPid(root);
92
+ const dashSession = getDashboardSession(root);
93
+ const dashboardSessionObj = dashPid
94
+ ? { status: 'running', pid: dashPid, url: dashSession?.url || null, started_at: dashSession?.started_at || null }
95
+ : dashSession
96
+ ? { status: 'stale', pid: dashSession.pid || null, url: dashSession.url || null, started_at: dashSession.started_at || null }
97
+ : { status: 'not_running', pid: null, url: null, started_at: null };
98
+
90
99
  console.log(JSON.stringify({
91
100
  version,
92
101
  protocol_mode: config.protocol_mode,
@@ -96,9 +105,11 @@ function renderGovernedStatus(context, opts) {
96
105
  state,
97
106
  provenance: state?.provenance || null,
98
107
  inherited_context: state?.inherited_context || null,
108
+ repo_decisions: state?.repo_decisions || null,
99
109
  continuity,
100
110
  connector_health: connectorHealth,
101
111
  workflow_kit_artifacts: workflowKitArtifacts,
112
+ dashboard_session: dashboardSessionObj,
102
113
  }, null, 2));
103
114
  return;
104
115
  }
@@ -123,6 +134,9 @@ function renderGovernedStatus(context, opts) {
123
134
  if (state?.inherited_context?.parent_run_id) {
124
135
  console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
125
136
  }
137
+ if (state?.repo_decisions?.length > 0) {
138
+ console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(`${state.repo_decisions.length} active`)}`);
139
+ }
126
140
  if (state?.accepted_integration_ref) {
127
141
  console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
128
142
  }
@@ -115,7 +115,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
115
115
  child = spawn(command, args, {
116
116
  cwd: runtime.cwd ? join(root, runtime.cwd) : root,
117
117
  stdio: ['pipe', 'pipe', 'pipe'],
118
- env: { ...process.env },
118
+ env: { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id },
119
119
  });
120
120
  } catch (err) {
121
121
  resolve({ ok: false, error: `Failed to spawn "${command}": ${err.message}`, logs });
@@ -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,
@@ -605,6 +606,14 @@ function renderContext(state, config, root, turn, role) {
605
606
  lines.push('');
606
607
  }
607
608
 
609
+ // Repo-level decisions that persist across runs
610
+ if (state.repo_decisions && state.repo_decisions.length > 0) {
611
+ const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions);
612
+ if (repoDecMd) {
613
+ lines.push(repoDecMd);
614
+ }
615
+ }
616
+
608
617
  // Inherited context from parent run (when --inherit-context was used)
609
618
  if (state.inherited_context) {
610
619
  // First turn gets the full rendering; subsequent turns get compact
package/src/lib/export.js CHANGED
@@ -7,6 +7,8 @@ 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
9
  import { normalizeRunProvenance } from './run-provenance.js';
10
+ import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
11
+ import { readRepoDecisions } from './repo-decisions.js';
10
12
 
11
13
  const EXPORT_SCHEMA_VERSION = '0.3';
12
14
 
@@ -23,10 +25,13 @@ const COORDINATOR_INCLUDED_ROOTS = [
23
25
  export const RUN_EXPORT_INCLUDED_ROOTS = [
24
26
  'agentxchain.json',
25
27
  'TALK.md',
28
+ '.agentxchain-dashboard.pid',
29
+ '.agentxchain-dashboard.json',
26
30
  '.agentxchain/state.json',
27
31
  '.agentxchain/session.json',
28
32
  '.agentxchain/history.jsonl',
29
33
  '.agentxchain/decision-ledger.jsonl',
34
+ '.agentxchain/repo-decisions.jsonl',
30
35
  '.agentxchain/hook-audit.jsonl',
31
36
  '.agentxchain/hook-annotations.jsonl',
32
37
  '.agentxchain/notification-audit.jsonl',
@@ -166,6 +171,59 @@ function countDirectoryFiles(files, prefix) {
166
171
  return Object.keys(files).filter((path) => path.startsWith(`${prefix}/`)).length;
167
172
  }
168
173
 
174
+ function buildDashboardSessionSummary(root) {
175
+ const dashPid = getDashboardPid(root);
176
+ const dashSession = getDashboardSession(root);
177
+
178
+ if (dashPid && dashSession) {
179
+ return {
180
+ status: 'running',
181
+ pid: dashPid,
182
+ url: dashSession.url || null,
183
+ started_at: dashSession.started_at || null,
184
+ };
185
+ }
186
+
187
+ if (dashPid && !dashSession) {
188
+ return {
189
+ status: 'pid_only',
190
+ pid: dashPid,
191
+ url: null,
192
+ started_at: null,
193
+ };
194
+ }
195
+
196
+ if (!dashPid && dashSession) {
197
+ return {
198
+ status: 'stale',
199
+ pid: dashSession.pid || null,
200
+ url: dashSession.url || null,
201
+ started_at: dashSession.started_at || null,
202
+ };
203
+ }
204
+
205
+ return {
206
+ status: 'not_running',
207
+ pid: null,
208
+ url: null,
209
+ started_at: null,
210
+ };
211
+ }
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
+
169
227
  export function buildDelegationSummary(files) {
170
228
  const historyData = files['.agentxchain/history.jsonl']?.data;
171
229
  if (!Array.isArray(historyData)) {
@@ -404,7 +462,9 @@ export function buildRunExport(startDir = process.cwd()) {
404
462
  staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
405
463
  intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
406
464
  coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
465
+ dashboard_session: buildDashboardSessionSummary(root),
407
466
  delegation_summary: buildDelegationSummary(files),
467
+ repo_decisions: buildRepoDecisionsSummary(root),
408
468
  },
409
469
  workspace: buildRunWorkspaceMetadata(root),
410
470
  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,
@@ -799,6 +805,61 @@ function readJsonlEntries(root, relPath) {
799
805
  .filter(Boolean);
800
806
  }
801
807
 
808
+ function collectPendingConcurrentSiblingDeclarations(root, state, currentTurn, historyEntries = []) {
809
+ const concurrentIds = new Set(
810
+ Array.isArray(currentTurn?.concurrent_with)
811
+ ? currentTurn.concurrent_with.filter((id) => typeof id === 'string' && id.length > 0)
812
+ : [],
813
+ );
814
+ const activeTurns = getActiveTurns(state);
815
+ for (const turn of Object.values(activeTurns)) {
816
+ if (
817
+ turn?.turn_id !== currentTurn?.turn_id
818
+ && Array.isArray(turn?.concurrent_with)
819
+ && turn.concurrent_with.includes(currentTurn?.turn_id)
820
+ ) {
821
+ concurrentIds.add(turn.turn_id);
822
+ }
823
+ }
824
+ if (concurrentIds.size === 0) {
825
+ return [];
826
+ }
827
+
828
+ const acceptedIds = new Set(
829
+ (Array.isArray(historyEntries) ? historyEntries : [])
830
+ .map((entry) => entry?.turn_id)
831
+ .filter((turnId) => typeof turnId === 'string' && turnId.length > 0),
832
+ );
833
+ const declarations = [];
834
+
835
+ for (const siblingTurnId of concurrentIds) {
836
+ if (siblingTurnId === currentTurn?.turn_id || acceptedIds.has(siblingTurnId) || !activeTurns[siblingTurnId]) {
837
+ continue;
838
+ }
839
+
840
+ const stagedSibling = loadHookStagedTurn(root, getTurnStagingResultPath(siblingTurnId));
841
+ const siblingResult = stagedSibling.turnResult;
842
+ if (!siblingResult || siblingResult.turn_id !== siblingTurnId) {
843
+ continue;
844
+ }
845
+
846
+ const siblingFiles = [...new Set(
847
+ (Array.isArray(siblingResult.files_changed) ? siblingResult.files_changed : [])
848
+ .filter((filePath) => typeof filePath === 'string' && filePath.length > 0),
849
+ )];
850
+ if (siblingFiles.length === 0) {
851
+ continue;
852
+ }
853
+
854
+ declarations.push({
855
+ turn_id: siblingTurnId,
856
+ files_changed: siblingFiles,
857
+ });
858
+ }
859
+
860
+ return declarations;
861
+ }
862
+
802
863
  function getObservedFiles(entry) {
803
864
  if (Array.isArray(entry?.observed_artifact?.files_changed)) {
804
865
  return entry.observed_artifact.files_changed;
@@ -1853,6 +1914,7 @@ export function initializeGovernedRun(root, config, options = {}) {
1853
1914
  const runId = generateId('run');
1854
1915
  const now = new Date().toISOString();
1855
1916
  const provenance = buildDefaultRunProvenance(options.provenance);
1917
+ const repoDecisions = getActiveRepoDecisions(root);
1856
1918
  const updatedState = {
1857
1919
  ...state,
1858
1920
  run_id: runId,
@@ -1867,6 +1929,7 @@ export function initializeGovernedRun(root, config, options = {}) {
1867
1929
  },
1868
1930
  provenance,
1869
1931
  inherited_context: options.inherited_context || null,
1932
+ repo_decisions: repoDecisions.length > 0 ? repoDecisions : null,
1870
1933
  };
1871
1934
 
1872
1935
  writeState(root, updatedState);
@@ -2368,12 +2431,39 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2368
2431
  }
2369
2432
 
2370
2433
  const turnResult = validation.turnResult;
2434
+
2435
+ // Validate cross-run decision overrides against repo-decisions.jsonl
2436
+ if (turnResult.decisions && turnResult.decisions.length > 0) {
2437
+ for (const dec of turnResult.decisions) {
2438
+ if (dec.overrides) {
2439
+ const overrideCheck = validateOverride(root, dec);
2440
+ if (!overrideCheck.ok) {
2441
+ return {
2442
+ ok: false,
2443
+ error: `Override validation failed: ${overrideCheck.error}`,
2444
+ error_code: 'override_validation_failed',
2445
+ };
2446
+ }
2447
+ }
2448
+ }
2449
+ }
2450
+
2371
2451
  const stagingFile = join(root, resolvedStagingPath);
2372
2452
  const now = new Date().toISOString();
2373
2453
  const baseline = currentTurn.baseline || null;
2374
2454
  const rawObservation = observeChanges(root, baseline);
2375
2455
  const historyEntries = readJsonlEntries(root, HISTORY_PATH);
2376
- const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries);
2456
+ const pendingConcurrentSiblingDeclarations = collectPendingConcurrentSiblingDeclarations(
2457
+ root,
2458
+ state,
2459
+ currentTurn,
2460
+ historyEntries,
2461
+ );
2462
+ const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries, {
2463
+ currentDeclaredFiles: turnResult.files_changed || [],
2464
+ concurrentSiblingIds: pendingConcurrentSiblingDeclarations.map((entry) => entry.turn_id),
2465
+ pendingConcurrentSiblingDeclarations,
2466
+ });
2377
2467
  const role = config.roles?.[turnResult.role];
2378
2468
  const runtimeId = turnResult.runtime_id;
2379
2469
  const runtime = config.runtimes?.[runtimeId];
@@ -2381,11 +2471,28 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2381
2471
  materializeDerivedReviewArtifact(root, turnResult, state, runtimeType, baseline);
2382
2472
  materializeDerivedProposalArtifact(root, turnResult, state, runtimeType);
2383
2473
  const writeAuthority = role?.write_authority || 'review_only';
2474
+
2475
+ // When concurrent siblings exist but have not yet been accepted, the
2476
+ // observation includes their file changes too. The attribution system
2477
+ // (attributeObservedChangesToTurn) only removes sibling files for
2478
+ // *later*-accepted turns. For the *first*-accepted concurrent turn,
2479
+ // undeclared files are expected noise from concurrency — downgrade to
2480
+ // warnings so the governance contract is not broken by turn-acceptance
2481
+ // ordering.
2482
+ const concurrentIds = new Set(
2483
+ Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
2484
+ );
2485
+ const acceptedTurnIds = new Set(historyEntries.map(h => h.turn_id));
2486
+ const hasUnacceptedConcurrentSiblings = [...concurrentIds].some(id => !acceptedTurnIds.has(id));
2487
+
2384
2488
  const diffComparison = compareDeclaredVsObserved(
2385
2489
  turnResult.files_changed || [],
2386
2490
  observation.files_changed,
2387
2491
  writeAuthority,
2388
- { observation_available: observation.observation_available },
2492
+ {
2493
+ observation_available: observation.observation_available,
2494
+ has_unaccepted_concurrent_siblings: hasUnacceptedConcurrentSiblings,
2495
+ },
2389
2496
  );
2390
2497
  if (diffComparison.errors.length > 0) {
2391
2498
  return {
@@ -3214,11 +3321,36 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3214
3321
  };
3215
3322
  writeAcceptanceJournal(root, journal);
3216
3323
 
3217
- // ── Commit order: history → ledger → talk → state → cleanup → journal ─
3324
+ // ── Commit order: history → ledger → repo-decisions → talk → state → cleanup → journal ─
3218
3325
  appendJsonl(root, HISTORY_PATH, historyEntry);
3219
3326
  for (const entry of ledgerEntries) {
3220
3327
  appendJsonl(root, LEDGER_PATH, entry);
3221
3328
  }
3329
+ // Persist repo-durable decisions and process overrides
3330
+ if (turnResult.decisions && turnResult.decisions.length > 0) {
3331
+ for (const dec of turnResult.decisions) {
3332
+ // Process override first (marks target as overridden in repo-decisions.jsonl)
3333
+ if (dec.overrides) {
3334
+ overrideRepoDecision(root, dec.overrides, dec.id);
3335
+ }
3336
+ // Write to repo-decisions.jsonl if repo-durable or overriding a repo decision
3337
+ if ((dec.durability || 'run') === 'repo' || dec.overrides) {
3338
+ appendRepoDecision(root, {
3339
+ id: dec.id,
3340
+ run_id: state.run_id,
3341
+ turn_id: turnResult.turn_id,
3342
+ role: turnResult.role,
3343
+ phase: state.phase,
3344
+ category: dec.category,
3345
+ statement: dec.statement,
3346
+ rationale: dec.rationale,
3347
+ status: 'active',
3348
+ overridden_by: null,
3349
+ created_at: now,
3350
+ });
3351
+ }
3352
+ }
3353
+ }
3222
3354
  appendTalk(root, talkSection);
3223
3355
  writeState(root, updatedState);
3224
3356