agentxchain 2.88.0 → 2.90.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/dashboard/app.js +2 -0
- package/dashboard/components/delegations.js +237 -0
- package/dashboard/components/timeline.js +23 -0
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/src/lib/coordinator-acceptance.js +1 -1
- package/src/lib/coordinator-barriers.js +2 -1
- package/src/lib/coordinator-config.js +58 -33
- package/src/lib/coordinator-recovery.js +1 -1
- package/src/lib/coordinator-state.js +3 -0
- package/src/lib/cross-repo-context.js +11 -10
- package/src/lib/dispatch-bundle.js +20 -2
- package/src/lib/export.js +88 -0
- package/src/lib/governed-state.js +29 -0
- package/src/lib/report.js +85 -0
- package/src/lib/token-budget.js +1 -0
package/dashboard/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { render as renderBlocked } from './components/blocked.js';
|
|
|
12
12
|
import { render as renderGate } from './components/gate.js';
|
|
13
13
|
import { render as renderInitiative } from './components/initiative.js';
|
|
14
14
|
import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
15
|
+
import { render as renderDelegations } from './components/delegations.js';
|
|
15
16
|
import { render as renderBlockers } from './components/blockers.js';
|
|
16
17
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
17
18
|
import { render as renderRunHistory } from './components/run-history.js';
|
|
@@ -20,6 +21,7 @@ import { render as renderCoordinatorTimeouts } from './components/coordinator-ti
|
|
|
20
21
|
|
|
21
22
|
const VIEWS = {
|
|
22
23
|
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderTimeline },
|
|
24
|
+
delegations: { fetch: ['state', 'history'], render: renderDelegations },
|
|
23
25
|
ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger'], render: renderLedger },
|
|
24
26
|
hooks: { fetch: ['audit', 'annotations', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderHooks },
|
|
25
27
|
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
function esc(str) {
|
|
2
|
+
if (str == null) return '';
|
|
3
|
+
return String(str)
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
12
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function statusColor(status) {
|
|
16
|
+
const colors = {
|
|
17
|
+
pending: 'var(--text-dim)',
|
|
18
|
+
active: 'var(--yellow)',
|
|
19
|
+
completed: 'var(--green)',
|
|
20
|
+
failed: 'var(--red)',
|
|
21
|
+
review_pending: 'var(--yellow)',
|
|
22
|
+
reviewed: 'var(--accent)',
|
|
23
|
+
unknown: 'var(--text-dim)',
|
|
24
|
+
};
|
|
25
|
+
return colors[status] || 'var(--text-dim)';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function summarizeReview(results) {
|
|
29
|
+
const counts = { completed: 0, failed: 0, other: 0 };
|
|
30
|
+
for (const result of results || []) {
|
|
31
|
+
if (result?.status === 'completed') counts.completed += 1;
|
|
32
|
+
else if (result?.status === 'failed') counts.failed += 1;
|
|
33
|
+
else counts.other += 1;
|
|
34
|
+
}
|
|
35
|
+
return counts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeDelegationChain(parentEntry, state, history) {
|
|
39
|
+
const issued = Array.isArray(parentEntry?.delegations_issued) ? parentEntry.delegations_issued : [];
|
|
40
|
+
const pendingQueue = Array.isArray(state?.delegation_queue)
|
|
41
|
+
? state.delegation_queue.filter((entry) => entry.parent_turn_id === parentEntry.turn_id)
|
|
42
|
+
: [];
|
|
43
|
+
const pendingReview = state?.pending_delegation_review?.parent_turn_id === parentEntry.turn_id
|
|
44
|
+
? state.pending_delegation_review
|
|
45
|
+
: null;
|
|
46
|
+
const reviewEntry = Array.isArray(history)
|
|
47
|
+
? history.find((entry) => entry?.delegation_review?.parent_turn_id === parentEntry.turn_id)
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
const delegations = issued.map((item) => {
|
|
51
|
+
const queueEntry = pendingQueue.find((entry) => entry.delegation_id === item.id) || null;
|
|
52
|
+
const reviewResult = Array.isArray(pendingReview?.delegation_results)
|
|
53
|
+
? pendingReview.delegation_results.find((entry) => entry.delegation_id === item.id)
|
|
54
|
+
: null;
|
|
55
|
+
const reviewHistoryResult = Array.isArray(reviewEntry?.delegation_review?.results)
|
|
56
|
+
? reviewEntry.delegation_review.results.find((entry) => entry.delegation_id === item.id)
|
|
57
|
+
: null;
|
|
58
|
+
const childTurn = Array.isArray(history)
|
|
59
|
+
? history.find((entry) => entry?.delegation_context?.delegation_id === item.id)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
delegation_id: item.id,
|
|
64
|
+
to_role: item.to_role,
|
|
65
|
+
charter: item.charter,
|
|
66
|
+
acceptance_contract: Array.isArray(item.acceptance_contract) ? item.acceptance_contract : [],
|
|
67
|
+
status: queueEntry?.status
|
|
68
|
+
|| reviewResult?.status
|
|
69
|
+
|| reviewHistoryResult?.status
|
|
70
|
+
|| childTurn?.status
|
|
71
|
+
|| 'unknown',
|
|
72
|
+
child_turn_id: queueEntry?.child_turn_id
|
|
73
|
+
|| reviewResult?.child_turn_id
|
|
74
|
+
|| reviewHistoryResult?.child_turn_id
|
|
75
|
+
|| childTurn?.turn_id
|
|
76
|
+
|| null,
|
|
77
|
+
summary: reviewResult?.summary
|
|
78
|
+
|| reviewHistoryResult?.summary
|
|
79
|
+
|| childTurn?.summary
|
|
80
|
+
|| null,
|
|
81
|
+
files_changed: reviewResult?.files_changed
|
|
82
|
+
|| reviewHistoryResult?.files_changed
|
|
83
|
+
|| childTurn?.files_changed
|
|
84
|
+
|| [],
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let chainStatus = 'unknown';
|
|
89
|
+
if (pendingReview) chainStatus = 'review_pending';
|
|
90
|
+
else if (pendingQueue.length > 0) chainStatus = delegations.some((entry) => entry.status === 'active') ? 'active' : 'pending';
|
|
91
|
+
else if (reviewEntry) chainStatus = 'reviewed';
|
|
92
|
+
else if (delegations.length > 0) chainStatus = delegations.every((entry) => entry.status === 'completed' || entry.status === 'failed') ? 'reviewed' : 'unknown';
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
parent_turn_id: parentEntry.turn_id,
|
|
96
|
+
parent_role: parentEntry.role,
|
|
97
|
+
parent_summary: parentEntry.summary || '(no summary)',
|
|
98
|
+
accepted_at: parentEntry.accepted_at || null,
|
|
99
|
+
status: chainStatus,
|
|
100
|
+
delegations,
|
|
101
|
+
pending_review: pendingReview,
|
|
102
|
+
review_entry: reviewEntry,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderAcceptanceContract(items) {
|
|
107
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
108
|
+
return '<span class="turn-detail">No acceptance contract recorded.</span>';
|
|
109
|
+
}
|
|
110
|
+
return `<ul>${items.map((item) => `<li>${esc(item)}</li>`).join('')}</ul>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderChainCard(chain) {
|
|
114
|
+
const reviewResults = chain.pending_review?.delegation_results
|
|
115
|
+
|| chain.review_entry?.delegation_review?.results
|
|
116
|
+
|| [];
|
|
117
|
+
const reviewCounts = summarizeReview(reviewResults);
|
|
118
|
+
|
|
119
|
+
let html = `<div class="turn-card">
|
|
120
|
+
<div class="turn-header">
|
|
121
|
+
${badge(chain.parent_role || 'unknown', '#fb923c')}
|
|
122
|
+
<span class="mono">${esc(chain.parent_turn_id)}</span>
|
|
123
|
+
${badge(chain.status.replace(/_/g, ' '), statusColor(chain.status))}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="turn-summary">${esc(chain.parent_summary)}</div>`;
|
|
126
|
+
|
|
127
|
+
if (chain.accepted_at) {
|
|
128
|
+
html += `<div class="turn-detail"><span class="detail-label">Accepted:</span> ${esc(chain.accepted_at)}</div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
html += `<div class="turn-detail"><span class="detail-label">Delegations:</span> ${esc(chain.delegations.length)}</div>`;
|
|
132
|
+
|
|
133
|
+
if (chain.delegations.length > 0) {
|
|
134
|
+
html += `<div class="turn-list">`;
|
|
135
|
+
for (const delegation of chain.delegations) {
|
|
136
|
+
html += `<div class="turn-card">
|
|
137
|
+
<div class="turn-header">
|
|
138
|
+
<span class="mono">${esc(delegation.delegation_id)}</span>
|
|
139
|
+
${badge(delegation.to_role || 'unknown', '#38bdf8')}
|
|
140
|
+
${badge(delegation.status, statusColor(delegation.status))}
|
|
141
|
+
</div>
|
|
142
|
+
<div class="turn-detail"><span class="detail-label">Charter:</span> ${esc(delegation.charter || '(none)')}</div>
|
|
143
|
+
<div class="turn-detail"><span class="detail-label">Acceptance Contract:</span>${renderAcceptanceContract(delegation.acceptance_contract)}</div>`;
|
|
144
|
+
if (delegation.child_turn_id) {
|
|
145
|
+
html += `<div class="turn-detail"><span class="detail-label">Child Turn:</span> <span class="mono">${esc(delegation.child_turn_id)}</span></div>`;
|
|
146
|
+
}
|
|
147
|
+
if (delegation.summary) {
|
|
148
|
+
html += `<div class="turn-detail"><span class="detail-label">Outcome:</span> ${esc(delegation.summary)}</div>`;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(delegation.files_changed) && delegation.files_changed.length > 0) {
|
|
151
|
+
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${delegation.files_changed.map((item) => esc(item)).join(', ')}</span></div>`;
|
|
152
|
+
}
|
|
153
|
+
html += `</div>`;
|
|
154
|
+
}
|
|
155
|
+
html += `</div>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (chain.pending_review) {
|
|
159
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Pending:</span> ${esc(chain.pending_review.parent_role)} is reviewing ${reviewResults.length} delegated result${reviewResults.length === 1 ? '' : 's'}.</div>`;
|
|
160
|
+
} else if (chain.review_entry) {
|
|
161
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Turn:</span> <span class="mono">${esc(chain.review_entry.turn_id)}</span></div>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (reviewResults.length > 0) {
|
|
165
|
+
html += `<div class="turn-detail"><span class="detail-label">Review Summary:</span> ${reviewCounts.completed} completed, ${reviewCounts.failed} failed</div>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
html += `</div>`;
|
|
169
|
+
return html;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function render({ state, history }) {
|
|
173
|
+
if (!state) {
|
|
174
|
+
return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const historyEntries = Array.isArray(history) ? history : [];
|
|
178
|
+
const parentTurns = historyEntries
|
|
179
|
+
.filter((entry) => Array.isArray(entry?.delegations_issued) && entry.delegations_issued.length > 0)
|
|
180
|
+
.reverse();
|
|
181
|
+
const chains = parentTurns.map((entry) => normalizeDelegationChain(entry, state, historyEntries));
|
|
182
|
+
const queue = Array.isArray(state.delegation_queue) ? state.delegation_queue : [];
|
|
183
|
+
const pendingReview = state.pending_delegation_review || null;
|
|
184
|
+
|
|
185
|
+
if (chains.length === 0 && queue.length === 0 && !pendingReview) {
|
|
186
|
+
return `<div class="placeholder"><h2>No Delegations</h2><p>This run has no delegation chains yet. Delegation state will appear here once a turn emits a <code class="mono">delegations</code> array.</p></div>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let html = `<div class="delegations-view">`;
|
|
190
|
+
|
|
191
|
+
html += `<div class="run-header">
|
|
192
|
+
<div class="run-meta">
|
|
193
|
+
<span class="mono run-id">${esc(state.run_id || 'unknown')}</span>
|
|
194
|
+
${badge(state.status || 'unknown', statusColor(state.status || 'unknown'))}
|
|
195
|
+
<span class="phase-label">Phase: <strong>${esc(state.phase || 'unknown')}</strong></span>
|
|
196
|
+
<span class="turn-count">${chains.length} chain${chains.length === 1 ? '' : 's'} recorded</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>`;
|
|
199
|
+
|
|
200
|
+
if (queue.length > 0 || pendingReview) {
|
|
201
|
+
html += `<div class="section"><h3>Live Delegation State</h3><div class="turn-list">`;
|
|
202
|
+
if (queue.length > 0) {
|
|
203
|
+
html += `<div class="turn-card">
|
|
204
|
+
<div class="turn-header">
|
|
205
|
+
<span class="mono">delegation_queue</span>
|
|
206
|
+
${badge(`${queue.length} item${queue.length === 1 ? '' : 's'}`, statusColor('pending'))}
|
|
207
|
+
</div>`;
|
|
208
|
+
for (const entry of queue) {
|
|
209
|
+
html += `<div class="turn-detail"><span class="detail-label">${esc(entry.delegation_id)}:</span> ${esc(entry.parent_role || 'unknown')} → ${esc(entry.to_role || 'unknown')} (${esc(entry.status || 'unknown')})</div>`;
|
|
210
|
+
}
|
|
211
|
+
html += `</div>`;
|
|
212
|
+
}
|
|
213
|
+
if (pendingReview) {
|
|
214
|
+
const counts = summarizeReview(pendingReview.delegation_results);
|
|
215
|
+
html += `<div class="turn-card">
|
|
216
|
+
<div class="turn-header">
|
|
217
|
+
<span class="mono">${esc(pendingReview.parent_turn_id)}</span>
|
|
218
|
+
${badge('review pending', statusColor('review_pending'))}
|
|
219
|
+
</div>
|
|
220
|
+
<div class="turn-detail"><span class="detail-label">Parent Role:</span> ${esc(pendingReview.parent_role || 'unknown')}</div>
|
|
221
|
+
<div class="turn-detail"><span class="detail-label">Results:</span> ${counts.completed} completed, ${counts.failed} failed</div>
|
|
222
|
+
</div>`;
|
|
223
|
+
}
|
|
224
|
+
html += `</div></div>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
html += `<div class="section"><h3>Delegation Chains</h3>`;
|
|
228
|
+
if (chains.length === 0) {
|
|
229
|
+
html += `<div class="placeholder compact"><p>No completed delegation chains recorded yet.</p></div>`;
|
|
230
|
+
} else {
|
|
231
|
+
html += `<div class="turn-list">${chains.map((chain) => renderChainCard(chain)).join('')}</div>`;
|
|
232
|
+
}
|
|
233
|
+
html += `</div>`;
|
|
234
|
+
|
|
235
|
+
html += `</div>`;
|
|
236
|
+
return html;
|
|
237
|
+
}
|
|
@@ -187,6 +187,23 @@ function renderTurnDetailPanel(turnId, annotations, audit, coordinatorAnnotation
|
|
|
187
187
|
return html;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function renderDelegationIssued(entry) {
|
|
191
|
+
const issued = Array.isArray(entry?.delegations_issued) ? entry.delegations_issued : [];
|
|
192
|
+
if (issued.length === 0) return '';
|
|
193
|
+
return `<div class="turn-detail"><span class="detail-label">Delegated:</span> ${issued.map((item) => `${esc(item.id)} → ${esc(item.to_role)}`).join(', ')}</div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderDelegationContext(context) {
|
|
197
|
+
if (!context) return '';
|
|
198
|
+
return `<div class="turn-detail"><span class="detail-label">Delegation:</span> <span class="mono">${esc(context.delegation_id)}</span> from ${esc(context.parent_role || 'unknown')} — ${esc(context.charter || '(no charter)')}</div>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderDelegationReview(review) {
|
|
202
|
+
if (!review) return '';
|
|
203
|
+
const resultCount = Array.isArray(review.results) ? review.results.length : 0;
|
|
204
|
+
return `<div class="turn-detail"><span class="detail-label">Delegation Review:</span> <span class="mono">${esc(review.parent_turn_id || 'unknown')}</span> with ${esc(resultCount)} result${resultCount === 1 ? '' : 's'}</div>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
190
207
|
function renderContinuityPanel(continuity) {
|
|
191
208
|
if (!continuity) return '';
|
|
192
209
|
|
|
@@ -323,6 +340,8 @@ export function render({ state, continuity, history, annotations, audit, connect
|
|
|
323
340
|
<span class="turn-status">${esc(turn.status || 'assigned')}</span>
|
|
324
341
|
${elapsedStr ? `<span class="turn-timing">Elapsed: ${esc(elapsedStr)}</span>` : ''}
|
|
325
342
|
</div>
|
|
343
|
+
${renderDelegationContext(turn.delegation_context)}
|
|
344
|
+
${renderDelegationReview(turn.delegation_review)}
|
|
326
345
|
</div>`;
|
|
327
346
|
}
|
|
328
347
|
html += `</div></div>`;
|
|
@@ -355,6 +374,10 @@ export function render({ state, continuity, history, annotations, audit, connect
|
|
|
355
374
|
html += `<div class="turn-summary">${esc(entry.summary)}</div>`;
|
|
356
375
|
}
|
|
357
376
|
|
|
377
|
+
html += renderDelegationIssued(entry);
|
|
378
|
+
html += renderDelegationContext(entry.delegation_context);
|
|
379
|
+
html += renderDelegationReview(entry.delegation_review);
|
|
380
|
+
|
|
358
381
|
if (files.length > 0) {
|
|
359
382
|
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${files.map(f => esc(f)).join(', ')}</span></div>`;
|
|
360
383
|
}
|
package/dashboard/index.html
CHANGED
|
@@ -377,6 +377,7 @@
|
|
|
377
377
|
<a href="#initiative">Initiative</a>
|
|
378
378
|
<a href="#cross-repo">Cross-Repo</a>
|
|
379
379
|
<a href="#timeline" class="active">Timeline</a>
|
|
380
|
+
<a href="#delegations">Delegations</a>
|
|
380
381
|
<a href="#ledger">Decisions</a>
|
|
381
382
|
<a href="#hooks">Hooks</a>
|
|
382
383
|
<a href="#blocked">Blocked</a>
|
package/package.json
CHANGED
|
@@ -267,7 +267,7 @@ function evaluateBarrierEffects(workspacePath, state, config, repoId, workstream
|
|
|
267
267
|
snapshotChanged = true;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
-
if (barrier.type === 'interface_alignment') {
|
|
270
|
+
if (barrier.type === 'interface_alignment' || barrier.type === 'named_decisions') {
|
|
271
271
|
const satisfiedRepos = getAlignedReposForBarrier(barrier, history);
|
|
272
272
|
if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
|
|
273
273
|
barrier.satisfied_repos = satisfiedRepos;
|
|
@@ -41,7 +41,7 @@ export function getAcceptedReposForWorkstream(history, workstreamId, requiredRep
|
|
|
41
41
|
|
|
42
42
|
export function getAlignedReposForBarrier(barrier, history) {
|
|
43
43
|
const requiredRepos = Array.isArray(barrier.required_repos) ? barrier.required_repos : [];
|
|
44
|
-
const alignmentDecisionIds = barrier.alignment_decision_ids || {};
|
|
44
|
+
const alignmentDecisionIds = barrier.required_decision_ids_by_repo || barrier.alignment_decision_ids || {};
|
|
45
45
|
const { repoDecisionIds } = collectAcceptedDecisionIds(history, barrier.workstream_id, requiredRepos);
|
|
46
46
|
const alignedRepos = [];
|
|
47
47
|
|
|
@@ -105,6 +105,7 @@ export function computeBarrierStatus(barrier, history, config) {
|
|
|
105
105
|
return computeOrderedRepoSequenceStatus(barrier, history, config);
|
|
106
106
|
|
|
107
107
|
case 'interface_alignment':
|
|
108
|
+
case 'named_decisions':
|
|
108
109
|
return computeInterfaceAlignmentStatus(barrier, history);
|
|
109
110
|
|
|
110
111
|
case 'shared_human_gate':
|
|
@@ -11,6 +11,7 @@ const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
|
11
11
|
const VALID_BARRIER_TYPES = new Set([
|
|
12
12
|
'all_repos_accepted',
|
|
13
13
|
'interface_alignment',
|
|
14
|
+
'named_decisions',
|
|
14
15
|
'ordered_repo_sequence',
|
|
15
16
|
'shared_human_gate',
|
|
16
17
|
]);
|
|
@@ -151,34 +152,30 @@ function validateWorkstreams(raw, repoIds, errors) {
|
|
|
151
152
|
);
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
validateDecisionRequirementBarrier(workstreamId, workstream, errors);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
detectWorkstreamCycles(raw.workstreams, errors);
|
|
158
159
|
return workstreamIds;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const alignment = workstream.interface_alignment;
|
|
167
|
-
if (!alignment || typeof alignment !== 'object' || Array.isArray(alignment)) {
|
|
162
|
+
function validateDecisionIdsByRepo(workstreamId, workstream, errors, sectionName, errorPrefix) {
|
|
163
|
+
const section = workstream[sectionName];
|
|
164
|
+
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
168
165
|
pushError(
|
|
169
166
|
errors,
|
|
170
|
-
|
|
171
|
-
`workstream "${workstreamId}" with completion_barrier "
|
|
167
|
+
`${errorPrefix}_invalid`,
|
|
168
|
+
`workstream "${workstreamId}" with completion_barrier "${workstream.completion_barrier}" must declare ${sectionName}.decision_ids_by_repo`,
|
|
172
169
|
);
|
|
173
170
|
return;
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
const byRepo =
|
|
173
|
+
const byRepo = section.decision_ids_by_repo;
|
|
177
174
|
if (!byRepo || typeof byRepo !== 'object' || Array.isArray(byRepo)) {
|
|
178
175
|
pushError(
|
|
179
176
|
errors,
|
|
180
|
-
|
|
181
|
-
`workstream "${workstreamId}"
|
|
177
|
+
`${errorPrefix}_decisions_invalid`,
|
|
178
|
+
`workstream "${workstreamId}" ${sectionName}.decision_ids_by_repo must be an object`,
|
|
182
179
|
);
|
|
183
180
|
return;
|
|
184
181
|
}
|
|
@@ -190,8 +187,8 @@ function validateInterfaceAlignment(workstreamId, workstream, errors) {
|
|
|
190
187
|
if (!(repoId in byRepo)) {
|
|
191
188
|
pushError(
|
|
192
189
|
errors,
|
|
193
|
-
|
|
194
|
-
`workstream "${workstreamId}" must declare
|
|
190
|
+
`${errorPrefix}_repo_missing`,
|
|
191
|
+
`workstream "${workstreamId}" must declare ${sectionName}.decision_ids_by_repo["${repoId}"]`,
|
|
195
192
|
);
|
|
196
193
|
continue;
|
|
197
194
|
}
|
|
@@ -200,8 +197,8 @@ function validateInterfaceAlignment(workstreamId, workstream, errors) {
|
|
|
200
197
|
if (!Array.isArray(decisionIds) || decisionIds.length === 0) {
|
|
201
198
|
pushError(
|
|
202
199
|
errors,
|
|
203
|
-
|
|
204
|
-
`workstream "${workstreamId}"
|
|
200
|
+
`${errorPrefix}_repo_invalid`,
|
|
201
|
+
`workstream "${workstreamId}" ${sectionName}.decision_ids_by_repo["${repoId}"] must be a non-empty array`,
|
|
205
202
|
);
|
|
206
203
|
continue;
|
|
207
204
|
}
|
|
@@ -211,16 +208,16 @@ function validateInterfaceAlignment(workstreamId, workstream, errors) {
|
|
|
211
208
|
if (typeof decisionId !== 'string' || !/^DEC-\d+$/.test(decisionId)) {
|
|
212
209
|
pushError(
|
|
213
210
|
errors,
|
|
214
|
-
|
|
215
|
-
`workstream "${workstreamId}"
|
|
211
|
+
`${errorPrefix}_decision_invalid`,
|
|
212
|
+
`workstream "${workstreamId}" ${sectionName} decision "${decisionId}" for repo "${repoId}" must match DEC-NNN`,
|
|
216
213
|
);
|
|
217
214
|
continue;
|
|
218
215
|
}
|
|
219
216
|
if (seen.has(decisionId)) {
|
|
220
217
|
pushError(
|
|
221
218
|
errors,
|
|
222
|
-
|
|
223
|
-
`workstream "${workstreamId}"
|
|
219
|
+
`${errorPrefix}_decision_duplicate`,
|
|
220
|
+
`workstream "${workstreamId}" ${sectionName} decision "${decisionId}" is duplicated for repo "${repoId}"`,
|
|
224
221
|
);
|
|
225
222
|
continue;
|
|
226
223
|
}
|
|
@@ -232,13 +229,49 @@ function validateInterfaceAlignment(workstreamId, workstream, errors) {
|
|
|
232
229
|
if (!repoIdSet.has(repoId)) {
|
|
233
230
|
pushError(
|
|
234
231
|
errors,
|
|
235
|
-
|
|
236
|
-
`workstream "${workstreamId}"
|
|
232
|
+
`${errorPrefix}_repo_unknown`,
|
|
233
|
+
`workstream "${workstreamId}" ${sectionName} references undeclared repo "${repoId}"`,
|
|
237
234
|
);
|
|
238
235
|
}
|
|
239
236
|
}
|
|
240
237
|
}
|
|
241
238
|
|
|
239
|
+
function validateDecisionRequirementBarrier(workstreamId, workstream, errors) {
|
|
240
|
+
if (workstream.completion_barrier === 'interface_alignment') {
|
|
241
|
+
validateDecisionIdsByRepo(
|
|
242
|
+
workstreamId,
|
|
243
|
+
workstream,
|
|
244
|
+
errors,
|
|
245
|
+
'interface_alignment',
|
|
246
|
+
'workstream_interface_alignment',
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (workstream.completion_barrier === 'named_decisions') {
|
|
252
|
+
validateDecisionIdsByRepo(
|
|
253
|
+
workstreamId,
|
|
254
|
+
workstream,
|
|
255
|
+
errors,
|
|
256
|
+
'named_decisions',
|
|
257
|
+
'workstream_named_decisions',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeDecisionIdsByRepo(section) {
|
|
263
|
+
return section?.decision_ids_by_repo
|
|
264
|
+
? {
|
|
265
|
+
decision_ids_by_repo: Object.fromEntries(
|
|
266
|
+
Object.entries(section.decision_ids_by_repo).map(([repoId, decisionIds]) => [
|
|
267
|
+
repoId,
|
|
268
|
+
Array.isArray(decisionIds) ? [...new Set(decisionIds)] : [],
|
|
269
|
+
]),
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
: null;
|
|
273
|
+
}
|
|
274
|
+
|
|
242
275
|
function detectWorkstreamCycles(workstreams, errors) {
|
|
243
276
|
const visiting = new Set();
|
|
244
277
|
const visited = new Set();
|
|
@@ -451,16 +484,8 @@ export function normalizeCoordinatorConfig(raw) {
|
|
|
451
484
|
entry_repo: workstream.entry_repo,
|
|
452
485
|
depends_on: Array.isArray(workstream.depends_on) ? [...new Set(workstream.depends_on)] : [],
|
|
453
486
|
completion_barrier: workstream.completion_barrier,
|
|
454
|
-
interface_alignment: workstream.interface_alignment
|
|
455
|
-
|
|
456
|
-
decision_ids_by_repo: Object.fromEntries(
|
|
457
|
-
Object.entries(workstream.interface_alignment.decision_ids_by_repo).map(([repoId, decisionIds]) => [
|
|
458
|
-
repoId,
|
|
459
|
-
Array.isArray(decisionIds) ? [...new Set(decisionIds)] : [],
|
|
460
|
-
]),
|
|
461
|
-
),
|
|
462
|
-
}
|
|
463
|
-
: null,
|
|
487
|
+
interface_alignment: normalizeDecisionIdsByRepo(workstream.interface_alignment),
|
|
488
|
+
named_decisions: normalizeDecisionIdsByRepo(workstream.named_decisions),
|
|
464
489
|
},
|
|
465
490
|
]),
|
|
466
491
|
),
|
|
@@ -366,7 +366,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
366
366
|
barriersChanged = true;
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
if (barrier.type === 'interface_alignment') {
|
|
369
|
+
if (barrier.type === 'interface_alignment' || barrier.type === 'named_decisions') {
|
|
370
370
|
const satisfiedRepos = getAlignedReposForBarrier(barrier, fullHistory);
|
|
371
371
|
if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
|
|
372
372
|
barrier.satisfied_repos = satisfiedRepos;
|
|
@@ -112,6 +112,9 @@ function bootstrapBarriers(config) {
|
|
|
112
112
|
status: 'pending',
|
|
113
113
|
required_repos: [...workstream.repos],
|
|
114
114
|
satisfied_repos: [],
|
|
115
|
+
required_decision_ids_by_repo: workstream.named_decisions?.decision_ids_by_repo
|
|
116
|
+
|| workstream.interface_alignment?.decision_ids_by_repo
|
|
117
|
+
|| null,
|
|
115
118
|
alignment_decision_ids: workstream.interface_alignment?.decision_ids_by_repo || null,
|
|
116
119
|
created_at: new Date().toISOString(),
|
|
117
120
|
};
|
|
@@ -61,6 +61,7 @@ function collectActiveBarriers(barriers, workstreamIds, targetRepoId) {
|
|
|
61
61
|
type: barrier.type || 'unknown',
|
|
62
62
|
status: barrier.status,
|
|
63
63
|
notes: barrier.notes || null,
|
|
64
|
+
required_decision_ids_by_repo: barrier.required_decision_ids_by_repo || barrier.alignment_decision_ids || null,
|
|
64
65
|
alignment_decision_ids: barrier.alignment_decision_ids || null,
|
|
65
66
|
}));
|
|
66
67
|
}
|
|
@@ -78,13 +79,13 @@ function buildRequiredFollowups(workstreamId, dependencyIds, upstreamAcceptances
|
|
|
78
79
|
|
|
79
80
|
for (const barrier of activeBarriers) {
|
|
80
81
|
if (
|
|
81
|
-
barrier.type === 'interface_alignment'
|
|
82
|
-
&& barrier.
|
|
83
|
-
&& Array.isArray(barrier.
|
|
84
|
-
&& barrier.
|
|
82
|
+
(barrier.type === 'interface_alignment' || barrier.type === 'named_decisions')
|
|
83
|
+
&& barrier.required_decision_ids_by_repo
|
|
84
|
+
&& Array.isArray(barrier.required_decision_ids_by_repo[targetRepoId])
|
|
85
|
+
&& barrier.required_decision_ids_by_repo[targetRepoId].length > 0
|
|
85
86
|
) {
|
|
86
87
|
followups.push(
|
|
87
|
-
`Accept declared
|
|
88
|
+
`Accept declared decision requirements for ${targetRepoId}: ${barrier.required_decision_ids_by_repo[targetRepoId].join(', ')}.`,
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -146,12 +147,12 @@ function renderContextMarkdown(snapshot) {
|
|
|
146
147
|
for (const barrier of snapshot.active_barriers) {
|
|
147
148
|
let suffix = '';
|
|
148
149
|
if (
|
|
149
|
-
barrier.type === 'interface_alignment'
|
|
150
|
-
&& barrier.
|
|
151
|
-
&& Array.isArray(barrier.
|
|
152
|
-
&& barrier.
|
|
150
|
+
(barrier.type === 'interface_alignment' || barrier.type === 'named_decisions')
|
|
151
|
+
&& barrier.required_decision_ids_by_repo
|
|
152
|
+
&& Array.isArray(barrier.required_decision_ids_by_repo[snapshot.target_repo_id])
|
|
153
|
+
&& barrier.required_decision_ids_by_repo[snapshot.target_repo_id].length > 0
|
|
153
154
|
) {
|
|
154
|
-
suffix = ` Required decision IDs for ${snapshot.target_repo_id}: ${barrier.
|
|
155
|
+
suffix = ` Required decision IDs for ${snapshot.target_repo_id}: ${barrier.required_decision_ids_by_repo[snapshot.target_repo_id].join(', ')}.`;
|
|
155
156
|
}
|
|
156
157
|
lines.push(`- ${barrier.barrier_id}: ${barrier.type} (${barrier.status})${suffix}`);
|
|
157
158
|
}
|
|
@@ -244,6 +244,8 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
244
244
|
lines.push('- Your artifact type should be `patch`.');
|
|
245
245
|
if (runtimeType === 'api_proxy' || runtimeType === 'remote_agent') {
|
|
246
246
|
lines.push('- **This runtime cannot write repo files directly.** When doing work, you MUST return proposed changes as structured JSON.');
|
|
247
|
+
lines.push('- For non-completion turns, set `artifact.type` to `patch`. Do NOT use `workspace` or `commit`.');
|
|
248
|
+
lines.push('- Use `artifact.type: "review"` only for completion-only final-phase turns that propose no file changes.');
|
|
247
249
|
lines.push('- Include a `proposed_changes` array in your turn result with each file change (omit or set to `[]` on completion-only turns):');
|
|
248
250
|
lines.push(' ```json');
|
|
249
251
|
lines.push(' "proposed_changes": [');
|
|
@@ -396,6 +398,10 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
396
398
|
if (role.write_authority === 'review_only') {
|
|
397
399
|
lines.push('- `objections`: **must be non-empty** (challenge requirement for review_only roles)');
|
|
398
400
|
}
|
|
401
|
+
if (role.write_authority === 'proposed' && (runtimeType === 'api_proxy' || runtimeType === 'remote_agent')) {
|
|
402
|
+
lines.push('- For `proposed` `api_proxy`/`remote_agent` turns with file changes, `artifact.type` must be `patch` and `proposed_changes` must be non-empty.');
|
|
403
|
+
lines.push('- Do NOT use `artifact.type: "workspace"` or `artifact.type: "commit"` on a `proposed` `api_proxy`/`remote_agent` turn.');
|
|
404
|
+
}
|
|
399
405
|
// List valid phase names explicitly to prevent gate-name confusion
|
|
400
406
|
const phaseNames = config.routing ? Object.keys(config.routing) : [];
|
|
401
407
|
if (phaseNames.length > 0) {
|
|
@@ -1266,6 +1272,7 @@ function readLastHistoryEntry(root, warnings = []) {
|
|
|
1266
1272
|
|
|
1267
1273
|
function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
1268
1274
|
const isReviewOnly = role.write_authority === 'review_only';
|
|
1275
|
+
const isProposed = role.write_authority === 'proposed';
|
|
1269
1276
|
return {
|
|
1270
1277
|
schema_version: '1.0',
|
|
1271
1278
|
run_id: state.run_id,
|
|
@@ -1306,13 +1313,24 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
|
1306
1313
|
: [{ command: '<exact command that was run>', exit_code: 0 }],
|
|
1307
1314
|
},
|
|
1308
1315
|
artifact: {
|
|
1309
|
-
type: isReviewOnly ? 'review' : 'workspace',
|
|
1310
|
-
ref: isReviewOnly ? null : 'git:dirty',
|
|
1316
|
+
type: isReviewOnly ? 'review' : (isProposed ? 'patch' : 'workspace'),
|
|
1317
|
+
ref: isReviewOnly ? null : (isProposed ? null : 'git:dirty'),
|
|
1311
1318
|
},
|
|
1312
1319
|
proposed_next_role: '<role_id that should act next>',
|
|
1313
1320
|
phase_transition_request: null,
|
|
1314
1321
|
run_completion_request: null,
|
|
1315
1322
|
needs_human_reason: null,
|
|
1323
|
+
...(isProposed
|
|
1324
|
+
? {
|
|
1325
|
+
proposed_changes: [
|
|
1326
|
+
{
|
|
1327
|
+
path: '<path/to/modified/file>',
|
|
1328
|
+
action: 'create',
|
|
1329
|
+
content: '<full file content>',
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
}
|
|
1333
|
+
: {}),
|
|
1316
1334
|
cost: {
|
|
1317
1335
|
input_tokens: 0,
|
|
1318
1336
|
output_tokens: 0,
|
package/src/lib/export.js
CHANGED
|
@@ -166,6 +166,93 @@ function countDirectoryFiles(files, prefix) {
|
|
|
166
166
|
return Object.keys(files).filter((path) => path.startsWith(`${prefix}/`)).length;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export function buildDelegationSummary(files) {
|
|
170
|
+
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
171
|
+
if (!Array.isArray(historyData)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Index history entries by delegation-related fields
|
|
176
|
+
const parentTurns = new Map(); // parent_turn_id -> { role, delegations_issued }
|
|
177
|
+
const childTurns = new Map(); // delegation_id -> { child entry }
|
|
178
|
+
const reviewTurns = new Map(); // parent_turn_id -> { review entry }
|
|
179
|
+
|
|
180
|
+
for (const entry of historyData) {
|
|
181
|
+
if (entry.delegations_issued && Array.isArray(entry.delegations_issued)) {
|
|
182
|
+
parentTurns.set(entry.turn_id, {
|
|
183
|
+
role: entry.role,
|
|
184
|
+
delegations_issued: entry.delegations_issued,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (entry.delegation_context) {
|
|
188
|
+
childTurns.set(entry.delegation_context.delegation_id, {
|
|
189
|
+
turn_id: entry.turn_id,
|
|
190
|
+
status: entry.status || 'completed',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (entry.delegation_review) {
|
|
194
|
+
reviewTurns.set(entry.delegation_review.parent_turn_id, {
|
|
195
|
+
turn_id: entry.turn_id,
|
|
196
|
+
results: entry.delegation_review.results || [],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let totalDelegationsIssued = 0;
|
|
202
|
+
const delegationChains = [];
|
|
203
|
+
|
|
204
|
+
for (const [parentTurnId, parent] of parentTurns) {
|
|
205
|
+
totalDelegationsIssued += parent.delegations_issued.length;
|
|
206
|
+
|
|
207
|
+
const review = reviewTurns.get(parentTurnId);
|
|
208
|
+
const reviewResultsByDelegation = new Map();
|
|
209
|
+
if (review) {
|
|
210
|
+
for (const r of review.results) {
|
|
211
|
+
if (r.delegation_id) {
|
|
212
|
+
reviewResultsByDelegation.set(r.delegation_id, r);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const delegations = parent.delegations_issued.map((del) => {
|
|
218
|
+
const child = childTurns.get(del.id);
|
|
219
|
+
const reviewResult = reviewResultsByDelegation.get(del.id);
|
|
220
|
+
return {
|
|
221
|
+
delegation_id: del.id,
|
|
222
|
+
to_role: del.to_role,
|
|
223
|
+
charter: del.charter,
|
|
224
|
+
status: reviewResult?.status || child?.status || 'pending',
|
|
225
|
+
child_turn_id: child?.turn_id || null,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let outcome;
|
|
230
|
+
if (!review) {
|
|
231
|
+
outcome = 'pending';
|
|
232
|
+
} else {
|
|
233
|
+
const statuses = delegations.map((d) => d.status);
|
|
234
|
+
const allCompleted = statuses.every((s) => s === 'completed');
|
|
235
|
+
const allFailed = statuses.every((s) => s === 'failed');
|
|
236
|
+
if (allCompleted) outcome = 'completed';
|
|
237
|
+
else if (allFailed) outcome = 'failed';
|
|
238
|
+
else outcome = 'mixed';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
delegationChains.push({
|
|
242
|
+
parent_turn_id: parentTurnId,
|
|
243
|
+
parent_role: parent.role,
|
|
244
|
+
delegations,
|
|
245
|
+
review_turn_id: review?.turn_id || null,
|
|
246
|
+
outcome,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
total_delegations_issued: totalDelegationsIssued,
|
|
252
|
+
delegation_chains: delegationChains,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
169
256
|
function isGitRepo(root) {
|
|
170
257
|
try {
|
|
171
258
|
execSync('git rev-parse --is-inside-work-tree', {
|
|
@@ -317,6 +404,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
317
404
|
staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
|
|
318
405
|
intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
|
|
319
406
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
407
|
+
delegation_summary: buildDelegationSummary(files),
|
|
320
408
|
},
|
|
321
409
|
workspace: buildRunWorkspaceMetadata(root),
|
|
322
410
|
files,
|
|
@@ -2628,6 +2628,35 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2628
2628
|
...(currentTurn.started_at ? { started_at: currentTurn.started_at } : {}),
|
|
2629
2629
|
accepted_at: now,
|
|
2630
2630
|
...(currentTurn.started_at ? { duration_ms: Math.max(0, new Date(now).getTime() - new Date(currentTurn.started_at).getTime()) } : {}),
|
|
2631
|
+
...(Array.isArray(turnResult.delegations) && turnResult.delegations.length > 0
|
|
2632
|
+
? {
|
|
2633
|
+
delegations_issued: turnResult.delegations.map((delegation) => ({
|
|
2634
|
+
id: delegation.id,
|
|
2635
|
+
to_role: delegation.to_role,
|
|
2636
|
+
charter: delegation.charter,
|
|
2637
|
+
acceptance_contract: delegation.acceptance_contract,
|
|
2638
|
+
})),
|
|
2639
|
+
}
|
|
2640
|
+
: {}),
|
|
2641
|
+
...(currentTurn.delegation_context
|
|
2642
|
+
? {
|
|
2643
|
+
delegation_context: {
|
|
2644
|
+
delegation_id: currentTurn.delegation_context.delegation_id,
|
|
2645
|
+
parent_turn_id: currentTurn.delegation_context.parent_turn_id,
|
|
2646
|
+
parent_role: currentTurn.delegation_context.parent_role,
|
|
2647
|
+
charter: currentTurn.delegation_context.charter,
|
|
2648
|
+
acceptance_contract: currentTurn.delegation_context.acceptance_contract,
|
|
2649
|
+
},
|
|
2650
|
+
}
|
|
2651
|
+
: {}),
|
|
2652
|
+
...(currentTurn.delegation_review
|
|
2653
|
+
? {
|
|
2654
|
+
delegation_review: {
|
|
2655
|
+
parent_turn_id: currentTurn.delegation_review.parent_turn_id,
|
|
2656
|
+
results: currentTurn.delegation_review.results,
|
|
2657
|
+
},
|
|
2658
|
+
}
|
|
2659
|
+
: {}),
|
|
2631
2660
|
};
|
|
2632
2661
|
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2633
2662
|
// Build ledger entries for the journal
|
package/src/lib/report.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
import { verifyExportArtifact } from './export-verifier.js';
|
|
2
|
+
import { buildDelegationSummary } from './export.js';
|
|
2
3
|
import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance.js';
|
|
3
4
|
|
|
4
5
|
export const GOVERNANCE_REPORT_VERSION = '0.1';
|
|
5
6
|
|
|
7
|
+
const VALID_DELEGATION_OUTCOMES = new Set(['completed', 'failed', 'mixed', 'pending']);
|
|
8
|
+
|
|
9
|
+
function normalizeDelegationSummary(summary) {
|
|
10
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
|
|
11
|
+
if (!Number.isInteger(summary.total_delegations_issued) || summary.total_delegations_issued < 0) return null;
|
|
12
|
+
if (!Array.isArray(summary.delegation_chains)) return null;
|
|
13
|
+
|
|
14
|
+
const chains = [];
|
|
15
|
+
for (const chain of summary.delegation_chains) {
|
|
16
|
+
if (!chain || typeof chain !== 'object' || Array.isArray(chain)) return null;
|
|
17
|
+
if (typeof chain.parent_turn_id !== 'string' || chain.parent_turn_id.length === 0) return null;
|
|
18
|
+
if (typeof chain.parent_role !== 'string' || chain.parent_role.length === 0) return null;
|
|
19
|
+
if (!Array.isArray(chain.delegations)) return null;
|
|
20
|
+
if (chain.review_turn_id !== null && (typeof chain.review_turn_id !== 'string' || chain.review_turn_id.length === 0)) return null;
|
|
21
|
+
if (!VALID_DELEGATION_OUTCOMES.has(chain.outcome)) return null;
|
|
22
|
+
|
|
23
|
+
const delegations = [];
|
|
24
|
+
for (const delegation of chain.delegations) {
|
|
25
|
+
if (!delegation || typeof delegation !== 'object' || Array.isArray(delegation)) return null;
|
|
26
|
+
if (typeof delegation.delegation_id !== 'string' || delegation.delegation_id.length === 0) return null;
|
|
27
|
+
if (typeof delegation.to_role !== 'string' || delegation.to_role.length === 0) return null;
|
|
28
|
+
if (typeof delegation.charter !== 'string') return null;
|
|
29
|
+
if (!['completed', 'failed', 'pending'].includes(delegation.status)) return null;
|
|
30
|
+
if (delegation.child_turn_id !== null && (typeof delegation.child_turn_id !== 'string' || delegation.child_turn_id.length === 0)) return null;
|
|
31
|
+
delegations.push({
|
|
32
|
+
delegation_id: delegation.delegation_id,
|
|
33
|
+
to_role: delegation.to_role,
|
|
34
|
+
charter: delegation.charter,
|
|
35
|
+
status: delegation.status,
|
|
36
|
+
child_turn_id: delegation.child_turn_id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
chains.push({
|
|
41
|
+
parent_turn_id: chain.parent_turn_id,
|
|
42
|
+
parent_role: chain.parent_role,
|
|
43
|
+
delegations,
|
|
44
|
+
review_turn_id: chain.review_turn_id,
|
|
45
|
+
outcome: chain.outcome,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
total_delegations_issued: summary.total_delegations_issued,
|
|
51
|
+
delegation_chains: chains,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractDelegationSummary(artifact) {
|
|
56
|
+
const fromSummary = normalizeDelegationSummary(artifact.summary?.delegation_summary);
|
|
57
|
+
if (fromSummary) return fromSummary;
|
|
58
|
+
return normalizeDelegationSummary(buildDelegationSummary(artifact.files || {}));
|
|
59
|
+
}
|
|
60
|
+
|
|
6
61
|
function yesNo(value) {
|
|
7
62
|
return value ? 'yes' : 'no';
|
|
8
63
|
}
|
|
@@ -846,6 +901,7 @@ function buildRunSubject(artifact) {
|
|
|
846
901
|
const recoverySummary = extractRecoverySummary(artifact);
|
|
847
902
|
const continuity = extractContinuityMetadata(artifact);
|
|
848
903
|
const governanceEvents = extractGovernanceEventDigest(artifact);
|
|
904
|
+
const delegationSummary = extractDelegationSummary(artifact);
|
|
849
905
|
|
|
850
906
|
return {
|
|
851
907
|
kind: 'governed_run',
|
|
@@ -881,6 +937,7 @@ function buildRunSubject(artifact) {
|
|
|
881
937
|
governance_events: governanceEvents,
|
|
882
938
|
gate_failures: gateFailures,
|
|
883
939
|
timeout_events: timeoutEvents,
|
|
940
|
+
delegation_summary: delegationSummary,
|
|
884
941
|
hook_summary: hookSummary,
|
|
885
942
|
gate_summary: gateSummary,
|
|
886
943
|
intake_links: intakeLinks,
|
|
@@ -1174,6 +1231,17 @@ export function formatGovernanceReportText(report) {
|
|
|
1174
1231
|
}
|
|
1175
1232
|
}
|
|
1176
1233
|
|
|
1234
|
+
if (run.delegation_summary?.delegation_chains?.length > 0) {
|
|
1235
|
+
lines.push('', 'Delegation Summary:');
|
|
1236
|
+
lines.push(` Total delegations issued: ${run.delegation_summary.total_delegations_issued}`);
|
|
1237
|
+
for (const chain of run.delegation_summary.delegation_chains) {
|
|
1238
|
+
lines.push(` - ${chain.parent_role} (${chain.parent_turn_id}) | outcome: ${chain.outcome} | review: ${chain.review_turn_id || 'pending'}`);
|
|
1239
|
+
for (const delegation of chain.delegations) {
|
|
1240
|
+
lines.push(` ${delegation.delegation_id} -> ${delegation.to_role} | ${delegation.status} | child: ${delegation.child_turn_id || 'pending'} | ${delegation.charter}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1177
1245
|
if (run.turns && run.turns.length > 0) {
|
|
1178
1246
|
lines.push('', 'Turn Timeline:');
|
|
1179
1247
|
for (let i = 0; i < run.turns.length; i++) {
|
|
@@ -1628,6 +1696,23 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1628
1696
|
}
|
|
1629
1697
|
}
|
|
1630
1698
|
|
|
1699
|
+
if (run.delegation_summary?.delegation_chains?.length > 0) {
|
|
1700
|
+
lines.push('', '## Delegation Summary', '');
|
|
1701
|
+
lines.push(`- Total delegations issued: ${run.delegation_summary.total_delegations_issued}`, '');
|
|
1702
|
+
lines.push('| Parent Role | Parent Turn | Outcome | Review Turn | Delegation | Child Turn | Status | Charter |', '|-------------|-------------|---------|-------------|------------|------------|--------|---------|');
|
|
1703
|
+
for (const chain of run.delegation_summary.delegation_chains) {
|
|
1704
|
+
for (let i = 0; i < chain.delegations.length; i++) {
|
|
1705
|
+
const delegation = chain.delegations[i];
|
|
1706
|
+
const parentRole = i === 0 ? chain.parent_role : '';
|
|
1707
|
+
const parentTurn = i === 0 ? `\`${chain.parent_turn_id}\`` : '';
|
|
1708
|
+
const outcome = i === 0 ? `\`${chain.outcome}\`` : '';
|
|
1709
|
+
const reviewTurn = i === 0 ? `\`${chain.review_turn_id || 'pending'}\`` : '';
|
|
1710
|
+
const charter = delegation.charter.replace(/\|/g, '\\|');
|
|
1711
|
+
lines.push(`| ${parentRole} | ${parentTurn} | ${outcome} | ${reviewTurn} | \`${delegation.delegation_id}\` → \`${delegation.to_role}\` | \`${delegation.child_turn_id || 'pending'}\` | \`${delegation.status}\` | ${charter} |`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1631
1716
|
if (run.turns && run.turns.length > 0) {
|
|
1632
1717
|
lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
|
|
1633
1718
|
for (let i = 0; i < run.turns.length; i++) {
|
package/src/lib/token-budget.js
CHANGED
|
@@ -18,6 +18,7 @@ const SEPARATOR = '\n\n---\n\n';
|
|
|
18
18
|
const SYSTEM_PROMPT = [
|
|
19
19
|
'You are acting as a governed agent in an AgentXchain protocol run.',
|
|
20
20
|
'Your task and rules are described in the user message.',
|
|
21
|
+
'You MUST obey the write-authority-specific rules in the prompt exactly.',
|
|
21
22
|
'You MUST respond with a valid JSON object matching the turn result schema provided in the prompt.',
|
|
22
23
|
'Do NOT wrap the JSON in markdown code fences. Respond with raw JSON only.',
|
|
23
24
|
].join('\n');
|