agentxchain 2.104.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 +12 -6
- package/bin/agentxchain.js +5 -5
- 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/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/history.js +21 -3
- 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/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
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { getEffectiveGateArtifacts } from './gate-evaluator.js';
|
|
17
|
+
import {
|
|
18
|
+
canRoleParticipateInRequiredFileProduction,
|
|
19
|
+
canRoleSatisfyWorkflowArtifactOwnership,
|
|
20
|
+
getRoleRuntimeCapabilityContract,
|
|
21
|
+
} from './runtime-capabilities.js';
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* Run all admission control checks against a governed config.
|
|
@@ -68,17 +73,20 @@ export function runAdmissionControl(config, rawConfig) {
|
|
|
68
73
|
.filter(({ role }) => role);
|
|
69
74
|
|
|
70
75
|
const hasFileProducer = rolesWithAuthority.some(({ role, runtime }) =>
|
|
71
|
-
|
|
76
|
+
canRoleParticipateInRequiredFileProduction(role, runtime));
|
|
72
77
|
|
|
73
78
|
// Only flag non-manual roles as review_only dead-ends
|
|
74
79
|
const nonManualRoles = rolesWithAuthority.filter(({ runtime }) => runtime?.type !== 'manual');
|
|
75
80
|
if (!hasFileProducer && nonManualRoles.length > 0) {
|
|
76
81
|
const roleSummary = nonManualRoles
|
|
77
|
-
.map(({ id, role }) =>
|
|
82
|
+
.map(({ id, role, runtime }) => {
|
|
83
|
+
const contract = getRoleRuntimeCapabilityContract(id, role, runtime);
|
|
84
|
+
return `${id}:${role.write_authority}/${runtime?.type || 'unknown'}->${contract.effective_write_path}`;
|
|
85
|
+
})
|
|
78
86
|
.join(', ');
|
|
79
87
|
const fileSummary = requiredArtifacts.map(a => a.path).join(', ');
|
|
80
88
|
errors.push(
|
|
81
|
-
`ADM-001: Phase "${phase}" gate "${exitGateId}" requires files (${fileSummary}) but
|
|
89
|
+
`ADM-001: Phase "${phase}" gate "${exitGateId}" requires files (${fileSummary}) but no routed role has a reachable file-production path (${roleSummary}).`
|
|
82
90
|
);
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -97,9 +105,10 @@ export function runAdmissionControl(config, rawConfig) {
|
|
|
97
105
|
const ownerRuntimeKey = ownerRole.runtime_id || ownerRole.runtime;
|
|
98
106
|
const ownerRuntime = runtimes?.[ownerRuntimeKey];
|
|
99
107
|
|
|
100
|
-
if (!
|
|
108
|
+
if (!canRoleSatisfyWorkflowArtifactOwnership(ownerRole, ownerRuntime)) {
|
|
109
|
+
const contract = getRoleRuntimeCapabilityContract(artifact.owned_by, ownerRole, ownerRuntime);
|
|
101
110
|
errors.push(
|
|
102
|
-
`ADM-004: Phase "${phase}" artifact "${artifact.path}" is owned_by "${artifact.owned_by}" but that role
|
|
111
|
+
`ADM-004: Phase "${phase}" artifact "${artifact.path}" is owned_by "${artifact.owned_by}" but that role resolves to workflow ownership "${contract.workflow_artifact_ownership}" via ${ownerRole.write_authority}/${ownerRuntime?.type || 'unknown'} (${contract.effective_write_path}).`
|
|
103
112
|
);
|
|
104
113
|
}
|
|
105
114
|
}
|
|
@@ -238,10 +247,3 @@ function matchesPhaseRule(rule, phase) {
|
|
|
238
247
|
if (rule.from_phase && rule.from_phase !== phase) return false;
|
|
239
248
|
return true;
|
|
240
249
|
}
|
|
241
|
-
|
|
242
|
-
function canRoleProduceFiles(role, runtime) {
|
|
243
|
-
if (!role) return false;
|
|
244
|
-
return runtime?.type === 'manual'
|
|
245
|
-
|| role.write_authority === 'authoritative'
|
|
246
|
-
|| role.write_authority === 'proposed';
|
|
247
|
-
}
|
package/src/lib/blocked-state.js
CHANGED
|
@@ -8,6 +8,15 @@ import {
|
|
|
8
8
|
derivePolicyEscalationRecoveryAction,
|
|
9
9
|
getActiveTurnCount,
|
|
10
10
|
} from './governed-state.js';
|
|
11
|
+
import { getEffectiveGateArtifacts } from './gate-evaluator.js';
|
|
12
|
+
import { getRoleRuntimeCapabilityContract } from './runtime-capabilities.js';
|
|
13
|
+
|
|
14
|
+
const RUNTIME_GUIDANCE_PRIORITY = new Map([
|
|
15
|
+
['invalid_binding', 1],
|
|
16
|
+
['review_only_remote_dead_end', 2],
|
|
17
|
+
['proposal_apply_required', 3],
|
|
18
|
+
['tool_defined_proof_not_strong_enough', 4],
|
|
19
|
+
]);
|
|
11
20
|
|
|
12
21
|
function isLegacyEscalationRecoveryAction(action) {
|
|
13
22
|
return action === 'Resolve the escalation, then run agentxchain step --resume'
|
|
@@ -69,6 +78,139 @@ function maybeRefreshRecoveryAction(state, config, persistedRecovery, turnRetain
|
|
|
69
78
|
return null;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
export function deriveRuntimeBlockedGuidance(state, config) {
|
|
82
|
+
if (!state || !config || typeof state !== 'object' || typeof config !== 'object') {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const failure = state.last_gate_failure;
|
|
87
|
+
if (!failure || typeof failure !== 'object') {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const phase = typeof failure.phase === 'string' && failure.phase
|
|
92
|
+
? failure.phase
|
|
93
|
+
: state.phase;
|
|
94
|
+
const gateId = typeof failure.gate_id === 'string' && failure.gate_id
|
|
95
|
+
? failure.gate_id
|
|
96
|
+
: config.routing?.[phase]?.exit_gate || null;
|
|
97
|
+
const missingFiles = Array.isArray(failure.missing_files)
|
|
98
|
+
? failure.missing_files.filter((path) => typeof path === 'string' && path.length > 0)
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
if (!phase || !gateId || missingFiles.length === 0 || !config.gates?.[gateId]) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const requiredArtifacts = getEffectiveGateArtifacts(config, config.gates[gateId], phase)
|
|
106
|
+
.filter((artifact) => artifact?.required !== false)
|
|
107
|
+
.filter((artifact) => missingFiles.includes(artifact.path));
|
|
108
|
+
|
|
109
|
+
if (requiredArtifacts.length === 0) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entryRole = config.routing?.[phase]?.entry_role || null;
|
|
114
|
+
const guidance = [];
|
|
115
|
+
|
|
116
|
+
for (const artifact of requiredArtifacts) {
|
|
117
|
+
const roleId = artifact.owned_by || entryRole;
|
|
118
|
+
if (!roleId) continue;
|
|
119
|
+
|
|
120
|
+
const role = config.roles?.[roleId];
|
|
121
|
+
if (!role) continue;
|
|
122
|
+
|
|
123
|
+
const runtimeId = role.runtime_id || role.runtime;
|
|
124
|
+
const runtime = config.runtimes?.[runtimeId];
|
|
125
|
+
if (!runtime) continue;
|
|
126
|
+
|
|
127
|
+
const contract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
|
|
128
|
+
const invalidBinding = contract.effective_write_path.startsWith('invalid_')
|
|
129
|
+
|| contract.workflow_artifact_ownership === 'invalid';
|
|
130
|
+
|
|
131
|
+
let code = null;
|
|
132
|
+
let command = null;
|
|
133
|
+
let reason = null;
|
|
134
|
+
|
|
135
|
+
if (invalidBinding) {
|
|
136
|
+
code = 'invalid_binding';
|
|
137
|
+
command = `Edit agentxchain.json for role "${roleId}", then run agentxchain validate`;
|
|
138
|
+
reason = `Artifact "${artifact.path}" is owned by "${roleId}", but ${role.write_authority}/${runtime.type} resolves to ${contract.effective_write_path}.`;
|
|
139
|
+
} else if (contract.workflow_artifact_ownership === 'no') {
|
|
140
|
+
code = 'review_only_remote_dead_end';
|
|
141
|
+
command = `Edit agentxchain.json for role "${roleId}", then run agentxchain validate`;
|
|
142
|
+
reason = `Artifact "${artifact.path}" is owned by "${roleId}", but ${role.write_authority}/${runtime.type} can only return review artifacts and cannot satisfy workflow ownership.`;
|
|
143
|
+
} else if (contract.workflow_artifact_ownership === 'proposal_apply_required') {
|
|
144
|
+
const turnId = failure.requested_by_turn || state.last_completed_turn_id || null;
|
|
145
|
+
code = 'proposal_apply_required';
|
|
146
|
+
command = turnId ? `agentxchain proposal apply ${turnId}` : 'agentxchain proposal list';
|
|
147
|
+
reason = `Artifact "${artifact.path}" is owned by "${roleId}", and ${role.write_authority}/${runtime.type} stages required files behind proposal apply.`;
|
|
148
|
+
} else if (contract.workflow_artifact_ownership === 'tool_defined') {
|
|
149
|
+
code = 'tool_defined_proof_not_strong_enough';
|
|
150
|
+
command = `agentxchain role show ${roleId}`;
|
|
151
|
+
reason = `Artifact "${artifact.path}" is owned by "${roleId}", but ${runtime.type} leaves the file-write contract tool-defined and not statically provable.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!code) continue;
|
|
155
|
+
guidance.push({
|
|
156
|
+
code,
|
|
157
|
+
phase,
|
|
158
|
+
gate_id: gateId,
|
|
159
|
+
role_id: roleId,
|
|
160
|
+
artifact_path: artifact.path,
|
|
161
|
+
command,
|
|
162
|
+
reason,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
return guidance
|
|
168
|
+
.sort((left, right) => {
|
|
169
|
+
const priority = (RUNTIME_GUIDANCE_PRIORITY.get(left.code) || 99)
|
|
170
|
+
- (RUNTIME_GUIDANCE_PRIORITY.get(right.code) || 99);
|
|
171
|
+
if (priority !== 0) return priority;
|
|
172
|
+
const commandCmp = left.command.localeCompare(right.command, 'en');
|
|
173
|
+
if (commandCmp !== 0) return commandCmp;
|
|
174
|
+
return left.artifact_path.localeCompare(right.artifact_path, 'en');
|
|
175
|
+
})
|
|
176
|
+
.filter((entry) => {
|
|
177
|
+
const key = `${entry.code}|${entry.role_id}|${entry.command}|${entry.artifact_path}`;
|
|
178
|
+
if (seen.has(key)) return false;
|
|
179
|
+
seen.add(key);
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function deriveGovernedRunNextActions(state, config = null) {
|
|
185
|
+
const recovery = deriveRecoveryDescriptor(state, config);
|
|
186
|
+
const runtimeGuidance = recovery?.runtime_guidance || deriveRuntimeBlockedGuidance(state, config);
|
|
187
|
+
const nextActions = [];
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
|
|
190
|
+
const pushAction = (command, reason) => {
|
|
191
|
+
if (typeof command !== 'string' || !command.trim() || typeof reason !== 'string' || !reason.trim()) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const key = `${command}|${reason}`;
|
|
195
|
+
if (seen.has(key)) return;
|
|
196
|
+
seen.add(key);
|
|
197
|
+
nextActions.push({ command, reason });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
for (const entry of runtimeGuidance) {
|
|
201
|
+
pushAction(entry.command, entry.reason);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (recovery?.recovery_action) {
|
|
205
|
+
const reason = runtimeGuidance.length > 0
|
|
206
|
+
? `After resolving the ${runtimeGuidance[0].code} blocker, continue the run.`
|
|
207
|
+
: `Run is blocked: ${recovery.typed_reason}.`;
|
|
208
|
+
pushAction(recovery.recovery_action, reason);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return nextActions;
|
|
212
|
+
}
|
|
213
|
+
|
|
72
214
|
export function deriveRecoveryDescriptor(state, config = null) {
|
|
73
215
|
if (!state || typeof state !== 'object') {
|
|
74
216
|
return null;
|
|
@@ -99,6 +241,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
99
241
|
const persistedRecovery = state.blocked_reason?.recovery;
|
|
100
242
|
if (persistedRecovery && typeof persistedRecovery === 'object') {
|
|
101
243
|
const refreshedRecoveryAction = maybeRefreshRecoveryAction(state, config, persistedRecovery, turnRetained);
|
|
244
|
+
const runtimeGuidance = deriveRuntimeBlockedGuidance(state, config);
|
|
102
245
|
return {
|
|
103
246
|
typed_reason: persistedRecovery.typed_reason || 'unknown_block',
|
|
104
247
|
owner: persistedRecovery.owner || 'human',
|
|
@@ -109,6 +252,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
109
252
|
? persistedRecovery.turn_retained
|
|
110
253
|
: turnRetained,
|
|
111
254
|
detail: persistedRecovery.detail ?? state.blocked_on ?? null,
|
|
255
|
+
runtime_guidance: runtimeGuidance,
|
|
112
256
|
};
|
|
113
257
|
}
|
|
114
258
|
|
|
@@ -126,6 +270,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
126
270
|
}),
|
|
127
271
|
turn_retained: turnRetained,
|
|
128
272
|
detail: state.blocked_on.slice('human:'.length) || null,
|
|
273
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
129
274
|
};
|
|
130
275
|
}
|
|
131
276
|
|
|
@@ -151,6 +296,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
151
296
|
recovery_action: recoveryAction,
|
|
152
297
|
turn_retained: turnRetained,
|
|
153
298
|
detail: state.escalation?.detail || state.escalation?.reason || state.blocked_on,
|
|
299
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
154
300
|
};
|
|
155
301
|
}
|
|
156
302
|
|
|
@@ -164,6 +310,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
164
310
|
}),
|
|
165
311
|
turn_retained: turnRetained,
|
|
166
312
|
detail: state.blocked_on.slice('dispatch:'.length) || state.blocked_on,
|
|
313
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
167
314
|
};
|
|
168
315
|
}
|
|
169
316
|
|
|
@@ -179,6 +326,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
179
326
|
}),
|
|
180
327
|
turn_retained: turnRetained,
|
|
181
328
|
detail: derivePolicyEscalationDetail(state, { policyId }),
|
|
329
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
182
330
|
};
|
|
183
331
|
}
|
|
184
332
|
|
|
@@ -190,6 +338,7 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
190
338
|
recovery_action: 'agentxchain resume',
|
|
191
339
|
turn_retained: false,
|
|
192
340
|
detail: `${scope} timeout exceeded`,
|
|
341
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
193
342
|
};
|
|
194
343
|
}
|
|
195
344
|
|
|
@@ -199,5 +348,6 @@ export function deriveRecoveryDescriptor(state, config = null) {
|
|
|
199
348
|
recovery_action: 'Inspect state.json and resolve manually before rerunning agentxchain step',
|
|
200
349
|
turn_retained: turnRetained,
|
|
201
350
|
detail: state.blocked_on,
|
|
351
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(state, config),
|
|
202
352
|
};
|
|
203
353
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function deriveConflictedTurnResolutionActions(turnId) {
|
|
2
|
+
if (typeof turnId !== 'string' || !turnId.trim()) {
|
|
3
|
+
throw new Error('deriveConflictedTurnResolutionActions requires a non-empty turnId');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const normalizedTurnId = turnId.trim();
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
command: `agentxchain reject-turn --turn ${normalizedTurnId} --reassign`,
|
|
10
|
+
description: 'reject and re-dispatch with conflict context',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
command: `agentxchain accept-turn --turn ${normalizedTurnId} --resolution human_merge`,
|
|
14
|
+
description: 'manually merge and re-accept',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
@@ -3,6 +3,7 @@ const CONTEXT_TITLE = '# Execution Context';
|
|
|
3
3
|
const SECTION_DEFINITIONS = [
|
|
4
4
|
{ id: 'current_state', header: 'Current State', required: true },
|
|
5
5
|
{ id: 'budget', header: null, required: false },
|
|
6
|
+
{ id: 'runtime_capability_contract', header: 'Runtime Capability Contract', required: false },
|
|
6
7
|
{ id: 'project_goal', header: 'Project Goal', required: true },
|
|
7
8
|
{ id: 'inherited_run_context', header: 'Inherited Run Context', required: true },
|
|
8
9
|
{ id: 'last_turn_header', header: 'Last Accepted Turn', required: true },
|
|
@@ -82,6 +83,7 @@ export function renderContextSections(sections) {
|
|
|
82
83
|
sectionMap.get('budget')?.content,
|
|
83
84
|
]);
|
|
84
85
|
|
|
86
|
+
appendTopLevelSection(lines, 'Runtime Capability Contract', [sectionMap.get('runtime_capability_contract')?.content]);
|
|
85
87
|
appendTopLevelSection(lines, 'Project Goal', [sectionMap.get('project_goal')?.content]);
|
|
86
88
|
appendTopLevelSection(lines, 'Inherited Run Context', [sectionMap.get('inherited_run_context')?.content]);
|
|
87
89
|
appendTopLevelSection(lines, 'Last Accepted Turn', [
|
|
@@ -4,7 +4,7 @@ import { captureBaselineRef, readSessionCheckpoint } from './session-checkpoint.
|
|
|
4
4
|
|
|
5
5
|
export const SESSION_RECOVERY_PATH = '.agentxchain/SESSION_RECOVERY.md';
|
|
6
6
|
|
|
7
|
-
function deriveRecommendedContinuityAction(state) {
|
|
7
|
+
export function deriveRecommendedContinuityAction(state) {
|
|
8
8
|
if (!state) {
|
|
9
9
|
return {
|
|
10
10
|
recommended_command: null,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { pushDetail } from './coordinator-presentation-detail.js';
|
|
2
|
+
import { getCoordinatorPendingGateDetails } from './coordinator-pending-gate-presentation.js';
|
|
3
|
+
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeMessage(value) {
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
if (value == null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getCoordinatorBlockerDetails(blocker) {
|
|
19
|
+
if (!isObject(blocker)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const details = [];
|
|
24
|
+
switch (blocker.code) {
|
|
25
|
+
case 'repo_run_id_mismatch':
|
|
26
|
+
pushDetail(details, 'Repo', blocker.repo_id, { mono: true });
|
|
27
|
+
pushDetail(details, 'Expected', blocker.expected_run_id, { mono: true });
|
|
28
|
+
pushDetail(details, 'Actual', blocker.actual_run_id, { mono: true });
|
|
29
|
+
break;
|
|
30
|
+
case 'repo_not_ready':
|
|
31
|
+
pushDetail(details, 'Repo', blocker.repo_id, { mono: true });
|
|
32
|
+
pushDetail(details, 'Current Phase', blocker.current_phase);
|
|
33
|
+
pushDetail(details, 'Required Phase', blocker.required_phase);
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return details;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function summarizeCoordinatorAttention(coordinatorBlockers) {
|
|
43
|
+
if (!isObject(coordinatorBlockers) || coordinatorBlockers.ok === false) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const active = isObject(coordinatorBlockers.active) ? coordinatorBlockers.active : {};
|
|
48
|
+
const blockers = Array.isArray(active.blockers)
|
|
49
|
+
? active.blockers.filter((blocker) => isObject(blocker) && blocker.code !== 'no_next_phase')
|
|
50
|
+
: [];
|
|
51
|
+
const nextActions = Array.isArray(coordinatorBlockers.next_actions)
|
|
52
|
+
? coordinatorBlockers.next_actions.filter((action) => isObject(action))
|
|
53
|
+
: [];
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
title: coordinatorBlockers.mode === 'pending_gate' ? 'Approval Snapshot' : 'Blocker Snapshot',
|
|
57
|
+
active,
|
|
58
|
+
blockers,
|
|
59
|
+
nextActions,
|
|
60
|
+
primaryBlocker: blockers[0] || null,
|
|
61
|
+
primaryAction: nextActions[0] || null,
|
|
62
|
+
additionalBlockerCount: Math.max(0, blockers.length - 1),
|
|
63
|
+
additionalActionCount: Math.max(0, nextActions.length - 1),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildCoordinatorAttentionSnapshotPresentation(coordinatorBlockers) {
|
|
68
|
+
const summary = summarizeCoordinatorAttention(coordinatorBlockers);
|
|
69
|
+
if (!summary) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const details = [];
|
|
74
|
+
pushDetail(details, 'Mode', coordinatorBlockers?.mode);
|
|
75
|
+
if (summary.title === 'Approval Snapshot') {
|
|
76
|
+
details.push(
|
|
77
|
+
...getCoordinatorPendingGateDetails({
|
|
78
|
+
pendingGate: coordinatorBlockers?.pending_gate,
|
|
79
|
+
active: summary.active,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
pushDetail(details, 'Type', summary.active?.gate_type);
|
|
84
|
+
pushDetail(details, 'Gate', summary.active?.gate_id, { mono: true });
|
|
85
|
+
pushDetail(details, 'Current Phase', summary.active?.current_phase);
|
|
86
|
+
pushDetail(details, 'Target Phase', summary.active?.target_phase);
|
|
87
|
+
}
|
|
88
|
+
if (summary.blockers.length > 0) {
|
|
89
|
+
pushDetail(details, 'Blockers', summary.blockers.length);
|
|
90
|
+
}
|
|
91
|
+
pushDetail(details, 'Primary Blocker', summary.primaryBlocker?.code, { mono: true });
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
title: summary.title,
|
|
95
|
+
subtitle: 'First-glance coordinator attention only. Full blocker diagnostics stay in the Blockers view.',
|
|
96
|
+
details,
|
|
97
|
+
summaryMessage: summary.primaryBlocker
|
|
98
|
+
? null
|
|
99
|
+
: summary.title === 'Approval Snapshot'
|
|
100
|
+
? 'All coordinator prerequisites are satisfied. Human approval is the remaining action.'
|
|
101
|
+
: normalizeMessage(coordinatorBlockers?.blocked_reason),
|
|
102
|
+
primaryBlocker: summary.primaryBlocker,
|
|
103
|
+
primaryBlockerDetails: getCoordinatorBlockerDetails(summary.primaryBlocker),
|
|
104
|
+
primaryAction: summary.primaryAction,
|
|
105
|
+
additionalBlockerCount: summary.additionalBlockerCount,
|
|
106
|
+
additionalActionCount: summary.additionalActionCount,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getCoordinatorAttentionStatusCard(coordinatorBlockers) {
|
|
111
|
+
const summary = summarizeCoordinatorAttention(coordinatorBlockers);
|
|
112
|
+
if (!summary || summary.blockers.length > 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (summary.title === 'Approval Snapshot') {
|
|
117
|
+
return {
|
|
118
|
+
title: 'Approval Snapshot',
|
|
119
|
+
message: 'All coordinator prerequisites are satisfied. Human approval is the remaining action.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
title: 'Gate Clear',
|
|
125
|
+
message: 'The coordinator gate has no outstanding blockers.',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function countLabel(count, noun) {
|
|
2
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function summarizeCoordinatorEvent(entry) {
|
|
6
|
+
const type = entry?.type || 'unknown';
|
|
7
|
+
const ts = entry?.timestamp || '';
|
|
8
|
+
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'run_initialized': {
|
|
11
|
+
const repoCount = entry?.repo_runs ? Object.keys(entry.repo_runs).length : 0;
|
|
12
|
+
return `Coordinator run initialized with ${countLabel(repoCount, 'repo')}`;
|
|
13
|
+
}
|
|
14
|
+
case 'turn_dispatched':
|
|
15
|
+
return `Dispatched turn to ${entry?.repo_id || 'unknown'} (${entry?.role || '?'}) in workstream ${entry?.workstream_id || 'unknown'}`;
|
|
16
|
+
case 'acceptance_projection': {
|
|
17
|
+
const turnRef = entry?.repo_turn_id ? ` (turn ${entry.repo_turn_id})` : '';
|
|
18
|
+
const summaryText = entry?.summary ? ` — ${entry.summary}` : '';
|
|
19
|
+
return `Projected acceptance from ${entry?.repo_id || 'unknown'}${turnRef}${summaryText}`;
|
|
20
|
+
}
|
|
21
|
+
case 'context_generated': {
|
|
22
|
+
const upstreamCount = Array.isArray(entry?.upstream_repo_ids) ? entry.upstream_repo_ids.length : 0;
|
|
23
|
+
return `Generated cross-repo context for ${entry?.target_repo_id || 'unknown'} from ${countLabel(upstreamCount, 'upstream repo')}`;
|
|
24
|
+
}
|
|
25
|
+
case 'phase_transition_requested':
|
|
26
|
+
return `Requested phase transition: ${entry?.from || '?'} → ${entry?.to || '?'}`;
|
|
27
|
+
case 'phase_transition_approved':
|
|
28
|
+
return `Phase transition approved: ${entry?.from || '?'} → ${entry?.to || '?'}`;
|
|
29
|
+
case 'run_completion_requested':
|
|
30
|
+
return `Requested run completion (gate: ${entry?.gate || 'unknown'})`;
|
|
31
|
+
case 'run_completed':
|
|
32
|
+
return 'Coordinator run completed';
|
|
33
|
+
case 'state_resynced': {
|
|
34
|
+
const resyncedCount = Array.isArray(entry?.resynced_repos) ? entry.resynced_repos.length : 0;
|
|
35
|
+
const barrierChangeCount = Array.isArray(entry?.barrier_changes) ? entry.barrier_changes.length : 0;
|
|
36
|
+
return `Resynced state for ${countLabel(resyncedCount, 'repo')}, ${countLabel(barrierChangeCount, 'barrier change')}`;
|
|
37
|
+
}
|
|
38
|
+
case 'blocked_resolved':
|
|
39
|
+
return `Blocked state resolved: ${entry?.from || '?'} → ${entry?.to || '?'}`;
|
|
40
|
+
default:
|
|
41
|
+
return `${type} event${ts ? ` at ${ts}` : ''}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { deriveCoordinatorNextActions } from './coordinator-next-actions.js';
|
|
2
|
+
import { collectCoordinatorRepoSnapshots } from './coordinator-repo-snapshots.js';
|
|
3
|
+
|
|
4
|
+
export function deriveCoordinatorGateNextActions(state, config) {
|
|
5
|
+
return deriveCoordinatorNextActions({
|
|
6
|
+
status: state?.status ?? null,
|
|
7
|
+
blockedReason: state?.blocked_reason ?? null,
|
|
8
|
+
pendingGate: state?.pending_gate ?? null,
|
|
9
|
+
repos: config ? collectCoordinatorRepoSnapshots(config) : [],
|
|
10
|
+
coordinatorRepoRuns: state?.repo_runs || {},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deriveTypedReason(code) {
|
|
15
|
+
if (code === 'hook_blocked') {
|
|
16
|
+
return 'hook_block';
|
|
17
|
+
}
|
|
18
|
+
if (code === 'hook_failed') {
|
|
19
|
+
return 'hook_failure';
|
|
20
|
+
}
|
|
21
|
+
return 'approval_failed';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deriveRecoveryDetail(code, gate, hookName) {
|
|
25
|
+
const gateLabel = gate ? `pending gate "${gate}"` : 'pending coordinator gate';
|
|
26
|
+
if (code === 'hook_blocked') {
|
|
27
|
+
return `Coordinator state is unchanged. Fix or reconfigure hook "${hookName || 'before_gate'}", then rerun approval for ${gateLabel}.`;
|
|
28
|
+
}
|
|
29
|
+
if (code === 'hook_failed') {
|
|
30
|
+
return `Coordinator state is unchanged. Fix hook "${hookName || 'before_gate'}" or its execution failure, then rerun approval for ${gateLabel}.`;
|
|
31
|
+
}
|
|
32
|
+
return `Coordinator state is unchanged. Resolve the approval failure, then follow the next coordinator action for ${gateLabel}.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeCoordinatorGateApprovalFailure({
|
|
36
|
+
state,
|
|
37
|
+
config,
|
|
38
|
+
code,
|
|
39
|
+
error,
|
|
40
|
+
hookName = null,
|
|
41
|
+
hookPhase = null,
|
|
42
|
+
}) {
|
|
43
|
+
const gate = state?.pending_gate?.gate ?? null;
|
|
44
|
+
const gateType = state?.pending_gate?.gate_type ?? null;
|
|
45
|
+
const nextActions = deriveCoordinatorGateNextActions(state, config);
|
|
46
|
+
const nextAction = nextActions[0]?.command ?? null;
|
|
47
|
+
const detail = deriveRecoveryDetail(code, gate, hookName);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
code: code || 'approval_failed',
|
|
52
|
+
error: error || 'Coordinator gate approval failed',
|
|
53
|
+
gate,
|
|
54
|
+
gate_type: gateType,
|
|
55
|
+
hook_phase: hookPhase || null,
|
|
56
|
+
hook_name: hookName || null,
|
|
57
|
+
next_action: nextAction,
|
|
58
|
+
next_actions: nextActions,
|
|
59
|
+
recovery_summary: {
|
|
60
|
+
typed_reason: deriveTypedReason(code),
|
|
61
|
+
owner: 'human',
|
|
62
|
+
recovery_action: nextAction,
|
|
63
|
+
detail,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeCoordinatorGateApprovalSuccess({
|
|
69
|
+
result,
|
|
70
|
+
gateType,
|
|
71
|
+
phaseTransitionMessagePrefix = 'Coordinator phase transition approved',
|
|
72
|
+
completionMessage = 'Coordinator run completion approved. Run is now complete.',
|
|
73
|
+
}) {
|
|
74
|
+
const nextActions = deriveCoordinatorGateNextActions(result?.state, result?.config);
|
|
75
|
+
const nextAction = nextActions[0]?.command ?? null;
|
|
76
|
+
|
|
77
|
+
if (gateType === 'phase_transition') {
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
gate_type: 'phase_transition',
|
|
81
|
+
status: result?.state?.status || null,
|
|
82
|
+
phase: result?.state?.phase || null,
|
|
83
|
+
message: `${phaseTransitionMessagePrefix}: ${result?.transition?.from} -> ${result?.transition?.to}`,
|
|
84
|
+
next_action: nextAction,
|
|
85
|
+
next_actions: nextActions,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
gate_type: 'run_completion',
|
|
92
|
+
status: result?.state?.status || null,
|
|
93
|
+
phase: result?.state?.phase || null,
|
|
94
|
+
message: completionMessage,
|
|
95
|
+
next_action: nextAction,
|
|
96
|
+
next_actions: nextActions,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function formatList(values) {
|
|
2
|
+
return values.join(', ');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildCoordinatorGateEvaluationPresentation({
|
|
6
|
+
gateType,
|
|
7
|
+
evaluation,
|
|
8
|
+
includeReady = false,
|
|
9
|
+
includeBlockerCount = true,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const normalizedGateType = gateType === 'run_completion' ? 'run_completion' : 'phase_transition';
|
|
12
|
+
const data = evaluation && typeof evaluation === 'object' ? evaluation : {};
|
|
13
|
+
const blockers = Array.isArray(data.blockers) ? data.blockers : [];
|
|
14
|
+
const details = [];
|
|
15
|
+
|
|
16
|
+
if (typeof data.gate_id === 'string' && data.gate_id.length > 0) {
|
|
17
|
+
details.push({ label: 'Gate', value: data.gate_id, mono: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof data.current_phase === 'string' && data.current_phase.length > 0) {
|
|
21
|
+
details.push({ label: 'Current Phase', value: data.current_phase });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof data.target_phase === 'string' && data.target_phase.length > 0) {
|
|
25
|
+
details.push({ label: 'Target Phase', value: data.target_phase });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(data.required_repos) && data.required_repos.length > 0) {
|
|
29
|
+
details.push({ label: 'Required Repos', value: formatList(data.required_repos) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(data.human_barriers) && data.human_barriers.length > 0) {
|
|
33
|
+
details.push({ label: 'Human Barriers', value: formatList(data.human_barriers) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (normalizedGateType === 'run_completion' && typeof data.requires_human_approval === 'boolean') {
|
|
37
|
+
details.push({
|
|
38
|
+
label: 'Human Approval',
|
|
39
|
+
value: data.requires_human_approval ? 'Required' : 'Not required',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (includeReady && typeof data.ready === 'boolean') {
|
|
44
|
+
details.push({ label: 'Ready', value: data.ready ? 'Yes' : 'No' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (includeBlockerCount) {
|
|
48
|
+
details.push({ label: 'Blockers', value: String(blockers.length) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
title: normalizedGateType === 'run_completion' ? 'Run Completion' : 'Phase Transition',
|
|
53
|
+
statusLabel: data.ready ? 'ready' : 'not ready',
|
|
54
|
+
details,
|
|
55
|
+
blockers,
|
|
56
|
+
};
|
|
57
|
+
}
|