karajan-code 4.31.1 → 4.33.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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +2 -12
  3. package/src/agents/base-agent.js +9 -20
  4. package/src/agents/claude-agent.js +2 -12
  5. package/src/agents/codex-agent.js +2 -12
  6. package/src/agents/dead-models.js +74 -0
  7. package/src/agents/gemini-agent.js +2 -12
  8. package/src/agents/model-errors.js +35 -0
  9. package/src/agents/opencode-agent.js +2 -12
  10. package/src/brain/agent-error-classifier.js +19 -1
  11. package/src/brain/role-fallback-chain.js +100 -0
  12. package/src/brain/with-brain-recovery.js +59 -3
  13. package/src/checks/action-pins.js +131 -0
  14. package/src/checks/project-checks.js +11 -0
  15. package/src/checks/repo-state.js +89 -0
  16. package/src/cli/advanced-commands.js +3 -0
  17. package/src/cli/register-meta.js +14 -0
  18. package/src/cli/register-pipeline.js +15 -1
  19. package/src/commands/bootstrap.js +131 -0
  20. package/src/commands/check.js +5 -0
  21. package/src/commands/code.js +57 -4
  22. package/src/commands/env.js +2 -1
  23. package/src/commands/go.js +9 -10
  24. package/src/commands/harden.js +3 -1
  25. package/src/commands/review-gate.js +28 -5
  26. package/src/environment/panel.js +52 -0
  27. package/src/environment/playbook.js +7 -6
  28. package/src/harden/config-templates.js +7 -1
  29. package/src/harden/guidelines-engine.js +7 -3
  30. package/src/harden/guidelines-templates.js +60 -10
  31. package/src/harden/hook-templates.js +4 -1
  32. package/src/privacy/diff-scope.js +58 -0
  33. package/src/privacy/scan.js +18 -0
  34. package/src/prompts/card-context.js +57 -0
  35. package/src/prompts/session-context.js +68 -0
  36. package/src/review/one-shot-review.js +5 -0
  37. package/src/review/ui-evidence.js +65 -0
  38. package/src/roles/agent-role.js +11 -2
  39. package/src/start/project-script.js +84 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * KJC-TSK-0870 — the maintenance that pinning moved onto kj.
