@yemi33/minions 0.1.2424 → 0.1.2425
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
|
@@ -6,31 +6,94 @@
|
|
|
6
6
|
return node;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
function chip(label, value) {
|
|
10
|
+
const span = el('span', 'kb-item-meta-chip');
|
|
11
|
+
span.textContent = value == null || value === '' ? label : label + ': ' + value;
|
|
12
|
+
return span;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Filter controls ────────────────────────────────────────────────────────
|
|
16
|
+
function buildControls() {
|
|
13
17
|
const controls = el('div');
|
|
14
|
-
controls.style.cssText = 'display:flex;gap:8px;margin-bottom:12px';
|
|
18
|
+
controls.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px';
|
|
19
|
+
|
|
15
20
|
const input = el('input');
|
|
16
21
|
input.id = 'memory-search-query';
|
|
17
22
|
input.placeholder = 'Search facts, episodes, paths, or lessons';
|
|
18
|
-
input.style.cssText = 'flex:1;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)';
|
|
23
|
+
input.style.cssText = 'flex:1;min-width:220px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)';
|
|
24
|
+
input.addEventListener('keydown', event => { if (event.key === 'Enter') search(); });
|
|
25
|
+
|
|
19
26
|
const status = el('select');
|
|
20
27
|
status.id = 'memory-search-status';
|
|
21
|
-
for (const value of ['active', 'superseded', 'retracted', 'expired']) {
|
|
28
|
+
for (const value of ['active', 'candidate', 'superseded', 'retracted', 'expired']) {
|
|
22
29
|
const option = el('option', '', value);
|
|
23
30
|
option.value = value;
|
|
24
31
|
status.appendChild(option);
|
|
25
32
|
}
|
|
33
|
+
|
|
26
34
|
const button = el('button', 'btn-primary-lg', 'Search');
|
|
27
35
|
button.addEventListener('click', search);
|
|
28
|
-
input.addEventListener('keydown', event => { if (event.key === 'Enter') search(); });
|
|
29
36
|
controls.append(input, status, button);
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
|
|
38
|
+
// Optional diagnostics filters (P-1ed2fb5f). Blank values are omitted from
|
|
39
|
+
// the request so an unset filter never narrows the search.
|
|
40
|
+
const filters = el('div');
|
|
41
|
+
filters.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;width:100%';
|
|
42
|
+
for (const [id, placeholder] of [
|
|
43
|
+
['memory-filter-project', 'project'],
|
|
44
|
+
['memory-filter-author', 'author'],
|
|
45
|
+
['memory-filter-audience', 'audience (author|project-team)'],
|
|
46
|
+
['memory-filter-sourceType', 'source type'],
|
|
47
|
+
['memory-filter-findingKey', 'finding key (project|pr|source)'],
|
|
48
|
+
]) {
|
|
49
|
+
const field = el('input');
|
|
50
|
+
field.id = id;
|
|
51
|
+
field.placeholder = placeholder;
|
|
52
|
+
field.style.cssText = 'flex:1;min-width:150px;padding:6px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-base)';
|
|
53
|
+
field.addEventListener('keydown', event => { if (event.key === 'Enter') search(); });
|
|
54
|
+
filters.appendChild(field);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const wrap = el('div');
|
|
58
|
+
wrap.append(controls, filters, buildDecisionLookup());
|
|
59
|
+
return wrap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildDecisionLookup() {
|
|
63
|
+
const row = el('div');
|
|
64
|
+
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;width:100%;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)';
|
|
65
|
+
const label = el('span', '', 'Why was a lesson selected/excluded?');
|
|
66
|
+
label.style.cssText = 'align-self:center;font-size:var(--text-base);color:var(--text-dim)';
|
|
67
|
+
const wi = el('input');
|
|
68
|
+
wi.id = 'memory-decision-wi';
|
|
69
|
+
wi.placeholder = 'work item id';
|
|
70
|
+
wi.style.cssText = 'flex:1;min-width:150px;padding:6px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-base)';
|
|
71
|
+
const dispatch = el('input');
|
|
72
|
+
dispatch.id = 'memory-decision-dispatch';
|
|
73
|
+
dispatch.placeholder = 'dispatch id';
|
|
74
|
+
dispatch.style.cssText = wi.style.cssText;
|
|
75
|
+
const go = el('button', 'pr-pager-btn', 'Trace decision');
|
|
76
|
+
go.addEventListener('click', lookupDecision);
|
|
77
|
+
row.append(label, wi, dispatch, go);
|
|
78
|
+
return row;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function open() {
|
|
82
|
+
document.getElementById('modal-title').textContent = 'Structured Memory';
|
|
83
|
+
const body = document.getElementById('modal-body');
|
|
84
|
+
body.textContent = '';
|
|
85
|
+
body.append(buildControls());
|
|
86
|
+
const results = el('div', 'empty', 'Searches structured memory across all scopes. Results include provenance, lifecycle, and promotion state.');
|
|
87
|
+
results.id = 'memory-search-results';
|
|
88
|
+
body.appendChild(results);
|
|
32
89
|
document.getElementById('modal').classList.add('open');
|
|
33
|
-
input.
|
|
90
|
+
const input = document.getElementById('memory-search-query');
|
|
91
|
+
if (input) input.focus();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function filterValue(id) {
|
|
95
|
+
const node = document.getElementById(id);
|
|
96
|
+
return node && node.value ? node.value.trim() : '';
|
|
34
97
|
}
|
|
35
98
|
|
|
36
99
|
async function search() {
|
|
@@ -39,15 +102,30 @@
|
|
|
39
102
|
const results = document.getElementById('memory-search-results');
|
|
40
103
|
const query = input && input.value.trim();
|
|
41
104
|
if (!query || !results) return;
|
|
105
|
+
results.className = '';
|
|
42
106
|
results.textContent = 'Searching...';
|
|
107
|
+
const params = new URLSearchParams({
|
|
108
|
+
q: query,
|
|
109
|
+
status: status ? status.value : 'active',
|
|
110
|
+
limit: '30',
|
|
111
|
+
});
|
|
112
|
+
for (const [id, key] of [
|
|
113
|
+
['memory-filter-project', 'project'],
|
|
114
|
+
['memory-filter-author', 'author'],
|
|
115
|
+
['memory-filter-audience', 'audience'],
|
|
116
|
+
['memory-filter-sourceType', 'sourceType'],
|
|
117
|
+
['memory-filter-findingKey', 'findingKey'],
|
|
118
|
+
]) {
|
|
119
|
+
const value = filterValue(id);
|
|
120
|
+
if (value) params.set(key, value);
|
|
121
|
+
}
|
|
43
122
|
try {
|
|
44
|
-
const
|
|
45
|
-
+ '&status=' + encodeURIComponent(status ? status.value : 'active') + '&limit=30';
|
|
46
|
-
const response = await fetch(url);
|
|
123
|
+
const response = await fetch('/api/memory-records/search?' + params.toString());
|
|
47
124
|
const data = await response.json();
|
|
48
125
|
if (!response.ok) throw new Error(data.error || 'search failed');
|
|
49
126
|
render(data.records || []);
|
|
50
127
|
} catch (error) {
|
|
128
|
+
results.className = 'empty';
|
|
51
129
|
results.textContent = 'Memory search failed: ' + error.message;
|
|
52
130
|
}
|
|
53
131
|
}
|
|
@@ -61,35 +139,293 @@
|
|
|
61
139
|
results.textContent = 'No matching memory records.';
|
|
62
140
|
return;
|
|
63
141
|
}
|
|
64
|
-
for (const record of records)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
for (const record of records) results.appendChild(buildRecordCard(record));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Promotion eligibility (P-1ed2fb5f): an unresolved candidate that has not yet
|
|
146
|
+
// been promoted or expired is eligible for promotion to an active lesson.
|
|
147
|
+
function promotionEligibility(record) {
|
|
148
|
+
const state = (record.metadata && record.metadata.candidateState) || null;
|
|
149
|
+
if (record.status !== 'candidate') {
|
|
150
|
+
return { eligible: false, label: 'not a candidate', state };
|
|
151
|
+
}
|
|
152
|
+
if (state === 'promoted' || state === 'expired') {
|
|
153
|
+
return { eligible: false, label: state, state };
|
|
154
|
+
}
|
|
155
|
+
return { eligible: true, label: 'promotion-eligible', state: state || 'unresolved' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildRecordCard(record) {
|
|
159
|
+
const meta = record.metadata || {};
|
|
160
|
+
const item = el('div', 'kb-item');
|
|
161
|
+
item.style.marginBottom = '8px';
|
|
162
|
+
const body = el('div', 'kb-item-body');
|
|
163
|
+
|
|
164
|
+
// Title + state badge.
|
|
165
|
+
const header = el('div', 'kb-item-title');
|
|
166
|
+
header.textContent = record.title || record.id;
|
|
167
|
+
const state = el('span', 'badge');
|
|
168
|
+
state.textContent = record.status;
|
|
169
|
+
state.style.cssText = 'margin-left:8px;font-size:var(--text-sm)';
|
|
170
|
+
header.appendChild(state);
|
|
171
|
+
body.appendChild(header);
|
|
172
|
+
|
|
173
|
+
// Provenance + confidence + facets metadata row.
|
|
174
|
+
const metaRow = el('div', 'kb-item-meta');
|
|
175
|
+
metaRow.appendChild(chip('type', record.memoryType));
|
|
176
|
+
metaRow.appendChild(chip('scope', record.scopeType + (record.scopeKey ? ':' + record.scopeKey : '')));
|
|
177
|
+
metaRow.appendChild(chip('trust', record.trust));
|
|
178
|
+
if (Number.isFinite(record.confidence)) metaRow.appendChild(chip('confidence', record.confidence.toFixed(2)));
|
|
179
|
+
const provenance = record.sourceRef || record.sourcePath || record.sourceType;
|
|
180
|
+
if (provenance) metaRow.appendChild(chip('provenance', provenance));
|
|
181
|
+
if (meta.audience) metaRow.appendChild(chip('audience', meta.audience));
|
|
182
|
+
if (meta.area) metaRow.appendChild(chip('area', meta.area));
|
|
183
|
+
if (meta.disposition) metaRow.appendChild(chip('disposition', meta.disposition));
|
|
184
|
+
if (Number.isFinite(record.ftsRank)) metaRow.appendChild(chip('rank', record.ftsRank.toFixed(2)));
|
|
185
|
+
body.appendChild(metaRow);
|
|
186
|
+
|
|
187
|
+
// Facets (bounded).
|
|
188
|
+
const facets = Array.isArray(meta.facets) ? meta.facets.slice(0, 12) : [];
|
|
189
|
+
if (facets.length) {
|
|
190
|
+
const facetRow = el('div', 'kb-item-meta');
|
|
191
|
+
for (const facet of facets) facetRow.appendChild(chip('facet', String(facet)));
|
|
192
|
+
body.appendChild(facetRow);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Promotion eligibility.
|
|
196
|
+
const eligibility = promotionEligibility(record);
|
|
197
|
+
const eligibilityRow = el('div', 'kb-item-meta');
|
|
198
|
+
const eligibilityChip = el('span', 'kb-item-meta-chip', eligibility.label);
|
|
199
|
+
if (eligibility.eligible) eligibilityChip.style.color = 'var(--good, #3fb950)';
|
|
200
|
+
eligibilityRow.appendChild(eligibilityChip);
|
|
201
|
+
if (eligibility.state) eligibilityRow.appendChild(chip('candidate-state', eligibility.state));
|
|
202
|
+
body.appendChild(eligibilityRow);
|
|
203
|
+
|
|
204
|
+
body.appendChild(el('div', 'kb-item-preview',
|
|
205
|
+
(record.body || '') + (record.bodyTruncated ? '\n\n[preview truncated]' : '')));
|
|
206
|
+
|
|
207
|
+
// Actions: existing pin/retract/restore + on-demand lifecycle timeline.
|
|
208
|
+
const actions = el('div');
|
|
209
|
+
actions.style.cssText = 'display:flex;gap:6px;margin-top:8px;flex-wrap:wrap';
|
|
210
|
+
for (const action of ['pin', record.status === 'active' ? 'retract' : 'restore']) {
|
|
211
|
+
const button = el('button', 'pr-pager-btn', action);
|
|
212
|
+
button.addEventListener('click', () => update(record.id, action));
|
|
213
|
+
actions.appendChild(button);
|
|
214
|
+
}
|
|
215
|
+
const timeline = el('div');
|
|
216
|
+
const lifecycleBtn = el('button', 'pr-pager-btn', 'lifecycle');
|
|
217
|
+
lifecycleBtn.addEventListener('click', () => loadLifecycle(record, timeline));
|
|
218
|
+
actions.appendChild(lifecycleBtn);
|
|
219
|
+
|
|
220
|
+
// Human-approved repository promotion (P-192d3660). Only surfaced for active
|
|
221
|
+
// review lessons the server marks eligible (high-confidence + >= 2 distinct
|
|
222
|
+
// valid findings). The action queues a reviewed work item; it never edits a
|
|
223
|
+
// checkout. `record.repoPromotion` is distinct from the candidate->lesson
|
|
224
|
+
// `promotionEligibility()` heuristic above.
|
|
225
|
+
const repo = record.repoPromotion;
|
|
226
|
+
const promoteHost = el('div');
|
|
227
|
+
if (repo && repo.eligible) {
|
|
228
|
+
const promoteBtn = el('button', 'btn-primary-lg', 'Promote to repo');
|
|
229
|
+
promoteBtn.style.cssText = 'padding:2px 10px;font-size:var(--text-sm)';
|
|
230
|
+
promoteBtn.addEventListener('click', () => togglePromoteForm(record, promoteHost));
|
|
231
|
+
actions.appendChild(promoteBtn);
|
|
232
|
+
} else if (repo && !repo.eligible && Array.isArray(repo.reasons) && repo.reasons.length) {
|
|
233
|
+
const reasonChip = chip('repo-promotion', repo.reasons.join(', '));
|
|
234
|
+
reasonChip.style.color = 'var(--text-dim)';
|
|
235
|
+
actions.appendChild(reasonChip);
|
|
236
|
+
}
|
|
237
|
+
body.appendChild(actions);
|
|
238
|
+
body.appendChild(promoteHost);
|
|
239
|
+
body.appendChild(timeline);
|
|
240
|
+
|
|
241
|
+
item.appendChild(body);
|
|
242
|
+
return item;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Repository promotion form (active lesson -> project skill / instruction) ─
|
|
246
|
+
function togglePromoteForm(record, host) {
|
|
247
|
+
if (host.firstChild) { host.textContent = ''; return; }
|
|
248
|
+
const form = el('div');
|
|
249
|
+
form.style.cssText = 'border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;margin-top:6px;display:flex;flex-direction:column;gap:6px';
|
|
250
|
+
form.appendChild(el('div', 'kb-item-title', 'Promote lesson to repository guidance'));
|
|
251
|
+
|
|
252
|
+
const kind = el('select');
|
|
253
|
+
for (const [value, label] of [['project-skill', 'Project skill'], ['repository-instruction', 'Repository instruction file']]) {
|
|
254
|
+
const option = el('option', '', label);
|
|
255
|
+
option.value = value;
|
|
256
|
+
kind.appendChild(option);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const project = el('input');
|
|
260
|
+
project.value = record.scopeKey || '';
|
|
261
|
+
project.placeholder = 'project';
|
|
262
|
+
|
|
263
|
+
const targetPath = el('input');
|
|
264
|
+
const pathHint = el('div', 'kb-item-meta');
|
|
265
|
+
function refreshHint() {
|
|
266
|
+
pathHint.textContent = '';
|
|
267
|
+
const isSkill = kind.value === 'project-skill';
|
|
268
|
+
targetPath.placeholder = isSkill ? 'skill-slug (e.g. verify-branch-before-fix)' : 'CLAUDE.md';
|
|
269
|
+
pathHint.appendChild(chip('', isSkill
|
|
270
|
+
? 'Creates <slug>/SKILL.md via a reviewed PR.'
|
|
271
|
+
: 'Allowed: CLAUDE.md, AGENTS.md (any dir), .github/copilot-instructions.md'));
|
|
272
|
+
}
|
|
273
|
+
kind.addEventListener('change', refreshHint);
|
|
274
|
+
refreshHint();
|
|
275
|
+
|
|
276
|
+
for (const [labelText, control] of [['Target', kind], ['Project', project], ['Path', targetPath]]) {
|
|
277
|
+
const row = el('div');
|
|
278
|
+
row.style.cssText = 'display:flex;gap:8px;align-items:center';
|
|
279
|
+
const label = el('label', '', labelText);
|
|
280
|
+
label.style.cssText = 'width:64px;font-size:var(--text-sm);color:var(--text-dim)';
|
|
281
|
+
control.style.cssText = 'flex:1;padding:6px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text)';
|
|
282
|
+
row.append(label, control);
|
|
283
|
+
form.appendChild(row);
|
|
284
|
+
}
|
|
285
|
+
form.appendChild(pathHint);
|
|
286
|
+
|
|
287
|
+
const confirm = el('button', 'btn-primary-lg', 'Confirm promotion');
|
|
288
|
+
confirm.style.cssText = 'align-self:flex-start;padding:4px 12px;font-size:var(--text-sm)';
|
|
289
|
+
confirm.addEventListener('click', () => submitPromotion(record.id, {
|
|
290
|
+
targetKind: kind.value,
|
|
291
|
+
project: project.value.trim(),
|
|
292
|
+
path: targetPath.value.trim(),
|
|
293
|
+
}, host));
|
|
294
|
+
form.appendChild(confirm);
|
|
295
|
+
host.appendChild(form);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function submitPromotion(id, payload, host) {
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetch('/api/memory-records/' + encodeURIComponent(id) + '/promote', {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
body: JSON.stringify(payload),
|
|
304
|
+
});
|
|
305
|
+
const data = await response.json();
|
|
306
|
+
if (!response.ok) throw new Error(data.error || 'promotion failed');
|
|
307
|
+
const message = data.duplicate
|
|
308
|
+
? 'Promotion already queued as work item ' + data.workItemId
|
|
309
|
+
: 'Queued promotion work item ' + data.workItemId;
|
|
310
|
+
showToast('kb-sweep-toast', message, true);
|
|
311
|
+
host.textContent = '';
|
|
312
|
+
} catch (error) {
|
|
313
|
+
showToast('kb-sweep-toast', 'Promotion failed: ' + error.message, false);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Lifecycle counters + timeline (on-demand per record) ───────────────────
|
|
318
|
+
async function loadLifecycle(record, container) {
|
|
319
|
+
container.textContent = 'Loading lifecycle...';
|
|
320
|
+
const meta = record.metadata || {};
|
|
321
|
+
const params = new URLSearchParams({ limit: '50' });
|
|
322
|
+
const findingKey = record.sourceType === 'review-learning'
|
|
323
|
+
? (record.sourceRef || meta.findingKey || '') : (meta.findingKey || '');
|
|
324
|
+
if (findingKey) params.set('findingKey', findingKey);
|
|
325
|
+
else params.set('memoryId', record.id);
|
|
326
|
+
try {
|
|
327
|
+
const response = await fetch('/api/memory-records/lifecycle?' + params.toString());
|
|
328
|
+
const data = await response.json();
|
|
329
|
+
if (!response.ok) throw new Error(data.error || 'lifecycle failed');
|
|
330
|
+
container.textContent = '';
|
|
331
|
+
container.appendChild(buildLifecycleSection(data.summary || { counters: {}, total: 0 }, data.events || []));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
container.textContent = 'Lifecycle failed: ' + error.message;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildLifecycleSection(summary, events) {
|
|
338
|
+
const section = el('div');
|
|
339
|
+
section.style.cssText = 'margin-top:8px;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm)';
|
|
340
|
+
const counters = el('div', 'kb-item-meta');
|
|
341
|
+
counters.appendChild(chip('events', String(summary.total || 0)));
|
|
342
|
+
for (const type of Object.keys(summary.counters || {})) {
|
|
343
|
+
counters.appendChild(chip(type, String(summary.counters[type])));
|
|
344
|
+
}
|
|
345
|
+
section.appendChild(counters);
|
|
346
|
+
if (!events.length) {
|
|
347
|
+
section.appendChild(el('div', 'kb-item-preview', 'No lifecycle events recorded.'));
|
|
348
|
+
return section;
|
|
349
|
+
}
|
|
350
|
+
const list = el('div');
|
|
351
|
+
for (const event of events.slice(0, 50)) {
|
|
352
|
+
const line = el('div', 'kb-item-meta');
|
|
353
|
+
line.appendChild(chip('when', new Date(event.createdAt).toISOString()));
|
|
354
|
+
line.appendChild(chip('event', event.eventType));
|
|
355
|
+
if (event.disposition) line.appendChild(chip('disposition', event.disposition));
|
|
356
|
+
if (event.agent) line.appendChild(chip('agent', event.agent));
|
|
357
|
+
if (event.workItemId) line.appendChild(chip('wi', event.workItemId));
|
|
358
|
+
list.appendChild(line);
|
|
359
|
+
}
|
|
360
|
+
section.appendChild(list);
|
|
361
|
+
return section;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Retrieval decision (why selected / excluded) ───────────────────────────
|
|
365
|
+
async function lookupDecision() {
|
|
366
|
+
const results = document.getElementById('memory-search-results');
|
|
367
|
+
if (!results) return;
|
|
368
|
+
const wi = filterValue('memory-decision-wi');
|
|
369
|
+
const dispatch = filterValue('memory-decision-dispatch');
|
|
370
|
+
if (!wi && !dispatch) {
|
|
371
|
+
results.className = 'empty';
|
|
372
|
+
results.textContent = 'Enter a work item id or dispatch id to trace a retrieval decision.';
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
if (dispatch) params.set('dispatchId', dispatch);
|
|
377
|
+
if (wi) params.set('workItemId', wi);
|
|
378
|
+
results.className = '';
|
|
379
|
+
results.textContent = 'Tracing decision...';
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch('/api/memory-records/retrieval-decision?' + params.toString());
|
|
382
|
+
const data = await response.json();
|
|
383
|
+
if (!response.ok) throw new Error(data.error || 'decision lookup failed');
|
|
384
|
+
results.textContent = '';
|
|
385
|
+
results.appendChild(buildDecisionSection(data.trace));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
results.className = 'empty';
|
|
388
|
+
results.textContent = 'Decision lookup failed: ' + error.message;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function buildDecisionSection(trace) {
|
|
393
|
+
const section = el('div', 'kb-item');
|
|
394
|
+
const body = el('div', 'kb-item-body');
|
|
395
|
+
if (!trace) {
|
|
396
|
+
body.appendChild(el('div', 'empty', 'No retrieval decision recorded for that work item or dispatch.'));
|
|
397
|
+
section.appendChild(body);
|
|
398
|
+
return section;
|
|
399
|
+
}
|
|
400
|
+
body.appendChild(el('div', 'kb-item-title', 'Retrieval decision'));
|
|
401
|
+
const metaRow = el('div', 'kb-item-meta');
|
|
402
|
+
metaRow.appendChild(chip('kind', trace.retrievalKind));
|
|
403
|
+
if (trace.project) metaRow.appendChild(chip('project', trace.project));
|
|
404
|
+
if (trace.workItemId) metaRow.appendChild(chip('wi', trace.workItemId));
|
|
405
|
+
if (trace.dispatchId) metaRow.appendChild(chip('dispatch', trace.dispatchId));
|
|
406
|
+
metaRow.appendChild(chip('candidates', String(trace.candidateCount)));
|
|
407
|
+
metaRow.appendChild(chip('selected', String(trace.selectedCount)));
|
|
408
|
+
if (trace.shadow) metaRow.appendChild(chip('shadow', 'yes'));
|
|
409
|
+
body.appendChild(metaRow);
|
|
410
|
+
|
|
411
|
+
const entries = Array.isArray(trace.decisionTrace) ? trace.decisionTrace : [];
|
|
412
|
+
if (!entries.length) {
|
|
413
|
+
body.appendChild(el('div', 'kb-item-preview', 'No per-candidate decision entries.'));
|
|
414
|
+
}
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
const line = el('div', 'kb-item-meta');
|
|
417
|
+
const verdict = el('span', 'kb-item-meta-chip', entry.selected ? 'selected' : 'excluded');
|
|
418
|
+
verdict.style.color = entry.selected ? 'var(--good, #3fb950)' : 'var(--text-dim)';
|
|
419
|
+
line.appendChild(verdict);
|
|
420
|
+
if (entry.id) line.appendChild(chip('id', entry.id));
|
|
421
|
+
if (Number.isFinite(entry.rank)) line.appendChild(chip('rank', String(entry.rank)));
|
|
422
|
+
if (Number.isFinite(entry.score)) line.appendChild(chip('score', entry.score.toFixed(3)));
|
|
423
|
+
const reasons = Array.isArray(entry.reasons) ? entry.reasons : [];
|
|
424
|
+
for (const reason of reasons) line.appendChild(chip('reason', reason));
|
|
425
|
+
body.appendChild(line);
|
|
92
426
|
}
|
|
427
|
+
section.appendChild(body);
|
|
428
|
+
return section;
|
|
93
429
|
}
|
|
94
430
|
|
|
95
431
|
async function update(id, action) {
|
|
@@ -108,5 +444,14 @@
|
|
|
108
444
|
}
|
|
109
445
|
}
|
|
110
446
|
|
|
111
|
-
window.MinionsMemorySearch = {
|
|
447
|
+
window.MinionsMemorySearch = {
|
|
448
|
+
open,
|
|
449
|
+
// Exposed for renderer/DOM-safety tests (P-1ed2fb5f).
|
|
450
|
+
buildRecordCard,
|
|
451
|
+
buildLifecycleSection,
|
|
452
|
+
buildDecisionSection,
|
|
453
|
+
promotionEligibility,
|
|
454
|
+
// Repository promotion (P-192d3660).
|
|
455
|
+
togglePromoteForm,
|
|
456
|
+
};
|
|
112
457
|
})();
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -301,12 +301,12 @@ function _qaBuildQueuedHtml(message) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
function _qaBuildLoadingHtml(loadingId, queueCount) {
|
|
304
|
-
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:var(--text-
|
|
304
|
+
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:var(--text-sm);color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
|
|
305
305
|
return '<div class="modal-qa-loading" id="' + loadingId + '">' +
|
|
306
306
|
'<div class="dot-pulse"><span></span><span></span><span></span></div> ' +
|
|
307
307
|
'<span id="' + loadingId + '-text">Thinking...</span> ' +
|
|
308
308
|
'<span id="' + loadingId + '-time" style="font-size:var(--text-sm);color:var(--muted)"></span>' +
|
|
309
|
-
' <button onclick="qaAbort()" style="font-size:var(--text-
|
|
309
|
+
' <button onclick="qaAbort()" style="font-size:var(--text-sm);padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
|
|
310
310
|
qaQueueBadge + '</div>';
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -357,7 +357,7 @@ function _qaBuildAssistantHtml(text, opts) {
|
|
|
357
357
|
return '<div class="modal-qa-a" style="' + style + '">' +
|
|
358
358
|
(opts?.isError ? '' : llmCopyBtn()) +
|
|
359
359
|
body +
|
|
360
|
-
'<div style="font-size:var(--text-
|
|
360
|
+
'<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px;text-align:right;' + pad + '">' + opts.elapsed + 's</div>' +
|
|
361
361
|
'</div>';
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -392,7 +392,7 @@ function _qaBuildActionFeedbackHtml(actionFeedback) {
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, segments, queueCount) {
|
|
395
|
-
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:var(--text-
|
|
395
|
+
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:var(--text-sm);color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
|
|
396
396
|
// Wrap in a column-flex container so the interleaved chain-of-thought (tool
|
|
397
397
|
// calls + partial text in arrival order, via ccSegmentsRender) stacks on top
|
|
398
398
|
// and the progress block sits at the bottom. Overrides the parent
|
|
@@ -405,7 +405,7 @@ function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, segments, qu
|
|
|
405
405
|
'<span class="dot-pulse"><span></span><span></span><span></span></span>' +
|
|
406
406
|
'<span id="' + loadingId + '-text">' + escHtml(label) + '</span>' +
|
|
407
407
|
'<span id="' + loadingId + '-time" style="font-size:var(--text-sm);color:var(--muted)">' + elapsedSeconds + 's</span>' +
|
|
408
|
-
'<button onclick="qaAbort()" style="font-size:var(--text-
|
|
408
|
+
'<button onclick="qaAbort()" style="font-size:var(--text-sm);padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
|
|
409
409
|
qaQueueBadge +
|
|
410
410
|
'</div>';
|
|
411
411
|
html += '</div>';
|
package/dashboard/js/qa.js
CHANGED
|
@@ -223,7 +223,7 @@ async function loadQaRunbooks() {
|
|
|
223
223
|
return '<div class="qa-runbook-row">' +
|
|
224
224
|
'<div class="qa-runbook-main">' +
|
|
225
225
|
'<div class="qa-runbook-name">' + escHtml(rb.name || id) + '</div>' +
|
|
226
|
-
'<div class="qa-runbook-meta">target: <code>' + escHtml(rb.target || '?') + '</code> · ' +
|
|
226
|
+
'<div class="qa-runbook-meta">target: <code>' + escHtml(rb.targetName || rb.target || '?') + '</code> · ' +
|
|
227
227
|
stepsCount + ' step' + (stepsCount === 1 ? '' : 's') + ' · ' +
|
|
228
228
|
artifactsCount + ' expected artifact' + (artifactsCount === 1 ? '' : 's') +
|
|
229
229
|
'</div>' +
|
|
@@ -267,12 +267,17 @@ async function qaRunRunbook(id) {
|
|
|
267
267
|
if (!id) return;
|
|
268
268
|
const rb = _qaRunbooksCache.find(function (r) { return (r.id || r.name) === id; });
|
|
269
269
|
const label = rb ? (rb.name || id) : id;
|
|
270
|
+
const targetName = rb ? (rb.targetName || rb.target || '') : '';
|
|
271
|
+
if (!targetName) {
|
|
272
|
+
alert('Cannot dispatch runbook "' + label + '": targetName is missing.');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
270
275
|
if (!confirm('Dispatch a validation agent for runbook "' + label + '"?')) return;
|
|
271
276
|
try {
|
|
272
277
|
const res = await fetch('/api/qa/runbooks/run', {
|
|
273
278
|
method: 'POST',
|
|
274
279
|
headers: { 'Content-Type': 'application/json' },
|
|
275
|
-
body: JSON.stringify({ id }),
|
|
280
|
+
body: JSON.stringify({ runbookId: id, targetName }),
|
|
276
281
|
});
|
|
277
282
|
if (!res.ok) {
|
|
278
283
|
const txt = await res.text().catch(() => '');
|
|
@@ -295,7 +300,9 @@ async function loadQaRuns() {
|
|
|
295
300
|
const res = await fetch('/api/qa/runs?limit=20');
|
|
296
301
|
if (res.ok) {
|
|
297
302
|
const data = await res.json();
|
|
298
|
-
|
|
303
|
+
if (data && Array.isArray(data.runs)) items = data.runs;
|
|
304
|
+
else if (data && Array.isArray(data.items)) items = data.items;
|
|
305
|
+
else if (Array.isArray(data)) items = data;
|
|
299
306
|
} else if (res.status !== 404) {
|
|
300
307
|
err = new Error('HTTP ' + res.status);
|
|
301
308
|
}
|
|
@@ -326,8 +333,8 @@ function _qaRenderRunRow(run) {
|
|
|
326
333
|
const id = run.id || run.runId || '';
|
|
327
334
|
const status = run.status || 'unknown';
|
|
328
335
|
const statusClass = 'qa-status-' + status.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
329
|
-
const runbook = run.runbookName || run.runbook || '';
|
|
330
|
-
const target = run.target || '';
|
|
336
|
+
const runbook = run.runbookId || run.runbookName || run.runbook || '';
|
|
337
|
+
const target = run.targetName || run.target || '';
|
|
331
338
|
const ts = run.startedAt || run.createdAt || '';
|
|
332
339
|
const workItemId = run.workItemId || run.workItem || '';
|
|
333
340
|
const agentId = run.agentId || '';
|
|
@@ -423,39 +430,44 @@ function _qaRenderArtifactPreviews(runId, artifacts) {
|
|
|
423
430
|
if (!runId || !Array.isArray(artifacts) || !artifacts.length) return '';
|
|
424
431
|
const parts = [];
|
|
425
432
|
for (const a of artifacts) {
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
// keeps the URL pattern observable in source-inspection tests.
|
|
430
|
-
const fileRaw = (typeof a === 'string') ? a : (a && (a.file || a.name || a.path) || '');
|
|
433
|
+
// Canonical records use { path, type }; retain the retired { file, kind }
|
|
434
|
+
// shape for older persisted runs.
|
|
435
|
+
const fileRaw = (typeof a === 'string') ? a : (a && (a.path || a.file || a.name) || '');
|
|
431
436
|
if (!fileRaw) continue;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
437
|
+
let artifactPath = String(fileRaw).replace(/\\/g, '/');
|
|
438
|
+
const rootedPrefix = '/engine/qa-artifacts/' + runId + '/';
|
|
439
|
+
const rootedPath = artifactPath.startsWith('/') ? artifactPath : '/' + artifactPath;
|
|
440
|
+
const rootedIndex = rootedPath.indexOf(rootedPrefix);
|
|
441
|
+
if (rootedIndex >= 0) artifactPath = rootedPath.slice(rootedIndex + rootedPrefix.length);
|
|
442
|
+
const pathParts = artifactPath.split('/');
|
|
443
|
+
if (!pathParts.length || pathParts.some(p => !p || p === '.' || p === '..' || p.includes('..'))) continue;
|
|
444
|
+
const file = pathParts[pathParts.length - 1];
|
|
445
|
+
const typeRaw = (typeof a === 'object' && a) ? (a.type || a.kind || '') : '';
|
|
446
|
+
const typeHint = String(typeRaw).toLowerCase();
|
|
435
447
|
const ext = (file.match(/\.([a-z0-9]+)$/i) || ['', ''])[1].toLowerCase();
|
|
436
|
-
const url = '/api/qa/artifacts/' + encodeURIComponent(runId) + '/' + encodeURIComponent(
|
|
437
|
-
if (
|
|
448
|
+
const url = '/api/qa/artifacts/' + encodeURIComponent(runId) + '/' + encodeURIComponent(artifactPath);
|
|
449
|
+
if (typeHint === 'screenshot' || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {
|
|
438
450
|
parts.push(
|
|
439
451
|
'<div class="qa-artifact qa-artifact-image">' +
|
|
440
|
-
'<img src="' + escHtml(url) + '" alt="' + escHtml(
|
|
441
|
-
'<div class="qa-artifact-caption">' + escHtml(
|
|
452
|
+
'<img src="' + escHtml(url) + '" alt="' + escHtml(artifactPath) + '" loading="lazy">' +
|
|
453
|
+
'<div class="qa-artifact-caption">' + escHtml(artifactPath) + '</div>' +
|
|
442
454
|
'</div>'
|
|
443
455
|
);
|
|
444
|
-
} else if (
|
|
456
|
+
} else if (typeHint === 'video' || ['mp4', 'webm', 'ogg', 'mov'].includes(ext)) {
|
|
445
457
|
parts.push(
|
|
446
458
|
'<div class="qa-artifact qa-artifact-video">' +
|
|
447
459
|
'<video controls src="' + escHtml(url) + '"></video>' +
|
|
448
|
-
'<div class="qa-artifact-caption">' + escHtml(
|
|
460
|
+
'<div class="qa-artifact-caption">' + escHtml(artifactPath) + '</div>' +
|
|
449
461
|
'</div>'
|
|
450
462
|
);
|
|
451
463
|
} else {
|
|
452
464
|
// Log / text — render an asynchronously-filled <pre>; first 40 lines
|
|
453
465
|
// fetched lazily so we don't N+1-flood the network on every poll.
|
|
454
|
-
const blockId = 'qa-log-' + runId + '-' +
|
|
466
|
+
const blockId = 'qa-log-' + runId + '-' + artifactPath.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
455
467
|
parts.push(
|
|
456
468
|
'<div class="qa-artifact qa-artifact-log">' +
|
|
457
469
|
'<div class="qa-artifact-caption">' +
|
|
458
|
-
escHtml(
|
|
470
|
+
escHtml(artifactPath) + ' ' +
|
|
459
471
|
'<a href="' + escHtml(url) + '" target="_blank" rel="noopener" class="qa-artifact-fulllink">View full</a>' +
|
|
460
472
|
'</div>' +
|
|
461
473
|
'<pre id="' + escHtml(blockId) + '" class="qa-artifact-log-preview" data-qa-log-url="' + escHtml(url) + '">Loading…</pre>' +
|
|
@@ -851,11 +863,12 @@ function _qaSummarizeTarget(target) {
|
|
|
851
863
|
|
|
852
864
|
async function _qaSessionPost(url, label, body) {
|
|
853
865
|
try {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
headers
|
|
857
|
-
body
|
|
858
|
-
}
|
|
866
|
+
const options = { method: 'POST' };
|
|
867
|
+
if (body !== undefined) {
|
|
868
|
+
options.headers = { 'Content-Type': 'application/json' };
|
|
869
|
+
options.body = JSON.stringify(body);
|
|
870
|
+
}
|
|
871
|
+
const res = await fetch(url, options);
|
|
859
872
|
const json = await res.json().catch(() => ({}));
|
|
860
873
|
if (!res.ok) {
|
|
861
874
|
alert(label + ' failed (' + res.status + '): ' + (json.error || 'unknown'));
|