agentxchain 2.103.0 → 2.105.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.
Files changed (66) hide show
  1. package/README.md +13 -7
  2. package/bin/agentxchain.js +16 -8
  3. package/dashboard/app.js +111 -7
  4. package/dashboard/components/blocked.js +95 -11
  5. package/dashboard/components/blockers.js +85 -86
  6. package/dashboard/components/coordinator-timeouts.js +13 -0
  7. package/dashboard/components/cross-repo.js +17 -12
  8. package/dashboard/components/gate.js +31 -11
  9. package/dashboard/components/initiative.js +173 -78
  10. package/dashboard/components/ledger.js +28 -0
  11. package/dashboard/components/live-status.js +39 -0
  12. package/dashboard/components/run-history.js +76 -1
  13. package/dashboard/components/timeline.js +5 -1
  14. package/dashboard/index.html +21 -0
  15. package/dashboard/live-observer.js +91 -0
  16. package/package.json +1 -1
  17. package/scripts/release-bump.sh +26 -3
  18. package/scripts/release-preflight.sh +82 -38
  19. package/src/commands/accept-turn.js +3 -3
  20. package/src/commands/decisions.js +98 -29
  21. package/src/commands/diff.js +27 -4
  22. package/src/commands/doctor.js +48 -16
  23. package/src/commands/generate.js +126 -1
  24. package/src/commands/history.js +21 -3
  25. package/src/commands/init.js +15 -97
  26. package/src/commands/multi.js +223 -54
  27. package/src/commands/phase.js +11 -13
  28. package/src/commands/reject-turn.js +1 -1
  29. package/src/commands/restart.js +28 -11
  30. package/src/commands/resume.js +6 -6
  31. package/src/commands/role.js +51 -14
  32. package/src/commands/run.js +5 -11
  33. package/src/commands/status.js +145 -13
  34. package/src/commands/step.js +36 -29
  35. package/src/lib/admission-control.js +14 -12
  36. package/src/lib/blocked-state.js +150 -0
  37. package/src/lib/conflict-actions.js +17 -0
  38. package/src/lib/context-section-parser.js +2 -0
  39. package/src/lib/continuity-status.js +1 -1
  40. package/src/lib/coordinator-blocker-presentation.js +127 -0
  41. package/src/lib/coordinator-event-narrative.js +43 -0
  42. package/src/lib/coordinator-gate-approval.js +98 -0
  43. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  44. package/src/lib/coordinator-next-actions.js +128 -0
  45. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  46. package/src/lib/coordinator-presentation-detail.js +11 -0
  47. package/src/lib/coordinator-repo-snapshots.js +53 -0
  48. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  49. package/src/lib/dashboard/actions.js +105 -29
  50. package/src/lib/dashboard/bridge-server.js +7 -0
  51. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  52. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  53. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  54. package/src/lib/dashboard/state-reader.js +36 -1
  55. package/src/lib/dispatch-bundle.js +23 -0
  56. package/src/lib/export-diff.js +70 -38
  57. package/src/lib/export-verifier.js +3 -0
  58. package/src/lib/history-diff-summary.js +249 -0
  59. package/src/lib/manual-qa-fallback.js +18 -0
  60. package/src/lib/normalized-config.js +27 -22
  61. package/src/lib/planning-artifacts.js +131 -0
  62. package/src/lib/recent-event-summary.js +132 -0
  63. package/src/lib/repo-decisions.js +69 -28
  64. package/src/lib/report.js +353 -145
  65. package/src/lib/run-diff.js +4 -0
  66. package/src/lib/runtime-capabilities.js +222 -0
