karajan-code 3.0.0 → 3.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 +14 -6
- package/docs/README.es.md +1 -1
- package/package.json +8 -2
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +21 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +462 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +273 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +93 -2
- package/packages/hu-board/src/format.js +79 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +43 -10
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +4 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/audit-test-diet.mjs +151 -0
- package/scripts/git-hooks/post-merge +13 -2
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/brain/standby-scheduler.js +10 -92
- package/src/brain/standby-store.js +12 -168
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/runner.js +10 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +36 -2
- package/src/cli/register-pipeline.js +3 -0
- package/src/cli/register-plan.js +10 -1
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/commands/sync.js +30 -2
- package/src/config/defaults.js +5 -0
- package/src/config/schema.js +2 -0
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/hu-sub-pipeline.js +52 -8
- package/src/orchestrator/stages/coder-stage.js +8 -2
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/plan/plan-hu-ops.js +16 -338
- package/src/plan/plan-id.js +4 -45
- package/src/plan/plan-validation.js +4 -82
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/rag/vec-store.js +17 -253
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +11 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/cli-ask-question.js +76 -0
- package/src/utils/doc-links.js +2 -0
- package/src/utils/os-detect.js +10 -0
- package/src/utils/paths.js +14 -111
- package/src/utils/port-check.js +6 -45
- package/src/utils/process.js +4 -188
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/run-registry.js +9 -116
- package/src/utils/shared-paths.js +12 -41
- package/src/utils/squeezr-detect.js +18 -0
- package/src/utils/squeezr-install.js +37 -0
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 5/8 — Board view (kanban, columns, story card, result helpers).
|
|
2
|
+
//
|
|
3
|
+
// Classic script. Loaded by index.html after utils/sessions-view.js, before
|
|
4
|
+
// app.js. Function declarations hoist to the global scope so app.js (and
|
|
5
|
+
// inline onclick handlers) can call them transparently.
|
|
6
|
+
//
|
|
7
|
+
// Globals consumed from app.js / earlier scripts:
|
|
8
|
+
// - api(), esc(), renderEmptyState(), renderProjectPicker(),
|
|
9
|
+
// renderPreflightPanel(), renderPlanRollup(), confirmRunWithPreflight(),
|
|
10
|
+
// runProject(), openGenericLogPanel(), humaniseProjectName(),
|
|
11
|
+
// resolveProjectMeta(), shortStoryId(), truncate(), scoreClass(),
|
|
12
|
+
// qualityBar(), timeAgo(), showError(), showConfirm(),
|
|
13
|
+
// showStoryDetail(), showOutcomeModal(), runSingleHuFromCard(),
|
|
14
|
+
// renameProjectModal()
|
|
15
|
+
// - mutable: selectedProject, lastOpenedLog, projectNameCache,
|
|
16
|
+
// projectInitialsCache
|
|
17
|
+
// All lookups are runtime-bound; load order only needs board-view.js to live
|
|
18
|
+
// in the same realm before app.js executes its bootstrap.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Renders the kanban board view.
|
|
22
|
+
*/
|
|
23
|
+
async function renderBoard() {
|
|
24
|
+
const app = document.getElementById('app');
|
|
25
|
+
app.innerHTML = '<div class="loading"><div class="loading__spinner"></div><p>Loading board...</p></div>';
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// No project selected → render the project picker instead of
|
|
29
|
+
// mixing every project's HUs in one kanban. The user's
|
|
30
|
+
// complaint: "if no project is selected, show me the list of
|
|
31
|
+
// projects". A mixed kanban hides which HU belongs to which
|
|
32
|
+
// project and is borderline useless on a multi-project setup.
|
|
33
|
+
if (!selectedProject) {
|
|
34
|
+
await renderProjectPicker();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [stories, projectCost] = await Promise.all([
|
|
39
|
+
api(`/api/projects/${encodeURIComponent(selectedProject)}/stories`),
|
|
40
|
+
api(`/api/projects/${encodeURIComponent(selectedProject)}/cost`).catch(() => null),
|
|
41
|
+
]);
|
|
42
|
+
const costSummary = projectCost ? formatProjectCostSummary(projectCost) : null;
|
|
43
|
+
|
|
44
|
+
// Pre-resolve project initials + name for every distinct project_id in
|
|
45
|
+
// the fetched stories so `renderStoryCard` is synchronous and the header
|
|
46
|
+
// can show the human-friendly project name (e.g. "Linux Assistant
|
|
47
|
+
// Orchestrator" instead of the raw slug). Cached globally → at most one
|
|
48
|
+
// API round-trip per project over the session.
|
|
49
|
+
const uniqueProjectIds = [...new Set(stories.map((s) => s.project_id))];
|
|
50
|
+
await Promise.all(uniqueProjectIds.map(resolveProjectMeta));
|
|
51
|
+
if (selectedProject && !projectNameCache[selectedProject]) {
|
|
52
|
+
await resolveProjectMeta(selectedProject);
|
|
53
|
+
}
|
|
54
|
+
const projectDisplayName = selectedProject
|
|
55
|
+
? (projectNameCache[selectedProject] || humaniseProjectName(selectedProject))
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
// KJC-TSK-0403: status/result ortogonal. 3 columnas (Pending /
|
|
59
|
+
// Running / Done) en vez de 4. Las HUs que fallaron NO van a una
|
|
60
|
+
// columna "Failed" — vuelven a Pending con result=fail y badge ✗.
|
|
61
|
+
// status=failed legacy se trata como pending (migración lazy).
|
|
62
|
+
const columns = {
|
|
63
|
+
pending: stories.filter((s) =>
|
|
64
|
+
['pending', 'certified', 'needs_context', 'blocked', 'failed'].includes(s.status)
|
|
65
|
+
),
|
|
66
|
+
running: stories.filter((s) => ['coding', 'reviewing'].includes(s.status)),
|
|
67
|
+
done: stories.filter((s) => s.status === 'done'),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (stories.length === 0) {
|
|
71
|
+
app.innerHTML = renderEmptyState();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// "Run plan" bulk action: visible when a project is selected AND at
|
|
76
|
+
// least one HU is still awaiting execution AND nothing's currently
|
|
77
|
+
// running (to avoid accidentally launching a second pipeline over a
|
|
78
|
+
// live one). Replaces the old "Mark as certified" button — the
|
|
79
|
+
// intermediate "certified" state was noise from the user's POV.
|
|
80
|
+
const awaitingCount = columns.pending.length;
|
|
81
|
+
const runningCount = columns.running.length;
|
|
82
|
+
const canRun = Boolean(selectedProject) && awaitingCount > 0 && runningCount === 0;
|
|
83
|
+
const isRunning = runningCount > 0;
|
|
84
|
+
|
|
85
|
+
// KJC-TSK-0403: 3 canonical lanes. Failed eliminado — HUs con
|
|
86
|
+
// result=fail aparecen en Pending con badge ✗.
|
|
87
|
+
const visibleColumns = [
|
|
88
|
+
{ title: 'Pending', cls: 'pending', rows: columns.pending },
|
|
89
|
+
{ title: 'Running', cls: 'running', rows: columns.running },
|
|
90
|
+
{ title: 'Done', cls: 'done', rows: columns.done },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
app.innerHTML = `
|
|
94
|
+
<div class="section-header">
|
|
95
|
+
<span class="section-header__title" title="${selectedProject ? esc(selectedProject) : ''}">Story Board${selectedProject ? ` - ${esc(projectDisplayName)}` : ''}</span>
|
|
96
|
+
${selectedProject ? `
|
|
97
|
+
<button class="control-btn project-rename-btn"
|
|
98
|
+
style="background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:0.9rem;padding:2px 6px;"
|
|
99
|
+
title="Renombrar este proyecto"
|
|
100
|
+
onclick="event.stopPropagation(); window.renameProjectModal('${esc(selectedProject)}', '${esc(projectDisplayName.replace(/'/g, '''))}');">✎</button>
|
|
101
|
+
` : ''}
|
|
102
|
+
<span class="section-header__count">${stories.length} stories</span>
|
|
103
|
+
${costSummary ? `<span class="section-header__cost" title="${esc(costSummary.tooltip)}">💵 ${esc(costSummary.label)}</span>` : ''}
|
|
104
|
+
${isRunning ? `
|
|
105
|
+
<button id="running-badge-btn" class="section-header__badge"
|
|
106
|
+
style="margin-left:auto;padding:4px 10px;font-size:0.8rem;background:var(--color-yellow,#eab308);color:#000;border-radius:var(--radius-sm);font-weight:600;border:none;cursor:pointer;"
|
|
107
|
+
title="Abrir el log de la HU en marcha">
|
|
108
|
+
⚙ ${runningCount} running…
|
|
109
|
+
</button>
|
|
110
|
+
<button id="stop-run-btn" class="control-btn"
|
|
111
|
+
style="padding:4px 10px;font-size:0.8rem;background:var(--color-red,#ef4444);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
|
|
112
|
+
title="Abortar todos los kj run en marcha de este proyecto (SIGTERM con escalado a SIGKILL tras 5s)">
|
|
113
|
+
⏹ Stop
|
|
114
|
+
</button>
|
|
115
|
+
` : ''}
|
|
116
|
+
${lastOpenedLog ? `
|
|
117
|
+
<button class="control-btn" id="view-log-btn"
|
|
118
|
+
style="${isRunning ? '' : 'margin-left:auto;'}padding:6px 12px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;"
|
|
119
|
+
title="Re-open the log of the most recent run/command">
|
|
120
|
+
📜 View log
|
|
121
|
+
</button>
|
|
122
|
+
` : ''}
|
|
123
|
+
${canRun ? `
|
|
124
|
+
<button class="control-btn" id="run-plan-btn"
|
|
125
|
+
style="margin-left:auto;padding:6px 14px;font-size:0.9rem;background:var(--color-green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
|
|
126
|
+
title="Launch kj run --plan over every plan in this project">
|
|
127
|
+
▶ Run plan (${awaitingCount} HU${awaitingCount === 1 ? '' : 's'})
|
|
128
|
+
</button>
|
|
129
|
+
` : ''}
|
|
130
|
+
</div>
|
|
131
|
+
<div id="preflight-panel" class="preflight-panel" style="margin:8px 0;">
|
|
132
|
+
<div class="preflight-panel__loading" style="font-size:0.8rem;color:var(--text-muted);padding:6px 10px;">
|
|
133
|
+
Comprobando estado del proyecto…
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div id="rag-panel" class="rag-panel" style="margin:8px 0;display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;">
|
|
137
|
+
<input id="rag-query" type="text" placeholder="🔍 RAG search: ask anything about this project's plans / onboarding / code…"
|
|
138
|
+
style="flex:1;min-width:260px;padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);" />
|
|
139
|
+
<select id="rag-scope" style="padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);">
|
|
140
|
+
<option value="all">All</option><option value="plans">Plans</option><option value="onboarding">Onboarding</option><option value="code">Code</option>
|
|
141
|
+
</select>
|
|
142
|
+
<button id="rag-search-btn" class="control-btn"
|
|
143
|
+
style="padding:6px 12px;font-size:0.85rem;background:var(--color-blue,#3b82f6);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;">
|
|
144
|
+
Search
|
|
145
|
+
</button>
|
|
146
|
+
<div id="rag-results" style="flex-basis:100%;font-size:0.8rem;color:var(--text-muted);"></div>
|
|
147
|
+
</div>
|
|
148
|
+
<div id="plan-rollup-banner"></div>
|
|
149
|
+
<div class="kanban">
|
|
150
|
+
${visibleColumns.map((c) => renderKanbanColumn(c.title, c.cls, c.rows)).join('')}
|
|
151
|
+
</div>
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
if (canRun) {
|
|
155
|
+
document.getElementById('run-plan-btn').addEventListener('click', async () => {
|
|
156
|
+
// Pre-run safety: ask preflight if anything risky is missing
|
|
157
|
+
// (no remote, dirty tree, missing agents, etc.). The modal
|
|
158
|
+
// shows blockers + warnings in plain Spanish and the user
|
|
159
|
+
// can cancel or proceed anyway. If everything is green we
|
|
160
|
+
// skip the modal and run straight away.
|
|
161
|
+
const ok = await confirmRunWithPreflight(selectedProject);
|
|
162
|
+
if (!ok) return;
|
|
163
|
+
await runProject(selectedProject);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Preflight panel is rendered AFTER the kanban so we can fetch
|
|
167
|
+
// it without blocking the initial paint. Replaces a placeholder
|
|
168
|
+
// div in-place (no full re-render).
|
|
169
|
+
renderPreflightPanel(selectedProject).catch(() => {});
|
|
170
|
+
// PR3: render the plan-level rollup banner if any plan in this
|
|
171
|
+
// project has a stamped outcome. Non-blocking — if it fails the
|
|
172
|
+
// user just doesn't see the banner.
|
|
173
|
+
renderPlanRollup(selectedProject).catch(() => {});
|
|
174
|
+
const viewLogBtn = document.getElementById('view-log-btn');
|
|
175
|
+
if (viewLogBtn && lastOpenedLog) {
|
|
176
|
+
viewLogBtn.addEventListener('click', () => openGenericLogPanel({
|
|
177
|
+
id: lastOpenedLog.id,
|
|
178
|
+
label: lastOpenedLog.label,
|
|
179
|
+
tailUrl: lastOpenedLog.tailUrl,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
// Wire the yellow "⚙ N running…" badge so it opens the live log of the
|
|
183
|
+
// first running HU. Without this the badge was decorative — and if the
|
|
184
|
+
// run was started before the current page load (refresh / new browser
|
|
185
|
+
// session) `lastOpenedLog` is null, so the "📜 View log" button isn't
|
|
186
|
+
// even rendered. Then the user has no way to see the live output.
|
|
187
|
+
//
|
|
188
|
+
// For a single-HU run the log lives at:
|
|
189
|
+
// ~/.karajan/hu-board-runs/<planId>--hu-<localHuId>.log
|
|
190
|
+
// which the /api/runs/:commandId/log endpoint serves when commandId
|
|
191
|
+
// matches the basename. The composite id stored in SQLite is
|
|
192
|
+
// "<projectId>::<localHuId>", so we strip the prefix to derive
|
|
193
|
+
// localHuId and concatenate with plan_id (already on the story row).
|
|
194
|
+
const runningBadgeBtn = document.getElementById('running-badge-btn');
|
|
195
|
+
if (runningBadgeBtn) {
|
|
196
|
+
runningBadgeBtn.addEventListener('click', async () => {
|
|
197
|
+
const running = stories.find((s) => s.status === 'coding' || s.status === 'reviewing');
|
|
198
|
+
if (!running) return;
|
|
199
|
+
const planId = running.plan_id;
|
|
200
|
+
if (!planId) {
|
|
201
|
+
await showError('Esta HU no tiene plan_id asociado, no puedo localizar su log.', { title: 'Sin log' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const localHuId = running.id.includes('::') ? running.id.split('::').pop() : running.id;
|
|
205
|
+
const logBasename = `${planId}--hu-${localHuId}`;
|
|
206
|
+
const tailUrl = (offset) => `/api/runs/${encodeURIComponent(logBasename)}/log?offset=${offset || 0}`;
|
|
207
|
+
lastOpenedLog = { id: logBasename, label: `HU ${localHuId}`, tailUrl };
|
|
208
|
+
openGenericLogPanel({ id: logBasename, label: `HU ${localHuId}`, tailUrl });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// KJC-TSK-0396: botón ⏹ Stop. Abortar todos los kj run vivos del
|
|
213
|
+
// proyecto. Si hay múltiples planes corriendo (caso raro pero
|
|
214
|
+
// posible), itera sobre cada planId único de las HUs running.
|
|
215
|
+
const stopRunBtn = document.getElementById('stop-run-btn');
|
|
216
|
+
if (stopRunBtn) {
|
|
217
|
+
stopRunBtn.addEventListener('click', async () => {
|
|
218
|
+
const runningHus = stories.filter((s) => s.status === 'coding' || s.status === 'reviewing');
|
|
219
|
+
const planIds = [...new Set(runningHus.map((s) => s.plan_id).filter(Boolean))];
|
|
220
|
+
if (planIds.length === 0) {
|
|
221
|
+
await showError('No hay plan_id asociado a las HUs en marcha.', { title: 'Sin runs' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const ok = await showConfirm(
|
|
225
|
+
`Esto matará ${planIds.length === 1 ? 'el proceso' : `los ${planIds.length} procesos`} del orquestador en marcha y dejará las HUs en curso (coding/reviewing) en pending para que puedas relanzarlas.\n\n¿Seguro?`,
|
|
226
|
+
{ title: 'Abortar kj run', okLabel: 'Stop', cancelLabel: 'Cancelar', destructive: true }
|
|
227
|
+
);
|
|
228
|
+
if (!ok) return;
|
|
229
|
+
const summaries = [];
|
|
230
|
+
for (const planId of planIds) {
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(`/api/runs/${encodeURIComponent(planId)}/stop`, {
|
|
233
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
|
|
234
|
+
});
|
|
235
|
+
const body = await res.json();
|
|
236
|
+
if (!res.ok) summaries.push(`${planId}: error ${body.error || res.status}`);
|
|
237
|
+
else summaries.push(`${planId}: ${body.stopped} SIGTERM, ${body.killed} SIGKILL, ${body.hu_reset_count || 0} HUs → pending`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
summaries.push(`${planId}: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
243
|
+
await renderBoard();
|
|
244
|
+
if (summaries.some((s) => s.includes('SIGKILL') && !s.startsWith('0 ')) && summaries.some((s) => /\b[1-9]\d* SIGKILL/.test(s))) {
|
|
245
|
+
await showError(
|
|
246
|
+
`Algún proceso no respondió a SIGTERM en 5s y fue forzado con SIGKILL:\n\n${summaries.join('\n')}`,
|
|
247
|
+
{ title: 'Forzado con SIGKILL' }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// RAG search panel (KJC-PCS-0049 Step 8) — input + scope + render hits.
|
|
254
|
+
const ragInput = document.getElementById('rag-query');
|
|
255
|
+
const ragScope = document.getElementById('rag-scope');
|
|
256
|
+
const ragBtn = document.getElementById('rag-search-btn');
|
|
257
|
+
const ragResults = document.getElementById('rag-results');
|
|
258
|
+
async function runRagSearch() {
|
|
259
|
+
const text = ragInput?.value?.trim();
|
|
260
|
+
if (!text) return;
|
|
261
|
+
ragResults.innerHTML = '<em>Searching…</em>';
|
|
262
|
+
try {
|
|
263
|
+
const res = await fetch('/api/rag/query', {
|
|
264
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ text, topK: 5, scope: ragScope?.value || 'all' }),
|
|
266
|
+
});
|
|
267
|
+
const body = await res.json();
|
|
268
|
+
if (!res.ok) { ragResults.innerHTML = `<span style="color:var(--color-red);">Error: ${esc(body.error || res.status)}</span>`; return; }
|
|
269
|
+
if (body.empty) { ragResults.innerHTML = '<em>No chunks indexed yet. Run <code>kj rag index</code> in this project\'s terminal first.</em>'; return; }
|
|
270
|
+
if (body.hits.length === 0) { ragResults.innerHTML = '<em>No hits.</em>'; return; }
|
|
271
|
+
ragResults.innerHTML = body.hits.map((h) => {
|
|
272
|
+
const label = h.metadata?.hu_id || h.metadata?.symbol || h.metadata?.headingPath?.join(' > ') || 'block';
|
|
273
|
+
const snippet = h.text.length > 240 ? `${esc(h.text.slice(0, 240))}…` : esc(h.text);
|
|
274
|
+
return `<div style="border-left:3px solid var(--color-blue,#3b82f6);padding:6px 10px;margin:6px 0;background:var(--bg-primary);">
|
|
275
|
+
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:4px;">[${esc(h.kind)} · ${esc(label)} · score=${h.score.toFixed(4)}] ${esc(h.source)}</div>
|
|
276
|
+
<div style="font-size:0.85rem;white-space:pre-wrap;">${snippet}</div>
|
|
277
|
+
</div>`;
|
|
278
|
+
}).join('');
|
|
279
|
+
} catch (err) {
|
|
280
|
+
ragResults.innerHTML = `<span style="color:var(--color-red);">Error: ${esc(err.message)}</span>`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
ragBtn?.addEventListener('click', runRagSearch);
|
|
284
|
+
ragInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runRagSearch(); });
|
|
285
|
+
} catch (err) {
|
|
286
|
+
app.innerHTML = `<div class="empty-state"><div class="empty-state__title">Error loading board</div><div class="empty-state__text">${esc(err.message)}</div></div>`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Renders a single kanban column.
|
|
292
|
+
* @param {string} title
|
|
293
|
+
* @param {string} cssClass
|
|
294
|
+
* @param {Array<object>} stories
|
|
295
|
+
* @returns {string}
|
|
296
|
+
*/
|
|
297
|
+
function renderKanbanColumn(title, cssClass, stories) {
|
|
298
|
+
// Empty lanes render the header (so the user keeps the 4-column
|
|
299
|
+
// mental map) but no body placeholder — "No stories" text on four
|
|
300
|
+
// empty columns was noise on fresh plans.
|
|
301
|
+
return `
|
|
302
|
+
<div class="kanban__column kanban__column--${cssClass}" data-column="${cssClass}"${stories.length === 0 ? ' style="opacity:0.55"' : ''}>
|
|
303
|
+
<div class="kanban__column-header">
|
|
304
|
+
<span class="kanban__column-title">${title}</span>
|
|
305
|
+
<span class="kanban__column-count" data-column-count>${stories.length}</span>
|
|
306
|
+
</div>
|
|
307
|
+
${stories.map(renderStoryCard).join('')}
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Renders a story card for the kanban board.
|
|
314
|
+
* @param {object} story
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
function renderStoryCard(story) {
|
|
318
|
+
const title = story.title || story.original_text || story.id;
|
|
319
|
+
const antipatterns = story.antipatterns ? JSON.parse(story.antipatterns) : [];
|
|
320
|
+
// Prefer denormalised counters stamped at sync time; fall back to
|
|
321
|
+
// parsing the JSON blob for pre-migration rows so the card still shows
|
|
322
|
+
// the AC count without a board DB nuke.
|
|
323
|
+
const acCount = typeof story.ac_count === 'number'
|
|
324
|
+
? story.ac_count
|
|
325
|
+
: (story.acceptance_criteria ? JSON.parse(story.acceptance_criteria).length : 0);
|
|
326
|
+
const testCount = typeof story.test_count === 'number' ? story.test_count : 0;
|
|
327
|
+
const blockedBy = story.blocked_by ? JSON.parse(story.blocked_by) : [];
|
|
328
|
+
const initials = projectInitialsCache[story.project_id] || 'kj';
|
|
329
|
+
const shortId = shortStoryId(story, initials);
|
|
330
|
+
|
|
331
|
+
// Human-readable dep list: `lao-001, lao-003` instead of the raw HU ids.
|
|
332
|
+
const shortDep = (depId) => {
|
|
333
|
+
const m = /_(\d+)(?!.*\d)/.exec(depId);
|
|
334
|
+
return `${initials}-${m ? m[1] : '?'}`;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Tests-first flag: a HU with zero acceptance_tests has no contract
|
|
338
|
+
// for the coder, so `Run plan` will refuse to execute it. Surface
|
|
339
|
+
// this on the card so the user edits the HU before trying to run.
|
|
340
|
+
const missingTestContract = testCount === 0 && ['pending', 'certified'].includes(story.status);
|
|
341
|
+
|
|
342
|
+
// PR4: per-HU ▶ Run button. Visible when:
|
|
343
|
+
// - the HU is in a re-runnable lifecycle state (certified / failed /
|
|
344
|
+
// pending / done — KJC-TSK-0394 step 2 adds `done` so the user can
|
|
345
|
+
// replay a HU whose `result` was fail or partial without having to
|
|
346
|
+
// "reset to pending" first) AND
|
|
347
|
+
// - it has at least one acceptance test (otherwise the run would
|
|
348
|
+
// refuse) AND
|
|
349
|
+
// - it isn't currently coding/reviewing AND
|
|
350
|
+
// - KJC-BUG-0048: it has no unresolved blocked_by deps. The card
|
|
351
|
+
// already shows "⏳ waits for: ..." below the title; the ▶ button
|
|
352
|
+
// must match that gating or the user can launch a HU whose deps
|
|
353
|
+
// don't exist yet, hitting the missing-prereq path at runtime.
|
|
354
|
+
const canRunHu = ['certified', 'failed', 'pending', 'done'].includes(story.status)
|
|
355
|
+
&& testCount > 0
|
|
356
|
+
&& !['coding', 'reviewing'].includes(story.status)
|
|
357
|
+
&& blockedBy.length === 0;
|
|
358
|
+
|
|
359
|
+
return `
|
|
360
|
+
<div class="story-card" data-story-id="${esc(story.id)}" data-status="${esc(story.status || 'pending')}" onclick="showStoryDetail('${esc(story.id)}')">
|
|
361
|
+
<div class="story-card__id" title="${esc(story.id)}">
|
|
362
|
+
${esc(shortId)}
|
|
363
|
+
${canRunHu ? `
|
|
364
|
+
<button class="story-card__run-btn"
|
|
365
|
+
style="float:right;padding:1px 6px;font-size:0.7rem;background:var(--color-green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;"
|
|
366
|
+
title="Lanzar solo esta HU"
|
|
367
|
+
onclick="event.stopPropagation(); window.runSingleHuFromCard('${esc(story.id)}', '${esc(title.replace(/'/g, '''))}');">▶</button>
|
|
368
|
+
` : ''}
|
|
369
|
+
</div>
|
|
370
|
+
<div class="story-card__title">${esc(truncate(title, 100))}</div>
|
|
371
|
+
<div class="story-card__meta" style="gap:10px">
|
|
372
|
+
${acCount > 0 ? `<span title="${acCount} acceptance criteria">📋 ${acCount} AC${acCount === 1 ? '' : 's'}</span>` : ''}
|
|
373
|
+
${testCount > 0 ? `<span title="${testCount} acceptance tests declared">✅ ${testCount} test${testCount === 1 ? '' : 's'}</span>` : ''}
|
|
374
|
+
${story.spec_section ? `<span title="Implements SPEC ${esc(story.spec_section)}" style="font-family:var(--font-mono, monospace)">📖 §${esc(story.spec_section)}</span>` : ''}
|
|
375
|
+
${story.quality_total !== null ? `
|
|
376
|
+
<span class="story-card__score ${scoreClass(story.quality_total)}" title="INVEST score">
|
|
377
|
+
${story.quality_total}/60 ${qualityBar(story.quality_total)}
|
|
378
|
+
</span>
|
|
379
|
+
` : ''}
|
|
380
|
+
${(() => {
|
|
381
|
+
const c = formatCost(story.cost_usd);
|
|
382
|
+
return c ? `<span class="story-card__cost" title="${esc(c.tooltip)}">💵 ${esc(c.label)}</span>` : '';
|
|
383
|
+
})()}
|
|
384
|
+
</div>
|
|
385
|
+
${missingTestContract ? `
|
|
386
|
+
<div class="story-card__meta" style="margin-top:4px;font-size:0.75rem;color:var(--color-yellow,#eab308);font-weight:600" title="This HU has no acceptance_tests declared — Run plan will reject it until you add at least one.">
|
|
387
|
+
⚠ missing test contract
|
|
388
|
+
</div>
|
|
389
|
+
` : ''}
|
|
390
|
+
${blockedBy.length > 0 ? `
|
|
391
|
+
<div class="story-card__meta" style="margin-top:4px;font-size:0.75rem;color:var(--text-muted)" title="This HU waits on: ${esc(blockedBy.join(', '))}">
|
|
392
|
+
⏳ waits for: ${blockedBy.map((d) => esc(shortDep(d))).join(', ')}
|
|
393
|
+
</div>
|
|
394
|
+
` : (story.status === 'pending' || story.status === 'certified') && !missingTestContract ? `
|
|
395
|
+
<div class="story-card__meta" style="margin-top:4px;font-size:0.75rem;color:var(--color-green)" title="No dependencies — runs first on the next 'Run plan'">
|
|
396
|
+
🟢 ready to run
|
|
397
|
+
</div>
|
|
398
|
+
` : ''}
|
|
399
|
+
${antipatterns.length > 0 ? `<div class="story-card__antipattern">${antipatterns.map((a) => esc(a)).join(', ')}</div>` : ''}
|
|
400
|
+
<div class="story-card__meta" style="margin-top:6px">
|
|
401
|
+
<span class="story-card__status status--${story.status}">${esc(story.status)}</span>
|
|
402
|
+
${renderResultBadge(computeEffectiveResult(story))}
|
|
403
|
+
<span class="story-card__time">${timeAgo(story.updated_at)}</span>
|
|
404
|
+
${renderOutcomeChip(story.outcome)}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* KJC-TSK-0394 step 5: result "efectivo" para la UI. Sobre el campo
|
|
412
|
+
* persistido `story.result` (si existe), o inferido del status legacy
|
|
413
|
+
* (done→pass, failed→fail).
|
|
414
|
+
*/
|
|
415
|
+
function computeEffectiveResult(story) {
|
|
416
|
+
if (!story) return null;
|
|
417
|
+
if (story.result !== undefined && story.result !== null) return story.result;
|
|
418
|
+
if (story.status === 'done') return 'pass';
|
|
419
|
+
if (story.status === 'failed') return 'fail';
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Render the per-HU `result` badge. Ortogonal al status.
|
|
425
|
+
* @param {string|null} result
|
|
426
|
+
* @returns {string} HTML
|
|
427
|
+
*/
|
|
428
|
+
function renderResultBadge(result) {
|
|
429
|
+
if (!result) return '';
|
|
430
|
+
const icons = { pass: '✓', fail: '✗', partial: '~' };
|
|
431
|
+
const titles = {
|
|
432
|
+
pass: 'Última ejecución: todos los tests pasaron',
|
|
433
|
+
fail: 'Última ejecución: falló',
|
|
434
|
+
partial: 'Última ejecución: pasó parcialmente',
|
|
435
|
+
};
|
|
436
|
+
const icon = icons[result];
|
|
437
|
+
if (!icon) return '';
|
|
438
|
+
return `<span class="story-card__result result--${esc(result)}" title="${esc(titles[result])}">${icon}</span>`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Render the per-HU outcome chip (📄 resumen). Tolerates both string and
|
|
443
|
+
* object shapes.
|
|
444
|
+
* @param {string|object|null} outcome
|
|
445
|
+
* @returns {string} HTML
|
|
446
|
+
*/
|
|
447
|
+
function renderOutcomeChip(outcome) {
|
|
448
|
+
if (!outcome) return '';
|
|
449
|
+
let parsed;
|
|
450
|
+
try { parsed = typeof outcome === 'string' ? JSON.parse(outcome) : outcome; }
|
|
451
|
+
catch { return ''; }
|
|
452
|
+
if (!parsed || typeof parsed !== 'object') return '';
|
|
453
|
+
const summary = parsed.summary || `Resultado: ${parsed.status || 'desconocido'}`;
|
|
454
|
+
return `
|
|
455
|
+
<span class="story-card__outcome-chip"
|
|
456
|
+
style="margin-left:auto;padding:2px 8px;font-size:0.7rem;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:var(--text-muted);"
|
|
457
|
+
title="${esc(summary)}"
|
|
458
|
+
onclick="event.stopPropagation(); showOutcomeModal('${esc(JSON.stringify(parsed).replace(/'/g, '''))}');">
|
|
459
|
+
📄 resumen
|
|
460
|
+
</span>
|
|
461
|
+
`;
|
|
462
|
+
}
|