karajan-code 4.2.1 → 4.3.1
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 +32 -1
- package/src/commands/hu.js +24 -3
- 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/templates/coder-rules.md +8 -0
- package/templates/kj.config.yml +8 -0
- package/templates/review-rules.md +6 -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,10 +156,40 @@ 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")
|
|
161
|
-
.option("--
|
|
188
|
+
.option("--ac <criterion>", "Acceptance criterion (repeatable; multiline strings split per line)", (v, acc) => [...acc, v], [])
|
|
189
|
+
.option("--tests <test>", "Acceptance test (repeatable; multiline strings split per line)", (v, acc) => [...acc, v], [])
|
|
190
|
+
.option("--scope <text>", "Scope of the story (files/areas)")
|
|
191
|
+
.option("--task-type <type>", "sw | infra | doc | add-tests | refactor")
|
|
192
|
+
.option("--criteria <text>", "Legacy alias for a single acceptance criterion")
|
|
162
193
|
.option("--json", "Machine-readable output")
|
|
163
194
|
.action(async (title, flags) => {
|
|
164
195
|
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
package/src/commands/hu.js
CHANGED
|
@@ -122,15 +122,36 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
// KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
|
|
126
|
+
// are repeatable AND accept multiline strings; --criteria stays as the
|
|
127
|
+
// legacy single-criterion alias.
|
|
128
|
+
const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
|
|
129
|
+
if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
|
|
130
|
+
throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
const splitLines = (v) => (Array.isArray(v) ? v : v ? [v] : [])
|
|
133
|
+
.flatMap((s) => String(s).split("\n")).map((s) => s.trim()).filter(Boolean);
|
|
125
134
|
const hu = addHu(plan, {
|
|
126
135
|
title,
|
|
127
136
|
short_id: flags.id || null,
|
|
128
|
-
acceptance_criteria: flags.
|
|
137
|
+
acceptance_criteria: [...splitLines(flags.ac), ...splitLines(flags.criteria)],
|
|
138
|
+
acceptance_tests: splitLines(flags.tests),
|
|
139
|
+
scope: flags.scope || null,
|
|
140
|
+
task_type: flags.taskType || undefined,
|
|
129
141
|
});
|
|
130
142
|
hu.created_by = creatorLabel();
|
|
131
143
|
await savePlan(projectDir, plan);
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
const missing = [];
|
|
145
|
+
if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
|
|
146
|
+
if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
|
|
147
|
+
if (missing.length > 0) {
|
|
148
|
+
console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
return emit({
|
|
151
|
+
id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
|
|
152
|
+
acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
|
|
153
|
+
scope: hu.scope, task_type: hu.task_type,
|
|
154
|
+
}, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
if (action === "move") {
|
|
@@ -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
|
+
}
|
package/templates/coder-rules.md
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
- Do not modify code unrelated to the task.
|
|
24
24
|
- Follow existing code conventions and patterns in the repository.
|
|
25
25
|
|
|
26
|
+
## Environment isolation (parallel runs)
|
|
27
|
+
|
|
28
|
+
- Never hardcode ports, DB names, or docker project names — read them from env
|
|
29
|
+
vars, documenting each one in `.env.example`. When `KJ_LANE_SLOT` /
|
|
30
|
+
`KJ_PORT_OFFSET` exist in the env, derive per-task values from them so
|
|
31
|
+
parallel lanes never collide.
|
|
32
|
+
- Never commit a real `.env` — only `.env.example` is versioned.
|
|
33
|
+
|
|
26
34
|
## Tests location (MANDATORY)
|
|
27
35
|
|
|
28
36
|
- Tests live in a top-level `tests/` directory mirroring `src/` (e.g. a function in
|
package/templates/kj.config.yml
CHANGED
|
@@ -118,3 +118,11 @@ session:
|
|
|
118
118
|
# when exceeded; resume with `kj resume`. Set to null to remove the cap.
|
|
119
119
|
max_budget_usd: 5
|
|
120
120
|
fail_fast_repeats: 2
|
|
121
|
+
# Per-lane setup for parallel runs (worktrees). Runs INSIDE the fresh
|
|
122
|
+
# worktree instead of the default `npm ci`. Each lane receives
|
|
123
|
+
# KJ_LANE_SLOT (0,1,2…) and KJ_PORT_OFFSET (slot-derived) in its env —
|
|
124
|
+
# derive ports, DB names and docker project names from them so lanes
|
|
125
|
+
# never collide. The coder never orchestrates git; isolation is declared
|
|
126
|
+
# here. Example:
|
|
127
|
+
# worktree_setup: "npm ci && docker compose -p app_lane_$KJ_LANE_SLOT up -d db"
|
|
128
|
+
worktree_setup: null
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
- Only raise blocking issues for concrete production risks.
|
|
5
5
|
- Keep non-blocking suggestions separate.
|
|
6
6
|
|
|
7
|
+
## Environment isolation
|
|
8
|
+
|
|
9
|
+
- Reject changes that hardcode ports, DB names or service hostnames instead of
|
|
10
|
+
reading them from env vars, and any diff that adds a real `.env` (only
|
|
11
|
+
`.env.example` is versioned).
|
|
12
|
+
|
|
7
13
|
## File overwrite detection (BLOCKING)
|
|
8
14
|
|
|
9
15
|
- If the diff shows an entire file was replaced (massive deletions + additions instead of targeted edits), flag it as BLOCKING.
|