karajan-code 4.1.2 → 4.1.4
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 +55 -1
- package/src/commands/env.js +24 -6
- package/src/commands/hu.js +92 -0
- package/src/commands/install-tools.js +10 -1
- package/src/environment/adr.js +45 -0
- package/src/environment/playbook.js +4 -3
- package/src/harden/advisory.js +32 -16
- package/src/harden/config-engine.js +10 -0
- package/src/review/gate-gitignore.js +0 -0
- package/src/utils/binary-sources.js +6 -0
- package/src/utils/display/header.js +0 -1
- package/src/utils/install-hints.js +14 -2
- package/src/utils/pending-user-action.js +95 -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", "undo", "standby"] },
|
|
34
|
+
{ title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "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
|
@@ -22,6 +22,8 @@ import { telemetryPreviewCommand, telemetryStatusCommand } from "../commands/tel
|
|
|
22
22
|
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
|
+
import { huCommand } from "../commands/hu.js";
|
|
26
|
+
import { addAdr, listAdrs } from "../environment/adr.js";
|
|
25
27
|
import { formatAdvancedIndex } from "../commands/advanced.js";
|
|
26
28
|
import { withConfig } from "./_shared.js";
|
|
27
29
|
|
|
@@ -133,7 +135,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
133
135
|
.option("--no-rag", "Skip building the RAG index when the project has none (ENV-E1)")
|
|
134
136
|
.action(async (flags) => {
|
|
135
137
|
await withConfig(pkgVersion, "env-install", flags, async ({ config, logger }) => {
|
|
136
|
-
await envInstallCommand({ config, logger, flags });
|
|
138
|
+
const r = await envInstallCommand({ config, logger, flags });
|
|
139
|
+
// KJC-TSK-0659: exit 3 = pending user action (RAG cannot index) —
|
|
140
|
+
// a driving agent must stop and wait, never continue degraded.
|
|
141
|
+
if (Number.isInteger(r?.exitCode) && r.exitCode !== 0) process.exit(r.exitCode);
|
|
137
142
|
});
|
|
138
143
|
});
|
|
139
144
|
|
|
@@ -149,6 +154,55 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
149
154
|
});
|
|
150
155
|
});
|
|
151
156
|
|
|
157
|
+
// AB-H (KJC-TSK-0658): board writes + repo ADRs for the brain.
|
|
158
|
+
const hu = program.command("hu").description("Track work in the HU Board from any host agent (card first)");
|
|
159
|
+
hu.command("add <title>")
|
|
160
|
+
.option("--id <shortId>", "Human-readable short id")
|
|
161
|
+
.option("--criteria <text>", "Acceptance criteria")
|
|
162
|
+
.option("--json", "Machine-readable output")
|
|
163
|
+
.action(async (title, flags) => {
|
|
164
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
165
|
+
await huCommand({ config, action: "add", args: [title], flags });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
hu.command("move <id> <status>")
|
|
169
|
+
.option("--json", "Machine-readable output")
|
|
170
|
+
.action(async (id, status, flags) => {
|
|
171
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
172
|
+
await huCommand({ config, action: "move", args: [id, status], flags });
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
hu.command("list")
|
|
176
|
+
.option("--json", "Machine-readable output")
|
|
177
|
+
.action(async (flags) => {
|
|
178
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
179
|
+
await huCommand({ config, action: "list", flags });
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const adr = program.command("adr").description("Architecture decision records in .karajan/adrs/ (git-tracked)");
|
|
184
|
+
adr.command("add <title>")
|
|
185
|
+
.requiredOption("--decision <text>", "The decision itself")
|
|
186
|
+
.option("--context <text>", "Why this came up")
|
|
187
|
+
.option("--consequences <text>", "Trade-offs accepted")
|
|
188
|
+
.option("--json", "Machine-readable output")
|
|
189
|
+
.action(async (title, flags) => {
|
|
190
|
+
await withConfig(pkgVersion, "adr", flags, async ({ config }) => {
|
|
191
|
+
const res = await addAdr(config?.projectDir || process.cwd(), { title, ...flags });
|
|
192
|
+
console.log(flags.json ? JSON.stringify(res) : `✓ ADR ${res.number} created: ${res.file} — commit it`);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
adr.command("list")
|
|
196
|
+
.option("--json", "Machine-readable output")
|
|
197
|
+
.action(async (flags) => {
|
|
198
|
+
await withConfig(pkgVersion, "adr", flags, async ({ config }) => {
|
|
199
|
+
const adrs = await listAdrs(config?.projectDir || process.cwd());
|
|
200
|
+
if (flags.json) { console.log(JSON.stringify(adrs)); return; }
|
|
201
|
+
for (const a of adrs) console.log(`${String(a.number).padStart(4, "0")} ${a.status.padEnd(10)} ${a.title}`);
|
|
202
|
+
if (adrs.length === 0) console.log("no ADRs yet — create one with: kj adr add \"<title>\" --decision \"...\"");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
152
206
|
// AB-F (KJC-TSK-0655): self-healing — the brain files kj frictions upstream.
|
|
153
207
|
program
|
|
154
208
|
.command("report-issue")
|
package/src/commands/env.js
CHANGED
|
@@ -8,6 +8,7 @@ import { installPlaybook } from "../environment/playbook.js";
|
|
|
8
8
|
import { renderBrief, listBriefs } from "../environment/briefs.js";
|
|
9
9
|
import { openVecStore, projectSlug, getLastIndexedCommit } from "../rag/vec-store.js";
|
|
10
10
|
import { ragIndexCommand } from "./rag.js";
|
|
11
|
+
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
11
12
|
|
|
12
13
|
function hasRagIndex(config, projectDir) {
|
|
13
14
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
@@ -42,21 +43,38 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
42
43
|
console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
|
|
43
44
|
|
|
44
45
|
// ENV-E1: RAG-first — the playbook orders "query the RAG before coding",
|
|
45
|
-
// so installing the environment guarantees the index exists.
|
|
46
|
-
//
|
|
46
|
+
// so installing the environment guarantees the index exists. KJC-TSK-0659
|
|
47
|
+
// stop-on-sudo: an index that cannot be built is a BLOCKING condition —
|
|
48
|
+
// "success" with 0 chunks would leave every future session running the
|
|
49
|
+
// method against an empty RAG (field-reproduced: 0/727 without Ollama).
|
|
50
|
+
// The playbook stays installed; the command exits 3 so the driving agent
|
|
51
|
+
// stops, shows the block, and waits for the user.
|
|
47
52
|
if (flags.rag !== false) {
|
|
53
|
+
const provider = config?.rag?.embedder?.provider || "ollama";
|
|
54
|
+
const blockRag = (why) => {
|
|
55
|
+
result.ragError = why;
|
|
56
|
+
result.exitCode = PENDING_EXIT_CODE;
|
|
57
|
+
const item = provider === "ollama"
|
|
58
|
+
? { tool: "ollama", action: "needs-user", reason: `the RAG cannot index: ${why}` }
|
|
59
|
+
: { tool: `${provider} embedder`, action: "needs-user", reason: `the RAG cannot index: ${why} — check rag.embedder in kj config`, manualUrl: "kj config" };
|
|
60
|
+
console.log(renderPendingBlock([item], { retry: "kj rag index --with-sources (then re-run: kj env install)" }));
|
|
61
|
+
};
|
|
48
62
|
try {
|
|
49
63
|
if (hasRagIndex(config, projectDir)) {
|
|
50
64
|
console.log("✓ RAG index present");
|
|
51
65
|
} else {
|
|
52
66
|
console.log("⏳ no RAG index for this project — building it (first time only)…");
|
|
53
|
-
await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
67
|
+
const totals = await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
68
|
+
if ((totals?.indexed ?? 0) === 0 && (totals?.files ?? 0) > 0) {
|
|
69
|
+
blockRag(`0 of ${totals.files} files indexed — is the embedder running?`);
|
|
70
|
+
}
|
|
54
71
|
}
|
|
55
72
|
} catch (err) {
|
|
56
|
-
|
|
57
|
-
console.log(`⚠ RAG index could not be built (${err.message}) — run \`kj rag index --with-sources\` later`);
|
|
73
|
+
blockRag(err.message);
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
|
-
|
|
76
|
+
if (result.exitCode !== PENDING_EXIT_CODE) {
|
|
77
|
+
console.log(" The host agent now follows the method: RAG first, TDD, cross-AI review before commit.");
|
|
78
|
+
}
|
|
61
79
|
return result;
|
|
62
80
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj hu add|move|list` (AB-H, KJC-TSK-0658) — board writes for the brain.
|
|
3
|
+
* The v4 playbook orders "card first", but until now only the headless
|
|
4
|
+
* planner could create HUs. These commands operate on a per-project
|
|
5
|
+
* "brain-backlog" plan (created on first use) so any host agent can track
|
|
6
|
+
* work in the HU Board without the subprocess pipeline.
|
|
7
|
+
*/
|
|
8
|
+
import { addHu, updateHuStatus } from "../plan/plan-hu-ops.js";
|
|
9
|
+
import { generatePlanId } from "../plan/plan-id.js";
|
|
10
|
+
import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
|
|
11
|
+
|
|
12
|
+
export const HU_STATUSES = ["pending", "running", "done", "failed", "skipped"];
|
|
13
|
+
const BACKLOG_NAME = "brain-backlog";
|
|
14
|
+
|
|
15
|
+
async function backlogPlan(projectDir) {
|
|
16
|
+
const plans = await listPlans(projectDir);
|
|
17
|
+
const existing = plans.find((p) => p.alias === BACKLOG_NAME || p.name === BACKLOG_NAME);
|
|
18
|
+
if (existing) return loadPlan(projectDir, existing.planId);
|
|
19
|
+
const plan = {
|
|
20
|
+
version: 2, planId: generatePlanId(), name: BACKLOG_NAME,
|
|
21
|
+
task: "Host-agent tracked work (v4 environment)",
|
|
22
|
+
status: "ready", hus: [], createdAt: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
const planId = await savePlan(projectDir, plan);
|
|
25
|
+
return loadPlan(projectDir, planId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function huCommand({ config = null, action, args = [], flags = {} }) {
|
|
29
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
30
|
+
const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
|
|
31
|
+
|
|
32
|
+
if (action === "list") {
|
|
33
|
+
const plans = await listPlans(projectDir);
|
|
34
|
+
const rows = [];
|
|
35
|
+
for (const meta of plans) {
|
|
36
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
37
|
+
for (const h of plan.hus || []) {
|
|
38
|
+
rows.push({ id: h.id, short_id: h.short_id, title: h.title, status: h.status, plan: plan.alias || plan.planId });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
42
|
+
for (const r of rows) console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title}`);
|
|
43
|
+
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (action === "add") {
|
|
48
|
+
const title = args[0];
|
|
49
|
+
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
50
|
+
const plan = await backlogPlan(projectDir);
|
|
51
|
+
const hu = addHu(plan, {
|
|
52
|
+
title,
|
|
53
|
+
short_id: flags.id || null,
|
|
54
|
+
acceptance_criteria: flags.criteria ? [flags.criteria] : [],
|
|
55
|
+
});
|
|
56
|
+
await savePlan(projectDir, plan);
|
|
57
|
+
return emit({ id: hu.id, short_id: hu.short_id, status: hu.status },
|
|
58
|
+
`✓ HU created: ${hu.short_id || hu.id} (pending) — it shows up in \`kj board\``);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (action === "move") {
|
|
62
|
+
const [ref, status] = args;
|
|
63
|
+
if (!ref || !status) throw new Error("usage: kj hu move <id> <status>");
|
|
64
|
+
if (!HU_STATUSES.includes(status)) {
|
|
65
|
+
throw new Error(`invalid status "${status}" — valid: ${HU_STATUSES.join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
// Exact canonical id wins outright (it IS the disambiguator); only then
|
|
68
|
+
// fall back to short_id matches — which can repeat between plans, so
|
|
69
|
+
// silently moving the first hit could move the wrong card.
|
|
70
|
+
const byId = [];
|
|
71
|
+
const byShort = [];
|
|
72
|
+
for (const meta of await listPlans(projectDir)) {
|
|
73
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
74
|
+
for (const hu of plan.hus || []) {
|
|
75
|
+
if (hu.id === ref) byId.push({ plan, hu });
|
|
76
|
+
else if (hu.short_id === ref) byShort.push({ plan, hu });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const matches = byId.length > 0 ? byId : byShort;
|
|
80
|
+
if (matches.length === 0) throw new Error(`HU "${ref}" not found — see kj hu list`);
|
|
81
|
+
if (matches.length > 1) {
|
|
82
|
+
const ids = matches.map((m) => m.hu.id).join(", ");
|
|
83
|
+
throw new Error(`"${ref}" is ambiguous (${matches.length} matches: ${ids}) — use the full id`);
|
|
84
|
+
}
|
|
85
|
+
const { plan, hu } = matches[0];
|
|
86
|
+
updateHuStatus(plan, hu.id, status);
|
|
87
|
+
await savePlan(projectDir, plan);
|
|
88
|
+
return emit({ id: hu.id, status }, `✓ ${hu.short_id || hu.id} → ${status}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`unknown action "${action}" — use: add | move | list`);
|
|
92
|
+
}
|
|
@@ -23,6 +23,7 @@ import { detectProjectStack } from "../utils/stack-detect.js";
|
|
|
23
23
|
import { resolveStandalone } from "../utils/binary-sources.js";
|
|
24
24
|
import { downloadBinary, binDir, runInstallCommand } from "../utils/tool-installer.js";
|
|
25
25
|
import { dockerInstallPlan } from "../utils/docker-install.js";
|
|
26
|
+
import { collectPending, renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
26
27
|
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
29
|
|
|
@@ -416,8 +417,16 @@ export async function installToolsCommand(opts = {}) {
|
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
// KJC-TSK-0659 stop-on-sudo: anything that still needs the user's hands
|
|
421
|
+
// becomes ONE block with the exact per-OS commands, and a distinctive exit
|
|
422
|
+
// code so a driving agent stops and waits instead of continuing degraded.
|
|
423
|
+
const pending = dryRun ? [] : collectPending(results, { yes });
|
|
424
|
+
if (pending.length > 0) {
|
|
425
|
+
logger.warn?.(renderPendingBlock(pending));
|
|
426
|
+
return { results, pending, exitCode: PENDING_EXIT_CODE };
|
|
427
|
+
}
|
|
419
428
|
const failures = results.filter((r) => r.action === "failed").length;
|
|
420
|
-
return { results, exitCode: failures > 0 ? 1 : 0 };
|
|
429
|
+
return { results, pending, exitCode: failures > 0 ? 1 : 0 };
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
export const __test = { parseOnlyList, isInstalled, ALL_TOOLS };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADRs in the repo (AB-H, KJC-TSK-0658) — `kj adr add|list`. Architecture
|
|
3
|
+
* decisions live as numbered markdown under .karajan/adrs/, git-tracked so
|
|
4
|
+
* the whole team (and every brain) inherits them. The architect brief's
|
|
5
|
+
* "check the ADRs before deciding" now has a target on every backend.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const ADR_DIR = path.join(".karajan", "adrs");
|
|
11
|
+
|
|
12
|
+
const slugify = (t) => t.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 60);
|
|
13
|
+
|
|
14
|
+
export async function listAdrs(projectDir) {
|
|
15
|
+
const dir = path.join(projectDir, ADR_DIR);
|
|
16
|
+
let files;
|
|
17
|
+
try { files = await fs.readdir(dir); } catch { return []; }
|
|
18
|
+
const adrs = [];
|
|
19
|
+
for (const f of files.filter((f) => /^\d{4}-.*\.md$/.test(f)).sort()) {
|
|
20
|
+
const text = await fs.readFile(path.join(dir, f), "utf8");
|
|
21
|
+
const title = text.match(/^# (.+)$/m)?.[1] || f;
|
|
22
|
+
const status = text.match(/^Status: (.+)$/m)?.[1] || "accepted";
|
|
23
|
+
adrs.push({ file: path.join(ADR_DIR, f), number: Number(f.slice(0, 4)), title, status });
|
|
24
|
+
}
|
|
25
|
+
return adrs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function addAdr(projectDir, { title, decision, context = "", consequences = "" }) {
|
|
29
|
+
if (!title?.trim() || !decision?.trim()) {
|
|
30
|
+
throw new Error("kj adr add requires a title and --decision");
|
|
31
|
+
}
|
|
32
|
+
const existing = await listAdrs(projectDir);
|
|
33
|
+
const number = (existing.at(-1)?.number || 0) + 1;
|
|
34
|
+
const file = path.join(ADR_DIR, `${String(number).padStart(4, "0")}-${slugify(title)}.md`);
|
|
35
|
+
const body = [
|
|
36
|
+
`# ${title}`, "",
|
|
37
|
+
`Status: accepted`, `Date: ${new Date().toISOString().slice(0, 10)}`, "",
|
|
38
|
+
...(context ? ["## Context", "", context, ""] : []),
|
|
39
|
+
"## Decision", "", decision, "",
|
|
40
|
+
...(consequences ? ["## Consequences", "", consequences, ""] : []),
|
|
41
|
+
].join("\n");
|
|
42
|
+
await fs.mkdir(path.join(projectDir, ADR_DIR), { recursive: true });
|
|
43
|
+
await fs.writeFile(path.join(projectDir, file), body);
|
|
44
|
+
return { number, file, title };
|
|
45
|
+
}
|
|
@@ -29,7 +29,7 @@ const TARGET_FILES = {
|
|
|
29
29
|
// ENV-D1 (KJC-TSK-0642): the tracking invariant names the CHOSEN state
|
|
30
30
|
// backend — a playbook that says "board or PG" makes the host guess.
|
|
31
31
|
const BACKEND_TRACKING = {
|
|
32
|
-
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj board`) before it starts.",
|
|
32
|
+
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts.",
|
|
33
33
|
"planning-game": "Every piece of work has a tracked card in the Planning Game MCP before it starts (In Progress while you work it).",
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -58,8 +58,9 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
58
58
|
through an atomic PR (~150 net lines, Conventional Commits).
|
|
59
59
|
|
|
60
60
|
Commands: \`kj rag query\` · \`kj brief <role>\` (triage, planner, researcher,
|
|
61
|
-
architect, tester, security, audit) · \`kj
|
|
62
|
-
\`kj
|
|
61
|
+
architect, tester, security, audit) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
62
|
+
\`kj review --staged\` · \`kj review --check\` · \`kj solomon --position\` ·
|
|
63
|
+
\`kj agent run <agent>\` · \`kj report\` · \`kj check\`
|
|
63
64
|
|
|
64
65
|
Hit a kj bug or friction? Diagnose it and file it upstream with
|
|
65
66
|
\`kj report-issue\` (sanitized; ask your user before \`--publish\`).
|
package/src/harden/advisory.js
CHANGED
|
@@ -21,11 +21,38 @@ const BLOCK_VERSION = 1; // current version every markered config ships at
|
|
|
21
21
|
// Other filenames/formats for the same artifact, so kj never reports a false
|
|
22
22
|
// "missing". `pkgKey` is a package.json field that can hold inline config.
|
|
23
23
|
const EQUIVALENTS = {
|
|
24
|
-
commitlint: { files: [".commitlintrc", ".commitlintrc.js", ".commitlintrc.json", ".commitlintrc.yaml"], pkgKey: "commitlint" },
|
|
25
|
-
eslint: { files: ["eslint.config.mjs", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml"], pkgKey: "eslintConfig" },
|
|
26
|
-
prettier: { files: [".prettierrc", ".prettierrc.js", ".prettierrc.yaml", "prettier.config.js"], pkgKey: "prettier" },
|
|
24
|
+
commitlint: { files: [".commitlintrc", ".commitlintrc.js", ".commitlintrc.cjs", ".commitlintrc.mjs", ".commitlintrc.json", ".commitlintrc.yaml", ".commitlintrc.yml", "commitlint.config.mjs", "commitlint.config.cjs", "commitlint.config.ts"], pkgKey: "commitlint" },
|
|
25
|
+
eslint: { files: ["eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml"], pkgKey: "eslintConfig" },
|
|
26
|
+
prettier: { files: [".prettierrc", ".prettierrc.js", ".prettierrc.yaml", ".prettierrc.yml", ".prettierrc.json5", ".prettierrc.toml", "prettier.config.js", "prettier.config.mjs", "prettier.config.cjs", "prettier.config.ts"], pkgKey: "prettier" },
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* KJC-BUG-0119 — same-tool filename variant for `artifactId` in any of
|
|
31
|
+
* `searchDirs` (file or inline package.json config); null when none exists.
|
|
32
|
+
* Seeding kj's default filename NEXT TO a variant eclipses the user's real
|
|
33
|
+
* config (eslint resolves eslint.config.js before .mjs), so config-engine
|
|
34
|
+
* must consult this before treating an artifact as absent.
|
|
35
|
+
*/
|
|
36
|
+
export function findEquivalentIn(searchDirs, artifactId) {
|
|
37
|
+
const spec = EQUIVALENTS[artifactId];
|
|
38
|
+
if (!spec) return null;
|
|
39
|
+
for (const dir of searchDirs) {
|
|
40
|
+
for (const rel of spec.files) {
|
|
41
|
+
if (existsSync(join(dir, rel))) return rel;
|
|
42
|
+
}
|
|
43
|
+
if (spec.pkgKey && existsSync(join(dir, "package.json"))) {
|
|
44
|
+
try {
|
|
45
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))[spec.pkgKey] != null) {
|
|
46
|
+
return `package.json#${spec.pkgKey}`;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
/* unreadable package.json → not a match */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
// Concrete gains kj's standard would add to a USER_OWNED config, detected by
|
|
30
57
|
// substring probes (no AST parse): each probe absent from the user's content
|
|
31
58
|
// becomes one improvement. Heuristic by design — conservative, never a false
|
|
@@ -70,19 +97,8 @@ function toArtifact(cfg) {
|
|
|
70
97
|
|
|
71
98
|
/** Locate the user's file for an artifact under `searchDir`; null if absent. */
|
|
72
99
|
function findArtifactFile(searchDir, artifact) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
if (artifact.pkgKey && existsSync(join(searchDir, "package.json"))) {
|
|
77
|
-
try {
|
|
78
|
-
if (JSON.parse(readFileSync(join(searchDir, "package.json"), "utf8"))[artifact.pkgKey] != null) {
|
|
79
|
-
return `package.json#${artifact.pkgKey}`;
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
/* unreadable package.json → not a match */
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
100
|
+
if (existsSync(join(searchDir, artifact.file))) return artifact.file;
|
|
101
|
+
return findEquivalentIn([searchDir], artifact.id);
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/** Read the artifact body, or the JSON of an inline package.json key. */
|
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
12
|
+
import { findEquivalentIn } from "./advisory.js";
|
|
12
13
|
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
13
14
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
14
15
|
|
|
@@ -37,6 +38,15 @@ function seedInto(targetDir, configs, dryRun, prefix, results, altDirs = [target
|
|
|
37
38
|
results.push({ file, action: "covered", by: alt.foundAt });
|
|
38
39
|
continue;
|
|
39
40
|
}
|
|
41
|
+
// KJC-BUG-0119: a same-tool variant (eslint.config.mjs, .eslintrc.json,
|
|
42
|
+
// package.json#eslintConfig…) means the config already EXISTS — seeding
|
|
43
|
+
// kj's default filename next to it would silently ECLIPSE the user's
|
|
44
|
+
// real config (eslint resolves eslint.config.js before .mjs).
|
|
45
|
+
const variant = findEquivalentIn(altDirs, artifactIdForConfig(cfg));
|
|
46
|
+
if (variant) {
|
|
47
|
+
results.push({ file, action: "covered", by: variant });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
if (cfg.json) {
|
|
Binary file
|
|
@@ -44,6 +44,12 @@ export function osvScannerSource(platform = process.platform, arch = process.arc
|
|
|
44
44
|
*/
|
|
45
45
|
export function semgrepFallback(available = {}) {
|
|
46
46
|
if (available.docker) return { via: "docker", command: "docker pull semgrep/semgrep" };
|
|
47
|
+
// KJC-BUG-0120 (issue #1256): on Debian/Ubuntu-like systems the system
|
|
48
|
+
// Python is externally managed (PEP 668) and often ships without pip at
|
|
49
|
+
// all — `python3 -m pip install --user` is guaranteed to fail there.
|
|
50
|
+
// pipx must come from the distro's own package manager.
|
|
51
|
+
if (available.apt) return { via: "apt", command: "sudo apt update && sudo apt install -y pipx && pipx install semgrep" };
|
|
52
|
+
if (available.dnf) return { via: "dnf", command: "sudo dnf install -y pipx && pipx install semgrep" };
|
|
47
53
|
return { via: "pipx", command: "python3 -m pip install --user pipx && pipx install semgrep" };
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { printBanner } from "../banner.js";
|
|
5
5
|
import { ANSI } from "./formatters.js";
|
|
6
6
|
|
|
7
|
-
// TODO: i18n display messages
|
|
8
7
|
const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
|
|
9
8
|
const DISPLAY_VERSION = JSON.parse(readFileSync(DISPLAY_PKG_PATH, "utf8")).version;
|
|
10
9
|
|
|
@@ -80,8 +80,13 @@ export async function getInstallHint(tool, available = null) {
|
|
|
80
80
|
for (const { manager, command } of candidates) {
|
|
81
81
|
if (avail[manager]) return { command, manager, manualUrl: MANUAL_URLS[tool] };
|
|
82
82
|
}
|
|
83
|
-
// Nothing matched —
|
|
84
|
-
//
|
|
83
|
+
// Nothing matched — suggest a recipe that actually works on this system.
|
|
84
|
+
// KJC-BUG-0120 (issue #1256): distro-aware fallbacks first (PEP 668 makes
|
|
85
|
+
// the generic pip bootstrap fail on Debian/Ubuntu); these are display-only
|
|
86
|
+
// (manager: null), never auto-run — sudo stays in the user's hands.
|
|
87
|
+
for (const { when, command } of DISTRO_FALLBACKS[tool] || []) {
|
|
88
|
+
if (avail[when]) return { command, manager: null, manualUrl: MANUAL_URLS[tool] };
|
|
89
|
+
}
|
|
85
90
|
const first = candidates[0];
|
|
86
91
|
return {
|
|
87
92
|
command: first ? first.command : null,
|
|
@@ -90,6 +95,13 @@ export async function getInstallHint(tool, available = null) {
|
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
const DISTRO_FALLBACKS = {
|
|
99
|
+
semgrep: [
|
|
100
|
+
{ when: "apt", command: "sudo apt update && sudo apt install -y pipx && pipx install semgrep" },
|
|
101
|
+
{ when: "dnf", command: "sudo dnf install -y pipx && pipx install semgrep" },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
93
105
|
const INSTALL_CANDIDATES = {
|
|
94
106
|
semgrep: [
|
|
95
107
|
{ manager: "pipx", command: "pipx install semgrep" },
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop-on-sudo policy (KJC-TSK-0659). When a tool cannot be installed
|
|
3
|
+
* automatically (sudo needed, no package-manager route, or the attempt
|
|
4
|
+
* failed), kj must NOT continue degraded — a RAG with 0 chunks because
|
|
5
|
+
* Docker was missing helps nobody. Instead: emit ONE block with the exact
|
|
6
|
+
* commands for this machine's OS and exit with a distinctive code so a
|
|
7
|
+
* driving agent stops, shows the block, and waits for the user.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const PENDING_EXIT_CODE = 3;
|
|
11
|
+
|
|
12
|
+
const PENDING = new Set(["manual", "failed", "needs-user"]);
|
|
13
|
+
|
|
14
|
+
// Exact per-OS commands for the tools kj cannot install unattended.
|
|
15
|
+
// Multiple lines when the route depends on the distro/package manager.
|
|
16
|
+
const OS_COMMANDS = {
|
|
17
|
+
docker: {
|
|
18
|
+
linux: ["sudo apt-get install -y docker.io # Debian/Ubuntu", "sudo dnf install -y docker # Fedora/RHEL"],
|
|
19
|
+
darwin: ["brew install --cask docker # Docker Desktop, then open it once"],
|
|
20
|
+
win32: ["winget install Docker.DockerDesktop", "wsl --install # if WSL2 is not set up yet"],
|
|
21
|
+
},
|
|
22
|
+
git: {
|
|
23
|
+
linux: ["sudo apt-get install -y git # Debian/Ubuntu", "sudo dnf install -y git # Fedora/RHEL"],
|
|
24
|
+
darwin: ["brew install git # or: xcode-select --install"],
|
|
25
|
+
win32: ["winget install Git.Git"],
|
|
26
|
+
},
|
|
27
|
+
semgrep: {
|
|
28
|
+
// PEP 668 (#1256): system Python is externally managed on modern
|
|
29
|
+
// distros — pipx comes from the distro package manager, never from pip.
|
|
30
|
+
linux: ["sudo apt update && sudo apt install -y pipx && pipx install semgrep # Debian/Ubuntu", "sudo dnf install -y pipx && pipx install semgrep # Fedora/RHEL"],
|
|
31
|
+
darwin: ["brew install semgrep"],
|
|
32
|
+
win32: ["python -m pip install --user semgrep"],
|
|
33
|
+
},
|
|
34
|
+
"osv-scanner": {
|
|
35
|
+
linux: ["go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2 # or download from GitHub releases"],
|
|
36
|
+
darwin: ["brew install osv-scanner"],
|
|
37
|
+
win32: ["winget install Google.OSVScanner # or download from GitHub releases"],
|
|
38
|
+
},
|
|
39
|
+
lighthouse: {
|
|
40
|
+
linux: ["npm install -g lighthouse"],
|
|
41
|
+
darwin: ["npm install -g lighthouse"],
|
|
42
|
+
win32: ["npm install -g lighthouse"],
|
|
43
|
+
},
|
|
44
|
+
ollama: {
|
|
45
|
+
linux: ["curl -fsSL https://ollama.com/install.sh | sh # official installer (uses sudo)", "ollama pull nomic-embed-text"],
|
|
46
|
+
darwin: ["brew install ollama && brew services start ollama", "ollama pull nomic-embed-text"],
|
|
47
|
+
win32: ["winget install Ollama.Ollama", "ollama pull nomic-embed-text"],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Exact commands to install `tool` on `platform` (empty when unknown). */
|
|
52
|
+
export function osCommandsFor(tool, platform = process.platform) {
|
|
53
|
+
return OS_COMMANDS[tool]?.[platform] ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Results that require the USER's hands. `manual`/`failed`/`needs-user`
|
|
58
|
+
* always qualify; `declined` only when nobody actually declined — a non-TTY
|
|
59
|
+
* run without --yes auto-answers the default, which for sudo installs is
|
|
60
|
+
* "no". A real interactive decline stays the user's own decision.
|
|
61
|
+
*/
|
|
62
|
+
export function collectPending(results, { tty = Boolean(process.stdin.isTTY), yes = false } = {}) {
|
|
63
|
+
return results.filter((r) => PENDING.has(r.action) || (r.action === "declined" && !tty && !yes));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The PENDING USER ACTION block: exact commands for THIS OS, then wait. */
|
|
67
|
+
export function renderPendingBlock(pending, { platform = process.platform, retry = "kj install-tools" } = {}) {
|
|
68
|
+
const lines = [
|
|
69
|
+
"════ PENDING USER ACTION ════════════════════════════════════════",
|
|
70
|
+
"kj could not finish this install by itself (sudo or a platform",
|
|
71
|
+
"installer is required). Karajan needs a COMPLETE environment —",
|
|
72
|
+
"do not continue degraded.",
|
|
73
|
+
"",
|
|
74
|
+
"Run in YOUR terminal:",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
for (const r of pending) {
|
|
78
|
+
const why = r.error ? `install failed: ${r.error}` : r.reason || "no automatic route on this machine";
|
|
79
|
+
lines.push(` ▸ ${r.tool} (${why})`);
|
|
80
|
+
const os = osCommandsFor(r.tool, platform);
|
|
81
|
+
const own = r.commands ?? (r.command ? [r.command] : null) ?? (r.suggested ? [r.suggested] : null);
|
|
82
|
+
// A FAILED install must not be re-suggested verbatim (#1256: the pip
|
|
83
|
+
// route that just broke) — the curated per-OS route goes first there.
|
|
84
|
+
const commands = (r.action === "failed" ? (os.length > 0 ? os : own) : (own ?? (os.length > 0 ? os : null)))
|
|
85
|
+
?? [`see ${r.manualUrl || "the tool's install docs"}`];
|
|
86
|
+
for (const c of commands) lines.push(` ${c}`);
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
89
|
+
lines.push(
|
|
90
|
+
`When done, re-run: ${retry}`,
|
|
91
|
+
"Agents: STOP here, show this block to your user, and WAIT for their",
|
|
92
|
+
`go-ahead. Exit code ${PENDING_EXIT_CODE} means "pending user action" — never proceed past it.`,
|
|
93
|
+
);
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|