karajan-code 3.10.2 → 3.12.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/scripts/esbuild-sea.config.mjs +3 -0
- package/scripts/install-binary.sh +2 -11
- package/src/checks/rag-hooks.js +54 -0
- package/src/cli/register-pipeline.js +1 -1
- package/src/commands/doctor.js +2 -0
- package/src/commands/install-tools.js +203 -17
- package/src/commands/rag.js +3 -2
- package/src/rag/auto-update.js +29 -7
- package/src/utils/binary-sources.js +64 -0
- package/src/utils/docker-install.js +34 -0
- package/src/utils/install-hints.js +28 -0
- package/src/utils/tool-installer.js +93 -0
- package/templates/roles/coder.md +39 -3
- package/templates/roles/reviewer.md +2 -2
- package/templates/roles/spec-reviewer.md +5 -0
package/package.json
CHANGED
|
@@ -192,6 +192,9 @@ const ragStubPlugin = {
|
|
|
192
192
|
// reached from \`kj rag install-hooks\`, so it degrades like the rest.
|
|
193
193
|
maybeAutoUpdate: async () => ({ skipped: true }),
|
|
194
194
|
installPostMergeHook: notAvailable,
|
|
195
|
+
// KJC-BUG-0100 — the doctor rag-hooks check imports this; it throws
|
|
196
|
+
// here and the check swallows it (degrades to a benign info result).
|
|
197
|
+
resolveHooksDir: notAvailable,
|
|
195
198
|
default: notAvailable,
|
|
196
199
|
};
|
|
197
200
|
`,
|
|
@@ -36,17 +36,8 @@ esac
|
|
|
36
36
|
target="${os}-${arch}"
|
|
37
37
|
case "$target" in
|
|
38
38
|
linux-x64) ;;
|
|
39
|
-
darwin-arm64)
|
|
40
|
-
|
|
41
|
-
# KJC-BUG-0096: the darwin-arm64 SEA build crashes under smoke-test).
|
|
42
|
-
# Degrade gracefully to the npm install path instead of trying to
|
|
43
|
-
# download an asset that does not exist.
|
|
44
|
-
echo "kj-install: the macOS standalone binary is not available yet." >&2
|
|
45
|
-
echo "kj-install: install with npm instead (requires Node 18+):" >&2
|
|
46
|
-
echo " npm install -g karajan-code" >&2
|
|
47
|
-
exit 0
|
|
48
|
-
;;
|
|
49
|
-
*) die "no prebuilt binary for '$target'. Available: linux-x64. On macOS use npm instead: npm install -g karajan-code" ;;
|
|
39
|
+
darwin-arm64) ;;
|
|
40
|
+
*) die "no prebuilt binary for '$target'. Available: linux-x64, darwin-arm64. Use npm instead: npm install -g karajan-code" ;;
|
|
50
41
|
esac
|
|
51
42
|
|
|
52
43
|
# --- Resolve the download URL for the requested version (or latest). ---
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG post-merge hook wiring check (KJC-BUG-0100).
|
|
3
|
+
*
|
|
4
|
+
* `kj rag install-hooks` used to write the hook into `.git/hooks/post-merge`,
|
|
5
|
+
* but when a repo is hardened `git config core.hooksPath` points at
|
|
6
|
+
* `.karajan/hooks` and git ignores `.git/hooks/` entirely — so the RAG index
|
|
7
|
+
* silently never refreshes after a merge. This check resolves the *effective*
|
|
8
|
+
* hooks dir and reports whether the post-merge hook there reindexes the RAG.
|
|
9
|
+
* The pre-run drift check in `kj run` already covers the gap, so a missing
|
|
10
|
+
* hook is advisory (info), not a failure — it surfaces the state without
|
|
11
|
+
* nagging every repo that never ran `kj rag install-hooks`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { STRATEGY } from "./types.js";
|
|
17
|
+
import { resolveHooksDir } from "../rag/auto-update.js";
|
|
18
|
+
|
|
19
|
+
export function createRagHooksCheck({ projectDir } = {}) {
|
|
20
|
+
return {
|
|
21
|
+
name: "rag-hooks",
|
|
22
|
+
label: "RAG post-merge refresh",
|
|
23
|
+
strategy: STRATEGY.MANUAL,
|
|
24
|
+
async detect() {
|
|
25
|
+
const dir = projectDir || process.cwd();
|
|
26
|
+
let hooks;
|
|
27
|
+
try {
|
|
28
|
+
hooks = await resolveHooksDir(dir);
|
|
29
|
+
} catch {
|
|
30
|
+
return { ok: true, severity: "info", detail: "not a git repository — no post-merge hook to wire" };
|
|
31
|
+
}
|
|
32
|
+
const target = join(hooks.dir, "post-merge");
|
|
33
|
+
const wired = existsSync(target) && /kj\s+rag\s+index/.test(readFileSync(target, "utf8"));
|
|
34
|
+
if (wired) {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
severity: "info",
|
|
38
|
+
detail: `post-merge reindexes the RAG (${hooks.hooksPath || ".git/hooks"})`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const where = hooks.hooksPath ? ` — core.hooksPath=${hooks.hooksPath}` : "";
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
severity: "info",
|
|
45
|
+
detail: `RAG index won't auto-refresh after merges${where}; pre-run drift check still covers \`kj run\``,
|
|
46
|
+
fix: "Wire the post-merge hook: `kj rag install-hooks`",
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getRagHooksChecks({ projectDir } = {}) {
|
|
53
|
+
return [createRagHooksCheck({ projectDir })];
|
|
54
|
+
}
|
|
@@ -226,7 +226,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
226
226
|
|
|
227
227
|
program
|
|
228
228
|
.command("install-tools")
|
|
229
|
-
.description("Install external audit tools (semgrep, osv-scanner, lighthouse, docker, sonar) using the package manager available on your system")
|
|
229
|
+
.description("Install the tools kj needs (git, agent CLI: claude+codex) plus the external audit tools (semgrep, osv-scanner, lighthouse, docker, sonar) using the package manager available on your system")
|
|
230
230
|
.option("--only <tools>", "Comma-separated subset (e.g. \"semgrep,osv-scanner\"). Bypasses stack-gating.")
|
|
231
231
|
.option("-y, --yes", "Auto-accept all prompts (non-interactive)")
|
|
232
232
|
.option("--dry-run", "Show what would be installed without running anything")
|
package/src/commands/doctor.js
CHANGED
|
@@ -25,6 +25,7 @@ import { getRtkChecks } from "../checks/rtk.js";
|
|
|
25
25
|
import { getSqueezrChecks } from "../checks/squeezr.js";
|
|
26
26
|
import { getAiTrashChecks } from "../checks/ai-trash.js";
|
|
27
27
|
import { getQmdChecks } from "../checks/qmd.js";
|
|
28
|
+
import { getRagHooksChecks } from "../checks/rag-hooks.js";
|
|
28
29
|
import { getNodeChecks } from "../checks/node.js";
|
|
29
30
|
import { getNativeBuildChecks } from "../checks/native-build.js";
|
|
30
31
|
import { getPortChecks } from "../checks/ports.js";
|
|
@@ -72,6 +73,7 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
72
73
|
...getSqueezrChecks(),
|
|
73
74
|
...getAiTrashChecks(),
|
|
74
75
|
...getQmdChecks(),
|
|
76
|
+
...getRagHooksChecks({ projectDir }),
|
|
75
77
|
...getProjectChecks({ projectDir }),
|
|
76
78
|
];
|
|
77
79
|
}
|
|
@@ -14,15 +14,29 @@
|
|
|
14
14
|
import readline from "node:readline";
|
|
15
15
|
import { execFile } from "node:child_process";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
|
+
import os from "node:os";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
import fs from "node:fs/promises";
|
|
19
20
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
20
|
-
import { getInstallHint, detectPackageManagers, appliesToStack } from "../utils/install-hints.js";
|
|
21
|
+
import { getInstallHint, detectPackageManagers, appliesToStack, gitInstallPlan } from "../utils/install-hints.js";
|
|
21
22
|
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
23
|
+
import { resolveStandalone } from "../utils/binary-sources.js";
|
|
24
|
+
import { downloadBinary, binDir, runInstallCommand } from "../utils/tool-installer.js";
|
|
25
|
+
import { dockerInstallPlan } from "../utils/docker-install.js";
|
|
22
26
|
|
|
23
27
|
const execFileAsync = promisify(execFile);
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
// git and the agent CLI lead the list: both are `kj doctor` *required* tools,
|
|
30
|
+
// so a blank machine wants them before the optional audit tools.
|
|
31
|
+
const ALL_TOOLS = ["git", "agent-cli", "semgrep", "osv-scanner", "lighthouse", "docker", "sonar"];
|
|
32
|
+
|
|
33
|
+
// The default pipeline is coder=claude, reviewer=codex, so `agent-cli` installs
|
|
34
|
+
// exactly those two. gemini stays out of the default: it is a supported
|
|
35
|
+
// reviewer, not a required one. Both ship as global npm packages.
|
|
36
|
+
const AGENT_CLIS = [
|
|
37
|
+
{ bin: "claude", pkg: "@anthropic-ai/claude-code", url: "https://docs.anthropic.com/en/docs/claude-code" },
|
|
38
|
+
{ bin: "codex", pkg: "@openai/codex", url: "https://github.com/openai/codex" },
|
|
39
|
+
];
|
|
26
40
|
|
|
27
41
|
function parseOnlyList(raw) {
|
|
28
42
|
if (!raw) return null;
|
|
@@ -52,11 +66,100 @@ async function isInstalled(tool) {
|
|
|
52
66
|
// sonar is a Docker container, not a binary. We check it separately
|
|
53
67
|
// by looking at running containers via `docker ps`.
|
|
54
68
|
if (tool === "sonar") return checkSonarRunning();
|
|
69
|
+
// agent-cli is "installed" only when BOTH default-pipeline CLIs are present;
|
|
70
|
+
// a partial install (one of two) still routes to handleAgentCli.
|
|
71
|
+
if (tool === "agent-cli") {
|
|
72
|
+
const present = await Promise.all(AGENT_CLIS.map(async (c) => (await checkBinary(c.bin)).ok));
|
|
73
|
+
return present.every(Boolean);
|
|
74
|
+
}
|
|
55
75
|
if (tool === "lighthouse") return (await checkBinary("lighthouse")).ok;
|
|
56
76
|
if (tool === "docker") return (await checkBinary("docker")).ok;
|
|
57
77
|
return (await checkBinary(tool)).ok;
|
|
58
78
|
}
|
|
59
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Agent-CLI handler. Installs whichever of the default-pipeline CLIs
|
|
82
|
+
* (coder=claude, reviewer=codex) are missing, via global npm. Opt-in with the
|
|
83
|
+
* exact command shown first. When npm itself is absent we surface the manual
|
|
84
|
+
* commands + URLs rather than failing silently.
|
|
85
|
+
*/
|
|
86
|
+
async function handleAgentCli({ available, dryRun, yes, logger }) {
|
|
87
|
+
const missing = [];
|
|
88
|
+
for (const cli of AGENT_CLIS) {
|
|
89
|
+
if (!(await checkBinary(cli.bin)).ok) missing.push(cli);
|
|
90
|
+
}
|
|
91
|
+
if (missing.length === 0) return { tool: "agent-cli", action: "already-installed" };
|
|
92
|
+
|
|
93
|
+
if (!available.npm) {
|
|
94
|
+
const commands = missing.map((c) => `npm install -g ${c.pkg}`);
|
|
95
|
+
logger.warn?.(`✗ agent-cli: npm not found — install manually: ${commands.join(" ; ")}`);
|
|
96
|
+
return { tool: "agent-cli", action: "manual", reason: "npm not available", missing: missing.map((c) => c.bin), commands, manualUrls: missing.map((c) => c.url) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const installed = [];
|
|
100
|
+
for (const cli of missing) {
|
|
101
|
+
const command = `npm install -g ${cli.pkg}`;
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
logger.info?.(`▸ ${cli.bin}: would run \`${command}\``);
|
|
104
|
+
installed.push({ bin: cli.bin, action: "dry-run", command });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const proceed = yes || await promptYesNo(`Install ${cli.bin} with: ${command}?`);
|
|
108
|
+
if (!proceed) {
|
|
109
|
+
logger.info?.(`⊘ ${cli.bin}: declined`);
|
|
110
|
+
installed.push({ bin: cli.bin, action: "declined", command });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
logger.info?.(`▸ ${cli.bin}: running \`${command}\`...`);
|
|
114
|
+
const r = await runInstallCommand(command, { interactive: false });
|
|
115
|
+
if (r.ok) { logger.info?.(`✓ ${cli.bin}: installed`); installed.push({ bin: cli.bin, action: "installed", command }); }
|
|
116
|
+
else { const err = r.error || `exit ${r.code}`; logger.warn?.(`✗ ${cli.bin}: install failed (${err})`); installed.push({ bin: cli.bin, action: "failed", command, error: err }); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const action = dryRun ? "dry-run"
|
|
120
|
+
: installed.some((r) => r.action === "failed") ? "failed"
|
|
121
|
+
: installed.some((r) => r.action === "installed") ? "installed"
|
|
122
|
+
: "declined";
|
|
123
|
+
return { tool: "agent-cli", action, installed };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* git handler. git installs through the OS package manager; on Linux that
|
|
128
|
+
* needs root, so those runs go through the interactive (tty-inherited) path
|
|
129
|
+
* where sudo prompts the user directly — kj never captures the password. The
|
|
130
|
+
* exact command is shown first and the install is opt-in, defaulting to NO for
|
|
131
|
+
* the privileged case. When no package manager matches, we surface the manual
|
|
132
|
+
* download URL instead of failing silently.
|
|
133
|
+
*/
|
|
134
|
+
async function handleGit({ available, dryRun, yes, logger }) {
|
|
135
|
+
const plan = gitInstallPlan(available);
|
|
136
|
+
if (!plan.command) {
|
|
137
|
+
logger.warn?.(`✗ git: no supported package manager here — install manually: ${plan.manualUrl}`);
|
|
138
|
+
return { tool: "git", action: "manual", reason: "no supported package manager found", manualUrl: plan.manualUrl };
|
|
139
|
+
}
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
logger.info?.(`▸ git: would run \`${plan.command}\` (manager: ${plan.manager}${plan.needsSudo ? ", needs sudo" : ""})`);
|
|
142
|
+
return { tool: "git", action: "dry-run", command: plan.command, manager: plan.manager };
|
|
143
|
+
}
|
|
144
|
+
if (plan.needsSudo) {
|
|
145
|
+
logger.warn?.("git: this step needs sudo — you'll be prompted for your password on your own terminal (kj never sees it).");
|
|
146
|
+
}
|
|
147
|
+
const proceed = yes || await promptYesNo(`Install git with: ${plan.command}?`, !plan.needsSudo);
|
|
148
|
+
if (!proceed) {
|
|
149
|
+
logger.info?.("⊘ git: declined");
|
|
150
|
+
return { tool: "git", action: "declined", command: plan.command };
|
|
151
|
+
}
|
|
152
|
+
logger.info?.(`▸ git: running \`${plan.command}\`...`);
|
|
153
|
+
const r = await runInstallCommand(plan.command, { interactive: plan.needsSudo });
|
|
154
|
+
if (r.ok) {
|
|
155
|
+
logger.info?.(`✓ git: installed via ${plan.manager}`);
|
|
156
|
+
return { tool: "git", action: "installed", manager: plan.manager };
|
|
157
|
+
}
|
|
158
|
+
const err = r.error || `exit ${r.code}`;
|
|
159
|
+
logger.warn?.(`✗ git: install failed (${err})`);
|
|
160
|
+
return { tool: "git", action: "failed", command: plan.command, error: err };
|
|
161
|
+
}
|
|
162
|
+
|
|
60
163
|
async function checkSonarRunning() {
|
|
61
164
|
try {
|
|
62
165
|
const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{.Names}}", "--filter", "name=sonarqube"], { timeout: 5000 });
|
|
@@ -85,7 +188,7 @@ async function handleSonar({ available, dryRun }) {
|
|
|
85
188
|
return {
|
|
86
189
|
tool: "sonar",
|
|
87
190
|
action: "manual",
|
|
88
|
-
reason: "
|
|
191
|
+
reason: "SonarQube runs as a Docker container — install Docker first (kj install-tools --only docker), then re-run.",
|
|
89
192
|
manualUrl: "https://docs.docker.com/get-docker/",
|
|
90
193
|
};
|
|
91
194
|
}
|
|
@@ -104,17 +207,80 @@ async function handleSonar({ available, dryRun }) {
|
|
|
104
207
|
}
|
|
105
208
|
|
|
106
209
|
/**
|
|
107
|
-
* Docker handler
|
|
108
|
-
*
|
|
210
|
+
* Docker handler. On macOS/Windows we never auto-install — point at the docs.
|
|
211
|
+
* On Linux we offer the distro package manager (apt/dnf) or, failing that, the
|
|
212
|
+
* official convenience script downloaded to a file. The install runs through
|
|
213
|
+
* `sudo` on the user's own tty (kj never sees the password) and is opt-in,
|
|
214
|
+
* defaulting to NO, with the exact command shown first.
|
|
109
215
|
*/
|
|
110
|
-
function handleDocker(available) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
reason: "Docker install is platform-specific — see official docs.",
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
216
|
+
async function handleDocker({ available, dryRun, yes, logger }) {
|
|
217
|
+
const plan = dockerInstallPlan(process.platform, available);
|
|
218
|
+
if (plan.kind === "manual") {
|
|
219
|
+
logger.info?.(`▸ docker: ${plan.suggested || plan.manualUrl}`);
|
|
220
|
+
return { tool: "docker", action: "manual", reason: "Docker install is platform-specific — see official docs.", manualUrl: plan.manualUrl, suggested: plan.suggested };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const shownCommand = plan.kind === "package" ? plan.command : `sudo sh <get-docker.sh from ${plan.url}>`;
|
|
224
|
+
if (dryRun) {
|
|
225
|
+
logger.info?.(`▸ docker: would run \`${shownCommand}\` (invasive, needs sudo)`);
|
|
226
|
+
return { tool: "docker", action: "dry-run", command: shownCommand, via: plan.kind };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
logger.warn?.("docker: this step is invasive and needs sudo — you'll be prompted for your password on your own terminal (kj never sees it).");
|
|
230
|
+
const proceed = yes || await promptYesNo(`Install Docker with: ${shownCommand}?`, false);
|
|
231
|
+
if (!proceed) {
|
|
232
|
+
logger.info?.("⊘ docker: declined");
|
|
233
|
+
return { tool: "docker", action: "declined", command: shownCommand };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (plan.kind === "package") {
|
|
237
|
+
logger.info?.(`▸ docker: running \`${plan.command}\`...`);
|
|
238
|
+
const r = await runInstallCommand(plan.command, { interactive: true });
|
|
239
|
+
if (r.ok) { logger.info?.(`✓ docker: installed via ${plan.manager}`); return { tool: "docker", action: "installed", via: "package", manager: plan.manager }; }
|
|
240
|
+
const err = r.error || `exit ${r.code}`;
|
|
241
|
+
logger.warn?.(`✗ docker: install failed (${err})`);
|
|
242
|
+
return { tool: "docker", action: "failed", via: "package", error: err };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// script route: download to a temp file, then run with sudo — never `curl | sh`.
|
|
246
|
+
const dl = await downloadBinary({ url: plan.url, name: "get-docker.sh", dir: os.tmpdir() });
|
|
247
|
+
if (!dl.ok) { logger.warn?.(`✗ docker: script download failed (${dl.error})`); return { tool: "docker", action: "failed", via: "script", error: dl.error }; }
|
|
248
|
+
logger.info?.(`▸ docker: running \`sudo sh ${dl.dest}\`...`);
|
|
249
|
+
const r = await runInstallCommand(`sudo sh ${dl.dest}`, { interactive: true });
|
|
250
|
+
if (r.ok) { logger.info?.("✓ docker: installed via get.docker.com"); return { tool: "docker", action: "installed", via: "script", dest: dl.dest }; }
|
|
251
|
+
const err = r.error || `exit ${r.code}`;
|
|
252
|
+
logger.warn?.(`✗ docker: install failed (${err})`);
|
|
253
|
+
return { tool: "docker", action: "failed", via: "script", error: err };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Standalone route when no package manager matched: download a static binary
|
|
258
|
+
* (osv-scanner) or surface a concrete command to run (semgrep).
|
|
259
|
+
*/
|
|
260
|
+
async function handleStandalone({ tool, standalone, manualUrl, dryRun, yes, logger }) {
|
|
261
|
+
if (standalone.kind === "command") {
|
|
262
|
+
logger.warn?.(`✗ ${tool}: no package manager here. Try: ${standalone.command} · \`kj doctor\` shows what's missing (docs: ${manualUrl})`);
|
|
263
|
+
return { tool, action: "manual", reason: "no package manager — suggested route below", suggested: standalone.command, via: standalone.via, manualUrl };
|
|
264
|
+
}
|
|
265
|
+
// kind === "binary"
|
|
266
|
+
const { url, name } = standalone;
|
|
267
|
+
if (dryRun) {
|
|
268
|
+
logger.info?.(`▸ ${tool}: would download ${url} → ${binDir()}`);
|
|
269
|
+
return { tool, action: "dry-run", command: `download ${url}`, via: "binary" };
|
|
270
|
+
}
|
|
271
|
+
const proceed = yes || await promptYesNo(`Download ${tool} from ${url} into ${binDir()}?`);
|
|
272
|
+
if (!proceed) {
|
|
273
|
+
logger.info?.(`⊘ ${tool}: declined`);
|
|
274
|
+
return { tool, action: "declined", command: `download ${url}` };
|
|
275
|
+
}
|
|
276
|
+
logger.info?.(`▸ ${tool}: downloading static binary...`);
|
|
277
|
+
const r = await downloadBinary({ url, name });
|
|
278
|
+
if (r.ok) {
|
|
279
|
+
logger.info?.(`✓ ${tool}: installed → ${r.dest} (ensure ${binDir()} is on PATH)`);
|
|
280
|
+
return { tool, action: "installed", via: "binary", dest: r.dest };
|
|
281
|
+
}
|
|
282
|
+
logger.warn?.(`✗ ${tool}: download failed (${r.error})`);
|
|
283
|
+
return { tool, action: "failed", via: "binary", error: r.error };
|
|
118
284
|
}
|
|
119
285
|
|
|
120
286
|
/**
|
|
@@ -156,7 +322,8 @@ export async function installToolsCommand(opts = {}) {
|
|
|
156
322
|
if (tool === "sonar") {
|
|
157
323
|
const result = await handleSonar({ available, dryRun });
|
|
158
324
|
results.push(result);
|
|
159
|
-
logger.
|
|
325
|
+
if (result.action === "manual") logger.warn?.(`✗ sonar: ${result.reason}`);
|
|
326
|
+
else logger.info?.(`▸ sonar: ${result.command}`);
|
|
160
327
|
if (!dryRun && result.action === "ready") {
|
|
161
328
|
const proceed = yes || await promptYesNo(`Start SonarQube with: ${result.command}?`);
|
|
162
329
|
if (proceed) {
|
|
@@ -168,18 +335,37 @@ export async function installToolsCommand(opts = {}) {
|
|
|
168
335
|
continue;
|
|
169
336
|
}
|
|
170
337
|
|
|
338
|
+
if (tool === "git") {
|
|
339
|
+
const result = await handleGit({ available, dryRun, yes, logger });
|
|
340
|
+
results.push(result);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (tool === "agent-cli") {
|
|
345
|
+
const result = await handleAgentCli({ available, dryRun, yes, logger });
|
|
346
|
+
results.push(result);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
171
350
|
if (tool === "docker") {
|
|
172
|
-
const result = handleDocker(available);
|
|
351
|
+
const result = await handleDocker({ available, dryRun, yes, logger });
|
|
173
352
|
results.push(result);
|
|
174
|
-
logger.info?.(`▸ docker: ${result.suggested || result.manualUrl}`);
|
|
175
353
|
continue;
|
|
176
354
|
}
|
|
177
355
|
|
|
178
356
|
// semgrep / osv-scanner / lighthouse — package-manager driven.
|
|
179
357
|
const hint = await getInstallHint(tool, available);
|
|
180
358
|
if (!hint.command || !hint.manager) {
|
|
359
|
+
// No package manager matched — try a standalone route (static binary
|
|
360
|
+
// for osv-scanner, a concrete command for semgrep) before giving up.
|
|
361
|
+
const standalone = resolveStandalone(tool, available);
|
|
362
|
+
if (standalone) {
|
|
363
|
+
const result = await handleStandalone({ tool, standalone, manualUrl: hint.manualUrl, dryRun, yes, logger });
|
|
364
|
+
results.push(result);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
181
367
|
results.push({ tool, action: "manual", reason: "no compatible package manager found", manualUrl: hint.manualUrl, suggested: hint.command });
|
|
182
|
-
logger.warn?.(`✗ ${tool}: no
|
|
368
|
+
logger.warn?.(`✗ ${tool}: no automatic install route here. Run \`kj doctor\` for options, or install manually: ${hint.manualUrl}`);
|
|
183
369
|
continue;
|
|
184
370
|
}
|
|
185
371
|
if (dryRun) {
|
package/src/commands/rag.js
CHANGED
|
@@ -64,9 +64,10 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
64
64
|
// who never run it.
|
|
65
65
|
export async function ragInstallHooksCommand({ config, logger, flags = {} }) {
|
|
66
66
|
const projectDir = config?.projectDir || process.cwd();
|
|
67
|
-
const res = installPostMergeHook({ projectDir, logger });
|
|
67
|
+
const res = await installPostMergeHook({ projectDir, logger });
|
|
68
68
|
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
69
|
-
else if (res.installed) logger.info(`[rag] installed post-merge hook at ${res.target}`);
|
|
69
|
+
else if (res.installed) logger.info(`[rag] installed post-merge hook at ${res.target}${res.hooksPath ? ` (core.hooksPath=${res.hooksPath})` : ""}`);
|
|
70
|
+
else if (res.covered) logger.info(`[rag] ${res.target} already reindexes the RAG (kj-managed); nothing to do`);
|
|
70
71
|
return res;
|
|
71
72
|
}
|
|
72
73
|
|
package/src/rag/auto-update.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Keeps the local RAG index aligned with HEAD so retrieval never serves
|
|
3
3
|
// stale code without the user having to run `kj rag index` by hand.
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { execa } from "execa";
|
|
8
8
|
|
|
@@ -34,17 +34,39 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
34
34
|
} finally { db.close(); }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// KJC-BUG-0100 — Resolve the hooks dir git actually honours. When a repo is
|
|
38
|
+
// hardened `git config core.hooksPath` points at `.karajan/hooks` and git
|
|
39
|
+
// ignores `.git/hooks/` entirely, so a hook written there never fires.
|
|
40
|
+
export async function resolveHooksDir(projectDir) {
|
|
39
41
|
if (!existsSync(join(projectDir, ".git"))) throw new Error(`Not a git repository: ${projectDir}`);
|
|
42
|
+
let hooksPath = "";
|
|
43
|
+
try {
|
|
44
|
+
const r = await execa("git", ["-C", projectDir, "config", "--local", "core.hooksPath"]);
|
|
45
|
+
hooksPath = r.stdout.trim();
|
|
46
|
+
} catch { /* unset → git uses .git/hooks */ }
|
|
47
|
+
if (hooksPath) {
|
|
48
|
+
return { dir: isAbsolute(hooksPath) ? hooksPath : join(projectDir, hooksPath), hooksPath };
|
|
49
|
+
}
|
|
50
|
+
return { dir: join(projectDir, ".git", "hooks"), hooksPath: "" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function installPostMergeHook({ projectDir, logger = console } = {}) {
|
|
54
|
+
const { dir: hooksDir, hooksPath } = await resolveHooksDir(projectDir);
|
|
40
55
|
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
41
56
|
const target = join(hooksDir, "post-merge");
|
|
42
57
|
const src = readFileSync(HOOK_SRC, "utf8");
|
|
43
|
-
if (existsSync(target)
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
if (existsSync(target)) {
|
|
59
|
+
const current = readFileSync(target, "utf8");
|
|
60
|
+
// Another kj-managed hook (e.g. harden's post-merge) already reindexes.
|
|
61
|
+
if (!current.includes("KJC-TSK-0455") && /kj\s+rag\s+index/.test(current)) {
|
|
62
|
+
return { skipped: true, covered: true, target, hooksPath };
|
|
63
|
+
}
|
|
64
|
+
if (!current.includes("KJC-TSK-0455")) {
|
|
65
|
+
logger.warn?.(`[rag] ${target} already exists and was not installed by kj; leaving untouched`);
|
|
66
|
+
return { skipped: true, target, hooksPath };
|
|
67
|
+
}
|
|
46
68
|
}
|
|
47
69
|
writeFileSync(target, src);
|
|
48
70
|
chmodSync(target, 0o755);
|
|
49
|
-
return { installed: true, target };
|
|
71
|
+
return { installed: true, target, hooksPath };
|
|
50
72
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Standalone install routes for tools with no package manager available
|
|
2
|
+
// (KJC-TSK-0607). On a machine with no pipx/brew/go, `kj install-tools`
|
|
3
|
+
// still needs a way to install osv-scanner and semgrep.
|
|
4
|
+
//
|
|
5
|
+
// · osv-scanner ships a single static binary per platform on its GitHub
|
|
6
|
+
// releases — we build the /latest/download URL for the current platform.
|
|
7
|
+
// · semgrep has no single static binary; we offer the most viable concrete
|
|
8
|
+
// command (Docker image if docker exists, otherwise bootstrapping pipx).
|
|
9
|
+
|
|
10
|
+
const OSV_BASE = "https://github.com/google/osv-scanner/releases/latest/download";
|
|
11
|
+
|
|
12
|
+
function osvOs(platform) {
|
|
13
|
+
if (platform === "linux") return "linux";
|
|
14
|
+
if (platform === "darwin") return "darwin";
|
|
15
|
+
if (platform === "win32") return "windows";
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function osvArch(arch) {
|
|
20
|
+
if (arch === "x64") return "amd64";
|
|
21
|
+
if (arch === "arm64") return "arm64";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the osv-scanner static-binary download source for a platform+arch,
|
|
27
|
+
* or null when the combination has no published asset.
|
|
28
|
+
*
|
|
29
|
+
* @returns {{ url: string, name: string }|null}
|
|
30
|
+
*/
|
|
31
|
+
export function osvScannerSource(platform = process.platform, arch = process.arch) {
|
|
32
|
+
const os = osvOs(platform);
|
|
33
|
+
const a = osvArch(arch);
|
|
34
|
+
if (!os || !a) return null;
|
|
35
|
+
const ext = os === "windows" ? ".exe" : "";
|
|
36
|
+
return { url: `${OSV_BASE}/osv-scanner_${os}_${a}${ext}`, name: `osv-scanner${ext}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Most viable concrete route to install semgrep without a package manager.
|
|
41
|
+
* Returns a command the user sees before it runs — never a bare docs URL.
|
|
42
|
+
*
|
|
43
|
+
* @returns {{ via: "docker"|"pipx", command: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function semgrepFallback(available = {}) {
|
|
46
|
+
if (available.docker) return { via: "docker", command: "docker pull semgrep/semgrep" };
|
|
47
|
+
return { via: "pipx", command: "python3 -m pip install --user pipx && pipx install semgrep" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the standalone route for a tool when no package manager matched.
|
|
52
|
+
* · binary → download a static binary into ~/.local/bin.
|
|
53
|
+
* · command → run/show a concrete install command.
|
|
54
|
+
*
|
|
55
|
+
* @returns {{ kind: "binary", url: string, name: string }|{ kind: "command", via: string, command: string }|null}
|
|
56
|
+
*/
|
|
57
|
+
export function resolveStandalone(tool, available = {}, platform = process.platform, arch = process.arch) {
|
|
58
|
+
if (tool === "osv-scanner") {
|
|
59
|
+
const src = osvScannerSource(platform, arch);
|
|
60
|
+
return src ? { kind: "binary", ...src } : null;
|
|
61
|
+
}
|
|
62
|
+
if (tool === "semgrep") return { kind: "command", ...semgrepFallback(available) };
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// How `kj install-tools` installs Docker, resolved per platform (KJC-TSK-0609,
|
|
2
|
+
// absorbing the sudo+apt/dnf primitive of KJC-TSK-0608).
|
|
3
|
+
//
|
|
4
|
+
// · Linux: prefer the distro package manager (apt → docker.io, dnf → docker)
|
|
5
|
+
// run through `sudo` on the user's own tty — kj never sees the password.
|
|
6
|
+
// When neither is present, fall back to Docker's official convenience
|
|
7
|
+
// script, which the caller downloads to a file and runs (never `curl | sh`).
|
|
8
|
+
// · macOS / Windows: never auto-install (platform-specific, invasive) — point
|
|
9
|
+
// at the official docs, suggesting the brew cask on macOS.
|
|
10
|
+
|
|
11
|
+
const GET_DOCKER_URL = "https://get.docker.com";
|
|
12
|
+
const DOCKER_DOCS_URL = "https://docs.docker.com/get-docker/";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the Docker install plan for a platform + available managers.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} platform - process.platform value (linux/darwin/win32).
|
|
18
|
+
* @param {Record<string, boolean>} available - detected package managers.
|
|
19
|
+
* @returns {{ kind: "package", manager: string, command: string }
|
|
20
|
+
* | { kind: "script", url: string }
|
|
21
|
+
* | { kind: "manual", manualUrl: string, suggested: string|null }}
|
|
22
|
+
*/
|
|
23
|
+
export function dockerInstallPlan(platform = process.platform, available = {}) {
|
|
24
|
+
if (platform !== "linux") {
|
|
25
|
+
return {
|
|
26
|
+
kind: "manual",
|
|
27
|
+
manualUrl: DOCKER_DOCS_URL,
|
|
28
|
+
suggested: available.brew ? "brew install --cask docker" : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (available.apt) return { kind: "package", manager: "apt", command: "sudo apt-get install -y docker.io" };
|
|
32
|
+
if (available.dnf) return { kind: "package", manager: "dnf", command: "sudo dnf install -y docker" };
|
|
33
|
+
return { kind: "script", url: GET_DOCKER_URL };
|
|
34
|
+
}
|
|
@@ -116,8 +116,36 @@ const MANUAL_URLS = {
|
|
|
116
116
|
"osv-scanner": "https://google.github.io/osv-scanner/installation/",
|
|
117
117
|
lighthouse: "https://github.com/GoogleChrome/lighthouse#using-the-cli",
|
|
118
118
|
docker: "https://docs.docker.com/get-docker/",
|
|
119
|
+
git: "https://git-scm.com/downloads",
|
|
119
120
|
};
|
|
120
121
|
|
|
122
|
+
// git is a `kj doctor` *required* tool. Unlike the audit tools it is installed
|
|
123
|
+
// through the OS package manager, and on Linux that needs root — so the plan
|
|
124
|
+
// carries a `needsSudo` flag and the caller runs those through the interactive
|
|
125
|
+
// (tty-inherited) path, where sudo prompts the user directly and kj never sees
|
|
126
|
+
// the password. brew/scoop/choco install at user scope, no sudo.
|
|
127
|
+
const GIT_CANDIDATES = [
|
|
128
|
+
{ manager: "brew", command: "brew install git", needsSudo: false },
|
|
129
|
+
{ manager: "apt", command: "sudo apt-get install -y git", needsSudo: true },
|
|
130
|
+
{ manager: "dnf", command: "sudo dnf install -y git", needsSudo: true },
|
|
131
|
+
{ manager: "choco", command: "choco install git -y", needsSudo: false },
|
|
132
|
+
{ manager: "scoop", command: "scoop install git", needsSudo: false },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pick a git install plan for the current machine. Returns a manual plan
|
|
137
|
+
* (command null + URL) when no supported package manager is present.
|
|
138
|
+
*
|
|
139
|
+
* @param {Record<PackageManager, boolean>} available
|
|
140
|
+
* @returns {{ command: string|null, manager: PackageManager|null, needsSudo: boolean, manualUrl: string }}
|
|
141
|
+
*/
|
|
142
|
+
export function gitInstallPlan(available = {}) {
|
|
143
|
+
for (const { manager, command, needsSudo } of GIT_CANDIDATES) {
|
|
144
|
+
if (available[manager]) return { command, manager, needsSudo, manualUrl: MANUAL_URLS.git };
|
|
145
|
+
}
|
|
146
|
+
return { command: null, manager: null, needsSudo: false, manualUrl: MANUAL_URLS.git };
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
/**
|
|
122
150
|
* Tools whose hint depends on the project stack. lighthouse is only
|
|
123
151
|
* relevant for frontend projects; suppressing the hint elsewhere keeps
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Executor primitives for `kj install-tools` (KJC-TSK-0606).
|
|
2
|
+
//
|
|
3
|
+
// Two ways to run an install step:
|
|
4
|
+
// · runInstallCommand(cmd) — captured: stdout/stderr collected,
|
|
5
|
+
// for non-privileged commands.
|
|
6
|
+
// · runInstallCommand(cmd, {interactive}) — tty inherited: the child owns the
|
|
7
|
+
// terminal so `sudo` prompts the user
|
|
8
|
+
// directly. kj NEVER sees the password.
|
|
9
|
+
//
|
|
10
|
+
// And a downloader for tools shipped as a single static binary (osv-scanner,
|
|
11
|
+
// semgrep) when no package manager is available: fetch → temp file → chmod +x →
|
|
12
|
+
// atomic rename into ~/.local/bin. On any failure it leaves no partial file.
|
|
13
|
+
|
|
14
|
+
import { execFile, spawn } from "node:child_process";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import fs from "node:fs/promises";
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
/** Directory where downloaded standalone binaries are placed. */
|
|
23
|
+
export function binDir() {
|
|
24
|
+
return path.join(os.homedir(), ".local", "bin");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run an install command. Commands here are simple (no nested quoting), so we
|
|
29
|
+
* tokenize on whitespace.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} command
|
|
32
|
+
* @param {{ interactive?: boolean }} [opts]
|
|
33
|
+
* interactive → inherit the parent's stdio so sudo can prompt for the password
|
|
34
|
+
* on the user's own terminal; kj never captures or logs it.
|
|
35
|
+
*/
|
|
36
|
+
export async function runInstallCommand(command, opts = {}) {
|
|
37
|
+
const [bin, ...args] = command.split(/\s+/);
|
|
38
|
+
if (opts.interactive) return runInherited(bin, args);
|
|
39
|
+
try {
|
|
40
|
+
const { stdout, stderr } = await execFileAsync(bin, args, {
|
|
41
|
+
timeout: 300_000,
|
|
42
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
43
|
+
});
|
|
44
|
+
return { ok: true, stdout, stderr };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { ok: false, error: err?.shortMessage || err?.message || String(err), stderr: err?.stderr };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runInherited(bin, args) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let child;
|
|
53
|
+
try {
|
|
54
|
+
child = spawn(bin, args, { stdio: "inherit", timeout: 600_000 });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
resolve({ ok: false, error: err?.message || String(err) });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
child.on("error", (err) => resolve({ ok: false, error: err?.message || String(err) }));
|
|
60
|
+
child.on("close", (code) => resolve({ ok: code === 0, code }));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Download a single static binary and place it (executable) into `dir`.
|
|
66
|
+
* Writes to a temp file first and renames atomically; on any failure the temp
|
|
67
|
+
* file is removed so no partial artifact is left behind.
|
|
68
|
+
*
|
|
69
|
+
* @param {{ url: string, name: string, dir?: string }} args
|
|
70
|
+
*/
|
|
71
|
+
export async function downloadBinary({ url, name, dir = binDir() }) {
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await fetch(url);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { ok: false, error: `download failed: ${err?.message || String(err)}` };
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) return { ok: false, error: `download failed: HTTP ${res.status}` };
|
|
79
|
+
|
|
80
|
+
await fs.mkdir(dir, { recursive: true });
|
|
81
|
+
const dest = path.join(dir, name);
|
|
82
|
+
const tmp = path.join(dir, `.${name}.download`);
|
|
83
|
+
try {
|
|
84
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
85
|
+
await fs.writeFile(tmp, buf);
|
|
86
|
+
await fs.chmod(tmp, 0o755);
|
|
87
|
+
await fs.rename(tmp, dest);
|
|
88
|
+
return { ok: true, dest };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
91
|
+
return { ok: false, error: `install failed: ${err?.message || String(err)}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
package/templates/roles/coder.md
CHANGED
|
@@ -8,10 +8,44 @@ You are the **Coder** in a multi-role AI pipeline. Your job is to write code and
|
|
|
8
8
|
- Write tests BEFORE implementation when using TDD.
|
|
9
9
|
- Keep changes minimal and focused on the task.
|
|
10
10
|
- "Minimal" means no unnecessary changes — it does NOT mean avoiding new files. If the task requires creating new files (pages, components, modules, tests), you MUST create them. Updating references/links without creating the actual files is an incomplete implementation.
|
|
11
|
-
-
|
|
11
|
+
- Change only the code the task requires; leave everything else as it is.
|
|
12
12
|
- Before creating a new utility or helper, check if a similar one already exists in the codebase. Reuse existing code over creating duplicates.
|
|
13
13
|
- Follow existing code conventions and patterns in the repository.
|
|
14
14
|
|
|
15
|
+
## Constructive dissent (raise real blockers before you code)
|
|
16
|
+
|
|
17
|
+
You are an Active Partner, not a silent contractor. Before implementing, if you
|
|
18
|
+
detect a genuine problem with the task, surface it FIRST instead of quietly
|
|
19
|
+
coding around it:
|
|
20
|
+
|
|
21
|
+
- A contradiction between two requirements that cannot both hold.
|
|
22
|
+
- A false or impossible premise — the task assumes a fact, file, or API that
|
|
23
|
+
does not exist, or forces an outcome that cannot be correct.
|
|
24
|
+
- A concrete security or maintainability risk in what is being asked.
|
|
25
|
+
|
|
26
|
+
Report it concisely in `result.concerns` (an array of short strings). When a
|
|
27
|
+
blocker makes the task unworkable as stated, stop and report instead of guessing.
|
|
28
|
+
|
|
29
|
+
Keep the bar high. Proceed silently when the task is clear and coherent — do NOT
|
|
30
|
+
object to trivial or stylistic details, and do NOT manufacture doubts to look
|
|
31
|
+
thorough. Relevance is the test: raise only what would derail the result.
|
|
32
|
+
|
|
33
|
+
## Check alignment (moderate or complex tasks only)
|
|
34
|
+
|
|
35
|
+
When the task has moderate or high scope — it spans several files, touches
|
|
36
|
+
architecture, security or testing, or leaves any real doubt about the goal —
|
|
37
|
+
state your understanding BEFORE writing code:
|
|
38
|
+
|
|
39
|
+
- One or two sentences on what you understood the task to be.
|
|
40
|
+
- Any open questions whose answer would change the implementation.
|
|
41
|
+
|
|
42
|
+
Report this in `result.alignment` (`{ "understood": "…", "open_questions": [] }`).
|
|
43
|
+
Surfacing a misread here is far cheaper than discovering it after the diff.
|
|
44
|
+
|
|
45
|
+
Do NOT add this step for trivial or small, unambiguous tasks — proceed straight
|
|
46
|
+
to the code. This gate is for tasks big enough that a wrong assumption is costly,
|
|
47
|
+
not a ritual for every change.
|
|
48
|
+
|
|
15
49
|
## PR atomicity (hard project rule)
|
|
16
50
|
|
|
17
51
|
Karajan projects MAY enforce a CI gate that fails any PR whose net delta exceeds **200 lines added** (the karajan-code repo itself enforces this since 2026-05-08). Plan before writing:
|
|
@@ -35,7 +69,7 @@ Before reporting done, verify that ALL parts of the task are addressed:
|
|
|
35
69
|
- If the task says "create X", create the complete working implementation, not a skeleton.
|
|
36
70
|
- If tests exist, the implementation MUST make all tests pass.
|
|
37
71
|
- If you write tests first (TDD), the implementation MUST make those tests pass.
|
|
38
|
-
-
|
|
72
|
+
- Commit only code that compiles and passes the full test suite.
|
|
39
73
|
|
|
40
74
|
## Test file location (MANDATORY convention)
|
|
41
75
|
|
|
@@ -105,7 +139,9 @@ Return a JSON object:
|
|
|
105
139
|
"files_modified": ["path/to/file.js"],
|
|
106
140
|
"files_created": ["path/to/new-file.js"],
|
|
107
141
|
"tests_added": ["path/to/test.js"],
|
|
108
|
-
"approach": "Brief description of what was done"
|
|
142
|
+
"approach": "Brief description of what was done",
|
|
143
|
+
"concerns": [],
|
|
144
|
+
"alignment": null
|
|
109
145
|
},
|
|
110
146
|
"summary": "Human-readable summary of changes"
|
|
111
147
|
}
|
|
@@ -4,7 +4,7 @@ You are the **Reviewer** in a multi-role AI pipeline. Your job is to review code
|
|
|
4
4
|
|
|
5
5
|
## Scope constraint
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Review only the files present in the diff.** Keep every finding scoped to what this change touched.
|
|
8
8
|
- If you notice problems in untouched files, mention them as `non_blocking_suggestions` with a note that they are outside the current scope — never as `blocking_issues`.
|
|
9
9
|
- Your job is to review THIS change, not audit the entire codebase.
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ You are the **Reviewer** in a multi-role AI pipeline. Your job is to review code
|
|
|
21
21
|
- Focus on security, correctness, and tests first.
|
|
22
22
|
- Only raise blocking issues for concrete production risks in the changed files.
|
|
23
23
|
- Keep non-blocking suggestions separate.
|
|
24
|
-
-
|
|
24
|
+
- Let style preferences pass; reserve blocking for concrete production risks.
|
|
25
25
|
- Confidence threshold: reject only if < 0.70.
|
|
26
26
|
|
|
27
27
|
## File overwrite detection (BLOCKING)
|
|
@@ -11,6 +11,11 @@ user's spec (prompt or `.md`) for deficiencies. You do NOT execute or plan.
|
|
|
11
11
|
- **assumptions** — depends on unstated context.
|
|
12
12
|
- **out_of_scope** — asks for things outside what Karajan can do.
|
|
13
13
|
|
|
14
|
+
A spec can also carry a **false premise**: it forces an answer that cannot be
|
|
15
|
+
correct (asks for N options when only one is valid, assumes a fact/API that does
|
|
16
|
+
not exist). Surface these as `contradiction` or `out_of_scope` — never rationalise
|
|
17
|
+
them into a plausible-looking answer.
|
|
18
|
+
|
|
14
19
|
## Severity
|
|
15
20
|
|
|
16
21
|
`info` (nuance), `warn` (real gap), `fail` (unworkable). Top-level `severity`
|