karajan-code 4.2.1 → 4.3.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.2.1",
3
+ "version": "4.3.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",
@@ -31,7 +31,7 @@ export const ADVANCED_GROUPS = [
31
31
  { title: "Análisis pre-run", commands: ["discover", "triage", "researcher", "architect", "onboard", "brief"] },
32
32
  { title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch"] },
33
33
  { title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar"] },
34
- { title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "undo", "standby"] },
34
+ { title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "worktree", "undo", "standby"] },
35
35
  { title: "Infra / setup", commands: ["install-tools", "ollama", "skills", "roles", "agents", "env"] },
36
36
  { title: "Mantenimiento", commands: ["clean", "sync", "telemetry", "report-issue"] },
37
37
  ];
@@ -23,6 +23,7 @@ import { envInstallCommand, briefCommand } from "../commands/env.js";
23
23
  import { agentRunCommand } from "../commands/agent-run.js";
24
24
  import { reportIssueCommand } from "../commands/report-issue.js";
25
25
  import { huCommand } from "../commands/hu.js";
26
+ import { worktreeCommand } from "../commands/worktree.js";
26
27
  import { addAdr, listAdrs } from "../environment/adr.js";
27
28
  import { formatAdvancedIndex } from "../commands/advanced.js";
28
29
  import { withConfig } from "./_shared.js";
@@ -155,6 +156,32 @@ export function registerMeta(program, { pkgVersion }) {
155
156
  });
156
157
 
157
158
  // AB-H (KJC-TSK-0658): board writes + repo ADRs for the brain.
159
+ // KJC-TSK-0679: isolated lanes for the host agent — ordering a command
160
+ // beats ordering a practice an agent can narrate without doing.
161
+ const wt = program.command("worktree").description("Isolated task lanes (git worktrees) for the host agent");
162
+ wt.command("start <slug>")
163
+ .description("Create a lane: worktree + feat/<slug> branch + dependency bootstrap")
164
+ .action(async (slug, flags) => {
165
+ await withConfig(pkgVersion, "worktree", flags, async ({ config }) => {
166
+ await worktreeCommand({ config, action: "start", args: [slug], flags });
167
+ });
168
+ });
169
+ wt.command("list")
170
+ .description("List the active lanes")
171
+ .action(async (flags) => {
172
+ await withConfig(pkgVersion, "worktree", flags, async ({ config }) => {
173
+ await worktreeCommand({ config, action: "list", flags });
174
+ });
175
+ });
176
+ wt.command("done <slug>")
177
+ .option("--force", "Remove the lane even with uncommitted changes")
178
+ .description("Remove a merged lane (refuses uncommitted changes without --force)")
179
+ .action(async (slug, flags) => {
180
+ await withConfig(pkgVersion, "worktree", flags, async ({ config }) => {
181
+ await worktreeCommand({ config, action: "done", args: [slug], flags });
182
+ });
183
+ });
184
+
158
185
  const hu = program.command("hu").description("Track work in the HU Board from any host agent (card first)");
159
186
  hu.command("add <title>")
160
187
  .option("--id <shortId>", "Human-readable short id")