@@ -0,0 +1,50 @@
1
+ import { loadCoordinatorConfig } from '../coordinator-config.js';
2
+ import { buildCoordinatorRepoStatusRows } from '../coordinator-repo-status-presentation.js';
3
+ import { loadCoordinatorState } from '../coordinator-state.js';
4
+
5
+ function getConfigErrorResponse(errors) {
6
+ const issueList = Array.isArray(errors) ? errors : [];
7
+ const missing = issueList.some((error) => typeof error === 'string' && error.startsWith('config_missing:'));
8
+
9
+ return {
10
+ ok: false,
11
+ status: missing ? 404 : 422,
12
+ body: {
13
+ ok: false,
14
+ code: missing ? 'coordinator_config_missing' : 'coordinator_config_invalid',
15
+ error: missing
16
+ ? 'Coordinator config not found. Run `agentxchain multi init` first.'
17
+ : 'Coordinator config is invalid.',
18
+ errors: issueList,
19
+ },
20
+ };
21
+ }
22
+
23
+ export function readCoordinatorRepoStatusRows(workspacePath) {
24
+ const configResult = loadCoordinatorConfig(workspacePath);
25
+ if (!configResult.ok) {
26
+ return getConfigErrorResponse(configResult.errors);
27
+ }
28
+
29
+ const state = loadCoordinatorState(workspacePath);
30
+ if (!state) {
31
+ return {
32
+ ok: false,
33
+ status: 404,
34
+ body: {
35
+ ok: false,
36
+ code: 'coordinator_state_missing',
37
+ error: 'Coordinator state not found. Run `agentxchain multi init` first.',
38
+ },
39
+ };
40
+ }
41
+
42
+ return {
43
+ ok: true,
44
+ status: 200,
45
+ body: buildCoordinatorRepoStatusRows({
46
+ config: configResult.config,
47
+ coordinatorRepoRuns: state.repo_runs || {},
48
+ }),
49
+ };
50
+ }
@@ -1,6 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import { loadProjectContext, loadProjectState } from '../config.js';
3
3
  import { loadCoordinatorConfig } from '../coordinator-config.js';
4
+ import { buildCoordinatorRepoStatusRows } from '../coordinator-repo-status-presentation.js';
4
5
  import { loadCoordinatorState } from '../coordinator-state.js';
5
6
  import { readJsonlFile } from './state-reader.js';
6
7
  import { buildTimeoutConfigSummary, evaluateDashboardTimeoutPressure, extractTimeoutEvents } from './timeout-status.js';
@@ -27,15 +28,26 @@ function emptyLiveTimeouts() {
27
28
  return { exceeded: [], warnings: [] };
28
29
  }
29
30
 
