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.
- package/package.json +1 -1
- package/src/agents/aider-agent.js +2 -12
- package/src/agents/base-agent.js +9 -20
- package/src/agents/claude-agent.js +2 -12
- package/src/agents/codex-agent.js +2 -12
- package/src/agents/dead-models.js +74 -0
- package/src/agents/gemini-agent.js +2 -12
- package/src/agents/model-errors.js +35 -0
- package/src/agents/opencode-agent.js +2 -12
- package/src/brain/agent-error-classifier.js +19 -1
- package/src/brain/role-fallback-chain.js +100 -0
- package/src/brain/with-brain-recovery.js +59 -3
- package/src/checks/action-pins.js +131 -0
- package/src/checks/project-checks.js +11 -0
- package/src/checks/repo-state.js +89 -0
- package/src/cli/advanced-commands.js +3 -0
- package/src/cli/register-meta.js +14 -0
- package/src/cli/register-pipeline.js +15 -1
- package/src/commands/bootstrap.js +131 -0
- package/src/commands/check.js +5 -0
- package/src/commands/code.js +57 -4
- package/src/commands/env.js +2 -1
- package/src/commands/go.js +9 -10
- package/src/commands/harden.js +3 -1
- package/src/commands/review-gate.js +28 -5
- package/src/environment/panel.js +52 -0
- package/src/environment/playbook.js +7 -6
- package/src/harden/config-templates.js +7 -1
- package/src/harden/guidelines-engine.js +7 -3
- package/src/harden/guidelines-templates.js +60 -10
- package/src/harden/hook-templates.js +4 -1
- package/src/privacy/diff-scope.js +58 -0
- package/src/privacy/scan.js +18 -0
- package/src/prompts/card-context.js +57 -0
- package/src/prompts/session-context.js +68 -0
- package/src/review/one-shot-review.js +5 -0
- package/src/review/ui-evidence.js +65 -0
- package/src/roles/agent-role.js +11 -2
- 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
|
+
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/check.js
CHANGED
|
@@ -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.");
|
package/src/commands/code.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/src/commands/env.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/go.js
CHANGED
|
@@ -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 {
|
|
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-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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) {
|
package/src/commands/harden.js
CHANGED
|
@@ -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
|
-
|
|
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,
|