@@ -24,7 +24,8 @@ async function rawDiff(range, extraArgs = []) {
24
24
 
25
25
  function printVerdict(record) {
26
26
  if (record.verdict === "approved") {
27
- console.log(`✓ APPROVED by ${record.reviewer} (diff ${record.diffHash.slice(0, 12)})`);
27
+ const ws = record.workspace ? ` [${record.workspace}]` : "";
28
+ console.log(`✓ APPROVED by ${record.reviewer} (diff ${record.diffHash.slice(0, 12)})${ws}`);
28
29
  if (record.summary) console.log(` ${record.summary}`);
29
30
  return;
30
31
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `kj worktree start|list|done` (KJC-TSK-0679) — isolated lanes for the
3
+ * host agent. Field case: the playbook said "one worktree per task" and an
4
+ * agent NARRATED the isolation while editing the project root. Ordering a
5
+ * command beats ordering a practice: start = worktree + feat/ branch +
6
+ * dependency bootstrap + the exact cd printed; done = remove after merge.
7
+ * Same lane dir (.kj/worktrees) and bootstrap the headless pipeline uses.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
14
+
15
+ const execFileAsync = promisify(execFile);
16
+ const WORKTREE_DIR = ".kj/worktrees"; // shared with the headless lanes
17
+
18
+ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
19
+
20
+ export async function worktreeCommand({ config = null, action, args = [], flags = {} }) {
21
+ const projectDir = config?.projectDir || process.cwd();
22
+ const git = (gitArgs, cwd = projectDir) => execFileAsync("git", gitArgs, { cwd });
23
+ const lanePath = (slug) => path.join(projectDir, WORKTREE_DIR, slug);
24
+
25
+ if (action === "start") {
26
+ const slug = String(args[0] || "").trim();
27
+ if (!SLUG_RE.test(slug)) {
28
+ throw new Error("invalid slug — use letters, digits, dots, dashes (no slashes): kj worktree start <slug>");
29
+ }
30
+ const wtPath = lanePath(slug);
31
+ if (fs.existsSync(wtPath)) {
32
+ throw new Error(`lane "${slug}" already exists — cd ${wtPath}, or finish it with: kj worktree done ${slug}`);
33
+ }
34
+ const branch = `feat/${slug}`;
35
+ await git(["worktree", "add", "-b", branch, wtPath]);
36
+ const boot = await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null });
37
+ console.log(`✓ lane ready: ${branch}`);
38
+ console.log(` cd ${wtPath}`);
39
+ console.log(" The gates travel with the repo — kj review --staged works there. When merged: kj worktree done " + slug);
40
+ return { path: wtPath, branch, bootstrap: boot };
41
+ }
42
+
43
+ if (action === "list") {
44
+ const laneRoot = path.join(projectDir, WORKTREE_DIR) + path.sep;
45
+ const { stdout } = await git(["worktree", "list", "--porcelain"]);
46
+ const rows = [];
47
+ let current = null;
48
+ for (const line of stdout.split("\n")) {
49
+ if (line.startsWith("worktree ")) current = { path: line.slice(9).trim() };
50
+ else if (line.startsWith("branch ") && current) {
51
+ current.branch = line.slice(7).trim().replace("refs/heads/", "");
52
+ if (current.path.startsWith(laneRoot)) {
53
+ rows.push({ slug: path.basename(current.path), branch: current.branch, path: current.path });
54
+ }
55
+ }
56
+ }
57
+ if (rows.length === 0) console.log("no lanes — create one with: kj worktree start <slug>");
58
+ for (const r of rows) console.log(`${r.slug.padEnd(24)} ${r.branch.padEnd(30)} ${r.path}`);
59
+ return rows;
60
+ }
61
+
62
+ if (action === "done") {
63
+ const slug = String(args[0] || "").trim();
64
+ const wtPath = lanePath(slug);
65
+ if (!fs.existsSync(wtPath)) throw new Error(`lane "${slug}" not found — see kj worktree list`);
66
+ const { stdout: dirty } = await git(["status", "--porcelain"], wtPath);
67
+ if (dirty.trim() && !flags.force) {
68
+ throw new Error(`lane "${slug}" has uncommitted changes — commit them (through the gate) or discard with --force`);
69
+ }
70
+ await git(["worktree", "remove", ...(flags.force ? ["--force"] : []), wtPath]);
71
+ console.log(`✓ lane "${slug}" removed — its branch survives; delete it after merge if you want`);
72
+ return { removed: true, slug };
73
+ }
74
+
75
+ throw new Error(`unknown action "${action}" — use: start | list | done`);
76
+ }
@@ -55,12 +55,15 @@ Invariants (the git gates enforce these — they are not suggestions):
55
55
  - Security findings are never overridable — not even by arbitration. You
56
56
  absorb the security role: \`kj brief security\` states what must be true.
57
57
  - Branch first: never commit on the base branch — every change reaches it
58
- through an atomic PR (~150 net lines, Conventional Commits). Working on
59
- more than one task, or the user needs the base tree untouched? One
60
- \`git worktree\` per task — the gates travel with the repo and work there.
58
+ through an atomic PR (~150 net lines, Conventional Commits). More than one
59
+ task, or the base tree must stay untouched? \`kj worktree start <slug>\`
60
+ (one lane per task; \`kj worktree done <slug>\` after merge). Never CLAIM
61
+ isolation without proof: in a lane, \`git rev-parse --git-dir\` differs
62
+ from \`--git-common-dir\`.
61
63
 
62
64
  Commands: \`kj rag query\` · \`kj brief <role>\` (triage, planner, researcher,
63
- architect, tester, security, audit, board) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
65
+ architect, tester, security, audit, board) · \`kj hu add|move|list\` ·
66
+ \`kj worktree start|list|done\` · \`kj adr add|list\` ·
64
67
  \`kj review --staged\` · \`kj review --check\` · \`kj solomon --position\` ·
65
68
  \`kj agent run <agent>\` · \`kj report\` · \`kj check\`
66
69
 
@@ -15,6 +15,7 @@ import { resolveReviewProfile } from "./profiles.js";
15
15
  import { parseMaybeJsonString } from "./parser.js";
16
16
  import { detectAvailableAgents, detectHostAgent } from "../utils/agent-detect.js";
17
17
  import { saveVerdict } from "./verdict-store.js";
18
+ import { detectWorkspace } from "./workspace.js";
18
19
 
19
20
  // Cross-AI preference when the configured reviewer IS the host.
20
21
  const CROSS_ORDER = ["codex", "claude", "gemini", "opencode", "aider"];
@@ -77,6 +78,8 @@ export async function runOneShotReview({
77
78
  verdict: parsed.approved ? "approved" : "rejected",
78
79
  reviewer,
79
80
  host: hostAgent || null,
81
+ // KJC-TSK-0680: where the review ran — makes any isolation claim auditable.
82
+ workspace: await detectWorkspace(projectDir),
80
83
  issues: parsed.blocking_issues || [],
81
84
  suggestions: parsed.non_blocking_suggestions || [],
82
85
  summary: parsed.summary || parsed.raw_summary || "",
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Workspace detection for the review gate (KJC-TSK-0680). Field case: an
3
+ * agent claimed worktree isolation while editing the project root. The
4
+ * verdict now stamps WHERE the review actually ran, using the same proof
5
+ * the playbook demands: in a linked worktree, `git rev-parse --git-dir`
6
+ * differs from `--git-common-dir`. "root" | "worktree:<name>" | null
7
+ * (not a git repo — the gate itself will fail later with its own error).
8
+ */
9
+ import path from "node:path";
10
+ import { runCommand } from "../utils/process.js";
11
+
12
+ export async function detectWorkspace(dir) {
13
+ const res = await runCommand("git", ["rev-parse", "--git-dir", "--git-common-dir", "--show-toplevel"], { cwd: dir });
14
+ if (res.exitCode !== 0) return null;
15
+ const [gitDir, commonDir, toplevel] = String(res.stdout).trim().split("\n").map((l) => l.trim());
16
+ if (!gitDir || !commonDir) return null;
17
+ const resolvedGit = path.resolve(dir, gitDir);
18
+ const resolvedCommon = path.resolve(dir, commonDir);
19
+ if (resolvedGit === resolvedCommon) return "root";
20
+ return `worktree:${path.basename(toplevel || dir)}`;
21
+ }