agentxchain 0.8.8 → 2.2.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 +136 -136
- package/bin/agentxchain.js +186 -5
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +14 -6
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/init.js +516 -0
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +97 -1
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +858 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/schema.js +121 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +137 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Review view — renders pending phase transitions and run completion gates.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data, returns HTML string. Testable in Node.js.
|
|
5
|
+
* Per DEC-DASH-002: read-only. Shows the exact CLI command to approve, no buttons.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function esc(str) {
|
|
9
|
+
if (!str) return '';
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isPhaseBoundaryEntry(entry) {
|
|
19
|
+
if (!entry || typeof entry !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (entry.phase_transition === true) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return typeof entry.phase_transition_request === 'string' && entry.phase_transition_request.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findPostGateTurns(history, requestedByTurn) {
|
|
31
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!requestedByTurn) {
|
|
36
|
+
return history.slice();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const requestedIdx = history.findIndex((entry) => entry.turn_id === requestedByTurn);
|
|
40
|
+
if (requestedIdx === -1) {
|
|
41
|
+
return history.slice();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Include the requesting turn and all turns after the most recent accepted
|
|
45
|
+
// phase-transition request. history.jsonl persists phase_transition_request,
|
|
46
|
+
// not a synthetic phase_transition marker.
|
|
47
|
+
let startIdx = 0;
|
|
48
|
+
for (let i = requestedIdx - 1; i >= 0; i--) {
|
|
49
|
+
if (isPhaseBoundaryEntry(history[i])) {
|
|
50
|
+
startIdx = i + 1;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return history.slice(startIdx, requestedIdx + 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function aggregateEvidence(turns) {
|
|
59
|
+
const summaries = [];
|
|
60
|
+
const allObjections = [];
|
|
61
|
+
const allRisks = [];
|
|
62
|
+
const allDecisions = [];
|
|
63
|
+
const allFiles = [];
|
|
64
|
+
|
|
65
|
+
for (const turn of turns) {
|
|
66
|
+
if (turn.summary) {
|
|
67
|
+
const role = turn.assigned_role || turn.role || turn.agent || 'agent';
|
|
68
|
+
summaries.push({ role, summary: turn.summary, turn_id: turn.turn_id });
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(turn.objections)) {
|
|
71
|
+
for (const obj of turn.objections) {
|
|
72
|
+
allObjections.push(obj);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(turn.risks)) {
|
|
76
|
+
for (const risk of turn.risks) {
|
|
77
|
+
allRisks.push(risk);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(turn.decisions)) {
|
|
81
|
+
for (const dec of turn.decisions) {
|
|
82
|
+
allDecisions.push(dec);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const files = turn.observed_artifact?.files_changed || turn.files_changed || [];
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
if (!allFiles.includes(f)) {
|
|
88
|
+
allFiles.push(f);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { summaries, objections: allObjections, risks: allRisks, decisions: allDecisions, files: allFiles };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderList(title, items, formatter = (item) => item) {
|
|
97
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rows = items
|
|
102
|
+
.map((item) => formatter(item))
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((item) => `<li>${esc(item)}</li>`)
|
|
105
|
+
.join('');
|
|
106
|
+
|
|
107
|
+
if (!rows) {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return `<div class="gate-support">
|
|
112
|
+
<p><strong>${esc(title)}:</strong></p>
|
|
113
|
+
<ul>${rows}</ul>
|
|
114
|
+
</div>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { findPostGateTurns, aggregateEvidence };
|
|
118
|
+
|
|
119
|
+
function findCoordinatorGateRequest(history, pendingGate) {
|
|
120
|
+
if (!Array.isArray(history) || !pendingGate?.gate) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requestedType = pendingGate.gate_type === 'phase_transition'
|
|
125
|
+
? 'phase_transition_requested'
|
|
126
|
+
: 'run_completion_requested';
|
|
127
|
+
|
|
128
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
129
|
+
const entry = history[i];
|
|
130
|
+
if (entry?.type === requestedType && entry.gate === pendingGate.gate) {
|
|
131
|
+
return { entry, index: i };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findCoordinatorGateEvidence(history, pendingGate) {
|
|
139
|
+
if (!Array.isArray(history) || history.length === 0 || !pendingGate) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const gateRequest = findCoordinatorGateRequest(history, pendingGate);
|
|
144
|
+
const endIndex = gateRequest ? gateRequest.index : history.length;
|
|
145
|
+
let startIndex = 0;
|
|
146
|
+
|
|
147
|
+
for (let i = endIndex - 1; i >= 0; i -= 1) {
|
|
148
|
+
if (history[i]?.type === 'phase_transition_approved') {
|
|
149
|
+
startIndex = i + 1;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return history
|
|
155
|
+
.slice(startIndex, endIndex)
|
|
156
|
+
.filter((entry) => entry?.type === 'acceptance_projection');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function aggregateCoordinatorEvidence(entries) {
|
|
160
|
+
const summaries = [];
|
|
161
|
+
const decisions = [];
|
|
162
|
+
const files = [];
|
|
163
|
+
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
summaries.push({
|
|
166
|
+
role: entry.repo_id || 'repo',
|
|
167
|
+
summary: entry.summary || 'Accepted turn projected',
|
|
168
|
+
turn_id: entry.repo_turn_id || entry.projection_ref || '-',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (Array.isArray(entry.decisions)) {
|
|
172
|
+
for (const decision of entry.decisions) {
|
|
173
|
+
decisions.push(decision);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const file of entry.files_changed || []) {
|
|
178
|
+
if (!files.includes(file)) {
|
|
179
|
+
files.push(file);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { summaries, decisions, objections: [], risks: [], files };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function render({
|
|
188
|
+
state,
|
|
189
|
+
history = [],
|
|
190
|
+
coordinatorState = null,
|
|
191
|
+
coordinatorHistory = [],
|
|
192
|
+
coordinatorBarriers = {},
|
|
193
|
+
}) {
|
|
194
|
+
const repoPendingTransition = state?.pending_phase_transition || null;
|
|
195
|
+
const repoPendingCompletion = state?.pending_run_completion || null;
|
|
196
|
+
const coordinatorPendingGate = coordinatorState?.pending_gate || null;
|
|
197
|
+
const pendingTransition = repoPendingTransition || (coordinatorPendingGate?.gate_type === 'phase_transition' ? coordinatorPendingGate : null);
|
|
198
|
+
const pendingCompletion = repoPendingCompletion || (coordinatorPendingGate?.gate_type === 'run_completion' ? coordinatorPendingGate : null);
|
|
199
|
+
const isCoordinator = Boolean(!repoPendingTransition && !repoPendingCompletion && coordinatorPendingGate);
|
|
200
|
+
|
|
201
|
+
if (!pendingTransition && !pendingCompletion) {
|
|
202
|
+
const status = state?.status || coordinatorState?.status || 'unknown';
|
|
203
|
+
if (status === 'paused') {
|
|
204
|
+
return `<div class="placeholder"><h2>Gate Review</h2><p>Run is paused. A human gate approval may be pending. Check <code class="mono">agentxchain status</code> for details.</p></div>`;
|
|
205
|
+
}
|
|
206
|
+
return `<div class="placeholder"><h2>Gate Review</h2><p>No pending gates. The run will pause when a human approval is required.</p></div>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let html = `<div class="gate-view">`;
|
|
210
|
+
|
|
211
|
+
if (pendingTransition) {
|
|
212
|
+
const postGateTurns = isCoordinator
|
|
213
|
+
? findCoordinatorGateEvidence(coordinatorHistory, pendingTransition)
|
|
214
|
+
: findPostGateTurns(history, pendingTransition.requested_by_turn);
|
|
215
|
+
const evidence = isCoordinator
|
|
216
|
+
? aggregateCoordinatorEvidence(postGateTurns)
|
|
217
|
+
: aggregateEvidence(postGateTurns);
|
|
218
|
+
html += `<div class="gate-card">
|
|
219
|
+
<h3>Phase Transition Gate</h3>
|
|
220
|
+
<dl class="detail-list">
|
|
221
|
+
<dt>From</dt><dd>${esc(pendingTransition.from || state?.phase || coordinatorState?.phase)}</dd>
|
|
222
|
+
<dt>To</dt><dd>${esc(pendingTransition.to)}</dd>`;
|
|
223
|
+
if (pendingTransition.gate) {
|
|
224
|
+
html += `<dt>Gate</dt><dd class="mono">${esc(pendingTransition.gate)}</dd>`;
|
|
225
|
+
}
|
|
226
|
+
if (pendingTransition.requested_by_turn) {
|
|
227
|
+
html += `<dt>Requested By</dt><dd class="mono">${esc(pendingTransition.requested_by_turn)}</dd>`;
|
|
228
|
+
}
|
|
229
|
+
if (postGateTurns.length > 0) {
|
|
230
|
+
html += `<dt>Evidence Turns</dt><dd>${postGateTurns.length} turn${postGateTurns.length !== 1 ? 's' : ''}</dd>`;
|
|
231
|
+
}
|
|
232
|
+
if (isCoordinator && Array.isArray(pendingTransition.required_repos) && pendingTransition.required_repos.length > 0) {
|
|
233
|
+
html += `<dt>Required Repos</dt><dd>${esc(pendingTransition.required_repos.join(', '))}</dd>`;
|
|
234
|
+
}
|
|
235
|
+
html += `</dl>`;
|
|
236
|
+
if (evidence.summaries.length > 0) {
|
|
237
|
+
html += `<div class="gate-evidence"><h4>Agent Summaries</h4><ul>`;
|
|
238
|
+
for (const s of evidence.summaries) {
|
|
239
|
+
html += `<li><strong>${esc(s.role)}</strong> (${esc(s.turn_id)}): ${esc(s.summary)}</li>`;
|
|
240
|
+
}
|
|
241
|
+
html += `</ul></div>`;
|
|
242
|
+
}
|
|
243
|
+
html += renderList('Objections', evidence.objections, (item) => item?.statement || item);
|
|
244
|
+
html += renderList('Risks', evidence.risks, (item) => item?.statement || item);
|
|
245
|
+
html += renderList('Decisions', evidence.decisions, (item) => item?.statement || item);
|
|
246
|
+
if (evidence.files.length > 0) {
|
|
247
|
+
html += `<div class="gate-support"><p><strong>Files Changed:</strong></p><ul>${evidence.files.map(f => `<li class="mono">${esc(f)}</li>`).join('')}</ul></div>`;
|
|
248
|
+
}
|
|
249
|
+
if (isCoordinator) {
|
|
250
|
+
const pendingBarriers = Object.entries(coordinatorBarriers || {}).filter(([, barrier]) => barrier?.status !== 'satisfied');
|
|
251
|
+
if (pendingBarriers.length > 0) {
|
|
252
|
+
html += `<div class="gate-support"><p><strong>Coordinator Barriers:</strong></p><ul>${pendingBarriers.map(([barrierId, barrier]) => (
|
|
253
|
+
`<li>${esc(`${barrierId}: ${barrier.status}`)}</li>`
|
|
254
|
+
)).join('')}</ul></div>`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
html += `
|
|
258
|
+
<div class="gate-action">
|
|
259
|
+
<p>Approve with:</p>
|
|
260
|
+
<pre class="recovery-command mono" data-copy="${isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-transition'}">${isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-transition'}</pre>
|
|
261
|
+
</div>
|
|
262
|
+
</div>`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (pendingCompletion) {
|
|
266
|
+
const postGateTurns = isCoordinator
|
|
267
|
+
? findCoordinatorGateEvidence(coordinatorHistory, pendingCompletion)
|
|
268
|
+
: findPostGateTurns(history, pendingCompletion.requested_by_turn);
|
|
269
|
+
const evidence = isCoordinator
|
|
270
|
+
? aggregateCoordinatorEvidence(postGateTurns)
|
|
271
|
+
: aggregateEvidence(postGateTurns);
|
|
272
|
+
html += `<div class="gate-card">
|
|
273
|
+
<h3>Run Completion Gate</h3>
|
|
274
|
+
<dl class="detail-list">`;
|
|
275
|
+
if (pendingCompletion.gate) {
|
|
276
|
+
html += `<dt>Gate</dt><dd class="mono">${esc(pendingCompletion.gate)}</dd>`;
|
|
277
|
+
}
|
|
278
|
+
if (pendingCompletion.requested_by_turn) {
|
|
279
|
+
html += `<dt>Requested By</dt><dd class="mono">${esc(pendingCompletion.requested_by_turn)}</dd>`;
|
|
280
|
+
}
|
|
281
|
+
if (postGateTurns.length > 0) {
|
|
282
|
+
html += `<dt>Evidence Turns</dt><dd>${postGateTurns.length} turn${postGateTurns.length !== 1 ? 's' : ''}</dd>`;
|
|
283
|
+
}
|
|
284
|
+
if (isCoordinator && Array.isArray(pendingCompletion.required_repos) && pendingCompletion.required_repos.length > 0) {
|
|
285
|
+
html += `<dt>Required Repos</dt><dd>${esc(pendingCompletion.required_repos.join(', '))}</dd>`;
|
|
286
|
+
}
|
|
287
|
+
html += `</dl>`;
|
|
288
|
+
if (evidence.summaries.length > 0) {
|
|
289
|
+
html += `<div class="gate-evidence"><h4>Agent Summaries</h4><ul>`;
|
|
290
|
+
for (const s of evidence.summaries) {
|
|
291
|
+
html += `<li><strong>${esc(s.role)}</strong> (${esc(s.turn_id)}): ${esc(s.summary)}</li>`;
|
|
292
|
+
}
|
|
293
|
+
html += `</ul></div>`;
|
|
294
|
+
}
|
|
295
|
+
html += renderList('Objections', evidence.objections, (item) => item?.statement || item);
|
|
296
|
+
html += renderList('Risks', evidence.risks, (item) => item?.statement || item);
|
|
297
|
+
html += renderList('Decisions', evidence.decisions, (item) => item?.statement || item);
|
|
298
|
+
if (evidence.files.length > 0) {
|
|
299
|
+
html += `<div class="gate-support"><p><strong>Files Changed:</strong></p><ul>${evidence.files.map(f => `<li class="mono">${esc(f)}</li>`).join('')}</ul></div>`;
|
|
300
|
+
}
|
|
301
|
+
html += `
|
|
302
|
+
<div class="gate-action">
|
|
303
|
+
<p>Approve with:</p>
|
|
304
|
+
<pre class="recovery-command mono" data-copy="${isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-completion'}">${isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-completion'}</pre>
|
|
305
|
+
</div>
|
|
306
|
+
</div>`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
html += `</div>`;
|
|
310
|
+
return html;
|
|
311
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Audit view — renders hook-audit.jsonl entries.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data, returns HTML string. Testable in Node.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function esc(str) {
|
|
8
|
+
if (!str) return '';
|
|
9
|
+
return String(str)
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getHookPhase(entry) {
|
|
18
|
+
return entry?.hook_phase || entry?.phase || '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getHookName(entry) {
|
|
22
|
+
return entry?.hook_name || entry?.hook || entry?.name || '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatAnnotations(entry) {
|
|
26
|
+
if (Array.isArray(entry?.annotations) && entry.annotations.length > 0) {
|
|
27
|
+
return entry.annotations
|
|
28
|
+
.map((annotation) => annotation && typeof annotation === 'object'
|
|
29
|
+
? `${annotation.key}: ${annotation.value}`
|
|
30
|
+
: null)
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(', ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return entry?.annotation || entry?.message || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function verdictBadge(verdict) {
|
|
39
|
+
const colors = {
|
|
40
|
+
allow: 'var(--green)',
|
|
41
|
+
warn: 'var(--yellow)',
|
|
42
|
+
block: 'var(--red)',
|
|
43
|
+
};
|
|
44
|
+
const color = colors[verdict] || 'var(--text-dim)';
|
|
45
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(verdict)}</span>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function filterAudit(audit, filters = {}) {
|
|
49
|
+
if (!Array.isArray(audit)) return [];
|
|
50
|
+
|
|
51
|
+
const phase = String(filters.phase || 'all').trim().toLowerCase();
|
|
52
|
+
const verdict = String(filters.verdict || 'all').trim().toLowerCase();
|
|
53
|
+
const hookName = String(filters.hookName || 'all').trim().toLowerCase();
|
|
54
|
+
|
|
55
|
+
return audit.filter((entry) => {
|
|
56
|
+
if (phase !== 'all') {
|
|
57
|
+
const entryPhase = String(getHookPhase(entry)).trim().toLowerCase();
|
|
58
|
+
if (entryPhase !== phase) return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (verdict !== 'all') {
|
|
62
|
+
const entryVerdict = String(entry.verdict || '').trim().toLowerCase();
|
|
63
|
+
if (entryVerdict !== verdict) return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hookName !== 'all') {
|
|
67
|
+
const entryHookName = String(getHookName(entry)).trim().toLowerCase();
|
|
68
|
+
if (entryHookName !== hookName) return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectHookPhases(audit) {
|
|
76
|
+
const unique = new Set();
|
|
77
|
+
for (const entry of audit) {
|
|
78
|
+
const phase = getHookPhase(entry);
|
|
79
|
+
if (phase) unique.add(phase);
|
|
80
|
+
}
|
|
81
|
+
return Array.from(unique).sort();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function collectHookNames(audit) {
|
|
85
|
+
const unique = new Set();
|
|
86
|
+
for (const entry of audit) {
|
|
87
|
+
const name = getHookName(entry);
|
|
88
|
+
if (name) unique.add(name);
|
|
89
|
+
}
|
|
90
|
+
return Array.from(unique).sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function render({ audit, annotations, filter = {} }) {
|
|
94
|
+
const hasAudit = Array.isArray(audit) && audit.length > 0;
|
|
95
|
+
const hasAnnotations = Array.isArray(annotations) && annotations.length > 0;
|
|
96
|
+
|
|
97
|
+
if (!hasAudit && !hasAnnotations) {
|
|
98
|
+
return `<div class="placeholder"><h2>Hook Audit</h2><p>No hook activity recorded.</p></div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let html = `<div class="hooks-view">`;
|
|
102
|
+
|
|
103
|
+
if (hasAudit) {
|
|
104
|
+
const filtered = filterAudit(audit, filter);
|
|
105
|
+
const phases = collectHookPhases(audit);
|
|
106
|
+
const hookNames = collectHookNames(audit);
|
|
107
|
+
const selectedPhase = filter.phase || 'all';
|
|
108
|
+
const selectedVerdict = filter.verdict || 'all';
|
|
109
|
+
const selectedHookName = filter.hookName || 'all';
|
|
110
|
+
|
|
111
|
+
html += `<div class="section"><h3>Hook Audit Log</h3>
|
|
112
|
+
<p class="section-subtitle">${filtered.length} of ${audit.length} hook execution${audit.length !== 1 ? 's' : ''}</p>
|
|
113
|
+
<div class="filter-bar">
|
|
114
|
+
<label class="filter-control">
|
|
115
|
+
<span>Phase</span>
|
|
116
|
+
<select data-view-control="hooks-phase">
|
|
117
|
+
<option value="all"${selectedPhase === 'all' ? ' selected' : ''}>All phases</option>
|
|
118
|
+
${phases.map((p) => `<option value="${esc(p)}"${selectedPhase === p ? ' selected' : ''}>${esc(p)}</option>`).join('')}
|
|
119
|
+
</select>
|
|
120
|
+
</label>
|
|
121
|
+
<label class="filter-control">
|
|
122
|
+
<span>Verdict</span>
|
|
123
|
+
<select data-view-control="hooks-verdict">
|
|
124
|
+
<option value="all"${selectedVerdict === 'all' ? ' selected' : ''}>All verdicts</option>
|
|
125
|
+
<option value="allow"${selectedVerdict === 'allow' ? ' selected' : ''}>allow</option>
|
|
126
|
+
<option value="warn"${selectedVerdict === 'warn' ? ' selected' : ''}>warn</option>
|
|
127
|
+
<option value="block"${selectedVerdict === 'block' ? ' selected' : ''}>block</option>
|
|
128
|
+
</select>
|
|
129
|
+
</label>
|
|
130
|
+
<label class="filter-control">
|
|
131
|
+
<span>Hook</span>
|
|
132
|
+
<select data-view-control="hooks-hookname">
|
|
133
|
+
<option value="all"${selectedHookName === 'all' ? ' selected' : ''}>All hooks</option>
|
|
134
|
+
${hookNames.map((n) => `<option value="${esc(n)}"${selectedHookName === n ? ' selected' : ''}>${esc(n)}</option>`).join('')}
|
|
135
|
+
</select>
|
|
136
|
+
</label>
|
|
137
|
+
</div>
|
|
138
|
+
<table class="data-table">
|
|
139
|
+
<thead><tr><th>Time</th><th>Phase</th><th>Hook</th><th>Verdict</th><th>Action</th><th>Duration</th></tr></thead>
|
|
140
|
+
<tbody>`;
|
|
141
|
+
|
|
142
|
+
for (const entry of filtered) {
|
|
143
|
+
const duration = entry.duration_ms != null ? `${entry.duration_ms}ms` : '-';
|
|
144
|
+
const action = entry.orchestrator_action || entry.action || 'continued';
|
|
145
|
+
html += `<tr>
|
|
146
|
+
<td class="mono">${esc(entry.timestamp || '-')}</td>
|
|
147
|
+
<td class="mono">${esc(getHookPhase(entry))}</td>
|
|
148
|
+
<td>${esc(getHookName(entry))}</td>
|
|
149
|
+
<td>${verdictBadge(entry.verdict)}</td>
|
|
150
|
+
<td class="mono">${esc(action)}</td>
|
|
151
|
+
<td class="mono">${esc(duration)}</td>
|
|
152
|
+
</tr>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
html += `</tbody></table></div>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (hasAnnotations) {
|
|
159
|
+
html += `<div class="section"><h3>Hook Annotations</h3>
|
|
160
|
+
<p class="section-subtitle">${annotations.length} annotation${annotations.length !== 1 ? 's' : ''}</p>
|
|
161
|
+
<div class="annotation-list">`;
|
|
162
|
+
|
|
163
|
+
for (const entry of annotations) {
|
|
164
|
+
const annotationText = formatAnnotations(entry);
|
|
165
|
+
html += `<div class="annotation-card">
|
|
166
|
+
<span class="mono">${esc(entry.turn_id || '-')}</span>
|
|
167
|
+
<span class="mono">${esc(getHookName(entry))}</span>
|
|
168
|
+
<span>${esc(annotationText || JSON.stringify(entry))}</span>
|
|
169
|
+
</div>`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
html += `</div></div>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
html += `</div>`;
|
|
176
|
+
return html;
|
|
177
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
function esc(str) {
|
|
2
|
+
if (!str) 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
|
+
active: 'var(--green)',
|
|
18
|
+
running: 'var(--green)',
|
|
19
|
+
paused: 'var(--yellow)',
|
|
20
|
+
blocked: 'var(--red)',
|
|
21
|
+
completed: 'var(--accent)',
|
|
22
|
+
linked: 'var(--text-dim)',
|
|
23
|
+
initialized: 'var(--accent)',
|
|
24
|
+
satisfied: 'var(--green)',
|
|
25
|
+
partially_satisfied: 'var(--yellow)',
|
|
26
|
+
pending: 'var(--text-dim)',
|
|
27
|
+
};
|
|
28
|
+
return colors[status] || 'var(--text-dim)';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function summarizeBarriers(barriers) {
|
|
32
|
+
const counts = { pending: 0, partially_satisfied: 0, satisfied: 0, other: 0 };
|
|
33
|
+
for (const barrier of Object.values(barriers || {})) {
|
|
34
|
+
const key = counts[barrier?.status] != null ? barrier.status : 'other';
|
|
35
|
+
counts[key] += 1;
|
|
36
|
+
}
|
|
37
|
+
return counts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function render({ coordinatorState, coordinatorBarriers = {}, barrierLedger = [] }) {
|
|
41
|
+
if (!coordinatorState) {
|
|
42
|
+
return `<div class="placeholder"><h2>No Initiative</h2><p>No coordinator run found. Start one with <code class="mono">agentxchain multi init</code></p></div>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const repoRuns = Object.entries(coordinatorState.repo_runs || {});
|
|
46
|
+
const barriers = Object.entries(coordinatorBarriers || {});
|
|
47
|
+
const pendingGate = coordinatorState.pending_gate || null;
|
|
48
|
+
const barrierCounts = summarizeBarriers(coordinatorBarriers);
|
|
49
|
+
const recentBarrierTransitions = Array.isArray(barrierLedger)
|
|
50
|
+
? barrierLedger.slice(-5).reverse()
|
|
51
|
+
: [];
|
|
52
|
+
|
|
53
|
+
let html = `<div class="initiative-view">`;
|
|
54
|
+
html += `<div class="run-header">
|
|
55
|
+
<div class="run-meta">
|
|
56
|
+
<span class="mono run-id">${esc(coordinatorState.super_run_id)}</span>
|
|
57
|
+
${badge(coordinatorState.status || 'unknown', statusColor(coordinatorState.status))}
|
|
58
|
+
<span class="phase-label">Phase: <strong>${esc(coordinatorState.phase || 'unknown')}</strong></span>
|
|
59
|
+
<span class="turn-count">${repoRuns.length} repo${repoRuns.length !== 1 ? 's' : ''}</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>`;
|
|
62
|
+
|
|
63
|
+
if (pendingGate || coordinatorState.blocked_reason) {
|
|
64
|
+
html += `<div class="section"><h3>Coordinator Attention</h3><div class="initiative-grid">`;
|
|
65
|
+
if (pendingGate) {
|
|
66
|
+
html += `<div class="gate-card">
|
|
67
|
+
<h3>Pending Gate</h3>
|
|
68
|
+
<dl class="detail-list">
|
|
69
|
+
<dt>Type</dt><dd>${esc(pendingGate.gate_type)}</dd>
|
|
70
|
+
<dt>Gate</dt><dd class="mono">${esc(pendingGate.gate)}</dd>`;
|
|
71
|
+
if (pendingGate.from) html += `<dt>From</dt><dd>${esc(pendingGate.from)}</dd>`;
|
|
72
|
+
if (pendingGate.to) html += `<dt>To</dt><dd>${esc(pendingGate.to)}</dd>`;
|
|
73
|
+
if (Array.isArray(pendingGate.required_repos) && pendingGate.required_repos.length > 0) {
|
|
74
|
+
html += `<dt>Repos</dt><dd>${esc(pendingGate.required_repos.join(', '))}</dd>`;
|
|
75
|
+
}
|
|
76
|
+
html += `</dl>
|
|
77
|
+
<div class="gate-action">
|
|
78
|
+
<p>Approve with:</p>
|
|
79
|
+
<pre class="recovery-command mono" data-copy="agentxchain multi approve-gate">agentxchain multi approve-gate</pre>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`;
|
|
82
|
+
}
|
|
83
|
+
if (coordinatorState.blocked_reason) {
|
|
84
|
+
html += `<div class="gate-card">
|
|
85
|
+
<h3>Blocked State</h3>
|
|
86
|
+
<p class="turn-summary">${esc(
|
|
87
|
+
typeof coordinatorState.blocked_reason === 'string'
|
|
88
|
+
? coordinatorState.blocked_reason
|
|
89
|
+
: JSON.stringify(coordinatorState.blocked_reason)
|
|
90
|
+
)}</p>
|
|
91
|
+
</div>`;
|
|
92
|
+
}
|
|
93
|
+
html += `</div></div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
html += `<div class="section"><h3>Repo Runs</h3><table class="data-table">
|
|
97
|
+
<thead><tr><th>Repo</th><th>Run</th><th>Status</th><th>Phase</th></tr></thead><tbody>`;
|
|
98
|
+
for (const [repoId, repoRun] of repoRuns) {
|
|
99
|
+
html += `<tr>
|
|
100
|
+
<td class="mono">${esc(repoId)}</td>
|
|
101
|
+
<td class="mono">${esc(repoRun.run_id || '-')}</td>
|
|
102
|
+
<td>${badge(repoRun.status || 'unknown', statusColor(repoRun.status))}</td>
|
|
103
|
+
<td>${esc(repoRun.phase || '-')}</td>
|
|
104
|
+
</tr>`;
|
|
105
|
+
}
|
|
106
|
+
html += `</tbody></table></div>`;
|
|
107
|
+
|
|
108
|
+
html += `<div class="section"><h3>Barrier Snapshot</h3>
|
|
109
|
+
<p class="section-subtitle">Pending ${barrierCounts.pending}, partial ${barrierCounts.partially_satisfied}, satisfied ${barrierCounts.satisfied}</p>`;
|
|
110
|
+
if (barriers.length === 0) {
|
|
111
|
+
html += `<div class="placeholder compact"><p>No barriers recorded.</p></div>`;
|
|
112
|
+
} else {
|
|
113
|
+
html += `<div class="turn-list">`;
|
|
114
|
+
for (const [barrierId, barrier] of barriers) {
|
|
115
|
+
html += `<div class="turn-card">
|
|
116
|
+
<div class="turn-header">
|
|
117
|
+
<span class="mono">${esc(barrierId)}</span>
|
|
118
|
+
${badge(barrier.status || 'unknown', statusColor(barrier.status))}
|
|
119
|
+
</div>
|
|
120
|
+
<div class="turn-detail"><span class="detail-label">Workstream:</span> ${esc(barrier.workstream_id || '-')}</div>
|
|
121
|
+
<div class="turn-detail"><span class="detail-label">Type:</span> ${esc(barrier.type || '-')}</div>`;
|
|
122
|
+
if (Array.isArray(barrier.required_repos) && barrier.required_repos.length > 0) {
|
|
123
|
+
html += `<div class="turn-detail"><span class="detail-label">Required Repos:</span> ${esc(barrier.required_repos.join(', '))}</div>`;
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(barrier.satisfied_repos) && barrier.satisfied_repos.length > 0) {
|
|
126
|
+
html += `<div class="turn-detail"><span class="detail-label">Satisfied Repos:</span> ${esc(barrier.satisfied_repos.join(', '))}</div>`;
|
|
127
|
+
}
|
|
128
|
+
html += `</div>`;
|
|
129
|
+
}
|
|
130
|
+
html += `</div>`;
|
|
131
|
+
}
|
|
132
|
+
html += `</div>`;
|
|
133
|
+
|
|
134
|
+
if (recentBarrierTransitions.length > 0) {
|
|
135
|
+
html += `<div class="section"><h3>Recent Barrier Transitions</h3><div class="annotation-list">`;
|
|
136
|
+
for (const entry of recentBarrierTransitions) {
|
|
137
|
+
html += `<div class="annotation-card">
|
|
138
|
+
<span class="mono">${esc(entry.barrier_id || '-')}</span>
|
|
139
|
+
<span>${esc(`${entry.previous_status || 'unknown'} -> ${entry.new_status || 'unknown'}`)}</span>
|
|
140
|
+
</div>`;
|
|
141
|
+
}
|
|
142
|
+
html += `</div></div>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
html += `</div>`;
|
|
146
|
+
return html;
|
|
147
|
+
}
|