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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Ledger view — renders the decision-ledger.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
|
+
export function filterEntries(ledger, filters = {}) {
|
|
18
|
+
if (!Array.isArray(ledger)) return [];
|
|
19
|
+
|
|
20
|
+
const query = String(filters.query || '').trim().toLowerCase();
|
|
21
|
+
const agent = String(filters.agent || 'all').trim().toLowerCase();
|
|
22
|
+
const phase = String(filters.phase || 'all').trim().toLowerCase();
|
|
23
|
+
const dateFrom = filters.dateFrom || '';
|
|
24
|
+
const dateTo = filters.dateTo || '';
|
|
25
|
+
|
|
26
|
+
return ledger.filter((entry) => {
|
|
27
|
+
const entryAgent = String(entry.agent || entry.role || '').trim().toLowerCase();
|
|
28
|
+
const decision = String(entry.decision || entry.summary || '').trim().toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (agent !== 'all' && entryAgent !== agent) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (phase !== 'all') {
|
|
35
|
+
const entryPhase = String(entry.phase || '').trim().toLowerCase();
|
|
36
|
+
if (entryPhase !== phase) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (dateFrom && entry.timestamp) {
|
|
42
|
+
if (entry.timestamp < dateFrom) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (dateTo && entry.timestamp) {
|
|
48
|
+
if (entry.timestamp > dateTo) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!query) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entryAgent.includes(query) || decision.includes(query);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectAgents(ledger) {
|
|
62
|
+
const unique = new Set();
|
|
63
|
+
for (const entry of ledger) {
|
|
64
|
+
const agent = entry.agent || entry.role;
|
|
65
|
+
if (agent) unique.add(agent);
|
|
66
|
+
}
|
|
67
|
+
return Array.from(unique).sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectPhases(ledger) {
|
|
71
|
+
const unique = new Set();
|
|
72
|
+
for (const entry of ledger) {
|
|
73
|
+
if (entry.phase) unique.add(entry.phase);
|
|
74
|
+
}
|
|
75
|
+
return Array.from(unique).sort();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasObjections(entry) {
|
|
79
|
+
return Array.isArray(entry.objections) && entry.objections.length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function render({ ledger, filter = {} }) {
|
|
83
|
+
if (!ledger || ledger.length === 0) {
|
|
84
|
+
return `<div class="placeholder"><h2>Decision Ledger</h2><p>No decisions recorded yet.</p></div>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filtered = filterEntries(ledger, filter);
|
|
88
|
+
const selectedAgent = filter.agent || 'all';
|
|
89
|
+
const selectedPhase = filter.phase || 'all';
|
|
90
|
+
const dateFrom = filter.dateFrom || '';
|
|
91
|
+
const dateTo = filter.dateTo || '';
|
|
92
|
+
const query = filter.query || '';
|
|
93
|
+
const agents = collectAgents(ledger);
|
|
94
|
+
const phases = collectPhases(ledger);
|
|
95
|
+
|
|
96
|
+
let html = `<div class="ledger-view">
|
|
97
|
+
<div class="section"><h3>Decision Ledger</h3>
|
|
98
|
+
<p class="section-subtitle">${filtered.length} of ${ledger.length} decision${ledger.length !== 1 ? 's' : ''} shown</p>
|
|
99
|
+
<div class="filter-bar">
|
|
100
|
+
<label class="filter-control">
|
|
101
|
+
<span>Agent</span>
|
|
102
|
+
<select data-view-control="ledger-agent">
|
|
103
|
+
<option value="all"${selectedAgent === 'all' ? ' selected' : ''}>All roles</option>
|
|
104
|
+
${agents.map((agent) => `<option value="${esc(agent)}"${selectedAgent === agent ? ' selected' : ''}>${esc(agent)}</option>`).join('')}
|
|
105
|
+
</select>
|
|
106
|
+
</label>
|
|
107
|
+
<label class="filter-control">
|
|
108
|
+
<span>Phase</span>
|
|
109
|
+
<select data-view-control="ledger-phase">
|
|
110
|
+
<option value="all"${selectedPhase === 'all' ? ' selected' : ''}>All phases</option>
|
|
111
|
+
${phases.map((p) => `<option value="${esc(p)}"${selectedPhase === p ? ' selected' : ''}>${esc(p)}</option>`).join('')}
|
|
112
|
+
</select>
|
|
113
|
+
</label>
|
|
114
|
+
<label class="filter-control">
|
|
115
|
+
<span>Search</span>
|
|
116
|
+
<input
|
|
117
|
+
type="search"
|
|
118
|
+
data-view-control="ledger-query"
|
|
119
|
+
value="${esc(query)}"
|
|
120
|
+
placeholder="Filter by role or decision"
|
|
121
|
+
autocomplete="off"
|
|
122
|
+
>
|
|
123
|
+
</label>
|
|
124
|
+
<label class="filter-control">
|
|
125
|
+
<span>From</span>
|
|
126
|
+
<input
|
|
127
|
+
type="date"
|
|
128
|
+
data-view-control="ledger-date-from"
|
|
129
|
+
value="${esc(dateFrom)}"
|
|
130
|
+
autocomplete="off"
|
|
131
|
+
>
|
|
132
|
+
</label>
|
|
133
|
+
<label class="filter-control">
|
|
134
|
+
<span>To</span>
|
|
135
|
+
<input
|
|
136
|
+
type="date"
|
|
137
|
+
data-view-control="ledger-date-to"
|
|
138
|
+
value="${esc(dateTo)}"
|
|
139
|
+
autocomplete="off"
|
|
140
|
+
>
|
|
141
|
+
</label>
|
|
142
|
+
</div>
|
|
143
|
+
<table class="data-table">
|
|
144
|
+
<thead><tr><th>Turn</th><th>Agent</th><th>Decision</th><th>Timestamp</th></tr></thead>
|
|
145
|
+
<tbody>`;
|
|
146
|
+
|
|
147
|
+
if (filtered.length === 0) {
|
|
148
|
+
html += `<tr><td colspan="4">No decisions match the current filters.</td></tr>`;
|
|
149
|
+
} else {
|
|
150
|
+
for (const entry of filtered) {
|
|
151
|
+
const objectionBadge = hasObjections(entry)
|
|
152
|
+
? ' <span class="objection-badge">objection</span>'
|
|
153
|
+
: '';
|
|
154
|
+
html += `<tr>
|
|
155
|
+
<td class="mono">${esc(String(entry.turn ?? entry.turn_id ?? ''))}</td>
|
|
156
|
+
<td>${esc(entry.agent || entry.role || '')}${objectionBadge}</td>
|
|
157
|
+
<td>${esc(entry.decision || entry.summary || '')}</td>
|
|
158
|
+
<td class="mono">${esc(entry.timestamp || '')}</td>
|
|
159
|
+
</tr>`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
html += `</tbody></table></div></div>`;
|
|
164
|
+
return html;
|
|
165
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline view — renders the governed run state + turn history.
|
|
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 getRole(entry) {
|
|
18
|
+
return entry?.assigned_role || entry?.role || entry?.agent || 'unknown';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getFiles(entry) {
|
|
22
|
+
if (Array.isArray(entry?.observed_artifact?.files_changed) && entry.observed_artifact.files_changed.length > 0) {
|
|
23
|
+
return entry.observed_artifact.files_changed;
|
|
24
|
+
}
|
|
25
|
+
return Array.isArray(entry?.files_changed) ? entry.files_changed : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatItem(item, type) {
|
|
29
|
+
if (typeof item === 'string') {
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
if (!item || typeof item !== 'object') {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (type === 'decision') {
|
|
37
|
+
const label = item.id ? `${item.id}: ` : '';
|
|
38
|
+
return `${label}${item.statement || item.summary || ''}`.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (type === 'objection') {
|
|
42
|
+
const prefix = item.id ? `${item.id}` : 'Objection';
|
|
43
|
+
const severity = item.severity ? ` (${item.severity})` : '';
|
|
44
|
+
return `${prefix}${severity}: ${item.statement || item.summary || ''}`.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return item.statement || item.summary || item.detail || '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderList(items, type) {
|
|
51
|
+
return items
|
|
52
|
+
.map((item) => formatItem(item, type))
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map((item) => `<li>${esc(item)}</li>`)
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function statusBadge(status) {
|
|
59
|
+
const colors = {
|
|
60
|
+
running: 'var(--green)',
|
|
61
|
+
assigned: 'var(--green)',
|
|
62
|
+
retrying: 'var(--yellow)',
|
|
63
|
+
conflicted: 'var(--red)',
|
|
64
|
+
paused: 'var(--yellow)',
|
|
65
|
+
blocked: 'var(--red)',
|
|
66
|
+
completed: 'var(--accent)',
|
|
67
|
+
idle: 'var(--text-dim)',
|
|
68
|
+
};
|
|
69
|
+
const color = colors[status] || 'var(--text-dim)';
|
|
70
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(status)}</span>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function roleBadge(role) {
|
|
74
|
+
const colors = {
|
|
75
|
+
pm: '#a78bfa',
|
|
76
|
+
dev: '#38bdf8',
|
|
77
|
+
qa: '#f472b6',
|
|
78
|
+
'eng-director': '#fb923c',
|
|
79
|
+
};
|
|
80
|
+
const color = colors[role] || 'var(--text-dim)';
|
|
81
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(role)}</span>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderTurnDetailPanel(turnId, annotations, audit) {
|
|
85
|
+
const turnAnnotations = Array.isArray(annotations)
|
|
86
|
+
? annotations.filter((a) => a.turn_id === turnId)
|
|
87
|
+
: [];
|
|
88
|
+
const turnAudit = Array.isArray(audit)
|
|
89
|
+
? audit.filter((a) => a.turn_id === turnId)
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
if (turnAnnotations.length === 0 && turnAudit.length === 0) {
|
|
93
|
+
return `<div class="turn-detail-panel"><p class="turn-detail">No hook evidence for this turn.</p></div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let html = `<div class="turn-detail-panel">`;
|
|
97
|
+
|
|
98
|
+
if (turnAudit.length > 0) {
|
|
99
|
+
html += `<div class="turn-detail"><span class="detail-label">Hook Audit (${turnAudit.length}):</span>
|
|
100
|
+
<table class="data-table">
|
|
101
|
+
<thead><tr><th>Phase</th><th>Hook</th><th>Verdict</th></tr></thead>
|
|
102
|
+
<tbody>`;
|
|
103
|
+
for (const entry of turnAudit) {
|
|
104
|
+
const phase = entry.hook_phase || entry.phase || '';
|
|
105
|
+
const hook = entry.hook_name || entry.hook || entry.name || '';
|
|
106
|
+
html += `<tr>
|
|
107
|
+
<td class="mono">${esc(phase)}</td>
|
|
108
|
+
<td>${esc(hook)}</td>
|
|
109
|
+
<td>${esc(entry.verdict || '')}</td>
|
|
110
|
+
</tr>`;
|
|
111
|
+
}
|
|
112
|
+
html += `</tbody></table></div>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (turnAnnotations.length > 0) {
|
|
116
|
+
html += `<div class="turn-detail"><span class="detail-label">Annotations (${turnAnnotations.length}):</span><ul>`;
|
|
117
|
+
for (const ann of turnAnnotations) {
|
|
118
|
+
const hookName = ann.hook_name || ann.hook || ann.name || '';
|
|
119
|
+
if (Array.isArray(ann.annotations)) {
|
|
120
|
+
for (const a of ann.annotations) {
|
|
121
|
+
html += `<li>${esc(hookName)}: ${esc(a.key || '')} = ${esc(a.value || '')}</li>`;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const text = ann.annotation || ann.message || '';
|
|
125
|
+
html += `<li>${esc(hookName)}: ${esc(text)}</li>`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
html += `</ul></div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
html += `</div>`;
|
|
132
|
+
return html;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function render({ state, history, annotations, audit }) {
|
|
136
|
+
if (!state) {
|
|
137
|
+
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>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const turnCount = Array.isArray(history) ? history.length : 0;
|
|
141
|
+
const activeTurns = state.active_turns ? Object.values(state.active_turns) : [];
|
|
142
|
+
|
|
143
|
+
let html = `<div class="timeline-view">`;
|
|
144
|
+
|
|
145
|
+
// Run header
|
|
146
|
+
html += `<div class="run-header">
|
|
147
|
+
<div class="run-meta">
|
|
148
|
+
<span class="mono run-id">${esc(state.run_id)}</span>
|
|
149
|
+
${statusBadge(state.status)}
|
|
150
|
+
<span class="phase-label">Phase: <strong>${esc(state.phase)}</strong></span>
|
|
151
|
+
<span class="turn-count">${turnCount} turn${turnCount !== 1 ? 's' : ''} completed</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>`;
|
|
154
|
+
|
|
155
|
+
// Active turns
|
|
156
|
+
if (activeTurns.length > 0) {
|
|
157
|
+
html += `<div class="section"><h3>Active Turns</h3><div class="turn-list">`;
|
|
158
|
+
for (const turn of activeTurns) {
|
|
159
|
+
html += `<div class="turn-card active">
|
|
160
|
+
<div class="turn-header">
|
|
161
|
+
${roleBadge(getRole(turn))}
|
|
162
|
+
<span class="mono">${esc(turn.turn_id)}</span>
|
|
163
|
+
<span class="turn-status">${esc(turn.status || 'assigned')}</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>`;
|
|
166
|
+
}
|
|
167
|
+
html += `</div></div>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Completed turns (from history, newest first)
|
|
171
|
+
if (turnCount > 0) {
|
|
172
|
+
html += `<div class="section"><h3>Turn History</h3><div class="turn-list">`;
|
|
173
|
+
const reversed = [...history].reverse();
|
|
174
|
+
for (const entry of reversed) {
|
|
175
|
+
const files = getFiles(entry);
|
|
176
|
+
const decisions = Array.isArray(entry.decisions) ? entry.decisions : [];
|
|
177
|
+
const objections = Array.isArray(entry.objections) ? entry.objections : [];
|
|
178
|
+
const risks = Array.isArray(entry.risks) ? entry.risks : [];
|
|
179
|
+
const verificationSummary = entry.normalized_verification?.evidence_summary
|
|
180
|
+
|| entry.verification?.evidence_summary
|
|
181
|
+
|| null;
|
|
182
|
+
|
|
183
|
+
html += `<div class="turn-card" data-turn-expand="${esc(entry.turn_id)}">
|
|
184
|
+
<div class="turn-header">
|
|
185
|
+
${roleBadge(getRole(entry))}
|
|
186
|
+
<span class="mono">${esc(entry.turn_id)}</span>
|
|
187
|
+
</div>`;
|
|
188
|
+
|
|
189
|
+
if (entry.summary) {
|
|
190
|
+
html += `<div class="turn-summary">${esc(entry.summary)}</div>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (files.length > 0) {
|
|
194
|
+
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${files.map(f => esc(f)).join(', ')}</span></div>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (decisions.length > 0) {
|
|
198
|
+
html += `<div class="turn-detail"><span class="detail-label">Decisions:</span><ul>${renderList(decisions, 'decision')}</ul></div>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (objections.length > 0) {
|
|
202
|
+
html += `<div class="turn-detail objections"><span class="detail-label">Objections:</span><ul>${renderList(objections, 'objection')}</ul></div>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (risks.length > 0) {
|
|
206
|
+
html += `<div class="turn-detail risks"><span class="detail-label">Risks:</span><ul>${renderList(risks, 'risk')}</ul></div>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (verificationSummary) {
|
|
210
|
+
html += `<div class="turn-detail"><span class="detail-label">Verification:</span> ${esc(verificationSummary)}</div>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
html += renderTurnDetailPanel(entry.turn_id, annotations, audit);
|
|
214
|
+
|
|
215
|
+
html += `</div>`;
|
|
216
|
+
}
|
|
217
|
+
html += `</div></div>`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
html += `</div>`;
|
|
221
|
+
return html;
|
|
222
|
+
}
|