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 +1 -1
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +27 -0
- package/src/commands/review-gate.js +2 -1
- package/src/commands/worktree.js +76 -0
- package/src/environment/playbook.js +7 -4
- package/src/review/one-shot-review.js +3 -0
- package/src/review/workspace.js +21 -0
package/package.json
CHANGED
|
@@ -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
|
];
|
package/src/cli/register-meta.js
CHANGED
|
@@ -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
|
-
|
|
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).
|
|
59
|
-
|
|
60
|
-
|
|
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\` ·
|
|
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
|
+
}
|