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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.1.7",
3
+ "version": "4.1.9",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karajan/hu-board",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "HU Story Board dashboard for Karajan Code",
5
5
  "type": "module",
6
6
  "private": true,
@@ -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">Context</summary>
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', async (req, res) => {
466
- try {
467
- const id = req.params.id;
468
- const dir = path.join(huStoriesDir(), id);
469
- const ok = deleteStory(id);
470
- if (!ok) return res.status(404).json({ error: 'Story not found' });
471
- addTombstone('story', id, { source: 'api', fsPaths: [dir] });
472
- let dirRemoved = false;
473
- try { await fsp.rm(dir, { recursive: true, force: true }); dirRemoved = true; } catch { /* */ }
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
- if (data.version !== 2 || !Array.isArray(data.hus) || data.hus.length === 0) return;
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
@@ -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,
@@ -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.deleted === 0) { fixProgress.finish("done"); break; }
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, -${ops.deleted} HU`);
374
- if (ops.added + ops.depsAdded + ops.deleted === 0) {
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
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { extractFirstJson } from "../utils/json-extract.js";
13
- import { addHu, removeHu } from "./plan-hu-ops.js";
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, deleted: 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
- if (removeHu(plan, huId)) counts.deleted += 1;
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.deleted > 0) {
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
  },