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 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, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.88.0",
3
+ "version": "2.90.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- validateInterfaceAlignment(workstreamId, workstream, errors);
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 validateInterfaceAlignment(workstreamId, workstream, errors) {
162
- if (workstream.completion_barrier !== 'interface_alignment') {
163
- return;
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
- 'workstream_interface_alignment_invalid',
171
- `workstream "${workstreamId}" with completion_barrier "interface_alignment" must declare interface_alignment.decision_ids_by_repo`,
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 = alignment.decision_ids_by_repo;
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
- 'workstream_interface_alignment_decisions_invalid',
181
- `workstream "${workstreamId}" interface_alignment.decision_ids_by_repo must be an object`,
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
- 'workstream_interface_alignment_repo_missing',
194
- `workstream "${workstreamId}" must declare interface_alignment.decision_ids_by_repo["${repoId}"]`,
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
- 'workstream_interface_alignment_repo_invalid',
204
- `workstream "${workstreamId}" interface_alignment.decision_ids_by_repo["${repoId}"] must be a non-empty array`,
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
- 'workstream_interface_alignment_decision_invalid',
215
- `workstream "${workstreamId}" interface_alignment decision "${decisionId}" for repo "${repoId}" must match DEC-NNN`,
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
- 'workstream_interface_alignment_decision_duplicate',
223
- `workstream "${workstreamId}" interface_alignment decision "${decisionId}" is duplicated for repo "${repoId}"`,
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
- 'workstream_interface_alignment_repo_unknown',
236
- `workstream "${workstreamId}" interface_alignment references undeclared repo "${repoId}"`,
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?.decision_ids_by_repo
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.alignment_decision_ids
83
- && Array.isArray(barrier.alignment_decision_ids[targetRepoId])
84
- && barrier.alignment_decision_ids[targetRepoId].length > 0
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 interface-alignment decisions for ${targetRepoId}: ${barrier.alignment_decision_ids[targetRepoId].join(', ')}.`,
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.alignment_decision_ids
151
- && Array.isArray(barrier.alignment_decision_ids[snapshot.target_repo_id])
152
- && barrier.alignment_decision_ids[snapshot.target_repo_id].length > 0
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.alignment_decision_ids[snapshot.target_repo_id].join(', ')}.`;
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++) {
@@ -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');