3
+ *
4
+ * Issue #1374 was right: `kj harden` generated workflows with moving tags
5
+ * (`actions/checkout@v4`), and kj's own `kj audit --security` flagged them, so
6
+ * five of the project's seven warnings came from files kj had just written.
7
+ * They are pinned to commit SHAs now.
8
+ *
9
+ * Pinning trades one risk for another: nobody can move the tag under you, and
10
+ * nobody delivers the upstream patch either. A pin that nobody revisits is a
11
+ * vulnerability with a long shelf life, and since kj writes those lines, kj is
12
+ * the one that has to notice. That is this check.
13
+ *
14
+ * It lives in `kj doctor` (a person asking about their environment) and NOT in
15
+ * `kj check` (a gate that CI runs): a gate that needs the network is a gate
16
+ * that fails on a plane, and turning red over an unreachable API is how a gate
17
+ * loses its credibility.
18
+ */
19
+
20
+ import { execFile } from "node:child_process";
21
+ import { promisify } from "node:util";
22
+
23
+ import { PINNED_ACTIONS } from "../harden/workflow-templates.js";
24
+
25
+ const execFileAsync = promisify(execFile);
26
+ // Nobody waits for a diagnostic. Both clients are capped, and the pins are
27
+ // asked in parallel, so an unreachable GitHub costs one timeout and not one
28
+ // per action.
29
+ const CALL_TIMEOUT_MS = 5_000;
30
+
31
+ const STRATEGY_MANUAL = "manual";
32
+ const API = "https://api.github.com/repos";
33
+
34
+ /** `owner/repo@sha # tag` as written in the templates. */
35
+ export function parsePin(pin) {
36
+ const m = /^([^@\s]+)@([0-9a-f]{40})\s*#\s*(\S+)$/i.exec(String(pin || "").trim());
37
+ return m ? { action: m[1], sha: m[2].toLowerCase(), tag: m[3] } : null;
38
+ }
39
+
40
+ const SHA_RE = /^[0-9a-f]{40}$/;
41
+ const clean = (s) => String(s || "").trim().toLowerCase();
42
+
43
+ /**
44
+ * The SHA a tag points at today, asked through `gh` when it is there.
45
+ *
46
+ * Measured, not assumed: plain unauthenticated `fetch` answered 403 on the
47
+ * first real run, because GitHub allows 60 calls an hour PER IP and any shared
48
+ * or NAT-ed address burns that between everyone behind it. A check that
49
+ * answers "could not check" almost always is decorative. `gh` carries the
50
+ * user's own credentials (5000/hour) and kj already relies on it elsewhere;
51
+ * `fetch` stays as the fallback for whoever has no gh.
52
+ *
53
+ * @returns {Promise<string|null>} null when the answer cannot be trusted.
54
+ */
55
+ export async function currentSha(action, tag, { fetchFn = fetch, ghFn = ghSha } = {}) {
56
+ const viaGh = await ghFn(action, tag);
57
+ if (viaGh) return viaGh;
58
+ const res = await fetchFn(`${API}/${action}/commits/${encodeURIComponent(tag)}`, {
59
+ headers: { Accept: "application/vnd.github.sha", "User-Agent": "karajan-code" },
60
+ signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
61
+ });
62
+ if (!res?.ok) return null;
63
+ const body = clean(await res.text());
64
+ return SHA_RE.test(body) ? body : null;
65
+ }
66
+
67
+ /** @returns {Promise<string|null>} null when gh is absent, unauthenticated or unhappy. */
68
+ async function ghSha(action, tag) {
69
+ try {
70
+ const { stdout } = await execFileAsync("gh", ["api", `repos/${action}/commits/${tag}`, "--jq", ".sha"], {
71
+ encoding: "utf8",
72
+ timeout: CALL_TIMEOUT_MS,
73
+ });
74
+ const sha = clean(stdout);
75
+ return SHA_RE.test(sha) ? sha : null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /** @internal Exported for dynamic import from tests. */
82
+ export function createActionPinsCheck({ pins = PINNED_ACTIONS, deps = {} } = {}) {
83
+ return {
84
+ name: "action-pins",
85
+ label: "actions fijadas por SHA",
86
+ strategy: STRATEGY_MANUAL,
87
+ describe: "Detect a pinned GitHub Action whose tag has moved on without it",
88
+ async detect() {
89
+ const entries = Object.entries(pins).map(([key, pin]) => ({ key, ...(parsePin(pin) || {}) }));
90
+ const malformed = entries.filter((e) => !e.sha);
91
+ const asked = await Promise.all(
92
+ entries.filter((x) => x.sha).map(async (e) => {
93
+ // offline, rate limited, DNS down, gh missing: not an answer
94
+ const latest = await currentSha(e.action, e.tag, deps).catch(() => null);
95
+ return { ...e, latest };
96
+ }),
97
+ );
98
+ const stale = asked.filter((e) => e.latest && e.latest !== e.sha);
99
+ const unchecked = asked.filter((e) => !e.latest).length;
100
+
101
+ if (malformed.length > 0) {
102
+ return {
103
+ ok: false,
104
+ severity: "warn",
105
+ detail: `pin mal formado: ${malformed.map((m) => m.key).join(", ")} — un pin que no se puede leer no se puede comprobar`,
106
+ fix: "revisa PINNED_ACTIONS en src/harden/workflow-templates.js: owner/repo@<sha de 40> # <tag>",
107
+ };
108
+ }
109
+ if (stale.length > 0) {
110
+ return {
111
+ ok: false,
112
+ severity: "warn",
113
+ detail: stale.map((s) => `${s.action} ${s.tag} apunta hoy a ${s.latest.slice(0, 12)} y kj fija ${s.sha.slice(0, 12)}`).join(" · "),
114
+ fix: stale.map((s) => `${s.action}@${s.latest} # ${s.tag}`).join(" · "),
115
+ };
116
+ }
117
+ // Never claim what was not looked at (the card's own acceptance
118
+ // criterion): an unreachable API is said, not passed off as green.
119
+ if (unchecked > 0) {
120
+ return {
121
+ ok: true,
122
+ severity: "info",
123
+ detail: unchecked === entries.length
124
+ ? "no se pudo comprobar ninguna action (¿sin red o con la API de GitHub limitando?)"
125
+ : `${unchecked} action(s) sin comprobar (¿sin red o con la API de GitHub limitando?)`,
126
+ };
127
+ }
128
+ return { ok: true, severity: "info", detail: `${entries.length} action(s) fijadas y al día` };
129
+ },
130
+ };
131
+ }
@@ -23,6 +23,8 @@ import { runCommand } from "../utils/process.js";
23
23
  import { checkBinary } from "../utils/agent-detect.js";
24
24
  import { getInstallCommand } from "../utils/os-detect.js";
25
25
  import { STRATEGY } from "./types.js";
26
+ import { createRepoStateCheck } from "./repo-state.js";
27
+ import { createActionPinsCheck } from "./action-pins.js";
26
28
 
27
29
  /**
28
30
  * Detect signals del proyecto. Devuelve un set de tags con qué hay en el
@@ -241,5 +243,14 @@ export function getProjectChecks({ projectDir }) {
241
243
  createToolCheck({ signal: "terraform", tool: "terraform", label: "Terraform (project signal)", signals }),
242
244
  createEnvConsistencyCheck({ projectDir, signals }),
243
245
  createGhRemoteCheck({ projectDir }),
246
+ // BOOT-B (KJC-TSK-0858): the state of the repo itself, which no other
247
+ // check looks at — the order it was set up in, whether the contract
248
+ // travels, whether this clone declares who works it.
249
+ createRepoStateCheck({ projectDir }),
250
+ // KJC-TSK-0870: pinning the actions by SHA (issue #1374) moved their
251
+ // maintenance onto kj. Doctor asks upstream whether a tag has moved on
252
+ // without its pin; `kj check` deliberately does not, because a gate that
253
+ // needs the network is a gate that fails on a plane.
254
+ createActionPinsCheck(),
244
255
  ];
245
256
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * BOOT-B (KJC-TSK-0858, epic KJC-PCS-0088) — the state of the REPOSITORY.
3
+ *
4
+ * The cold-start map of 20-sep found projects raised outside the method, and
5
+ * nobody noticed until a gate blocked with no explanation: the harness
6
+ * installed before git, the gate marker never committed (so a clone inherits
7
+ * nothing), no identity declared (so the Sentinel later denies every git and
8
+ * gh call). None of the other checks looks at this: they check tools, ports,
9
+ * index coverage, the method's own report — never whether the repo itself was
10
+ * set up in the right order.
11
+ *
12
+ * Two rules, both learned the hard way in this codebase:
13
+ * - The bootstrap phase is NOT a defect. A repo with no commit yet is
14
+ * starting, and saying otherwise is the false alarm that teaches people to
15
+ * ignore the output.
16
+ * - Every symptom carries the literal command that repairs it. A diagnosis
17
+ * nobody can act on is a complaint.
18
+ */
19
+
20
+ import { execFileSync } from "node:child_process";
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+
24
+ const STRATEGY_MANUAL = "manual";
25
+
26
+ const git = (projectDir, args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
27
+ const gitOk = (projectDir, args) => { try { git(projectDir, args); return true; } catch { return false; } };
28
+
29
+ /** @internal Exported for dynamic import from tests. */
30
+ export function createRepoStateCheck({ projectDir }) {
31
+ return {
32
+ name: "repo-state",
33
+ label: "estado del repositorio",
34
+ strategy: STRATEGY_MANUAL,
35
+ describe: "Detect a project raised or carried outside the method (order, contract, identity)",
36
+ async detect() {
37
+ const dir = projectDir;
38
+ const has = (rel) => existsSync(join(dir, rel));
39
+ const hardened = has(".karajan/hooks/pre-commit") || has(".karajan/review-gate");
40
+
41
+ // 1. No repository. The gates live in git hooks, so there is nothing to
42
+ // govern yet — and if the harness is already there, it governs nothing.
43
+ if (!has(".git")) {
44
+ return {
45
+ ok: false,
46
+ severity: "warn",
47
+ detail: hardened
48
+ ? "el proyecto está sin repositorio y el harness ya está escrito: no gobierna nada"
49
+ : "el proyecto está sin repositorio, así que ningún gate del método puede actuar",
50
+ fix: "kj bootstrap (crea el repositorio y deja el método en orden), o git init si prefieres hacerlo a mano",
51
+ };
52
+ }
53
+
54
+ // 2. The harness written before git existed: nothing tracks the contract,
55
+ // so cloning inherits nothing even though this clone looks hardened.
56
+ const hasCommits = gitOk(dir, ["rev-parse", "--verify", "HEAD"]);
57
+ if (!hasCommits) {
58
+ return hardened
59
+ ? { ok: true, severity: "info", detail: "fase de arranque: el método está escrito y falta el primer commit" }
60
+ : { ok: true, severity: "info", detail: "fase de arranque: repositorio recién creado, sin método todavía" };
61
+ }
62
+
63
+ const symptoms = [];
64
+ // Against HEAD, not the index (catch de codex): a file merely STAGED is
65
+ // not inherited by a clone, which is the whole point of this symptom.
66
+ const committed = (rel) => gitOk(dir, ["cat-file", "-e", `HEAD:${rel}`]);
67
+ if (hardened && !committed(".karajan/review-gate") && !committed(".karajan/hooks/pre-commit")) {
68
+ symptoms.push({
69
+ detail: "el harness está instalado pero su contrato no está en git: quien clone no hereda ningún gate",
70
+ fix: "git add .karajan/review-gate .karajan/hooks && git commit -m \"chore: el contrato del método\"",
71
+ });
72
+ }
73
+ if (hardened && !has(".karajan/identity.local.yml")) {
74
+ symptoms.push({
75
+ detail: "este clon no declara identidad, y el Sentinel denegará cada git y cada gh hasta que la declares",
76
+ fix: "kj identity set --gh <usuario> --email <correo>",
77
+ });
78
+ }
79
+
80
+ if (symptoms.length === 0) return { ok: true, severity: "info", detail: "el repositorio está en orden" };
81
+ return {
82
+ ok: false,
83
+ severity: "warn",
84
+ detail: symptoms.map((s) => s.detail).join(" · "),
85
+ fix: symptoms.map((s) => s.fix).join(" · "),
86
+ };
87
+ },
88
+ };
89
+ }
@@ -9,6 +9,9 @@
9
9
  export const CORE_COMMANDS = [
10
10
  "go",
11
11
  "start",
12
+ // BOOT-A (KJC-TSK-0857): starting a project from nothing is as core as it
13
+ // gets — it is the first command a new project ever runs.
14
+ "bootstrap",
12
15
  "init",
13
16
  "run",
14
17
  "plan",
@@ -5,6 +5,8 @@ import { architectCommand } from "../commands/architect.js";
5
5
  import { onboardCommand } from "../commands/onboard.js";
6
6
  import { startCommand } from "../commands/start.js";
7
7
  import { goCommand } from "../commands/go.js";
8
+ import { bootstrapCommand } from "../commands/bootstrap.js";
9
+ import { PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
8
10
  import { identityCommand } from "../commands/identity.js";
9
11
  import { policyCommand } from "../commands/policy.js";
10
12
  import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
@@ -158,6 +160,18 @@ export function registerMeta(program, { pkgVersion }) {
158
160
  });
159
161
  });
160
162
 
163
+ // BOOT-A (KJC-TSK-0857, cold-start ADR): the pieces existed, the ORDER did
164
+ // not. One command from an empty directory to a project under the method.
165
+ program
166
+ .command("bootstrap")
167
+ .description("Del directorio vacío al método listo: repositorio, configuración, harness, gate y RAG, en el orden que funciona")
168
+ .action(async (flags) => {
169
+ await withConfig(pkgVersion, "bootstrap", flags, async ({ config, logger }) => {
170
+ const res = await bootstrapCommand({ config, logger, flags });
171
+ if (!res.ok) process.exitCode = PENDING_EXIT_CODE;
172
+ });
173
+ });
174
+
161
175
  // MGL-A (KJC-TSK-0808): the muggle launcher — one command, at most one
162
176
  // question, and the person lands inside a governed conversation.
163
177
  program
@@ -179,11 +179,21 @@ export function registerPipeline(program, { pkgVersion }) {
179
179
  .argument("[task]", "Task description (REQUIRED — provide as argument or via --task-file)")
180
180
  .option("--task-file <path>", "Read the task from a file (e.g. .md)")
181
181
  .option("--coder <name>")
182
+ // KJC-TSK-0864: --coder is the pipeline's word for it, --agent is the
183
+ // session's. Same knob, two vocabularies; naming both and disagreeing is
184
+ // an error, never a silent precedence.
185
+ .option("--agent <name>", "The agent that writes (alias of --coder)")
186
+ .option("--card <ref>", "Card/HU the work belongs to — its statement and criteria reach the coder")
182
187
  .option("--coder-model <name>")
183
188
  .action(async (task, flags) => {
189
+ if (flags.agent && flags.coder && flags.agent !== flags.coder) {
190
+ console.error(`kj code: --agent ${flags.agent} and --coder ${flags.coder} name different agents — pick one`);
191
+ process.exit(2);
192
+ }
193
+ if (flags.agent) flags.coder = flags.agent;
184
194
  await withConfig(pkgVersion, "code", flags, async ({ config, logger }) => {
185
195
  const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
186
- await codeCommand({ task: resolvedTask, config, logger });
196
+ await codeCommand({ task: resolvedTask, config, logger, flags });
187
197
  });
188
198
  });
189
199
 
@@ -239,6 +249,10 @@ export function registerPipeline(program, { pkgVersion }) {
239
249
  .option("--prune-days <n>", "TTL in days for --prune (default: 14)")
240
250
  .option("--dry-run", "With --prune: report what would be removed without deleting")
241
251
  .option("--no-sonar", "Skip the deterministic Sonar pre-gate that runs before the cross-AI verdict")
252
+ // BOOT-D (KJC-TSK-0863): how the walkthrough gets INTO the verdict. Without
253
+ // this the rule could only complain, which is the gate with no exit.
254
+ .option("--walked <route>", "Recorrido comprobado como lo haría una persona (repetible) — queda en el veredicto", (v, prev) => [...(prev || []), v], [])
255
+ .option("--walked-with <tool>", "Con qué se comprobó el recorrido (por ejemplo chrome-devtools)")
242
256
  .action(async (task, flags) => {
243
257
  await withConfig(pkgVersion, "review", flags, async ({ config, logger }) => {
244
258
  // ENV-B1 (KJC-TSK-0637): the gate mode records a verdict tied to
@@ -0,0 +1,131 @@
1
+ /**
2
+ * BOOT-A (KJC-TSK-0857, epic KJC-PCS-0088) — `kj bootstrap`: from an empty
3
+ * directory to a project under the method, in one command.
4
+ *
5
+ * It invents nothing. The pieces already existed; what was missing was the
6
+ * ORDER and somebody imposing it. The field report that opened the cold-start
7
+ * ADR found nine walls on that path, and several of them fired precisely
8
+ * because the repository had been raised by hand, in the wrong sequence.
9
+ *
10
+ * Rules this command obeys:
11
+ * - Idempotent. What is already there is reported as `already`, never redone.
12
+ * - Fail loud, never half. A step that needs the person's hands STOPS the
13
+ * sequence and says which one; nothing downstream pretends it ran.
14
+ * - No guarantees without git: the gates live in git hooks, so the repo comes
15
+ * first and a git that cannot run is the end of the line, not a warning.
16
+ */
17
+
18
+ import { execFileSync } from "node:child_process";
19
+ import { existsSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { ensureGitRepo } from "./init.js";
22
+ import { initCommand } from "./init.js";
23
+ import { envInstallCommand } from "./env.js";
24
+ import { runStartScript, START_SCRIPT_CONTRACT } from "../start/project-script.js";
25
+
26
+ const STEP_LABEL = {
27
+ git: "repositorio",
28
+ config: "configuración del proyecto",
29
+ method: "método activo (harness, gate y RAG)",
30
+ contract: "commit del contrato",
31
+ start: "arranque del proyecto",
32
+ };
33
+
34
+ /** What kj generates and the whole team must inherit by cloning. */
35
+ const CONTRACT_PATHS = [
36
+ ".gitignore",
37
+ ".karajan/hooks",
38
+ ".karajan/review-gate",
39
+ ".karajan/adrs",
40
+ ".karajan/policy.yml",
41
+ ".claude",
42
+ "CLAUDE.md",
43
+ "AGENTS.md",
44
+ "GEMINI.md",
45
+ ];
46
+ const CONTRACT_MESSAGE = "chore(bootstrap): el contrato del método, para que quien clone lo herede";
47
+
48
+ const hasCommits = (projectDir, git) => {
49
+ try { git(["rev-parse", "--verify", "HEAD"]); return true; } catch { return false; }
50
+ };
51
+
52
+ /**
53
+ * @returns {Promise<{ok: boolean, pending: string|null, steps: Array<{name: string, status: "done"|"already"|"pending"}>}>}
54
+ */
55
+ export async function bootstrapCommand({ config = {}, logger = console, flags = {}, deps = {} } = {}) {
56
+ const projectDir = config.projectDir || process.cwd();
57
+ const steps = [];
58
+ const say = (name, status, detail) => {
59
+ steps.push({ name, status });
60
+ const mark = status === "already" ? "·" : "✓";
61
+ logger.info?.(`${mark} ${STEP_LABEL[name]}${detail ? ` — ${detail}` : ""}`);
62
+ };
63
+ const stop = (name, why) => {
64
+ steps.push({ name, status: "pending" });
65
+ logger.error?.(`✗ ${STEP_LABEL[name]} — ${why}`);
66
+ return { ok: false, pending: name, steps };
67
+ };
68
+
69
+ // 1. The repository. Without it there are no hooks, and without hooks there
70
+ // are no guarantees: this is the one step that cannot be skipped.
71
+ const hadRepo = existsSync(join(projectDir, ".git"));
72
+ if (!ensureGitRepo({ projectDir, logger: { info() {}, warn() {} }, gitFn: deps.gitFn })) {
73
+ return stop("git", "git no está disponible y las garantías de Karajan viven en sus hooks");
74
+ }
75
+ say("git", hadRepo ? "already" : "done", hadRepo ? "ya existía" : "creado, sin commit todavía");
76
+
77
+ // 2. The project's own configuration.
78
+ const hasConfig = existsSync(join(projectDir, ".karajan", "kj.config.yml"));
79
+ if (hasConfig) say("config", "already", "ya estaba");
80
+ else {
81
+ const init = await (deps.init ?? initCommand)({ logger, flags: { ...flags, noInteractive: true } });
82
+ if (init?.exitCode) return stop("config", "queda algo que solo puedes hacer tú, lo tienes arriba");
83
+ say("config", "done");
84
+ }
85
+
86
+ // 3. The method itself: harness, verdict gate, playbook and RAG index. This
87
+ // is the step that turns an installed kj into an enforced one.
88
+ const hasGate = existsSync(join(projectDir, ".karajan", "review-gate"));
89
+ if (hasGate) say("method", "already", "ya estaba activo");
90
+ else {
91
+ const env = await (deps.env ?? envInstallCommand)({ config, logger, flags: { yes: true } });
92
+ if (env?.exitCode) return stop("method", "queda algo que solo puedes hacer tú, lo tienes arriba");
93
+ say("method", "done");
94
+ }
95
+
96
+ // 4. The contract commit. project-new.md asked the USER for it, and it was
97
+ // the commit their own freshly installed gates rejected: the review gate
98
+ // (KJC-BUG-0165) and the branch guard (KJC-BUG-0186) both exempt it now,
99
+ // so kj can make it itself instead of leaving the person to fight it.
100
+ // Only what kj generated, only while the repo has no commit, never the
101
+ // person's own code. This is NOT the supervisor seal, which stays a human
102
+ // act with its own four layers (ADR 0009).
103
+ const git = deps.gitRun ?? ((args) => execFileSync("git", args, { cwd: projectDir, encoding: "utf8" }));
104
+ if (hasCommits(projectDir, git)) say("contract", "already", "el repositorio ya tiene historia");
105
+ else {
106
+ const present = CONTRACT_PATHS.filter((p) => existsSync(join(projectDir, p)));
107
+ if (present.length === 0) say("contract", "already", "no hay contrato que commitear");
108
+ else {
109
+ try {
110
+ git(["add", "--", ...present]);
111
+ git(["commit", "-m", CONTRACT_MESSAGE]);
112
+ } catch (err) {
113
+ // Nunca explotar aquí: lo más probable es que falte la identidad del
114
+ // clon, y ese cauce ya lo pide el paso anterior (KJC-BUG-0188).
115
+ return stop("contract", `git no pudo commitear el contrato: ${String(err.message).split("\n")[0]}`);
116
+ }
117
+ say("contract", "done", `${present.length} ruta(s) del contrato`);
118
+ }
119
+ }
120
+
121
+ // 5. Does the project actually run? kj verified the method; nobody verified
122
+ // the application (BOOT-C, KJC-TSK-0862). It REPORTS, never blocks: a
123
+ // start script written badly must not stop work over something unrelated,
124
+ // and a tree that was already broken has to be said BEFORE implementing.
125
+ const start = await (deps.start ?? runStartScript)({ projectDir, config });
126
+ if (start.status === "absent") say("start", "already", `sin guion de arranque — ${START_SCRIPT_CONTRACT}`);
127
+ else if (start.status === "ok") say("start", "done", "el proyecto arranca y su humo pasa");
128
+ else say("start", "already", `el árbol YA venía roto (${start.status}) — no lo ha causado este trabajo`);
129
+
130
+ return { ok: true, pending: null, steps };
131
+ }
@@ -9,6 +9,7 @@ import { collectMethodStats, formatMethodStats } from "../checks/method.js";
9
9
  import { createRagCoverageCheck } from "../checks/rag-coverage.js";
10
10
  import { checkAiSurface, formatAiSurface } from "../checks/ai-surface.js";
11
11
  import { detectObservedAgents } from "../utils/agent-detect.js";
12
+ import { panelSummary } from "../environment/panel.js";
12
13
  import { loadConfig } from "../config.js";
13
14
 
14
15
  export async function checkCommand({ projectDir = process.cwd(), profile = "standard", json = false, logger = console } = {}) {
@@ -44,6 +45,10 @@ export async function checkCommand({ projectDir = process.cwd(), profile = "stan
44
45
  if (method) logger.info?.(` method: ${formatMethodStats(method)}`);
45
46
  logger.info?.(` ${ragCoverage.ok ? "✓" : "✗"} rag-coverage: ${ragCoverage.detail}`);
46
47
  if (aiSurface) logger.info?.(` ${formatAiSurface(aiSurface)}`);
48
+ // KJC-TSK-0865: who writes and who reviews is the user's choice, so it is
49
+ // said out loud next to everything else that describes the project. Never
50
+ // a check: it has no ok/fail, it informs.
51
+ logger.info?.(` ${panelSummary(config)}`);
47
52
  if (ok) logger.info?.("Harness OK.");
48
53
  else if (result.ok) logger.info?.("RAG index drift detected — a gate cannot protect what it cannot see.");
49
54
  else logger.info?.("Harness drift detected — run `kj harden` to repair.");
@@ -2,13 +2,35 @@ import fs from "node:fs/promises";
2
2
  import { createAgent } from "../agents/index.js";
3
3
  import { assertAgentsAvailable } from "../agents/availability.js";
4
4
  import { buildCoderPrompt } from "../prompts/coder.js";
5
+ import { resolveCardContext } from "../prompts/card-context.js";
6
+ import { composeTask, resolveRagContext } from "../prompts/session-context.js";
5
7
  import { resolveRole } from "../config.js";
8
+ import { withBrainRecovery, DEFAULT_RECOVERY_POLICY } from "../brain/with-brain-recovery.js";
9
+ import { buildRoleFallbackChain } from "../brain/role-fallback-chain.js";
10
+ import { ERROR_CLASS } from "../brain/agent-error-classifier.js";
11
+
12
+ // KJC-TSK-0859: the agents no longer substitute a dead model behind your back,
13
+ // so the command needs the declared chain. A one-shot CLI must not inherit the
14
+ // pipeline's standby either: sleeping five hours inside `kj code` would be
15
+ // worse than the failure. A quota wall takes the next candidate at once, and
16
+ // with none left it stops and says what it tried.
17
+ const ONE_SHOT_POLICY = Object.freeze({
18
+ ...DEFAULT_RECOVERY_POLICY,
19
+ classes: {
20
+ ...DEFAULT_RECOVERY_POLICY.classes,
21
+ [ERROR_CLASS.QUOTA_EXHAUSTED_DAILY]: { mode: "abort", maxRetries: 0, fallbackEligible: true, fallbackImmediate: true },
22
+ [ERROR_CLASS.QUOTA_EXHAUSTED_MONTHLY]: { mode: "abort", maxRetries: 0, fallbackEligible: true, fallbackImmediate: true },
23
+ },
24
+ });
6
25
  import { withCliRunLog } from "../utils/cli-run-log.js";
7
26
  import { createCliProgressReporter } from "../utils/cli-progress.js";
8
27
 
9
- export async function codeCommand({ task, config, logger }) {
28
+ export async function codeCommand({ task, config, logger, flags = {} }) {
10
29
  return withCliRunLog("code", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
11
30
  const coderRole = resolveRole(config, "coder");
31
+ // KJC-TSK-0864: no quota, not authenticated or simply absent? This THROWS
32
+ // with the agent named. The one thing it never does is quietly fall back
33
+ // to whoever happens to be reachable.
12
34
  await assertAgentsAvailable([coderRole.provider]);
13
35
  logger.info(`Coder (${coderRole.provider}) starting...`);
14
36
  runLog.logText(`[coder] provider=${coderRole.provider}`);
@@ -21,11 +43,39 @@ export async function codeCommand({ task, config, logger }) {
21
43
  try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
22
44
  }
23
45
  }
24
- const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
46
+ const card = await resolveCardContext({ projectDir: config.projectDir, ref: flags.card || null });
47
+ if (card?.external) logger.warn(`card ${card.huId} lives on a board kj cannot read — passing the reference, not its text`);
48
+ else if (card) logger.info(`Card ${card.huId}: ${card.title}`);
49
+ // The method's first invariant is that the RAG answers before you assume.
50
+ // The session asks on the coder's behalf: a subprocess coder cannot run
51
+ // `kj rag query` itself, so it used to guess the codebase instead.
52
+ const rag = await resolveRagContext({ task, config, logger });
53
+ // The pipeline always passed projectDir (it arms the directory-boundary
54
+ // rule and the skills section) and the provider. `kj code` did not, so the
55
+ // declared coder got a weaker prompt from the session than from `kj run`.
56
+ const prompt = await buildCoderPrompt({
57
+ task: composeTask(task, [card?.section, rag?.section]),
58
+ coderRules,
59
+ methodology: config.development?.methodology || "tdd",
60
+ projectDir: config.projectDir,
61
+ provider: coderRole.provider,
62
+ huId: card?.huId ?? null,
63
+ acceptanceTests: card?.acceptanceTests ?? null,
64
+ serenaEnabled: Boolean(config.serena?.enabled),
65
+ rtkAvailable: Boolean(config.rtk?.available),
66
+ });
25
67
  const progress = createCliProgressReporter({ role: "coder" });
26
68
  let result;
27
69
  try {
28
- result = await coder.runTask({ prompt, onOutput: progress.onOutput, role: "coder" });
70
+ result = await withBrainRecovery({
71
+ agent: { runTask: (args) => coder.runTask(args), provider: coderRole.provider, model: coderRole.model },
72
+ taskArgs: { prompt, onOutput: progress.onOutput, role: "coder" },
73
+ role: "coder",
74
+ provider: coderRole.provider,
75
+ logger,
76
+ policy: ONE_SHOT_POLICY,
77
+ fallback: buildRoleFallbackChain({ config, role: "coder", logger }),
78
+ });
29
79
  progress.finish(result.ok ? "done" : "failed");
30
80
  } catch (err) { progress.finish("failed"); throw err; }
31
81
  if (!result.ok) {
@@ -39,7 +89,10 @@ export async function codeCommand({ task, config, logger }) {
39
89
  logger.warn(result.error);
40
90
  }
41
91
  logger.info(`Coder completed (exit ${result.exitCode})`);
92
+ // The work is in the tree, not committed: the gate is the next step and a
93
+ // DIFFERENT AI runs it. Saying so here is what keeps the panel honest.
94
+ logger.info(`The work is in the tree. Review it before committing: kj review --staged (a different AI than ${coderRole.provider}).`);
42
95
  runLog.logText(`[coder] finished (exit=${result.exitCode})`);
43
- return { ok: true };
96
+ return { ok: true, provider: coderRole.provider, card: card?.huId ?? null, ragSources: rag?.sources ?? [] };
44
97
  });
45
98
  }
@@ -71,6 +71,7 @@ export async function envInstallCommand({ config = null, logger = null, flags =
71
71
  projectDir, target: flags.target || "all",
72
72
  stateBackend: config?.state_backend || "hu-board",
73
73
  boardName: config?.board?.name || null,
74
+ config, // KJC-TSK-0865: the panel travels with the method
74
75
  });
75
76
  console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
76
77
 
@@ -213,7 +214,7 @@ export async function envInstallCommand({ config = null, logger = null, flags =
213
214
  // CLAUDE.md, but the running session loaded its context BEFORE — nobody
214
215
  // re-reads it. Print the method so it enters THIS conversation now.
215
216
  console.log("\n— The Karajan method below is IN EFFECT from this very message. If your session started before this install, apply it from now on:\n");
216
- console.log(renderPlaybook({ stateBackend: config?.state_backend || "hu-board", boardName: config?.board?.name || null }));
217
+ console.log(renderPlaybook({ stateBackend: config?.state_backend || "hu-board", boardName: config?.board?.name || null, config }));
217
218
  }
218
219
  return result;
219
220
  }
@@ -12,8 +12,7 @@ import path from "node:path";
12
12
  import { spawn } from "node:child_process";
13
13
  import { checkBinary } from "../utils/agent-detect.js";
14
14
  import { createCliAskQuestion } from "../utils/cli-ask-question.js";
15
- import { envInstallCommand } from "./env.js";
16
- import { ensureGitRepo } from "./init.js";
15
+ import { bootstrapCommand } from "./bootstrap.js";
17
16
  import { boardCommand } from "./board.js";
18
17
  // The interactive launchers a muggle can live inside (v1: the two the epic
19
18
  // names). Auth heuristics are the same cheap file checks reviewer-fallback
@@ -44,9 +43,6 @@ export function buildGoPrompt() {
44
43
  "Empieza presentándote en dos frases y preguntando qué quiere construir o cambiar hoy.",
45
44
  ].join("\n");
46
45
  }
47
- async function defaultPrepare({ config, logger }) {
48
- return envInstallCommand({ config, logger, flags: { yes: true } });
49
- }
50
46
  export async function defaultBoard({ config, logger, runBoard = boardCommand, openPath = "/?maggle=1" }) {
51
47
  const port = config.hu_board?.port || 4000;
52
48
  await runBoard({ action: "start", port, bind: "127.0.0.1", logger });
@@ -96,14 +92,17 @@ export async function goCommand({ config = {}, logger = console, flags = {}, dep
96
92
  // Prepare ONCE: decisions already taken are never re-asked.
97
93
  if (!existsSync(path.join(projectDir, ".karajan", "review-gate"))) {
98
94
  logger.info?.("Preparando tu proyecto (solo la primera vez)…");
99
- // KJC-BUG-0191: empezar en una carpeta vacía es lo natural, y kj env
100
- // install se niega a correr sin repositorio. Lo creamos nosotros.
101
- if (!ensureGitRepo({ projectDir, logger })) {
102
- logger.error?.("No he podido preparar el control de versiones de tu proyecto (git). Instálalo y vuelve a escribir: kj go");
95
+ // BOOT-A paso 3 (KJC-TSK-0857): el orden vive en UN sitio, kj bootstrap.
96
+ // Antes esta puerta cableaba su propia secuencia (git init y env install),
97
+ // así que la puerta del maggle y la técnica podían separarse sin que nadie
98
+ // se diera cuenta. Ahora las dos hacen lo mismo.
99
+ const booted = await (deps.bootstrap ?? bootstrapCommand)({ config, logger, flags: {}, deps: {} });
100
+ if (!booted.ok) {
101
+ logger.error?.(`Falta una cosa que solo puedes hacer tú, la tienes justo arriba (${booted.pending}). Cuando la hagas, vuelve a escribir: kj go`);
103
102
  process.exitCode = 1;
104
103
  return 1;
105
104
  }
106
- const prepared = await (deps.prepare ?? defaultPrepare)({ config, logger });
105
+ const prepared = deps.prepare ? await deps.prepare({ config, logger }) : null;
107
106
  // Algo depende de tus manos (arriba tienes el detalle). Sin eso el
108
107
  // proyecto quedaría a medias, así que paramos aquí en vez de seguir.
109
108
  if (prepared?.exitCode) {
@@ -171,7 +171,9 @@ export async function hardenCommand({
171
171
  ? installWorkflows({ projectDir, language: roots[0]?.language ?? null, profile, mutation, dryRun })
172
172
  : null;
173
173
  const withGuidelines = guidelines && profile !== "minimal";
174
- const gl = withGuidelines ? installGuidelines({ projectDir, dryRun }) : null;
174
+ // KJC-BUG-0199 (issue #1773): the same detected language the workflows and
175
+ // the configs already use — a Python project must not be told to use `const`.
176
+ const gl = withGuidelines ? installGuidelines({ projectDir, language: roots[0]?.language ?? null, dryRun }) : null;
175
177
  const out = {
176
178
  ok: true,
177
179
  ...result,