@karajan-family/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/register-pipeline.js +15 -1
- package/src/commands/bootstrap.js +56 -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
|
+
}
|
|
@@ -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
|
|
@@ -15,16 +15,38 @@
|
|
|
15
15
|
* first and a git that cannot run is the end of the line, not a warning.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { existsSync } from "node:fs";
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import { ensureGitRepo } from "./init.js";
|
|
21
22
|
import { initCommand } from "./init.js";
|
|
22
23
|
import { envInstallCommand } from "./env.js";
|
|
24
|
+
import { runStartScript, START_SCRIPT_CONTRACT } from "../start/project-script.js";
|
|
23
25
|
|
|
24
26
|
const STEP_LABEL = {
|
|
25
27
|
git: "repositorio",
|
|
26
28
|
config: "configuración del proyecto",
|
|
27
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; }
|
|
28
50
|
};
|
|
29
51
|
|
|
30
52
|
/**
|
|
@@ -71,5 +93,39 @@ export async function bootstrapCommand({ config = {}, logger = console, flags =
|
|
|
71
93
|
say("method", "done");
|
|
72
94
|
}
|
|
73
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
|
+
|
|
74
130
|
return { ok: true, pending: null, steps };
|
|
75
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,
|
|
@@ -15,12 +15,14 @@ import { ensureGateTrackable } from "../review/gate-gitignore.js";
|
|
|
15
15
|
import { runSonarPregate, formatSonarFinding, addedLinesByFile } from "../review/sonar-pregate.js";
|
|
16
16
|
import { checkSonarRequirement, SONAR_RULE_ID } from "../review/sonar-requirement.js";
|
|
17
17
|
import { checkRagRequirement, checkRagVerdict, ragBlock, RAG_RULE_ID } from "../review/rag-requirement.js";
|
|
18
|
+
import { checkUiEvidence, uiBlock } from "../review/ui-evidence.js";
|
|
18
19
|
import { readRagLedger } from "../review/rag-ledger.js";
|
|
19
20
|
import { runMutationPregate, formatSurvivor } from "../review/mutation-pregate.js";
|
|
20
21
|
import { checkCardFirst } from "../review/card-first.js";
|
|
21
22
|
import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
22
23
|
import { checkTestsWithCode } from "../review/tests-with-code.js";
|
|
23
24
|
import { loadPrivacyList, scanText } from "../privacy/scan.js";
|
|
25
|
+
import { isGeneratedPath, splitAddedByFile } from "../privacy/diff-scope.js";
|
|
24
26
|
import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
|
|
25
27
|
import { loadStandingExceptions, recordPolicyException } from "../policy/exceptions.js";
|
|
26
28
|
import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
|
|
@@ -229,17 +231,24 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
229
231
|
// don't publish. In the SEA binary the privacy module is stubbed and
|
|
230
232
|
// throws: the gate degrades with a note instead of crashing.
|
|
231
233
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
// KJC-BUG-0203: per file, so a finding can name where it lives and so
|
|
235
|
+
// build output is judged as what it is. Generic heuristics are silenced
|
|
236
|
+
// there (nobody typed a minified bundle); a denylist hit is not, because
|
|
237
|
+
// the incident behind this scanner was personal data inside a build.
|
|
238
|
+
const list = loadPrivacyList();
|
|
239
|
+
const findings = splitAddedByFile(diff).flatMap(({ file, added }) => {
|
|
240
|
+
const found = scanText(added, { list, source: file });
|
|
241
|
+
return isGeneratedPath(file) ? found.filter((f) => f.severity === "block") : found;
|
|
242
|
+
});
|
|
234
243
|
const blocks = findings.filter((f) => f.severity === "block");
|
|
235
244
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
236
|
-
for (const f of warns) console.log(`⚠ privacy: [${f.type}]
|
|
245
|
+
for (const f of warns) console.log(`⚠ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked} — personal data? move it out before it ships`);
|
|
237
246
|
const hardened = config?.privacy?.generic === "block" && warns.length > 0;
|
|
238
247
|
if (blocks.length > 0 || hardened) {
|
|
239
248
|
if (process.env.KJ_ALLOW_PII === "1") {
|
|
240
249
|
console.log(`⚠ privacy exempt: ${blocks.length} denylist hit(s) — KJ_ALLOW_PII=1 (explicit escape hatch)`);
|
|
241
250
|
} else {
|
|
242
|
-
for (const f of blocks) console.log(`✗ privacy: [${f.type}]
|
|
251
|
+
for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
|
|
243
252
|
const reason = `${blocks.length || warns.length} personal-data finding(s) in the staged diff — this must not reach the repo (KJ_ALLOW_PII=1 to override consciously)`;
|
|
244
253
|
console.log(`✗ privacy gate: ${reason}`);
|
|
245
254
|
process.exitCode = 1;
|
|
@@ -482,6 +491,20 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
482
491
|
+ twins.map((t) => `- ${t}`).join("\n");
|
|
483
492
|
}
|
|
484
493
|
|
|
494
|
+
// BOOT-D (KJC-TSK-0863): green is not proof for what a person SEES. kj does
|
|
495
|
+
// not drive the browser (the host has it); it demands the walkthrough and
|
|
496
|
+
// records it in the verdict, bound to the diff like sonar and rag. It WARNS:
|
|
497
|
+
// the last proof, never the only one, and a gate that fires often teaches
|
|
498
|
+
// people to skip gates.
|
|
499
|
+
const walked = Array.isArray(flags.walked) ? flags.walked : [];
|
|
500
|
+
const uiReq = checkUiEvidence({
|
|
501
|
+
stagedFiles: changedFiles,
|
|
502
|
+
evidence: walked.length > 0 ? { walked, tool: flags.walkedWith ?? null } : null,
|
|
503
|
+
standingExceptions: std.standing,
|
|
504
|
+
});
|
|
505
|
+
if (uiReq.warn) console.log(`⚠ ${uiReq.reason}`);
|
|
506
|
+
const uiRecord = uiBlock(uiReq);
|
|
507
|
+
|
|
485
508
|
// MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
|
|
486
509
|
// SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
|
|
487
510
|
// el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
|
|
@@ -504,7 +527,7 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
504
527
|
}
|
|
505
528
|
}
|
|
506
529
|
|
|
507
|
-
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonarRecord, rag: ragRecord });
|
|
530
|
+
const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonarRecord, rag: ragRecord, ui: uiRecord });
|
|
508
531
|
printVerdict(record);
|
|
509
532
|
process.exitCode = record.verdict === "approved" ? 0 : 1;
|
|
510
533
|
return record;
|