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.
- package/README.md +13 -7
- package/bin/agentxchain.js +16 -8
- package/dashboard/app.js +111 -7
- package/dashboard/components/blocked.js +95 -11
- package/dashboard/components/blockers.js +85 -86
- package/dashboard/components/coordinator-timeouts.js +13 -0
- package/dashboard/components/cross-repo.js +17 -12
- package/dashboard/components/gate.js +31 -11
- package/dashboard/components/initiative.js +173 -78
- package/dashboard/components/ledger.js +28 -0
- package/dashboard/components/live-status.js +39 -0
- package/dashboard/components/run-history.js +76 -1
- package/dashboard/components/timeline.js +5 -1
- package/dashboard/index.html +21 -0
- package/dashboard/live-observer.js +91 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +26 -3
- package/scripts/release-preflight.sh +82 -38
- package/src/commands/accept-turn.js +3 -3
- package/src/commands/decisions.js +98 -29
- package/src/commands/diff.js +27 -4
- package/src/commands/doctor.js +48 -16
- package/src/commands/generate.js +126 -1
- package/src/commands/history.js +21 -3
- package/src/commands/init.js +15 -97
- package/src/commands/multi.js +223 -54
- package/src/commands/phase.js +11 -13
- package/src/commands/reject-turn.js +1 -1
- package/src/commands/restart.js +28 -11
- package/src/commands/resume.js +6 -6
- package/src/commands/role.js +51 -14
- package/src/commands/run.js +5 -11
- package/src/commands/status.js +145 -13
- package/src/commands/step.js +36 -29
- package/src/lib/admission-control.js +14 -12
- package/src/lib/blocked-state.js +150 -0
- package/src/lib/conflict-actions.js +17 -0
- package/src/lib/context-section-parser.js +2 -0
- package/src/lib/continuity-status.js +1 -1
- package/src/lib/coordinator-blocker-presentation.js +127 -0
- package/src/lib/coordinator-event-narrative.js +43 -0
- package/src/lib/coordinator-gate-approval.js +98 -0
- package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
- package/src/lib/coordinator-next-actions.js +128 -0
- package/src/lib/coordinator-pending-gate-presentation.js +79 -0
- package/src/lib/coordinator-presentation-detail.js +11 -0
- package/src/lib/coordinator-repo-snapshots.js +53 -0
- package/src/lib/coordinator-repo-status-presentation.js +134 -0
- package/src/lib/dashboard/actions.js +105 -29
- package/src/lib/dashboard/bridge-server.js +7 -0
- package/src/lib/dashboard/coordinator-blockers.js +17 -0
- package/src/lib/dashboard/coordinator-repo-status.js +50 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
- package/src/lib/dashboard/state-reader.js +36 -1
- package/src/lib/dispatch-bundle.js +23 -0
- package/src/lib/export-diff.js +70 -38
- package/src/lib/export-verifier.js +3 -0
- package/src/lib/history-diff-summary.js +249 -0
- package/src/lib/manual-qa-fallback.js +18 -0
- package/src/lib/normalized-config.js +27 -22
- package/src/lib/planning-artifacts.js +131 -0
- package/src/lib/recent-event-summary.js +132 -0
- package/src/lib/repo-decisions.js +69 -28
- package/src/lib/report.js +353 -145
- package/src/lib/run-diff.js +4 -0
- 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
|
|
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:
|
|
37
|
-
status:
|
|
38
|
-
phase:
|
|
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:
|
|
60
|
-
status:
|
|
61
|
-
phase:
|
|
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:
|
|
81
|
-
status:
|
|
82
|
-
phase:
|
|
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(
|
|
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
|
-
|
|
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);
|
package/src/lib/export-diff.js
CHANGED
|
@@ -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.
|
|
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(
|
|
240
|
+
repo_ids: normalizeStringArray(repoStatusEntries.map((entry) => entry.repo_id)),
|
|
239
241
|
repos_with_events: normalizeStringArray(aggregatedEvents.repos_with_events),
|
|
240
|
-
|
|
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:
|
|
406
|
-
if (left.status && 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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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']);
|