30
- function readRepoTimeoutSnapshot(repoId, repo, repoRun) {
31
+ function getRepoStatusPresentation(repoStatusRow) {
32
+ return {
33
+ run_id: repoStatusRow?.run_id ?? null,
34
+ status: repoStatusRow?.status ?? null,
35
+ phase: repoStatusRow?.phase ?? null,
36
+ details: Array.isArray(repoStatusRow?.details) ? repoStatusRow.details : [],
37
+ };
38
+ }
39
+
40
+ function readRepoTimeoutSnapshot(repoId, repo, repoStatusRow) {
41
+ const presentation = getRepoStatusPresentation(repoStatusRow);
31
42
  const context = loadProjectContext(repo.resolved_path);
32
43
  if (!context) {
33
44
  return {
34
45
  repo_id: repoId,
35
46
  path: repo.path,
36
- run_id: repoRun?.run_id ?? null,
37
- status: repoRun?.status ?? null,
38
- phase: repoRun?.phase ?? null,
47
+ run_id: presentation.run_id,
48
+ status: presentation.status,
49
+ phase: presentation.phase,
50
+ details: presentation.details,
39
51
  configured: false,
40
52
  config: null,
41
53
  live: null,
@@ -56,9 +68,10 @@ function readRepoTimeoutSnapshot(repoId, repo, repoRun) {
56
68
  return {
57
69
  repo_id: repoId,
58
70
  path: repo.path,
59
- run_id: repoRun?.run_id ?? null,
60
- status: repoRun?.status ?? null,
61
- phase: repoRun?.phase ?? null,
71
+ run_id: presentation.run_id,
72
+ status: presentation.status,
73
+ phase: presentation.phase,
74
+ details: presentation.details,
62
75
  configured,
63
76
  config: buildTimeoutConfigSummary(context.config.timeouts, context.config.routing),
64
77
  live: configured ? emptyLiveTimeouts() : null,
@@ -77,9 +90,10 @@ function readRepoTimeoutSnapshot(repoId, repo, repoRun) {
77
90
  return {
78
91
  repo_id: repoId,
79
92
  path: repo.path,
80
- run_id: state.run_id ?? repoRun?.run_id ?? null,
81
- status: state.status ?? repoRun?.status ?? null,
82
- phase: state.phase ?? repoRun?.phase ?? null,
93
+ run_id: presentation.run_id,
94
+ status: presentation.status,
95
+ phase: presentation.phase,
96
+ details: presentation.details,
83
97
  configured,
84
98
  config: buildTimeoutConfigSummary(context.config.timeouts, context.config.routing),
85
99
  live,
@@ -109,8 +123,17 @@ export function readCoordinatorTimeoutStatus(workspacePath) {
109
123
 
110
124
  const coordinatorDir = join(workspacePath, '.agentxchain', 'multirepo');
111
125
  const coordinatorEvents = extractTimeoutEvents(readJsonlFile(coordinatorDir, 'decision-ledger.jsonl'));
126
+ const repoStatusRows = buildCoordinatorRepoStatusRows({
127
+ config: configResult.config,
128
+ coordinatorRepoRuns: coordinatorState.repo_runs || {},
129
+ });
130
+ const repoStatusById = new Map(repoStatusRows.map((row) => [row.repo_id, row]));
112
131
  const repos = configResult.config.repo_order.map((repoId) => (
113
- readRepoTimeoutSnapshot(repoId, configResult.config.repos[repoId], coordinatorState.repo_runs?.[repoId] ?? null)
132
+ readRepoTimeoutSnapshot(
133
+ repoId,
134
+ configResult.config.repos[repoId],
135
+ repoStatusById.get(repoId) ?? null,
136
+ )
114
137
  ));
115
138
 
116
139
  const summary = {
@@ -8,13 +8,20 @@
8
8
 
9
9
  import { readFileSync, existsSync } from 'fs';
10
10
  import { join, normalize, resolve } from 'path';
11
+ import {
12
+ deriveGovernedRunNextActions,
13
+ deriveRuntimeBlockedGuidance,
14
+ } from '../blocked-state.js';
15
+ import { loadProjectContext } from '../config.js';
11
16
  import { getContinuityStatus } from '../continuity-status.js';
17
+ import { readRepoDecisions, summarizeRepoDecisions } from '../repo-decisions.js';
12
18
 
13
19
  const STATE_FILE = 'state.json';
14
20
  const SESSION_FILE = 'session.json';
15
21
  const SESSION_RECOVERY_FILE = 'SESSION_RECOVERY.md';
16
22
  const HISTORY_FILE = 'history.jsonl';
17
23
  const LEDGER_FILE = 'decision-ledger.jsonl';
24
+ const REPO_DECISIONS_FILE = 'repo-decisions.jsonl';
18
25
  const HOOK_AUDIT_FILE = 'hook-audit.jsonl';
19
26
  const HOOK_ANNOTATIONS_FILE = 'hook-annotations.jsonl';
20
27
  const EVENTS_FILE = 'events.jsonl';
@@ -50,6 +57,7 @@ export const FILE_TO_RESOURCE = Object.fromEntries(
50
57
  );
51
58
  FILE_TO_RESOURCE[normalizeRelativePath(SESSION_RECOVERY_FILE)] = '/api/continuity';
52
59
  FILE_TO_RESOURCE[normalizeRelativePath('run-history.jsonl')] = '/api/run-history';
60
+ FILE_TO_RESOURCE[normalizeRelativePath(REPO_DECISIONS_FILE)] = '/api/repo-decisions-summary';
53
61
 
54
62
  export const WATCH_DIRECTORIES = [
55
63
  '',
@@ -88,6 +96,24 @@ export function readJsonlFile(agentxchainDir, filename) {
88
96
  return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
89
97
  }
90
98
 
99
+ function enrichGovernedState(agentxchainDir, state) {
100
+ if (!state || typeof state !== 'object') {
101
+ return state;
102
+ }
103
+
104
+ const workspacePath = resolve(agentxchainDir, '..');
105
+ const context = loadProjectContext(workspacePath);
106
+ if (!context || context.config?.protocol_mode !== 'governed') {
107
+ return state;
108
+ }
109
+
110
+ return {
111
+ ...state,
112
+ runtime_guidance: deriveRuntimeBlockedGuidance(state, context.config),
113
+ next_actions: deriveGovernedRunNextActions(state, context.config),
114
+ };
115
+ }
116
+
91
117
  /**
92
118
  * Read a resource by its API path. Returns { data, format } or null.
93
119
  */
@@ -98,6 +124,12 @@ export function readResource(agentxchainDir, resourcePath) {
98
124
  const data = getContinuityStatus(root, state);
99
125
  return { data, format: 'json' };
100
126
  }
127
+ if (resourcePath === '/api/repo-decisions-summary') {
128
+ const root = resolve(agentxchainDir, '..');
129
+ const context = loadProjectContext(root);
130
+ const data = summarizeRepoDecisions(readRepoDecisions(root), context?.config || null);
131
+ return { data, format: 'json' };
132
+ }
101
133
 
102
134
  const filename = RESOURCE_MAP[resourcePath];
103
135
  if (!filename) return null;
@@ -106,6 +138,9 @@ export function readResource(agentxchainDir, resourcePath) {
106
138
  const data = readJsonlFile(agentxchainDir, filename);
107
139
  return data !== null ? { data, format: 'jsonl' } : null;
108
140
  }
109
- const data = readJsonFile(agentxchainDir, filename);
141
+ let data = readJsonFile(agentxchainDir, filename);
142
+ if (resourcePath === '/api/state') {
143
+ data = enrichGovernedState(agentxchainDir, data);
144
+ }
110
145
  return data !== null ? { data, format: 'json' } : null;
111
146
  }
@@ -29,6 +29,7 @@ import {
29
29
  getDispatchTurnDir,
30
30
  getTurnStagingResultPath,
31
31
  } from './turn-paths.js';
32
+ import { getRoleRuntimeCapabilityContract } from './runtime-capabilities.js';
32
33
 
33
34
  const HISTORY_PATH = '.agentxchain/history.jsonl';
34
35
  const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
@@ -183,6 +184,7 @@ function renderPrompt(role, roleId, turn, state, config, root) {
183
184
  }
184
185
 
185
186
  const lines = [];
187
+ const runtimeContract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
186
188
 
187
189
  // Identity block
188
190
  lines.push(`# Turn Assignment: ${role.title} (${roleId})`);
@@ -511,6 +513,8 @@ function getWorkflowPromptResponsibilities(config, phase, roleId, root) {
511
513
  function renderContext(state, config, root, turn, role) {
512
514
  const warnings = [];
513
515
  const lines = [];
516
+ const runtime = config.runtimes?.[turn.runtime_id];
517
+ const runtimeContract = getRoleRuntimeCapabilityContract(turn.assigned_role, role, runtime);
514
518
 
515
519
  lines.push('# Execution Context');
516
520
  lines.push('');
@@ -616,6 +620,25 @@ function renderContext(state, config, root, turn, role) {
616
620
  lines.push('');
617
621
  }
618
622
 
623
+ lines.push('## Runtime Capability Contract');
624
+ lines.push('');
625
+ lines.push(`- **Runtime:** ${turn.runtime_id} (${runtimeContract.runtime_contract.runtime_type})`);
626
+ lines.push(`- **Transport:** ${runtimeContract.runtime_contract.transport}`);
627
+ lines.push(`- **Can write files:** ${runtimeContract.runtime_contract.can_write_files}`);
628
+ lines.push(`- **Review/manual behavior:** ${runtimeContract.runtime_contract.review_only_behavior}`);
629
+ lines.push(`- **Proposal support:** ${runtimeContract.runtime_contract.proposal_support}`);
630
+ lines.push(`- **Requires local binary:** ${runtimeContract.runtime_contract.requires_local_binary ? 'yes' : 'no'}`);
631
+ lines.push(`- **Workflow artifact ownership:** ${runtimeContract.runtime_contract.workflow_artifact_ownership}`);
632
+ lines.push(`- **Effective write path for this role:** ${runtimeContract.effective_write_path}`);
633
+ lines.push(`- **Effective workflow artifact ownership for this role:** ${runtimeContract.workflow_artifact_ownership}`);
634
+ if (runtimeContract.notes.length > 0) {
635
+ lines.push('- **Notes:**');
636
+ for (const note of runtimeContract.notes) {
637
+ lines.push(` - ${note}`);
638
+ }
639
+ }
640
+ lines.push('');
641
+
619
642
  // Repo-level decisions that persist across runs
620
643
  if (state.repo_decisions && state.repo_decisions.length > 0) {
621
644
  const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions, config);
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
+ import { buildCoordinatorRepoStatusEntries } from './coordinator-repo-status-presentation.js';
3
4
 
4
5
  const FAILED_STATUSES = new Set(['failed', 'error', 'crashed']);
6
+ const BLOCKED_OR_FAILED_STATUSES = new Set(['blocked', 'failed', 'error', 'crashed']);
5
7
 
6
8
  const RUN_EXPORT_SCALAR_FIELDS = [
7
9
  ['run_id', 'Run ID'],
@@ -151,7 +153,7 @@ function buildCoordinatorExportDiff(leftArtifact, rightArtifact, refs) {
151
153
  repo_ids: buildListChange('Repos', left.repo_ids, right.repo_ids),
152
154
  repos_with_events: buildListChange('Repos with events', left.repos_with_events, right.repos_with_events),
153
155
  };
154
- const repo_status_changes = buildMapChanges('Repo status', left.repo_run_statuses, right.repo_run_statuses);
156
+ const repo_status_changes = buildMapChanges('Repo status', left.repo_statuses, right.repo_statuses);
155
157
  const repo_export_changes = buildBooleanMapChanges('Repo export ok', left.repo_export_status, right.repo_export_status);
156
158
  const event_type_changes = buildNumericMapChanges('Event type', left.event_type_counts, right.event_type_counts);
157
159
 
@@ -221,8 +223,8 @@ function normalizeRunExport(artifact) {
221
223
  function normalizeCoordinatorExport(artifact) {
222
224
  const summary = artifact.summary || {};
223
225
  const aggregatedEvents = summary.aggregated_events || {};
224
- const repoRunStatuses = summary.repo_run_statuses || {};
225
226
  const repos = artifact.repos || {};
227
+ const repoStatusEntries = buildCoordinatorExportRepoStatusEntries(artifact);
226
228
 
227
229
  return {
228
230
  export_kind: artifact.export_kind,
@@ -235,9 +237,16 @@ function normalizeCoordinatorExport(artifact) {
235
237
  history_entries: toNumber(summary.history_entries),
236
238
  decision_entries: toNumber(summary.decision_entries),
237
239
  total_events: toNumber(aggregatedEvents.total_events),
238
- repo_ids: normalizeStringArray(Object.keys(repos)),
240
+ repo_ids: normalizeStringArray(repoStatusEntries.map((entry) => entry.repo_id)),
239
241
  repos_with_events: normalizeStringArray(aggregatedEvents.repos_with_events),
240
- repo_run_statuses: normalizeStringMap(repoRunStatuses),
242
+ repo_statuses: normalizeStringMap(Object.fromEntries(
243
+ repoStatusEntries.map((entry) => [entry.repo_id, entry.status]),
244
+ )),
245
+ coordinator_repo_statuses: normalizeStringMap(Object.fromEntries(
246
+ repoStatusEntries
247
+ .filter((entry) => entry.coordinator_status != null)
248
+ .map((entry) => [entry.repo_id, entry.coordinator_status]),
249
+ )),
241
250
  repo_export_status: normalizeBooleanMap(Object.fromEntries(
242
251
  Object.entries(repos).map(([repoId, repoEntry]) => [repoId, repoEntry?.ok === true]),
243
252
  )),
@@ -245,6 +254,26 @@ function normalizeCoordinatorExport(artifact) {
245
254
  };
246
255
  }
247
256
 
257
+ function buildCoordinatorExportRepoStatusEntries(artifact) {
258
+ const summaryRepoStatuses = artifact.summary?.repo_run_statuses || {};
259
+ const coordinatorRepoRuns = Object.fromEntries(
260
+ Object.entries(summaryRepoStatuses).map(([repoId, status]) => [repoId, { status: status || null }]),
261
+ );
262
+ const repoSnapshots = Object.entries(artifact.repos || {}).map(([repoId, repoEntry]) => ({
263
+ repo_id: repoId,
264
+ ok: repoEntry?.ok === true,
265
+ status: repoEntry?.ok ? (repoEntry.export?.summary?.status ?? null) : null,
266
+ run_id: repoEntry?.ok ? (repoEntry.export?.summary?.run_id ?? null) : null,
267
+ phase: repoEntry?.ok ? (repoEntry.export?.summary?.phase ?? null) : null,
268
+ }));
269
+
270
+ return buildCoordinatorRepoStatusEntries({
271
+ config: artifact.config,
272
+ coordinatorRepoRuns,
273
+ repoSnapshots,
274
+ });
275
+ }
276
+
248
277
  function buildFieldChanges(left, right, fields) {
249
278
  return Object.fromEntries(
250
279
  fields.map(([field, label]) => [field, {
@@ -402,8 +431,8 @@ function detectRunRegressions(left, right) {
402
431
  const regressions = [];
403
432
  let counter = 0;
404
433
 
405
- // Status regression: completed/active -> failed/error/crashed
406
- if (left.status && right.status && !FAILED_STATUSES.has(left.status) && FAILED_STATUSES.has(right.status)) {
434
+ // Status regression: successful/non-terminal -> blocked/failed/error/crashed
435
+ if (left.status && right.status && !BLOCKED_OR_FAILED_STATUSES.has(left.status) && BLOCKED_OR_FAILED_STATUSES.has(right.status)) {
407
436
  regressions.push({
408
437
  id: `REG-STATUS-${String(++counter).padStart(3, '0')}`,
409
438
  category: 'status',
@@ -553,40 +582,43 @@ function detectCoordinatorRegressions(left, right) {
553
582
  // Start with the run-level regressions that apply to coordinator summaries
554
583
  const regressions = detectRunRegressions(left, right);
555
584
  let counter = regressions.length;
556
-
557
- // Repo status regressions: child repo completed -> failed
558
- const allRepoIds = new Set([...Object.keys(left.repo_run_statuses || {}), ...Object.keys(right.repo_run_statuses || {})]);
559
- for (const repoId of allRepoIds) {
560
- const leftStatus = (left.repo_run_statuses || {})[repoId] || null;
561
- const rightStatus = (right.repo_run_statuses || {})[repoId] || null;
562
- if (leftStatus && rightStatus && !FAILED_STATUSES.has(leftStatus) && FAILED_STATUSES.has(rightStatus)) {
563
- regressions.push({
564
- id: `REG-REPO-STATUS-${String(++counter).padStart(3, '0')}`,
565
- category: 'repo_status',
566
- severity: 'error',
567
- message: `Child repo "${repoId}" status regressed from ${leftStatus} to ${rightStatus}`,
568
- field: `repo_run_statuses.${repoId}`,
569
- left: leftStatus,
570
- right: rightStatus,
571
- });
585
+ const terminalComparison = left.status === 'completed' && right.status === 'completed';
586
+
587
+ // Repo status regressions: child repo success/non-terminal -> blocked/failed
588
+ if (!terminalComparison) {
589
+ const allRepoIds = new Set([...Object.keys(left.repo_statuses || {}), ...Object.keys(right.repo_statuses || {})]);
590
+ for (const repoId of allRepoIds) {
591
+ const leftStatus = (left.repo_statuses || {})[repoId] || null;
592
+ const rightStatus = (right.repo_statuses || {})[repoId] || null;
593
+ if (leftStatus && rightStatus && !BLOCKED_OR_FAILED_STATUSES.has(leftStatus) && BLOCKED_OR_FAILED_STATUSES.has(rightStatus)) {
594
+ regressions.push({
595
+ id: `REG-REPO-STATUS-${String(++counter).padStart(3, '0')}`,
596
+ category: 'repo_status',
597
+ severity: 'error',
598
+ message: `Child repo "${repoId}" status regressed from ${leftStatus} to ${rightStatus}`,
599
+ field: `repo_statuses.${repoId}`,
600
+ left: leftStatus,
601
+ right: rightStatus,
602
+ });
603
+ }
572
604
  }
573
- }
574
605
 
575
- // Repo export regressions: ok true -> false
576
- const allExportRepoIds = new Set([...Object.keys(left.repo_export_status || {}), ...Object.keys(right.repo_export_status || {})]);
577
- for (const repoId of allExportRepoIds) {
578
- const leftOk = (left.repo_export_status || {})[repoId];
579
- const rightOk = (right.repo_export_status || {})[repoId];
580
- if (leftOk === true && rightOk === false) {
581
- regressions.push({
582
- id: `REG-REPO-EXPORT-${String(++counter).padStart(3, '0')}`,
583
- category: 'repo_export',
584
- severity: 'error',
585
- message: `Child repo "${repoId}" export regressed from ok to failed`,
586
- field: `repo_export_status.${repoId}`,
587
- left: true,
588
- right: false,
589
- });
606
+ // Repo export regressions: ok true -> false
607
+ const allExportRepoIds = new Set([...Object.keys(left.repo_export_status || {}), ...Object.keys(right.repo_export_status || {})]);
608
+ for (const repoId of allExportRepoIds) {
609
+ const leftOk = (left.repo_export_status || {})[repoId];
610
+ const rightOk = (right.repo_export_status || {})[repoId];
611
+ if (leftOk === true && rightOk === false) {
612
+ regressions.push({
613
+ id: `REG-REPO-EXPORT-${String(++counter).padStart(3, '0')}`,
614
+ category: 'repo_export',
615
+ severity: 'error',
616
+ message: `Child repo "${repoId}" export regressed from ok to failed`,
617
+ field: `repo_export_status.${repoId}`,
618
+ left: true,
619
+ right: false,
620
+ });
621
+ }
590
622
  }
591
623
  }
592
624
 
@@ -420,6 +420,9 @@ function verifyRepoDecisionsSummary(artifact, errors) {
420
420
  if (!isDeepStrictEqual(summary.overridden, expected.overridden)) {
421
421
  addError(errors, 'summary.repo_decisions.overridden', 'must match reconstructed overridden decisions from repo-decisions.jsonl');
422
422
  }
423
+ if (!isDeepStrictEqual(summary.operator_summary, expected.operator_summary)) {
424
+ addError(errors, 'summary.repo_decisions.operator_summary', 'must match reconstructed repo decision operator summary');
425
+ }
423
426
  }
424
427
 
425
428
  const VALID_DASHBOARD_STATUSES = new Set(['running', 'pid_only', 'stale', 'not_running']);