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
package/dashboard/app.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard app — router, data fetching, and component mounting.
|
|
3
|
+
*
|
|
4
|
+
* Loaded as an ES module from index.html. Fetches state from the bridge
|
|
5
|
+
* server API and delegates rendering to view components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { render as renderTimeline } from './components/timeline.js';
|
|
9
|
+
import { render as renderLedger } from './components/ledger.js';
|
|
10
|
+
import { render as renderHooks } from './components/hooks.js';
|
|
11
|
+
import { render as renderBlocked } from './components/blocked.js';
|
|
12
|
+
import { render as renderGate } from './components/gate.js';
|
|
13
|
+
import { render as renderInitiative } from './components/initiative.js';
|
|
14
|
+
import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
15
|
+
|
|
16
|
+
const VIEWS = {
|
|
17
|
+
timeline: { fetch: ['state', 'history', 'audit', 'annotations'], render: renderTimeline },
|
|
18
|
+
ledger: { fetch: ['ledger'], render: renderLedger },
|
|
19
|
+
hooks: { fetch: ['audit', 'annotations'], render: renderHooks },
|
|
20
|
+
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
21
|
+
gate: { fetch: ['state', 'history', 'coordinatorState', 'coordinatorHistory', 'coordinatorBarriers'], render: renderGate },
|
|
22
|
+
initiative: { fetch: ['coordinatorState', 'coordinatorBarriers', 'barrierLedger'], render: renderInitiative },
|
|
23
|
+
'cross-repo': { fetch: ['coordinatorState', 'coordinatorHistory'], render: renderCrossRepo },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const API_MAP = {
|
|
27
|
+
state: '/api/state',
|
|
28
|
+
history: '/api/history',
|
|
29
|
+
ledger: '/api/ledger',
|
|
30
|
+
audit: '/api/hooks/audit',
|
|
31
|
+
annotations: '/api/hooks/annotations',
|
|
32
|
+
coordinatorState: '/api/coordinator/state',
|
|
33
|
+
coordinatorHistory: '/api/coordinator/history',
|
|
34
|
+
coordinatorBarriers: '/api/coordinator/barriers',
|
|
35
|
+
barrierLedger: '/api/coordinator/barrier-ledger',
|
|
36
|
+
coordinatorAudit: '/api/coordinator/hooks/audit',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const viewState = {
|
|
40
|
+
ledger: {
|
|
41
|
+
agent: 'all',
|
|
42
|
+
query: '',
|
|
43
|
+
phase: 'all',
|
|
44
|
+
dateFrom: '',
|
|
45
|
+
dateTo: '',
|
|
46
|
+
},
|
|
47
|
+
hooks: {
|
|
48
|
+
phase: 'all',
|
|
49
|
+
verdict: 'all',
|
|
50
|
+
hookName: 'all',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let activeViewName = null;
|
|
55
|
+
let activeViewData = null;
|
|
56
|
+
|
|
57
|
+
function escapeHtml(str) {
|
|
58
|
+
if (str == null) return '';
|
|
59
|
+
return String(str)
|
|
60
|
+
.replace(/&/g, '&')
|
|
61
|
+
.replace(/</g, '<')
|
|
62
|
+
.replace(/>/g, '>')
|
|
63
|
+
.replace(/"/g, '"')
|
|
64
|
+
.replace(/'/g, ''');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function fetchData(keys) {
|
|
68
|
+
const results = {};
|
|
69
|
+
await Promise.all(keys.map(async (key) => {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(API_MAP[key]);
|
|
72
|
+
results[key] = res.ok ? await res.json() : null;
|
|
73
|
+
} catch {
|
|
74
|
+
results[key] = null;
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function currentView() {
|
|
81
|
+
return (location.hash || '#timeline').slice(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function pickInitialView() {
|
|
85
|
+
if (location.hash) {
|
|
86
|
+
return currentView();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [stateResult, coordinatorResult] = await Promise.all([
|
|
90
|
+
fetchData(['state']).catch(() => ({ state: null })),
|
|
91
|
+
fetchData(['coordinatorState']).catch(() => ({ coordinatorState: null })),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
if (!stateResult.state && coordinatorResult.coordinatorState) {
|
|
95
|
+
location.hash = '#initiative';
|
|
96
|
+
return 'initiative';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return currentView();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildRenderData(viewName, data) {
|
|
103
|
+
if (viewName === 'ledger') {
|
|
104
|
+
return {
|
|
105
|
+
...data,
|
|
106
|
+
filter: viewState.ledger,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (viewName === 'hooks') {
|
|
110
|
+
return {
|
|
111
|
+
...data,
|
|
112
|
+
filter: viewState.hooks,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderView(viewName, data) {
|
|
119
|
+
const container = document.getElementById('view-container');
|
|
120
|
+
const view = VIEWS[viewName];
|
|
121
|
+
if (!view) {
|
|
122
|
+
container.innerHTML = `<div class="placeholder"><h2>Unknown View</h2><p>View "${escapeHtml(viewName)}" not found.</p></div>`;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
container.innerHTML = view.render(buildRenderData(viewName, data));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function loadView(viewName, { refresh = true } = {}) {
|
|
130
|
+
const view = VIEWS[viewName];
|
|
131
|
+
if (!view) {
|
|
132
|
+
renderView(viewName, null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const shouldRefetch = refresh || activeViewName !== viewName || !activeViewData;
|
|
137
|
+
activeViewName = viewName;
|
|
138
|
+
if (shouldRefetch) {
|
|
139
|
+
activeViewData = await fetchData(view.fetch);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
renderView(viewName, activeViewData);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── WebSocket connection ──────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
let ws = null;
|
|
148
|
+
let reconnectDelay = 1000;
|
|
149
|
+
|
|
150
|
+
function connect() {
|
|
151
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
152
|
+
ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
|
153
|
+
|
|
154
|
+
const statusDot = document.getElementById('ws-status');
|
|
155
|
+
const statusLabel = document.getElementById('ws-label');
|
|
156
|
+
|
|
157
|
+
ws.onopen = () => {
|
|
158
|
+
statusDot.classList.remove('disconnected');
|
|
159
|
+
statusLabel.textContent = 'Connected';
|
|
160
|
+
reconnectDelay = 1000;
|
|
161
|
+
loadView(currentView());
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
ws.onmessage = (event) => {
|
|
165
|
+
try {
|
|
166
|
+
const msg = JSON.parse(event.data);
|
|
167
|
+
if (msg.type === 'invalidate') {
|
|
168
|
+
loadView(currentView());
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
ws.onclose = () => {
|
|
174
|
+
statusDot.classList.add('disconnected');
|
|
175
|
+
statusLabel.textContent = 'Disconnected';
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
178
|
+
connect();
|
|
179
|
+
}, reconnectDelay);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
ws.onerror = () => ws.close();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Router ─────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function updateNav() {
|
|
188
|
+
const view = currentView();
|
|
189
|
+
document.querySelectorAll('nav a').forEach(a => {
|
|
190
|
+
a.classList.toggle('active', a.getAttribute('href') === '#' + view);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
window.addEventListener('hashchange', () => {
|
|
195
|
+
updateNav();
|
|
196
|
+
loadView(currentView());
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
document.addEventListener('input', (event) => {
|
|
200
|
+
const view = currentView();
|
|
201
|
+
if (!activeViewData) return;
|
|
202
|
+
|
|
203
|
+
const control = event.target?.dataset?.viewControl;
|
|
204
|
+
if (!control) return;
|
|
205
|
+
|
|
206
|
+
if (view === 'ledger') {
|
|
207
|
+
if (control === 'ledger-query') {
|
|
208
|
+
viewState.ledger.query = event.target.value;
|
|
209
|
+
renderView('ledger', activeViewData);
|
|
210
|
+
} else if (control === 'ledger-date-from') {
|
|
211
|
+
viewState.ledger.dateFrom = event.target.value;
|
|
212
|
+
renderView('ledger', activeViewData);
|
|
213
|
+
} else if (control === 'ledger-date-to') {
|
|
214
|
+
viewState.ledger.dateTo = event.target.value;
|
|
215
|
+
renderView('ledger', activeViewData);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
document.addEventListener('change', (event) => {
|
|
221
|
+
const view = currentView();
|
|
222
|
+
if (!activeViewData) return;
|
|
223
|
+
|
|
224
|
+
const control = event.target?.dataset?.viewControl;
|
|
225
|
+
if (!control) return;
|
|
226
|
+
|
|
227
|
+
if (view === 'ledger') {
|
|
228
|
+
if (control === 'ledger-agent') {
|
|
229
|
+
viewState.ledger.agent = event.target.value;
|
|
230
|
+
renderView('ledger', activeViewData);
|
|
231
|
+
} else if (control === 'ledger-phase') {
|
|
232
|
+
viewState.ledger.phase = event.target.value;
|
|
233
|
+
renderView('ledger', activeViewData);
|
|
234
|
+
} else if (control === 'ledger-date-from') {
|
|
235
|
+
viewState.ledger.dateFrom = event.target.value;
|
|
236
|
+
renderView('ledger', activeViewData);
|
|
237
|
+
} else if (control === 'ledger-date-to') {
|
|
238
|
+
viewState.ledger.dateTo = event.target.value;
|
|
239
|
+
renderView('ledger', activeViewData);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (view === 'hooks') {
|
|
244
|
+
if (control === 'hooks-phase') {
|
|
245
|
+
viewState.hooks.phase = event.target.value;
|
|
246
|
+
renderView('hooks', activeViewData);
|
|
247
|
+
} else if (control === 'hooks-verdict') {
|
|
248
|
+
viewState.hooks.verdict = event.target.value;
|
|
249
|
+
renderView('hooks', activeViewData);
|
|
250
|
+
} else if (control === 'hooks-hookname') {
|
|
251
|
+
viewState.hooks.hookName = event.target.value;
|
|
252
|
+
renderView('hooks', activeViewData);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ── Turn expand toggle ──────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
document.addEventListener('click', (event) => {
|
|
260
|
+
const turnCard = event.target.closest('[data-turn-expand]');
|
|
261
|
+
if (!turnCard) return;
|
|
262
|
+
// Don't toggle if clicking inside the detail panel itself
|
|
263
|
+
if (event.target.closest('.turn-detail-panel')) return;
|
|
264
|
+
turnCard.toggleAttribute('data-expanded');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── Copy to clipboard ────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
document.addEventListener('click', (event) => {
|
|
270
|
+
const target = event.target.closest('[data-copy]');
|
|
271
|
+
if (!target) return;
|
|
272
|
+
|
|
273
|
+
const text = target.getAttribute('data-copy');
|
|
274
|
+
if (!text) return;
|
|
275
|
+
|
|
276
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
277
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
278
|
+
target.classList.add('copied');
|
|
279
|
+
target.setAttribute('data-copied', 'Copied!');
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
target.classList.remove('copied');
|
|
282
|
+
target.removeAttribute('data-copied');
|
|
283
|
+
}, 1500);
|
|
284
|
+
}).catch(() => {
|
|
285
|
+
fallbackSelect(target);
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
fallbackSelect(target);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
function fallbackSelect(el) {
|
|
293
|
+
const range = document.createRange();
|
|
294
|
+
range.selectNodeContents(el);
|
|
295
|
+
const sel = window.getSelection();
|
|
296
|
+
sel.removeAllRanges();
|
|
297
|
+
sel.addRange(range);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
pickInitialView().finally(() => {
|
|
303
|
+
updateNav();
|
|
304
|
+
connect();
|
|
305
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocked State view — renders current blocked state with recovery info.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data, returns HTML string. Testable in Node.js.
|
|
5
|
+
* Per DEC-DASH-002: read-only. Shows copyable CLI recovery commands, no write actions.
|
|
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 getHookPhase(entry) {
|
|
19
|
+
return entry?.hook_phase || entry?.phase || '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getHookName(entry) {
|
|
23
|
+
return entry?.hook_name || entry?.hook || entry?.name || '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selectRelevantAuditEntries(state, audit) {
|
|
27
|
+
if (!Array.isArray(audit) || audit.length === 0) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const blocked = state?.blocked_reason || state?.blocked_state || {};
|
|
32
|
+
const blockedOn = String(state?.blocked_on || '');
|
|
33
|
+
const hookMatch = blockedOn.match(/^hook:([^:]+):(.+)$/);
|
|
34
|
+
|
|
35
|
+
if (hookMatch) {
|
|
36
|
+
const [, hookPhase, hookName] = hookMatch;
|
|
37
|
+
const matchingEntries = audit.filter((entry) => (
|
|
38
|
+
getHookPhase(entry) === hookPhase && getHookName(entry) === hookName
|
|
39
|
+
));
|
|
40
|
+
if (matchingEntries.length > 0) {
|
|
41
|
+
return matchingEntries.slice(-3);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const category = String(blocked.category || blocked.reason || '').toLowerCase();
|
|
46
|
+
if (category.includes('validation') || blockedOn.startsWith('validator:')) {
|
|
47
|
+
const validationEntries = audit.filter((entry) => (
|
|
48
|
+
getHookPhase(entry).includes('validation')
|
|
49
|
+
));
|
|
50
|
+
if (validationEntries.length > 0) {
|
|
51
|
+
return validationEntries.slice(-3);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return audit.slice(-3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function render({ state, audit = [], coordinatorState = null, coordinatorAudit = [] }) {
|
|
59
|
+
const activeState = state?.status === 'blocked' ? state : coordinatorState;
|
|
60
|
+
const activeAudit = activeState === state ? audit : coordinatorAudit;
|
|
61
|
+
const isCoordinator = activeState === coordinatorState;
|
|
62
|
+
|
|
63
|
+
if (!activeState || activeState.status !== 'blocked') {
|
|
64
|
+
return `<div class="placeholder"><h2>Blocked State</h2><p>Run is not currently blocked.</p></div>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const blocked = activeState.blocked_reason || activeState.blocked_state || {};
|
|
68
|
+
const recovery = blocked.recovery || {};
|
|
69
|
+
const reason = blocked.category || blocked.reason || activeState.blocked_on || blocked || 'Unknown';
|
|
70
|
+
const detail = recovery.detail || blocked.detail || null;
|
|
71
|
+
const recoveryAction = recovery.recovery_action || blocked.recovery_action || blocked.recovery_command || null;
|
|
72
|
+
const blockedBy = activeState.blocked_on || blocked.blocked_by || blocked.source || null;
|
|
73
|
+
const turnId = blocked.turn_id || null;
|
|
74
|
+
const owner = recovery.owner || null;
|
|
75
|
+
const typedReason = recovery.typed_reason || null;
|
|
76
|
+
const turnRetained = typeof recovery.turn_retained === 'boolean' ? recovery.turn_retained : null;
|
|
77
|
+
const blockedAt = blocked.blocked_at || null;
|
|
78
|
+
const relevantAudit = selectRelevantAuditEntries(activeState, activeAudit);
|
|
79
|
+
|
|
80
|
+
let html = `<div class="blocked-view">
|
|
81
|
+
<div class="blocked-banner">
|
|
82
|
+
<div class="blocked-icon">BLOCKED</div>
|
|
83
|
+
<div class="blocked-reason">${esc(reason)}</div>
|
|
84
|
+
</div>`;
|
|
85
|
+
|
|
86
|
+
// Metadata
|
|
87
|
+
html += `<div class="section"><h3>Block Details</h3><dl class="detail-list">`;
|
|
88
|
+
if (blockedBy) html += `<dt>Blocked By</dt><dd>${esc(blockedBy)}</dd>`;
|
|
89
|
+
if (typedReason) html += `<dt>Recovery Type</dt><dd>${esc(typedReason)}</dd>`;
|
|
90
|
+
if (owner) html += `<dt>Owner</dt><dd>${esc(owner)}</dd>`;
|
|
91
|
+
if (turnId) html += `<dt>Turn</dt><dd class="mono">${esc(turnId)}</dd>`;
|
|
92
|
+
if (blocked.phase) html += `<dt>Phase</dt><dd>${esc(blocked.phase)}</dd>`;
|
|
93
|
+
if (blocked.hook) html += `<dt>Hook</dt><dd class="mono">${esc(blocked.hook)}</dd>`;
|
|
94
|
+
if (detail) html += `<dt>Detail</dt><dd>${esc(detail)}</dd>`;
|
|
95
|
+
if (turnRetained != null) html += `<dt>Turn Retained</dt><dd>${turnRetained ? 'yes' : 'no'}</dd>`;
|
|
96
|
+
if (blockedAt) html += `<dt>Blocked At</dt><dd class="mono">${esc(blockedAt)}</dd>`;
|
|
97
|
+
html += `</dl></div>`;
|
|
98
|
+
|
|
99
|
+
// Recovery command
|
|
100
|
+
if (recoveryAction) {
|
|
101
|
+
html += `<div class="section"><h3>Recovery</h3>
|
|
102
|
+
<p class="recovery-hint">Run this command to recover:</p>
|
|
103
|
+
<pre class="recovery-command mono" data-copy="${esc(recoveryAction)}">${esc(recoveryAction)}</pre>
|
|
104
|
+
</div>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isCoordinator && activeState.pending_gate) {
|
|
108
|
+
html += `<div class="section"><h3>Pending Gate</h3>
|
|
109
|
+
<dl class="detail-list">
|
|
110
|
+
<dt>Gate</dt><dd class="mono">${esc(activeState.pending_gate.gate || '-')}</dd>
|
|
111
|
+
<dt>Type</dt><dd>${esc(activeState.pending_gate.gate_type || '-')}</dd>
|
|
112
|
+
</dl>
|
|
113
|
+
<pre class="recovery-command mono" data-copy="agentxchain multi approve-gate">agentxchain multi approve-gate</pre>
|
|
114
|
+
</div>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isCoordinator && activeState.repo_runs && Object.keys(activeState.repo_runs).length > 0) {
|
|
118
|
+
html += `<div class="section"><h3>Repo Status</h3><div class="annotation-list">`;
|
|
119
|
+
for (const [repoId, repoRun] of Object.entries(activeState.repo_runs)) {
|
|
120
|
+
html += `<div class="annotation-card">
|
|
121
|
+
<span class="mono">${esc(repoId)}</span>
|
|
122
|
+
<span>${esc(`${repoRun.status || 'unknown'}${repoRun.phase ? ` [${repoRun.phase}]` : ''}`)}</span>
|
|
123
|
+
</div>`;
|
|
124
|
+
}
|
|
125
|
+
html += `</div></div>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (relevantAudit.length > 0) {
|
|
129
|
+
html += `<div class="section"><h3>Recent Audit Context</h3><div class="annotation-list">`;
|
|
130
|
+
for (const entry of relevantAudit) {
|
|
131
|
+
const duration = entry.duration_ms != null ? `${entry.duration_ms}ms` : '-';
|
|
132
|
+
const verdict = entry.verdict || 'unknown';
|
|
133
|
+
const action = entry.orchestrator_action || entry.action || 'continued';
|
|
134
|
+
html += `<div class="annotation-card">
|
|
135
|
+
<span class="mono">${esc(getHookPhase(entry) || '-')}</span>
|
|
136
|
+
<span class="mono">${esc(getHookName(entry) || '-')}</span>
|
|
137
|
+
<span>${esc(`${verdict} -> ${action} (${duration})`)}</span>
|
|
138
|
+
</div>`;
|
|
139
|
+
}
|
|
140
|
+
html += `</div></div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
html += `</div>`;
|
|
144
|
+
return html;
|
|
145
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 hashColor(seed) {
|
|
12
|
+
const palette = ['#38bdf8', '#fb7185', '#34d399', '#f59e0b', '#a78bfa', '#f97316'];
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (const char of String(seed || 'repo')) {
|
|
15
|
+
hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0;
|
|
16
|
+
}
|
|
17
|
+
return palette[Math.abs(hash) % palette.length];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function badge(label, color) {
|
|
21
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function describeEvent(entry) {
|
|
25
|
+
switch (entry?.type) {
|
|
26
|
+
case 'run_initialized':
|
|
27
|
+
return {
|
|
28
|
+
title: 'Coordinator Initialized',
|
|
29
|
+
detail: `${Object.keys(entry.repo_runs || {}).length} repo runs linked or initialized`,
|
|
30
|
+
};
|
|
31
|
+
case 'turn_dispatched':
|
|
32
|
+
return {
|
|
33
|
+
title: 'Turn Dispatched',
|
|
34
|
+
detail: `${entry.role || 'agent'} dispatched to ${entry.repo_id} for ${entry.workstream_id}`,
|
|
35
|
+
};
|
|
36
|
+
case 'acceptance_projection':
|
|
37
|
+
return {
|
|
38
|
+
title: 'Acceptance Projected',
|
|
39
|
+
detail: entry.summary || `${entry.repo_id} accepted ${entry.repo_turn_id || 'a turn'}`,
|
|
40
|
+
};
|
|
41
|
+
case 'context_generated':
|
|
42
|
+
return {
|
|
43
|
+
title: 'Context Generated',
|
|
44
|
+
detail: `${entry.target_repo_id} received cross-repo context from ${(entry.upstream_repo_ids || []).join(', ') || 'no upstream repos'}`,
|
|
45
|
+
};
|
|
46
|
+
case 'phase_transition_requested':
|
|
47
|
+
return {
|
|
48
|
+
title: 'Phase Gate Requested',
|
|
49
|
+
detail: `${entry.from || 'unknown'} -> ${entry.to || 'unknown'} (${entry.gate || 'gate'})`,
|
|
50
|
+
};
|
|
51
|
+
case 'phase_transition_approved':
|
|
52
|
+
return {
|
|
53
|
+
title: 'Phase Gate Approved',
|
|
54
|
+
detail: `${entry.from || 'unknown'} -> ${entry.to || 'unknown'}`,
|
|
55
|
+
};
|
|
56
|
+
case 'run_completion_requested':
|
|
57
|
+
return {
|
|
58
|
+
title: 'Completion Gate Requested',
|
|
59
|
+
detail: entry.gate || 'initiative_ship',
|
|
60
|
+
};
|
|
61
|
+
case 'run_completed':
|
|
62
|
+
return {
|
|
63
|
+
title: 'Initiative Completed',
|
|
64
|
+
detail: entry.gate || 'completion approved',
|
|
65
|
+
};
|
|
66
|
+
case 'state_resynced':
|
|
67
|
+
return {
|
|
68
|
+
title: 'Coordinator Resynced',
|
|
69
|
+
detail: `${(entry.resynced_repos || []).length} repos updated`,
|
|
70
|
+
};
|
|
71
|
+
default:
|
|
72
|
+
return {
|
|
73
|
+
title: entry?.type || 'Unknown Event',
|
|
74
|
+
detail: entry?.repo_id || entry?.workstream_id || 'Coordinator history event',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function render({ coordinatorState, coordinatorHistory = [] }) {
|
|
80
|
+
if (!coordinatorState) {
|
|
81
|
+
return `<div class="placeholder"><h2>No Cross-Repo Timeline</h2><p>No coordinator run found. Start one with <code class="mono">agentxchain multi init</code></p></div>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const events = Array.isArray(coordinatorHistory) ? [...coordinatorHistory].reverse() : [];
|
|
85
|
+
if (events.length === 0) {
|
|
86
|
+
return `<div class="placeholder"><h2>Cross-Repo Timeline</h2><p>No coordinator history recorded yet.</p></div>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let html = `<div class="timeline-view"><div class="section"><h3>Cross-Repo Timeline</h3><div class="turn-list">`;
|
|
90
|
+
for (const entry of events) {
|
|
91
|
+
const event = describeEvent(entry);
|
|
92
|
+
const repoId = entry.repo_id || entry.target_repo_id || null;
|
|
93
|
+
const workstreamId = entry.workstream_id || null;
|
|
94
|
+
const repoColor = hashColor(repoId || workstreamId || entry.type);
|
|
95
|
+
|
|
96
|
+
html += `<div class="turn-card">
|
|
97
|
+
<div class="turn-header">
|
|
98
|
+
<strong>${esc(event.title)}</strong>`;
|
|
99
|
+
if (repoId) {
|
|
100
|
+
html += badge(repoId, repoColor);
|
|
101
|
+
}
|
|
102
|
+
if (workstreamId) {
|
|
103
|
+
html += badge(workstreamId, 'var(--accent)');
|
|
104
|
+
}
|
|
105
|
+
if (entry.timestamp) {
|
|
106
|
+
html += `<span class="turn-status mono">${esc(entry.timestamp)}</span>`;
|
|
107
|
+
}
|
|
108
|
+
html += `</div>
|
|
109
|
+
<div class="turn-summary">${esc(event.detail)}</div>`;
|
|
110
|
+
|
|
111
|
+
if (entry.repo_turn_id) {
|
|
112
|
+
html += `<div class="turn-detail"><span class="detail-label">Repo Turn:</span> <span class="mono">${esc(entry.repo_turn_id)}</span></div>`;
|
|
113
|
+
}
|
|
114
|
+
if (entry.context_ref) {
|
|
115
|
+
html += `<div class="turn-detail"><span class="detail-label">Context Ref:</span> <span class="mono">${esc(entry.context_ref)}</span></div>`;
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(entry.barrier_changes) && entry.barrier_changes.length > 0) {
|
|
118
|
+
html += `<div class="turn-detail"><span class="detail-label">Barrier Changes:</span><ul>${entry.barrier_changes.map((change) => (
|
|
119
|
+
`<li>${esc(`${change.barrier_id}: ${change.previous_status} -> ${change.new_status}`)}</li>`
|
|
120
|
+
)).join('')}</ul></div>`;
|
|
121
|
+
}
|
|
122
|
+
html += `</div>`;
|
|
123
|
+
}
|
|
124
|
+
html += `</div></div></div>`;
|
|
125
|
+
return html;
|
|
126
|
+
}
|