karajan-code 4.49.5 → 4.51.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.49.5",
3
+ "version": "4.51.0",
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",
@@ -60,7 +60,6 @@
60
60
  "packages/ai-trash/src/",
61
61
  "packages/ai-trash/bin/",
62
62
  "packages/ai-trash/package.json",
63
- "docs/karajan-code-logo-small.png",
64
63
  "docs/README.es.md",
65
64
  "LICENSE",
66
65
  "README.md"
@@ -29,7 +29,7 @@ async function withoutSetupInstalls(projectDir, setup, violations) {
29
29
  const installed = new Set(added.exitCode === 0 ? added.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : []);
30
30
  const kept = [];
31
31
  for (const v of violations) {
32
- if (v.rule_id === "defaults.supervisor.write" && installed.has(v.file)) {
32
+ if ((v.rule_id === "defaults.supervisor.write" || v.rule_id === "defaults.governance.write") && installed.has(v.file)) {
33
33
  const changed = await runCommand("git", ["-C", projectDir, "diff", "--quiet", setup, "HEAD", "--", v.file]);
34
34
  if (changed.exitCode === 0) continue;
35
35
  }
@@ -315,6 +315,7 @@ export function registerMeta(program, { pkgVersion }) {
315
315
  .option("--scope <text>", "Scope of the story (files/areas)")
316
316
  .option("--task-type <type>", "sw | infra | doc | add-tests | refactor")
317
317
  .option("--criteria <text>", "Legacy alias for a single acceptance criterion")
318
+ .option("--ref <id>", "Id of this card's replica in the external tracker (Planning Game, Jira, Linear...)")
318
319
  .option("--json", "Machine-readable output")
319
320
  .action(async (title, flags) => {
320
321
  await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
@@ -62,15 +62,9 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
62
62
  const projectDir = config?.projectDir || process.cwd();
63
63
  const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
64
64
 
65
- // KJC-TSK-0684 (issue #1287): with an external board declared, kj never
66
- // maintains a parallel HU Board — writes are refused with the pointer.
67
- if (config?.state_backend === "external" && (action === "add" || action === "move")) {
68
- const name = config?.board?.name || "an external board";
69
- throw new Error(
70
- `this project's board lives in ${name} (state_backend: external) — create and move cards there, `
71
- + `through your agent's MCP/tools. kj does not mirror external boards.`
72
- );
73
- }
65
+ // KJC-BUG-0305 (issue #2042): the HU Board is always kj's board. An external
66
+ // tracker coexists with it: each HU points at its replica there (--ref).
67
+ const externalBoard = config?.state_backend === "external" ? (config?.board?.name || "the external board") : null;
74
68
 
75
69
  if (action === "list") {
76
70
  const plans = await listPlans(projectDir);
@@ -84,12 +78,14 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
84
78
  // KJC-TSK-0661: provenance — where a card came from is part of
85
79
  // the card. Older HUs predate the stamp: shown as "?".
86
80
  created_by: h.created_by || null, created_at: h.createdAt || null,
81
+ external_ref: h.external_ref || null,
87
82
  });
88
83
  }
89
84
  }
90
85
  if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
91
86
  for (const r of rows) {
92
- const origin = `${r.plan} · by ${r.created_by || "?"}${r.created_at ? ` · ${r.created_at.slice(0, 10)}` : ""}`;
87
+ const suffixes = [r.created_at?.slice(0, 10), r.external_ref].filter(Boolean).map((s) => ` · ${s}`).join("");
88
+ const origin = `${r.plan} · by ${r.created_by || "?"}${suffixes}`;
93
89
  console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
94
90
  }
95
91
  if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
@@ -164,7 +160,11 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
164
160
  task_type: flags.taskType || undefined,
165
161
  });
166
162
  hu.created_by = creatorLabel();
163
+ if (flags.ref) hu.external_ref = String(flags.ref).trim();
167
164
  await savePlan(projectDir, plan);
165
+ if (externalBoard && !hu.external_ref) {
166
+ console.log(`→ replicate it in ${externalBoard} through your agent's MCP/tools and link both: kj hu add ... --ref <id in ${externalBoard}>`);
167
+ }
168
168
  const missing = [];
169
169
  if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
170
170
  if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
@@ -174,7 +174,7 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
174
174
  return emit({
175
175
  id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
176
176
  acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
177
- scope: hu.scope, task_type: hu.task_type,
177
+ scope: hu.scope, task_type: hu.task_type, external_ref: hu.external_ref || null,
178
178
  }, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
179
179
  }
180
180
 
@@ -113,7 +113,10 @@ export async function stewardSweepCommand({ flags = {}, config = {}, logger = co
113
113
  try {
114
114
  const sync = await syncProposedWork({ projectDir, config, results, sweptAt: report.sweptAt });
115
115
  if (!sync.synced) logger.warn?.(`⚠ steward: broken invariants not carded — ${sync.reason}`);
116
- else if (sync.created || sync.updated || sync.resolved) logger.info?.(`steward board: ${sync.created} card(s) proposed, ${sync.updated} updated, ${sync.resolved} resolved`);
116
+ else if (sync.created || sync.updated || sync.resolved) {
117
+ logger.info?.(`steward board: ${sync.created} card(s) proposed, ${sync.updated} updated, ${sync.resolved} resolved`);
118
+ if (sync.replicateIn) logger.info?.(`→ replicate them in ${sync.replicateIn} through your agent's board tools (kj hu list shows them)`);
119
+ }
117
120
  } catch (err) { logger.warn?.(`⚠ steward: carding the broken invariants failed (${err.message}) — the report and the seal stand`); }
118
121
 
119
122
  // A report nobody can see is not shared state — say it, do not fix their tree.
@@ -30,20 +30,20 @@ const TARGET_FILES = {
30
30
 
31
31
  // ENV-D1 (KJC-TSK-0642): the tracking invariant names the CHOSEN state
32
32
  // backend — a playbook that says "board or PG" makes the host guess.
33
- // KJC-TSK-0684 (issue #1287): external boards (Linear, Trello, Jira…) are
34
- // first-class — card-first points at THE project's board, worked through
35
- // the host agent's own MCP/tools. kj never mirrors it: a half-empty
36
- // parallel board is worse than none.
33
+ // KJC-BUG-0305 (issue #2042, replaces KJC-TSK-0684): the HU Board is always
34
+ // kj's board. An external board (Linear, Trello, Jira…) coexists with it: the
35
+ // host agent replicates each card there through its own MCP/tools, and
36
+ // `kj hu add --ref` links the two.
37
37
  const BACKEND_TRACKING = {
38
38
  "hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts — and `kj hu list` BEFORE `kj hu add`: reuse the existing card if one covers the work. Cards are permanent — never delete or recreate one; discard with `kj hu move <id> skipped`. `kj brief board` explains the board.",
39
- "planning-game": "Every piece of work has a tracked card in the Planning Game MCP before it starts (In Progress while you work it).",
40
39
  };
41
40
 
42
41
  const externalTracking = (boardName) =>
43
- `Every piece of work has a tracked card in ${boardName} — the project's board — before it starts (use its MCP/tools; move the card as you work it). kj does not mirror it: never create a parallel HU Board.`;
42
+ `Every piece of work has a card in the HU Board before it starts (\`kj hu list\`, then \`kj hu add --ref <id>\`) and its replica in ${boardName}, created through its MCP/tools; move both as you work it.`;
44
43
 
45
44
  function trackingLine(stateBackend, boardName) {
46
45
  if (stateBackend === "external") return externalTracking(boardName || "the project's external board");
46
+ if (stateBackend === "planning-game") return externalTracking("the Planning Game");
47
47
  return BACKEND_TRACKING[stateBackend] || BACKEND_TRACKING["hu-board"];
48
48
  }
49
49
 
@@ -26,6 +26,10 @@ const HOOKS_PREFIX = ".karajan/hooks/";
26
26
  // veredictos son locales por diseño); lo que viaja y se firma es su HUELLA,
27
27
  // dentro de una procedencia que sí está trackeada.
28
28
  const HARNESS_PREFIX = ".karajan/harness/";
29
+ // KJC-TSK-1032 (owner's decision, 13-oct): the governance config that travels
30
+ // with the repo is sealed with the supervisor, so a policy change enters only
31
+ // with the human's signed act. kj.config.yml is machine-local: CI never sees it.
32
+ export const GOVERNANCE_FILES = [".karajan/policy.yml", ".karajan/rules.yml"];
29
33
 
30
34
  const sha256 = (abs) => createHash("sha256").update(readFileSync(abs)).digest("hex");
31
35
 
@@ -79,7 +83,7 @@ export async function commitSupervisorRegeneration({
79
83
  // KJC-BUG-0244 (grebla #958): el commit sellado solo lleva el supervisor, pero
80
84
  // un stage ajeno se quedaba en esta rama y el siguiente commit caía aquí con la
81
85
  // card equivocada. Se para ANTES de pedir nada al humano y se nombra.
82
- const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE);
86
+ const foreign = run(["diff", "--cached", "--name-only"]).split("\n").filter((f) => f && !f.startsWith(HOOKS_PREFIX) && f !== PROVENANCE_FILE && !GOVERNANCE_FILES.includes(f));
83
87
  if (foreign.length > 0) {
84
88
  throw new Error(
85
89
  `harden --commit: hay cambios en el stage que no son del supervisor (${foreign.slice(0, 5).join(", ")}${foreign.length > 5 ? ", …" : ""}): commitéalos en su rama o sácalos del stage (git restore --staged <ficheros>) antes de sellar`,
@@ -88,13 +92,17 @@ export async function commitSupervisorRegeneration({
88
92
  // Capa 4 (test adversarial 6-sep: un huérfano a init con pty falso y
89
93
  // prompts a ciegas llegó hasta aquí): nonce aleatorio tecleado de vuelta.
90
94
  seal.confirm(deps.confirm);
91
- const drift = supervisorDrift({ projectDir, gitFn: run });
95
+ const tracked = new Set(run(["ls-files", "--", ...GOVERNANCE_FILES]).split("\n").filter(Boolean));
96
+ const governance = GOVERNANCE_FILES.filter((f) => existsSync(join(projectDir, f)) || tracked.has(f));
97
+ const governanceDrift = governance.length === 0 ? [] : run(["status", "--porcelain", "--", ...governance]).split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
98
+ const drift = [...supervisorDrift({ projectDir, gitFn: run }), ...governanceDrift];
92
99
  // La provenance describe SIEMPRE el estado COMPLETO del supervisor (cazado
93
100
  // en el primer estreno real: un sello parcial pisaba al anterior y dejaba
94
101
  // ficheros sin respaldo). Borrados del drift ⇒ deleted (catch de codex).
95
102
  const current = [
96
103
  ...readdirSync(join(projectDir, HOOKS_PREFIX)).map((f) => HOOKS_PREFIX + f),
97
104
  ...harnessGuards(projectDir),
105
+ ...governance.filter((f) => existsSync(join(projectDir, f))),
98
106
  ].sort();
99
107
  // KJC-BUG-0197: un guardia BORRADO se anota como borrado, igual que un hook.
100
108
  // No hacía falta para desencadenar el re-sello (la lista encoge, así que la
@@ -159,7 +167,7 @@ export async function commitSupervisorRegeneration({
159
167
  // Por DIRECTORIO, no por fichero: un rename ya staged deja la ruta vieja
160
168
  // sin existir y `git add -- <vieja>` falla; `-A` sobre el dir del
161
169
  // supervisor versiona altas, cambios, borrados y renombrados por igual.
162
- run(["add", "-A", "--", PROVENANCE_FILE, ".karajan/hooks"]);
170
+ run(["add", "-A", "--", PROVENANCE_FILE, ".karajan/hooks", ...governance]);
163
171
  run([
164
172
  "commit",
165
173
  "--no-verify",
@@ -168,6 +176,7 @@ export async function commitSupervisorRegeneration({
168
176
  "--",
169
177
  PROVENANCE_FILE,
170
178
  ".karajan/hooks",
179
+ ...governance,
171
180
  ]);
172
181
  logger.info?.(`harden --commit: ${hashed.length} fichero(s) de supervisor versionados con procedencia sellada`);
173
182
  return { committed: true, files: hashed };
@@ -31,6 +31,13 @@ export const SUPERVISOR_DEFAULTS = [
31
31
  pattern: /\.claude\/settings\.json\b|\.karajan\/(hooks|harness)\/|\.karajan\/supervisor-provenance\.json\b/,
32
32
  message: "nombra ficheros del supervisor — solo el humano los modifica, fuera de la sesión",
33
33
  },
34
+ {
35
+ // KJC-TSK-1033 (owner's decision, 13-oct): a policy change is authorized where
36
+ // it is USED (commit, CI, release), whatever wrote it: only the human's seal lifts it.
37
+ id: "defaults.governance.write",
38
+ pattern: /(^|\/)\.karajan\/(policy|rules)\.yml\b/,
39
+ message: "es configuración de gobierno: solo entra sellada por la persona (kj harden --commit, firmado desde su móvil)",
40
+ },
34
41
  ];
35
42
 
36
43
  // Tools del harness: mapeo acción → capability del kernel. Registro cerrado
@@ -11,7 +11,7 @@ import { resolve, sep } from "node:path";
11
11
  import { renderCanonicalHook } from "../harden/harden-engine.js";
12
12
  import { provenanceSignature } from "../harden/phone-sign.js";
13
13
  import { canonicalHarnessBody } from "../harden/sentinel-hooks.js";
14
- import { PROVENANCE_FILE } from "../harden/supervisor-commit.js";
14
+ import { GOVERNANCE_FILES, PROVENANCE_FILE } from "../harden/supervisor-commit.js";
15
15
 
16
16
  const HOOKS_PREFIX = ".karajan/hooks/";
17
17
  const HARNESS_PREFIX = ".karajan/harness/";
@@ -67,6 +67,15 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
67
67
  const ROOTS = [HOOKS_PREFIX, HARNESS_PREFIX].map((p) => [p, resolve(projectDir, p)]);
68
68
  for (const entry of entries) {
69
69
  if (typeof entry?.file !== "string") continue;
70
+ // KJC-TSK-1032: the governance config has no canonical render; the sealed
71
+ // hash, signed by the roster where there is one, is the proof.
72
+ if (GOVERNANCE_FILES.includes(entry.file)) {
73
+ const abs = resolve(projectDir, entry.file);
74
+ let st = null;
75
+ try { st = lstatSync(abs); } catch { /* absent */ }
76
+ if (entry.deleted === true ? st === null : st?.isFile() && sha256(readFileSync(abs)) === entry.sha256) ok.add(entry.file);
77
+ continue;
78
+ }
70
79
  const kind = ROOTS.find(([prefix]) => entry.file.startsWith(prefix));
71
80
  if (!kind) continue;
72
81
  const [prefix, root] = kind;
@@ -130,13 +139,14 @@ export function verifiedSupervisorFiles({ projectDir, trackedOnly = false }) {
130
139
  * mas laxo de lo que parece no pase por silencio (KJC-BUG-0212).
131
140
  */
132
141
  export function liftSealedSupervisorViolations({ projectDir, violations, trackedOnly = false }) {
133
- const RULE = "defaults.supervisor.write";
134
- if (!violations.some((v) => v.rule_id === RULE && v.file)) return { violations, lifted: 0 };
142
+ // KJC-TSK-1033: the governance config is lifted by the same seal.
143
+ const RULES = new Set(["defaults.supervisor.write", "defaults.governance.write"]);
144
+ if (!violations.some((v) => RULES.has(v.rule_id) && v.file)) return { violations, lifted: 0 };
135
145
  const sealed = verifiedSupervisorFiles({ projectDir, trackedOnly });
136
146
  // La violación sobre la PROPIA provenance solo se alza si la provenance
137
147
  // entera verificó — a medias no describe la verdad y sigue denegada.
138
148
  const liftable = (v) =>
139
- v.rule_id === RULE
149
+ RULES.has(v.rule_id)
140
150
  && v.file
141
151
  && (sealed.files.has(v.file) || (v.file === PROVENANCE_FILE && sealed.complete === true));
142
152
  const kept = violations.filter((v) => !liftable(v));
@@ -2,8 +2,8 @@
2
2
  * Card-first gate (KJC-TSK-0686, MG-A of épica KJC-PCS-0068): the rule
3
3
  * that work starts from a card moves from playbook text into the gate.
4
4
  * hu-board → the branch must reference a LIVE card (block by default);
5
- * planning-game/external → card-shaped reference required (warn default,
6
- * block via method_gates.card_first). Exemptions explicit and visible:
5
+ * planning-game/external → a live HU on the HU Board first, else a card-shaped
6
+ * reference (warn default, block via method_gates.card_first). Exemptions explicit and visible:
7
7
  * base branch and configured prefixes (releases). No escape (ADR 0015): a
8
8
  * gate an agent can switch off is a suggestion.
9
9
  */
@@ -40,7 +40,8 @@ async function findLiveCardInBranch(projectDir, branch) {
40
40
  for (const meta of await listPlans(projectDir)) {
41
41
  const plan = await loadPlan(projectDir, meta.planId);
42
42
  for (const h of plan.hus || []) {
43
- for (const ref of [h.short_id, h.id]) {
43
+ // KJC-BUG-0353: an HU is also named by the id of its replica in an external tracker.
44
+ for (const ref of [h.short_id, h.id, h.external_ref]) {
44
45
  if (ref && refInBranch(ref, needle)) {
45
46
  if (LIVE_STATUSES.has(h.status)) return { ref, live: true, status: h.status };
46
47
  closedHit = { ref, live: false, status: h.status };
@@ -83,6 +84,10 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
83
84
  // result names its level so nobody mistakes branch-ref for tracker.
84
85
  const boardName = config.board?.name || backend;
85
86
  const effective = policy === "auto" ? "warn" : policy;
87
+ // KJC-BUG-0353: the HU Board is always kj's board (KJC-BUG-0305) — a live HU
88
+ // the branch names, or whose replica it names, is checked here, not guessed.
89
+ const huHit = await findLiveCardInBranch(projectDir, branch);
90
+ if (huHit?.live) return { ok: true, mode: "pass", ref: huHit.ref, level: "hu-board" };
86
91
  if (CARD_REF_RE.test(branch)) {
87
92
  const ref = branch.match(CARD_REF_RE)[0];
88
93
  const { verifyFn = verifyCardAlive } = deps;
@@ -105,7 +110,8 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
105
110
  reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
106
111
  };
107
112
  }
108
- return { ok: true, mode: "pass", ref, level: v.level, ...(v.note ? { note: v.note } : {}) };
113
+ const huNote = `no live HU on the HU Board links ${ref}: kj hu add "…" --ref ${ref}`;
114
+ return { ok: true, mode: "pass", ref, level: v.level, note: v.note ? `${v.note}; ${huNote}` : huNote };
109
115
  }
110
116
  const reason = `no card reference in branch "${branch}" — card-first on ${boardName}: create the card there and name the branch after it (e.g. feat/ABC-123-summary)`;
111
117
  return effective === "block"
@@ -7,11 +7,9 @@
7
7
  * a daily sweep must never flood the board with twins — and a green
8
8
  * invariant resolves it with the green evidence.
9
9
  *
10
- * kj only writes the board it owns (hu-board). planning-game and external
11
- * boards are never mirrored (KJC-TSK-0684: a half-empty parallel board is
12
- * worse than none): there the sweep reports synced:false LOUDLY, and the
13
- * Sentinel's inevitable notice (STW-C) pushes the host agent — who does
14
- * have the board's MCP — to card them.
10
+ * KJC-BUG-0305: the HU Board is always kj's board, so the cards land there on
11
+ * every backend. With planning-game or an external board the result names it
12
+ * (replicateIn): the host agent, who has that board's MCP, replicates them.
15
13
  */
16
14
  import fs from "node:fs";
17
15
  import path from "node:path";
@@ -34,13 +32,11 @@ const cardText = (r, brokenSince) => [
34
32
  */
35
33
  export async function syncProposedWork({ projectDir, config = {}, results = [], sweptAt }) {
36
34
  const backend = config.state_backend || "hu-board";
37
- if (backend !== "hu-board") {
38
- return { synced: false, reason: `the board lives in ${backend} — kj never mirrors a board it does not own; card the broken invariants through your agent's board tools` };
39
- }
35
+ const replica = backend === "hu-board" ? {} : { replicateIn: backend };
40
36
  const map = readMap(projectDir);
41
37
  const broken = results.filter((r) => r.verdict === "broken");
42
38
  const okAgain = results.filter((r) => r.verdict === "ok" && map[r.id]);
43
- if (broken.length === 0 && okAgain.length === 0) return { synced: true, created: 0, updated: 0, resolved: 0 };
39
+ if (broken.length === 0 && okAgain.length === 0) return { synced: true, created: 0, updated: 0, resolved: 0, ...replica };
44
40
 
45
41
  const plan = await backlogPlan(projectDir);
46
42
  let created = 0, updated = 0, resolved = 0;
@@ -64,5 +60,5 @@ export async function syncProposedWork({ projectDir, config = {}, results = [],
64
60
  await savePlan(projectDir, plan);
65
61
  fs.mkdirSync(path.dirname(mapPath(projectDir)), { recursive: true });
66
62
  fs.writeFileSync(mapPath(projectDir), `${JSON.stringify(map, null, 2)}\n`);
67
- return { synced: true, created, updated, resolved };
63
+ return { synced: true, created, updated, resolved, ...replica };
68
64
  }
Binary file