karajan-code 4.1.7 → 4.1.9
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/package.json +1 -1
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/utils/modals.js +20 -1
- package/packages/hu-board/src/routes/api.js +9 -14
- package/packages/hu-board/src/sync.js +13 -1
- package/src/commands/hu.js +13 -0
- package/src/commands/plan/fix.js +1 -1
- package/src/commands/plan/generate.js +2 -2
- package/src/environment/briefs.js +12 -0
- package/src/environment/playbook.js +2 -2
- package/src/plan/plan-fixer.js +26 -4
- package/src/spec-review/run-spec-review.js +12 -2
package/package.json
CHANGED
|
@@ -208,9 +208,27 @@ function showPromptModal(prompt) {
|
|
|
208
208
|
if (activePromptId === prompt.promptId) return;
|
|
209
209
|
activePromptId = prompt.promptId;
|
|
210
210
|
const dlg = ensureDialog();
|
|
211
|
+
// KJC-BUG-0125 (issue #1275): findings render as a readable, open list —
|
|
212
|
+
// "4 findings at severity fail" alone gives the user nothing to act on.
|
|
213
|
+
// Each finding shows its message and the concrete suggestion.
|
|
214
|
+
const sevColor = (s) => (s === 'fail' ? '#f87171' : s === 'warn' ? '#fbbf24' : '#60a5fa');
|
|
215
|
+
const findings = prompt.context?.detail?.findings || prompt.context?.findings;
|
|
216
|
+
const findingsBlock = Array.isArray(findings) && findings.length
|
|
217
|
+
? `<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px;max-height:280px;overflow:auto">`
|
|
218
|
+
+ findings.map((f) => `
|
|
219
|
+
<div style="border-left:3px solid ${sevColor(f.severity)};padding:6px 10px;background:var(--bg-primary);border-radius:var(--radius-sm)">
|
|
220
|
+
<div style="font-size:0.78rem;color:var(--text-muted)">
|
|
221
|
+
<strong style="color:${sevColor(f.severity)}">${esc(String(f.severity || '').toUpperCase())}</strong>
|
|
222
|
+
· ${esc(f.category || '')}${f.id ? ` · ${esc(f.id)}` : ''}
|
|
223
|
+
</div>
|
|
224
|
+
<div style="font-size:0.86rem;line-height:1.45;margin-top:2px">${esc(f.message || '')}</div>
|
|
225
|
+
${f.suggestion ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-top:2px">→ ${esc(f.suggestion)}</div>` : ''}
|
|
226
|
+
</div>`).join('')
|
|
227
|
+
+ `</div>`
|
|
228
|
+
: '';
|
|
211
229
|
const ctxBlock = prompt.context
|
|
212
230
|
? `<details style="margin-top:8px">
|
|
213
|
-
<summary style="cursor:pointer;color:var(--text-muted);font-size:0.85rem">
|
|
231
|
+
<summary style="cursor:pointer;color:var(--text-muted);font-size:0.85rem">Raw context</summary>
|
|
214
232
|
<pre style="white-space:pre-wrap;font-size:0.78rem;background:var(--bg-primary);padding:8px;border-radius:var(--radius-sm);overflow:auto;max-height:240px">${esc(JSON.stringify(prompt.context, null, 2))}</pre>
|
|
215
233
|
</details>`
|
|
216
234
|
: '';
|
|
@@ -221,6 +239,7 @@ function showPromptModal(prompt) {
|
|
|
221
239
|
</div>
|
|
222
240
|
<div style="padding:14px 18px">
|
|
223
241
|
<div style="white-space:pre-wrap;font-size:0.95rem;line-height:1.55">${esc(prompt.question)}</div>
|
|
242
|
+
${findingsBlock}
|
|
224
243
|
${ctxBlock}
|
|
225
244
|
<textarea id="prompt-answer" rows="3"
|
|
226
245
|
placeholder="Type your answer (or 'stop' to abort the run)"
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
getSessionsByProject,
|
|
33
33
|
getSessionDetail,
|
|
34
34
|
deleteProject,
|
|
35
|
-
deleteStory,
|
|
36
35
|
deleteSession,
|
|
37
36
|
getKjHome,
|
|
38
37
|
getHuBoardRunsDir,
|
|
@@ -462,19 +461,15 @@ router.delete('/prompts/:promptId', async (req, res) => {
|
|
|
462
461
|
* it. Pre-tombstones this endpoint was a DB-only soft hide that the next
|
|
463
462
|
* chokidar event undid silently.
|
|
464
463
|
*/
|
|
465
|
-
router.delete('/stories/:id',
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
res.json({ deleted: true, dirRemoved });
|
|
475
|
-
} catch (err) {
|
|
476
|
-
res.status(500).json({ error: err.message });
|
|
477
|
-
}
|
|
464
|
+
router.delete('/stories/:id', (req, res) => {
|
|
465
|
+
// KJC-TSK-0671 (absolute product rule): stories are NEVER deleted — the
|
|
466
|
+
// delete-and-recreate "fix" agents improvise loses history and produced
|
|
467
|
+
// the field saga this closes. Discard goes through the lifecycle instead.
|
|
468
|
+
res.status(405).json({
|
|
469
|
+
error: 'Stories are never deleted. Discard one with: kj hu move <id> skipped '
|
|
470
|
+
+ '(archived, history kept). Dashboard warnings are fixed with the command '
|
|
471
|
+
+ 'they name — see kj brief board.',
|
|
472
|
+
});
|
|
478
473
|
});
|
|
479
474
|
|
|
480
475
|
/**
|
|
@@ -358,7 +358,19 @@ export function syncPlanFile(filePath) {
|
|
|
358
358
|
try {
|
|
359
359
|
const raw = readFileSync(filePath, 'utf-8');
|
|
360
360
|
const data = JSON.parse(raw);
|
|
361
|
-
|
|
361
|
+
// KJC-BUG-0124 (issue #1277): a strict `version !== 2` silently hid
|
|
362
|
+
// every plan whose version drifted — the board showed 0 stories with no
|
|
363
|
+
// trace, and users concluded their work was lost. Tolerant now: any
|
|
364
|
+
// v2+ plan syncs, with a LOUD warn on version drift (fired before the
|
|
365
|
+
// empty check so drift is visible even on empty plans). Pre-v2 plans
|
|
366
|
+
// keep their historical silent skip (they never synced and carry no
|
|
367
|
+
// hus[]), and an empty v2 plan has nothing to sync — skipping it
|
|
368
|
+
// loses nothing.
|
|
369
|
+
if (typeof data.version !== 'number' || data.version < 2) return;
|
|
370
|
+
if (data.version > 2) {
|
|
371
|
+
console.warn(`[sync] ${filePath}: plan version ${data.version} (expected 2) — syncing anyway; re-save with current kj to normalize`);
|
|
372
|
+
}
|
|
373
|
+
if (!Array.isArray(data.hus) || data.hus.length === 0) return;
|
|
362
374
|
|
|
363
375
|
// Bug fix: pre-patch, `projectId = data.planId` — every plan was a separate
|
|
364
376
|
// "project" on the board, so 2 464 plans across the same repo produced
|
package/src/commands/hu.js
CHANGED
|
@@ -66,6 +66,19 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
66
66
|
const title = args[0];
|
|
67
67
|
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
68
68
|
const plan = await backlogPlan(projectDir);
|
|
69
|
+
// KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
|
|
70
|
+
// recreate "fix" an agent improvises loses history and duplicates ids —
|
|
71
|
+
// refuse it with the norm spelled out.
|
|
72
|
+
if (flags.id) {
|
|
73
|
+
const dup = (plan.hus || []).find((h) => h.short_id === flags.id);
|
|
74
|
+
if (dup) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
|
|
77
|
+
+ `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
|
|
78
|
+
+ `or discard it with: kj hu move ${flags.id} skipped`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
const hu = addHu(plan, {
|
|
70
83
|
title,
|
|
71
84
|
short_id: flags.id || null,
|
package/src/commands/plan/fix.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function planFixCommand({ config, planId, prompt, logger, json }) {
|
|
|
60
60
|
});
|
|
61
61
|
if (!fix.ok) { fixProgress.finish("failed"); break; }
|
|
62
62
|
const ops = applyFixerPatch(plan, fix.patch);
|
|
63
|
-
if (ops.added + ops.depsAdded + ops.
|
|
63
|
+
if (ops.added + ops.depsAdded + ops.archived === 0) { fixProgress.finish("done"); break; }
|
|
64
64
|
const review2 = await reviewPlan({
|
|
65
65
|
agent: planner, task: enrichedTask, hus: plan.hus,
|
|
66
66
|
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
@@ -370,8 +370,8 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
370
370
|
break;
|
|
371
371
|
}
|
|
372
372
|
const ops = applyFixerPatch(plan, fix.patch);
|
|
373
|
-
runLog.logText(`[planner] self-fix iter ${i} applied: +${ops.added} HU, +${ops.depsAdded} dep,
|
|
374
|
-
if (ops.added + ops.depsAdded + ops.
|
|
373
|
+
runLog.logText(`[planner] self-fix iter ${i} applied: +${ops.added} HU, +${ops.depsAdded} dep, ${ops.archived} archived`);
|
|
374
|
+
if (ops.added + ops.depsAdded + ops.archived === 0) {
|
|
375
375
|
fixProgress.finish("done");
|
|
376
376
|
runLog.logText(`[planner] self-fix iter ${i} empty patch — stopping`);
|
|
377
377
|
break;
|
|
@@ -82,6 +82,18 @@ const B = {
|
|
|
82
82
|
],
|
|
83
83
|
"findings with severity, category and file:line — or an explicit 'no security surface touched'."),
|
|
84
84
|
},
|
|
85
|
+
board: {
|
|
86
|
+
purpose: "What the HU Board is and how to act on it without breaking it",
|
|
87
|
+
render: () => brief("Board",
|
|
88
|
+
"the HU Board is the permanent record of work: every card traces to who created it and what happened to it — a dashboard warning is fixed with the command it names, never by rebuilding the board.",
|
|
89
|
+
[
|
|
90
|
+
"Cards are NEVER deleted or recreated — not to fix warnings, not to tidy up. Discard = `kj hu move <id> skipped` (archived, history kept).",
|
|
91
|
+
"States move only via `kj hu move <id> <pending|running|done|failed|skipped>`.",
|
|
92
|
+
"A duplicate short_id means the card already exists: update that card, never add a twin.",
|
|
93
|
+
"A warning is information, not damage: run the command it names; if it looks like a kj bug, `kj report-issue` — never work around it by destroying state.",
|
|
94
|
+
],
|
|
95
|
+
"the board reflects reality: the same cards before and after your action, with truthful states and provenance intact."),
|
|
96
|
+
},
|
|
85
97
|
audit: {
|
|
86
98
|
purpose: "Final look before shipping — does the whole thing hold?",
|
|
87
99
|
render: () => brief("Audit",
|
|
@@ -29,7 +29,7 @@ const TARGET_FILES = {
|
|
|
29
29
|
// ENV-D1 (KJC-TSK-0642): the tracking invariant names the CHOSEN state
|
|
30
30
|
// backend — a playbook that says "board or PG" makes the host guess.
|
|
31
31
|
const BACKEND_TRACKING = {
|
|
32
|
-
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts.",
|
|
32
|
+
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts. Cards are permanent — never delete or recreate one; discard with `kj hu move <id> skipped`. `kj brief board` explains the board.",
|
|
33
33
|
"planning-game": "Every piece of work has a tracked card in the Planning Game MCP before it starts (In Progress while you work it).",
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -58,7 +58,7 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
58
58
|
through an atomic PR (~150 net lines, Conventional Commits).
|
|
59
59
|
|
|
60
60
|
Commands: \`kj rag query\` · \`kj brief <role>\` (triage, planner, researcher,
|
|
61
|
-
architect, tester, security, audit) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
61
|
+
architect, tester, security, audit, board) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
62
62
|
\`kj review --staged\` · \`kj review --check\` · \`kj solomon --position\` ·
|
|
63
63
|
\`kj agent run <agent>\` · \`kj report\` · \`kj check\`
|
|
64
64
|
|
package/src/plan/plan-fixer.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
13
|
-
import { addHu
|
|
13
|
+
import { addHu } from "./plan-hu-ops.js";
|
|
14
14
|
import { normaliseAcceptanceTests } from "./plan-schema.js";
|
|
15
15
|
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
16
16
|
|
|
@@ -111,9 +111,31 @@ export async function applyReviewerFeedback({ agent, task, hus, findings, onOutp
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function applyFixerPatch(plan, patch) {
|
|
114
|
-
const counts = { added: 0, depsAdded: 0,
|
|
114
|
+
const counts = { added: 0, depsAdded: 0, archived: 0 };
|
|
115
|
+
// KJC-TSK-0669 (absolute rule): HUs are NEVER deleted — not even by the
|
|
116
|
+
// fix loop. Overlap resolutions ARCHIVE in place (status skipped +
|
|
117
|
+
// provenance), so history survives and ids stay referenceable. The
|
|
118
|
+
// references are cleaned exactly like removeHu used to, because a
|
|
119
|
+
// skipped dependency never satisfies the scheduler (deadlock otherwise).
|
|
115
120
|
for (const huId of patch.deletions || []) {
|
|
116
|
-
|
|
121
|
+
const hu = plan.hus.find(h => h.id === huId);
|
|
122
|
+
if (!hu) continue;
|
|
123
|
+
if (hu.status !== "skipped") {
|
|
124
|
+
hu.status = "skipped";
|
|
125
|
+
hu.archived_by = "plan-reviewer";
|
|
126
|
+
hu.archive_reason = "scope overlap resolved by the plan-review fix loop";
|
|
127
|
+
hu.updatedAt = new Date().toISOString();
|
|
128
|
+
counts.archived += 1;
|
|
129
|
+
}
|
|
130
|
+
for (const other of plan.hus) {
|
|
131
|
+
if (other.id === huId) continue;
|
|
132
|
+
if (Array.isArray(other.blocked_by) && other.blocked_by.includes(huId)) {
|
|
133
|
+
other.blocked_by = other.blocked_by.filter(d => d !== huId);
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(other.reuse) && other.reuse.includes(huId)) {
|
|
136
|
+
other.reuse = other.reuse.filter(d => d !== huId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
117
139
|
}
|
|
118
140
|
// KJC-BUG-0053: las HUs añadidas por el fixer dejaban `short_id` y
|
|
119
141
|
// `blocked_by` vacíos porque addHu no recibía esos campos. Resultado:
|
|
@@ -163,7 +185,7 @@ export function applyFixerPatch(plan, patch) {
|
|
|
163
185
|
hu.blocked_by = [...blocked, on];
|
|
164
186
|
counts.depsAdded += 1;
|
|
165
187
|
}
|
|
166
|
-
if (counts.added + counts.depsAdded + counts.
|
|
188
|
+
if (counts.added + counts.depsAdded + counts.archived > 0) {
|
|
167
189
|
plan.updatedAt = new Date().toISOString();
|
|
168
190
|
}
|
|
169
191
|
return counts;
|
|
@@ -11,6 +11,16 @@ import { RISK } from "../autonomy/policy.js";
|
|
|
11
11
|
|
|
12
12
|
const MAX_REFINE_ITERATIONS = 5;
|
|
13
13
|
|
|
14
|
+
// KJC-BUG-0125 (issue #1275): the prompt used to carry only counts and
|
|
15
|
+
// categories — the dashboard modal showed "4 findings at severity fail"
|
|
16
|
+
// with nothing to act on. The full findings travel with the prompt now
|
|
17
|
+
// (capped: a runaway reviewer must not blow up the prompt payload).
|
|
18
|
+
const MAX_FINDINGS_IN_PROMPT = 20;
|
|
19
|
+
const findingsForPrompt = (findings) => findings.slice(0, MAX_FINDINGS_IN_PROMPT).map((f) => ({
|
|
20
|
+
id: f.id, severity: f.severity, category: f.category,
|
|
21
|
+
message: f.message, suggestion: f.suggestion || null,
|
|
22
|
+
}));
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Decide the gate action (`continue` | `refine`) autonomously: the Arbiter
|
|
16
26
|
* picks PROCEED (proceed with the best reading) or RETRY_DIFFERENT_APPROACH
|
|
@@ -24,7 +34,7 @@ async function decideGateAutonomously(resolve, { severity, findings }) {
|
|
|
24
34
|
{ value: "PROCEED", label: "proceed with the best reading", conservative: true },
|
|
25
35
|
{ value: "RETRY_DIFFERENT_APPROACH", label: "auto-refine the spec" },
|
|
26
36
|
],
|
|
27
|
-
context: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))] },
|
|
37
|
+
context: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], findings: findingsForPrompt(findings) },
|
|
28
38
|
risk: severity === "fail" ? RISK.HIGH : RISK.LOW,
|
|
29
39
|
});
|
|
30
40
|
return choice === "RETRY_DIFFERENT_APPROACH" ? "refine" : "continue";
|
|
@@ -71,7 +81,7 @@ export async function runSpecReview({ spec, config, logger, askQuestion, flags =
|
|
|
71
81
|
const answer = await askQuestion(
|
|
72
82
|
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: continue)`,
|
|
73
83
|
{
|
|
74
|
-
detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 },
|
|
84
|
+
detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1, findings: findingsForPrompt(findings) },
|
|
75
85
|
// KJC-BUG-0109: mirrors the interactive default — --yes presses Enter.
|
|
76
86
|
defaultAnswer: "continue",
|
|
77
87
|
},
|