karajan-code 4.52.0 → 4.53.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 +2 -1
- package/scripts/contract-claude.mjs +24 -0
- package/scripts/esbuild-sea.config.mjs +7 -0
- package/scripts/record-agent-fixtures.mjs +98 -0
- package/src/agents/claude-agent.js +8 -0
- package/src/agents/model-registry.js +5 -0
- package/src/agents/opencode-agent.js +16 -1
- package/src/audit/issue-filter.js +19 -1
- package/src/checks/audit-cycle.js +41 -0
- package/src/checks/claude-contract-check.js +27 -0
- package/src/checks/method.js +3 -2
- package/src/checks/release-check.js +7 -1
- package/src/checks/sonar.js +30 -12
- package/src/checks/supply-chain-check.js +47 -0
- package/src/cli/register-meta.js +6 -1
- package/src/cli/register-pipeline.js +9 -0
- package/src/cli/register-plan.js +10 -0
- package/src/commands/check.js +11 -1
- package/src/commands/config.js +2 -0
- package/src/commands/harden.js +3 -0
- package/src/commands/hu.js +24 -3
- package/src/commands/identity.js +4 -1
- package/src/commands/init.js +34 -1
- package/src/commands/plan/generate.js +5 -0
- package/src/commands/plan/license.js +35 -0
- package/src/commands/plan/ready.js +1 -1
- package/src/commands/plan/validate.js +1 -1
- package/src/commands/report.js +14 -0
- package/src/commands/review-gate.js +22 -0
- package/src/commands/worktree.js +15 -6
- package/src/config/defaults.js +10 -1
- package/src/config/overrides.js +8 -0
- package/src/config/schema.js +7 -0
- package/src/harden/act-sign.js +2 -2
- package/src/harden/branch-protection.js +97 -0
- package/src/harden/phone-invite.js +15 -8
- package/src/harden/phone-texts.js +17 -0
- package/src/harden/security-level.js +18 -2
- package/src/harden/sentinel-hooks.js +2 -1
- package/src/hu/complexity-tier.js +32 -0
- package/src/hu/lane-prune.js +24 -0
- package/src/hu/model-router.js +7 -16
- package/src/hu/routing-metrics.js +16 -0
- package/src/hu/store.js +2 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +2 -0
- package/src/orchestrator/hu-sub-pipeline.js +19 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +10 -2
- package/src/orchestrator/stages/sonar-stage.js +19 -1
- package/src/plan/plan-schema.js +4 -1
- package/src/plan/rework-license.js +38 -0
- package/src/prompts/hu-reviewer.js +5 -2
- package/src/prompts/planner.js +9 -1
- package/src/report/model-tally.js +39 -0
- package/src/review/branch-overlap.js +40 -0
- package/src/review/dep-cooldown.js +87 -0
- package/src/review/one-shot-review.js +2 -2
- package/src/review/parser.js +5 -0
- package/src/review/publish-pr.js +54 -0
- package/src/review/verdict-store.js +19 -7
- package/src/roles/sonar-role.js +4 -2
- package/src/sonar/api.js +7 -0
- package/src/utils/injection-guard.js +59 -43
- package/src/utils/injection-patterns.json +36 -0
- package/src/utils/model-selector.js +47 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.53.0",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"setup": "node scripts/install.js",
|
|
77
77
|
"test": "vitest run",
|
|
78
78
|
"test:scenarios": "vitest run --config tests/scenarios/vitest.config.js --root .",
|
|
79
|
+
"contract:claude": "node scripts/contract-claude.mjs",
|
|
79
80
|
"test:watch": "vitest",
|
|
80
81
|
"test:coverage": "vitest run --coverage",
|
|
81
82
|
"lint": "eslint src/ packages/",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* KJC-TSK-1063 (QS-C4): npm run contract:claude. With the claude installed on
|
|
4
|
+
* this machine (and its session), records the contract answers again when its
|
|
5
|
+
* version differs from the recorded one, then replays the contract test. The
|
|
6
|
+
* new recordings go in a PR like any other change. --force records anyway.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { claudeContractCheck } from "../src/checks/claude-contract-check.js";
|
|
11
|
+
|
|
12
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
13
|
+
const step = (cmd, args) => {
|
|
14
|
+
const res = spawnSync(cmd, args, { cwd: root, stdio: "inherit" });
|
|
15
|
+
if (res.status !== 0) process.exit(res.status ?? 1);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const check = await claudeContractCheck(root);
|
|
19
|
+
console.log(`${check.ok ? "✓" : "⚠"} ${check.detail}`);
|
|
20
|
+
if (check.ok && !process.argv.includes("--force")) process.exit(0);
|
|
21
|
+
step("node", ["scripts/record-agent-fixtures.mjs", "claude"]);
|
|
22
|
+
step("node", ["scripts/record-agent-fixtures.mjs", "claude", "--task"]);
|
|
23
|
+
step("npx", ["vitest", "run", "tests/agents/agent-contract-fixtures.test.js"]);
|
|
24
|
+
console.log("✓ kj reads the installed claude: commit tests/agents/fixtures/claude in a PR");
|
|
@@ -35,6 +35,13 @@ const seaTransformPlugin = {
|
|
|
35
35
|
modified = true;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// KJC-TSK-0703: the injection catalog is data read next to its module; inlined the same way.
|
|
39
|
+
const CATALOG_READ = /readFileSync\(new URL\("(\.\/injection-patterns\.json)", import\.meta\.url\), "utf8"\)/g;
|
|
40
|
+
if (CATALOG_READ.test(contents)) {
|
|
41
|
+
contents = contents.replace(CATALOG_READ, (_m, rel) => JSON.stringify(readFileSync(path.resolve(path.dirname(args.path), rel), "utf8")));
|
|
42
|
+
modified = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
// Replace import.meta.dirname with __dirname
|
|
39
46
|
if (contents.includes("import.meta.dirname")) {
|
|
40
47
|
contents = contents.replaceAll("import.meta.dirname", "__dirname");
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* QS-B (KJC-TSK-1039): records what each agent CLI REALLY emits for a review,
|
|
4
|
+
* so the contract tests replay real answers instead of the ones we imagine.
|
|
5
|
+
*
|
|
6
|
+
* node scripts/record-agent-fixtures.mjs claude codex
|
|
7
|
+
*
|
|
8
|
+
* Each CLI runs through its own kj adapter (same argv kj uses) in a throwaway
|
|
9
|
+
* directory; claude is recorded twice, the second time with a SessionStart hook
|
|
10
|
+
* in that directory, the way a session with hooks meets it (#2151). The answer
|
|
11
|
+
* is redacted (home, user, emails, session ids) and must pass the privacy scan
|
|
12
|
+
* before tests/agents/fixtures/<cli>/review.json is written.
|
|
13
|
+
*/
|
|
14
|
+
import { copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { createAgent } from "../src/agents/index.js";
|
|
18
|
+
import { buildEnvironment, defaultEnvironment } from "../src/infrastructure/environment.js";
|
|
19
|
+
import { loadPrivacyList, scanPaths } from "../src/privacy/scan.js";
|
|
20
|
+
|
|
21
|
+
const PROMPT = [
|
|
22
|
+
"You are reviewing this diff. Answer ONLY with one JSON object:",
|
|
23
|
+
'{"approved": boolean, "blocking_issues": [], "non_blocking_suggestions": [], "summary": string, "confidence": number}',
|
|
24
|
+
"diff --git a/sum.js b/sum.js",
|
|
25
|
+
"+export const sum = (a, b) => a + b;",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
const OUT = new URL("../tests/agents/fixtures/", import.meta.url).pathname;
|
|
29
|
+
const logger = { info() {}, warn() {}, error() {}, debug() {} };
|
|
30
|
+
|
|
31
|
+
export function redact(text, { home = homedir(), user = userInfo().username } = {}) {
|
|
32
|
+
return String(text ?? "")
|
|
33
|
+
.replaceAll(home, "/home/user")
|
|
34
|
+
.replaceAll(new RegExp(`\\b${user}\\b`, "g"), "user")
|
|
35
|
+
.replaceAll(/[\w.+-]+@[\w-]+\.[\w.]+/g, "user@example.com")
|
|
36
|
+
.replaceAll(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "redacted-id");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The init event describes the recording machine (tools, MCP servers, plugins,
|
|
40
|
+
// paths): only its shape stays, so it leaks nothing about that machine.
|
|
41
|
+
const isInit = (event) => event?.type === "system" && event.subtype === "init";
|
|
42
|
+
// An allowlist, not a denylist: each CLI version adds fields about the machine
|
|
43
|
+
// (socket paths, memory paths, terminal commands). Only the event's shape stays.
|
|
44
|
+
const INIT_KEEP = ["type", "subtype", "session_id", "uuid", "model", "permissionMode", "apiKeySource", "claude_code_version", "output_style"];
|
|
45
|
+
const trimEvent = (event) => (isInit(event) ? Object.fromEntries(INIT_KEEP.filter((k) => k in event).map((k) => [k, event[k]])) : event);
|
|
46
|
+
|
|
47
|
+
// Both shapes claude emits: NDJSON (stream-json) and one JSON array (json, the coder's runTask).
|
|
48
|
+
export function trimInitEvents(stdout) {
|
|
49
|
+
const text = String(stdout ?? "");
|
|
50
|
+
if (text.trim().startsWith("[")) {
|
|
51
|
+
try { return JSON.stringify(JSON.parse(text).map(trimEvent)); } catch { /* not one array: read it by lines */ }
|
|
52
|
+
}
|
|
53
|
+
return text.split("\n").map((line) => {
|
|
54
|
+
let event;
|
|
55
|
+
try { event = JSON.parse(line); } catch { return line; }
|
|
56
|
+
return isInit(event) ? JSON.stringify(trimEvent(event)) : line;
|
|
57
|
+
}).join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// #2151: a SessionStart hook makes claude open its answer with hook events.
|
|
61
|
+
function withSessionHook(dir) {
|
|
62
|
+
mkdirSync(join(dir, ".claude"), { recursive: true });
|
|
63
|
+
const hooks = { SessionStart: [{ hooks: [{ type: "command", command: "echo kj-fixture-hook" }] }] };
|
|
64
|
+
writeFileSync(join(dir, ".claude", "settings.json"), `${JSON.stringify({ hooks }, null, 2)}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// KJC-TSK-1044: the coder's answer, with no tool to use and one exact line to say.
|
|
68
|
+
const TASK_PROMPT = "Reply with exactly this line and nothing else: kj-contract-ok";
|
|
69
|
+
|
|
70
|
+
async function record(cli, { hooks = false, task = false } = {}) {
|
|
71
|
+
process.chdir(mkdtempSync(join(tmpdir(), "kj-fixtures-")));
|
|
72
|
+
if (hooks) withSessionHook(process.cwd());
|
|
73
|
+
const calls = [];
|
|
74
|
+
const runner = { run: async (cmd, args, opts) => { const res = await defaultEnvironment.runner.run(cmd, args, opts); calls.push(res); return res; } };
|
|
75
|
+
const agent = createAgent(cli, {}, logger, buildEnvironment({ runner }));
|
|
76
|
+
const version = (await defaultEnvironment.runner.run(cli, ["--version"], {})).stdout?.trim() || "unknown";
|
|
77
|
+
if (task) await agent.runTask({ prompt: TASK_PROMPT, role: "coder" });
|
|
78
|
+
else await agent.reviewTask({ prompt: PROMPT, role: "reviewer" });
|
|
79
|
+
const last = calls.at(-1);
|
|
80
|
+
if (!last) throw new Error(`${cli}: the adapter ran no command`);
|
|
81
|
+
// Scanned in the throwaway directory: a fixture with a privacy hit never reaches the repo.
|
|
82
|
+
const candidate = join(process.cwd(), "fixture.json");
|
|
83
|
+
writeFileSync(candidate, `${JSON.stringify({ cli, version: redact(version), exitCode: last.exitCode, stdout: redact(trimInitEvents(last.stdout)), stderr: redact(last.stderr) }, null, 2)}\n`);
|
|
84
|
+
const blocks = scanPaths([candidate], { list: loadPrivacyList() }).filter((f) => f.severity === "block");
|
|
85
|
+
if (blocks.length > 0) throw new Error(`${cli}: ${blocks.length} privacy hit(s) in the recording; fix the redaction (left in ${candidate})`);
|
|
86
|
+
const file = join(OUT, cli, `${task ? "task" : "review"}${hooks ? "-hooks" : ""}.json`);
|
|
87
|
+
mkdirSync(join(OUT, cli), { recursive: true });
|
|
88
|
+
copyFileSync(candidate, file);
|
|
89
|
+
return `${cli} ${version}: exit ${last.exitCode} → ${file}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --task records the coder (runTask) instead of the reviewer.
|
|
93
|
+
const task = process.argv.includes("--task");
|
|
94
|
+
const clis = process.argv.slice(2).filter((a) => a !== "--task");
|
|
95
|
+
for (const cli of clis) {
|
|
96
|
+
console.log(await record(cli, { task }));
|
|
97
|
+
if (cli === "claude" && !task) console.log(await record(cli, { hooks: true }));
|
|
98
|
+
}
|
|
@@ -342,7 +342,15 @@ function buildPromptArgs(task) {
|
|
|
342
342
|
return { args: ["-p", "--allowedTools", ...ALLOWED_TOOLS], input: String(task.prompt ?? "") };
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// KJC-TSK-0914: the model claude runs on when nobody declares one; whatever the
|
|
346
|
+
// user declares (roles.<role>.model, coder/reviewer options, --coder-model) wins.
|
|
347
|
+
export const CLAUDE_DEFAULT_MODEL = "claude-opus-5-5";
|
|
348
|
+
|
|
345
349
|
export class ClaudeAgent extends BaseAgent {
|
|
350
|
+
getRoleModel(role) {
|
|
351
|
+
return super.getRoleModel(role) ?? CLAUDE_DEFAULT_MODEL;
|
|
352
|
+
}
|
|
353
|
+
|
|
346
354
|
async runTask(task) {
|
|
347
355
|
const role = task.role || "coder";
|
|
348
356
|
const model = this.getRoleModel(role);
|
|
@@ -86,6 +86,11 @@ registerModel("gpt-5.4-mini", { provider: "openai", pricing: { input_per_million
|
|
|
86
86
|
|
|
87
87
|
// Default models & General aliases
|
|
88
88
|
registerModel("codex", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
89
|
+
// KJC-TSK-0699: the models the codex CLI lists (0.146.1). List prices after the
|
|
90
|
+
// 2026-07-30 cut; Sol at its base rate, not the temporary promotion.
|
|
91
|
+
registerModel("gpt-5.6-sol", { provider: "openai", pricing: { input_per_million: 5.0, output_per_million: 30.0 } });
|
|
92
|
+
registerModel("gpt-5.6-terra", { provider: "openai", pricing: { input_per_million: 2.0, output_per_million: 12.0 } });
|
|
93
|
+
registerModel("gpt-5.6-luna", { provider: "openai", pricing: { input_per_million: 0.2, output_per_million: 1.2 } });
|
|
89
94
|
registerModel("o4-mini", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
90
95
|
registerModel("o3", { provider: "openai", pricing: { input_per_million: 10, output_per_million: 40 } });
|
|
91
96
|
|
|
@@ -33,6 +33,20 @@ function extractOpenCodeUsage(text) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* KJC-BUG-0358: with --format json opencode reports a failure as an error event
|
|
38
|
+
* on stdout ({"type":"error","error":{"name","data":{"message"}}}) and leaves
|
|
39
|
+
* stderr empty. Returns that message, or "" when there is no error event.
|
|
40
|
+
*/
|
|
41
|
+
function openCodeErrorEvent(stdout) {
|
|
42
|
+
for (const line of String(stdout ?? "").split("\n")) {
|
|
43
|
+
let event;
|
|
44
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
45
|
+
if (event?.type === "error") return event.error?.data?.message ?? event.error?.name ?? "opencode reported an error";
|
|
46
|
+
}
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export class OpenCodeAgent extends BaseAgent {
|
|
37
51
|
async runTask(task) {
|
|
38
52
|
const role = task.role || "coder";
|
|
@@ -57,6 +71,7 @@ export class OpenCodeAgent extends BaseAgent {
|
|
|
57
71
|
timeout: task.timeoutMs
|
|
58
72
|
});
|
|
59
73
|
const usage = extractOpenCodeUsage(res.stdout) ?? extractOpenCodeUsage(res.stderr);
|
|
60
|
-
|
|
74
|
+
const error = res.exitCode === 0 ? res.stderr : res.stderr || openCodeErrorEvent(res.stdout);
|
|
75
|
+
return { ok: res.exitCode === 0, output: res.stdout, error, exitCode: res.exitCode, ...usage };
|
|
61
76
|
}
|
|
62
77
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// 2. Inline ignore: `// karajan-audit-ignore: <tool>:<ruleId>` on the issue
|
|
12
12
|
// line or the previous one. Legacy `// karajan-sonar-ignore: <ruleId>`
|
|
13
13
|
// keeps working when the issue's tool is "sonar".
|
|
14
|
+
// 3. Sonar only: S1135 on the Spanish word «todo» (not an uppercase TODO
|
|
15
|
+
// tag) is suppressed by reading the issue's line (KJC-TSK-1042).
|
|
14
16
|
//
|
|
15
17
|
// Issues filtered out are kept in the `suppressed` list with `_suppressedBy`
|
|
16
18
|
// metadata for auditability — no silent dropping.
|
|
@@ -53,6 +55,19 @@ export const DEFAULT_FALSE_POSITIVES = [
|
|
|
53
55
|
},
|
|
54
56
|
];
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* KJC-TSK-1042: Sonar's TODO-tag rule (S1135, every language) matches the word
|
|
60
|
+
* regardless of case, so a Spanish comment like «todo el fichero» is reported
|
|
61
|
+
* as a pending task. Only the uppercase TODO tag counts. Returns the reason to
|
|
62
|
+
* suppress, or null; a line that cannot be read keeps the finding.
|
|
63
|
+
*/
|
|
64
|
+
export function spanishTodoWord(issue, absPath) {
|
|
65
|
+
if (!/:S1135$/.test(issue.rule ?? "") || !issue.line || !fs.existsSync(absPath)) return null;
|
|
66
|
+
const text = fs.readFileSync(absPath, "utf8").split("\n")[issue.line - 1] ?? "";
|
|
67
|
+
if (!/\btodo\b/i.test(text) || /\bTODO\b/.test(text)) return null;
|
|
68
|
+
return "the Spanish word «todo», not an uppercase TODO tag";
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
export function hasInlineIgnore(filePath, line, ruleId, tool = "sonar") {
|
|
57
72
|
if (!filePath || !line || !ruleId) return false;
|
|
58
73
|
if (!fs.existsSync(filePath)) return false;
|
|
@@ -109,11 +124,14 @@ export function filterFalsePositives(issues, opts = {}) {
|
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
// 2. Inline ignore (only
|
|
127
|
+
// 2. Inline ignore, then the Spanish word «todo» (only if no static rule matched).
|
|
113
128
|
if (!matched && issue.rule && issue.line && path) {
|
|
114
129
|
const absPath = path.startsWith("/") ? path : `${projectRoot}/${path}`;
|
|
115
130
|
if (hasInlineIgnore(absPath, issue.line, issue.rule, tool)) {
|
|
116
131
|
matched = { tool, rule: issue.rule, path, reason: "inline `// karajan-audit-ignore`" };
|
|
132
|
+
} else if (tool === "sonar") {
|
|
133
|
+
const reason = spanishTodoWord(issue, absPath);
|
|
134
|
+
if (reason) matched = { tool, rule: issue.rule, path, reason };
|
|
117
135
|
}
|
|
118
136
|
}
|
|
119
137
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0719 (SPD-C): the audit was due by calendar and staleness only. Now
|
|
3
|
+
* it is due by work cycles too: every N cards closed (distinct card refs in
|
|
4
|
+
* the commits) or M commits since the last recorded audit. The marker is that
|
|
5
|
+
* audit's timestamp, so running kj audit resets the counter. kj check shows it.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { runCommand } from "../utils/process.js";
|
|
9
|
+
import { getAuditHistoryDbPath, getLatestPreviousRun, openAuditHistoryDb } from "../audit/audit-history.js";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_EVERY_CARDS = 10;
|
|
12
|
+
export const DEFAULT_EVERY_COMMITS = 60;
|
|
13
|
+
const CARD_REF = /\b[A-Z]{2,5}-(?:TSK|BUG)-\d{4}\b/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The last audit that finished with a score, or null; never creates the
|
|
17
|
+
* database to find out. A run without a score swept nothing, so it is no marker.
|
|
18
|
+
*/
|
|
19
|
+
export function readLatestAudit(projectDir) {
|
|
20
|
+
if (!existsSync(getAuditHistoryDbPath(projectDir))) return null;
|
|
21
|
+
const db = openAuditHistoryDb(projectDir);
|
|
22
|
+
try { return getLatestPreviousRun(db); } finally { db.close(); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function auditCycle({ projectDir = process.cwd(), config = {}, run = runCommand, latestAudit = readLatestAudit } = {}) {
|
|
26
|
+
const everyCards = config?.audit_cycle?.every_cards ?? DEFAULT_EVERY_CARDS;
|
|
27
|
+
const everyCommits = config?.audit_cycle?.every_commits ?? DEFAULT_EVERY_COMMITS;
|
|
28
|
+
const last = latestAudit(projectDir);
|
|
29
|
+
if (!last) return { due: true, line: "⚠ audit due: no audit recorded yet: run kj audit" };
|
|
30
|
+
// A work commit is a non-merge one: a merge brings no work of its own, and the
|
|
31
|
+
// commits it brings in are already in this log (it walks every parent), so
|
|
32
|
+
// counting merges too would double each PR in a merge workflow.
|
|
33
|
+
const res = await run("git", ["-C", projectDir, "log", `--since=${last.timestamp}`, "--no-merges", "--format=%s"]);
|
|
34
|
+
if (res.exitCode !== 0) return { due: false, line: `⚠ audit cycle not checked: git log failed (${(res.stderr || "").trim()})` };
|
|
35
|
+
const lines = res.stdout.split("\n").filter(Boolean);
|
|
36
|
+
const cards = new Set(lines.flatMap((s) => s.match(CARD_REF) ?? [])).size;
|
|
37
|
+
const commits = lines.length;
|
|
38
|
+
const due = cards >= everyCards || commits >= everyCommits;
|
|
39
|
+
const counter = `${cards}/${everyCards} cards, ${commits}/${everyCommits} commits since the last audit (${last.timestamp.slice(0, 10)})`;
|
|
40
|
+
return { due, cards, commits, line: due ? `⚠ audit due: ${counter}: run kj audit` : `audit cycle: ${counter}` };
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1063 (QS-C4): the agent contract recordings (tests/agents/fixtures)
|
|
3
|
+
* note the claude version they were made with. Anthropic updates claude on its
|
|
4
|
+
* own; a newer one may change its output and break kj. The check lives on the
|
|
5
|
+
* maintainer's machine, with the claude session already there, not in CI with
|
|
6
|
+
* a credential: kj release check is red until npm run contract:claude records
|
|
7
|
+
* again with the installed claude and the contract test passes.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { runCommand } from "../utils/process.js";
|
|
12
|
+
|
|
13
|
+
const RECORDING = join("tests", "agents", "fixtures", "claude", "review.json");
|
|
14
|
+
|
|
15
|
+
export const versionOf = (text) => /\d+\.\d+\.\d+/.exec(String(text ?? ""))?.[0] ?? null;
|
|
16
|
+
|
|
17
|
+
/** null where the project carries no contract recordings (any project but kj itself). */
|
|
18
|
+
export async function claudeContractCheck(projectDir, run = runCommand) {
|
|
19
|
+
const file = join(projectDir, RECORDING);
|
|
20
|
+
if (!existsSync(file)) return null;
|
|
21
|
+
const recorded = versionOf(JSON.parse(readFileSync(file, "utf8")).version);
|
|
22
|
+
const res = await run("claude", ["--version"], { cwd: projectDir }).catch((err) => ({ exitCode: 1, stdout: "", stderr: err.message }));
|
|
23
|
+
const installed = res.exitCode === 0 ? versionOf(res.stdout) : null;
|
|
24
|
+
if (!installed) return { name: "claude-contract", ok: false, detail: "claude --version gave no version: the contract with claude cannot be checked on this machine" };
|
|
25
|
+
if (installed === recorded) return { name: "claude-contract", ok: true, detail: `contract recorded with the installed claude ${installed}` };
|
|
26
|
+
return { name: "claude-contract", ok: false, detail: `claude ${installed} installed, contract recorded with ${recorded}: run npm run contract:claude and commit the recordings in a PR` };
|
|
27
|
+
}
|
package/src/checks/method.js
CHANGED
|
@@ -68,8 +68,9 @@ export async function collectMethodStats({ projectDir, run = runCommand, sample
|
|
|
68
68
|
export function sonarProof(v) {
|
|
69
69
|
const s = v?.sonar;
|
|
70
70
|
if (!s || v.verdict !== "approved") return null;
|
|
71
|
-
// A pipeline stamp has no mode:
|
|
72
|
-
|
|
71
|
+
// A pipeline stamp has no mode: it ran and covered every staged source, or
|
|
72
|
+
// not (KJC-TSK-0839; a stamp from before it carries no lists and counts as run).
|
|
73
|
+
if (s.source === "pipeline") return s.ran && (s.uncovered || []).length === 0 ? "proved" : "unproved";
|
|
73
74
|
// A block without a mode was written before the requirement existed
|
|
74
75
|
// (the block landed one PR before fail-closed): not retroactive either.
|
|
75
76
|
if (!s.mode) return null;
|
|
@@ -11,6 +11,8 @@ import { runCommand } from "../utils/process.js";
|
|
|
11
11
|
import { loadPrivacyList, scanPaths } from "../privacy/scan.js";
|
|
12
12
|
import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
|
|
13
13
|
import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
14
|
+
import { npmTokenAgeCheck } from "./supply-chain-check.js";
|
|
15
|
+
import { claudeContractCheck } from "./claude-contract-check.js";
|
|
14
16
|
|
|
15
17
|
// KJC-TSK-0769 — the effect boundary: what ships is re-evaluated against the
|
|
16
18
|
// policy IN FORCE now, not the one each PR was merged under. Artifact rules
|
|
@@ -247,7 +249,11 @@ export async function runReleaseCheck({ projectDir = process.cwd(), config = {},
|
|
|
247
249
|
const pack = await packPrivacyCheck(projectDir, pkg);
|
|
248
250
|
const dual = await dualPublishCheck(projectDir, pkg);
|
|
249
251
|
const scenarios = await scenarioBatteryCheck(projectDir, pkg);
|
|
250
|
-
|
|
252
|
+
// KJC-TSK-1059: what publishes to npm also says how old its long-lived tokens are (a warning).
|
|
253
|
+
const tokens = pkg?.name && !pkg.private ? await npmTokenAgeCheck({ maxDays: config?.supply_chain?.npm_token_max_days }) : null;
|
|
254
|
+
// KJC-TSK-1063: kj never ships unchecked against the claude installed here.
|
|
255
|
+
const claudeContract = await claudeContractCheck(projectDir);
|
|
256
|
+
checks.push(...[pack, dual, scenarios, tokens, claudeContract].filter(Boolean), await policyRangeCheck(projectDir), ...await declaredItems(projectDir, config, version));
|
|
251
257
|
if (forCommand === null) return { ok: checks.every((c) => c.ok), version, checks };
|
|
252
258
|
// A lifted check stays RED in the report: the fact is not falsified, it just
|
|
253
259
|
// stops blocking the command that exists to repair it. The mark travels ON
|
package/src/checks/sonar.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isSonarReachable } from "../sonar/manager.js";
|
|
11
|
+
import { getProjectQualityProfiles } from "../sonar/api.js";
|
|
11
12
|
import { STRATEGY } from "./types.js";
|
|
12
13
|
import { withDocLink } from "../utils/doc-links.js";
|
|
13
14
|
|
|
@@ -22,6 +23,24 @@ function sonarHost(config) {
|
|
|
22
23
|
return config.sonarqube?.host || "http://localhost:9000";
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** KJC-TSK-1051 (TEAM-E): the project runs on the team's quality profile, or the check says why not. */
|
|
27
|
+
async function teamProfileResult(config, { host, profile }) {
|
|
28
|
+
let profiles;
|
|
29
|
+
try {
|
|
30
|
+
profiles = await getProjectQualityProfiles(config);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { ok: false, severity: "warn", detail: `Reachable at ${host}, but the quality profile could not be read (${err.message})`, extra: { host } };
|
|
33
|
+
}
|
|
34
|
+
if (profiles.includes(profile)) return { ok: true, severity: "fail", detail: `Reachable at ${host}, on the team profile ${profile}`, extra: { host } };
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
severity: "warn",
|
|
38
|
+
detail: `Reachable at ${host}, but the project runs on ${profiles.join(", ") || "no profile"}, not the team profile ${profile}`,
|
|
39
|
+
fix: `An admin assigns ${profile} to the project in SonarQube (Project Settings > Quality Profiles).`,
|
|
40
|
+
extra: { host },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
/**
|
|
26
45
|
* Informative check: Sonar enabled/disabled status.
|
|
27
46
|
* Produces an OK result with disabled description when Sonar is off.
|
|
@@ -51,18 +70,17 @@ function createSonarStatusCheck() {
|
|
|
51
70
|
} catch {
|
|
52
71
|
reachable = false;
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
73
|
+
// KJC-TSK-1051 (TEAM-E): an external SonarQube is the team's server, never started here.
|
|
74
|
+
const external = config.sonarqube?.external === true;
|
|
75
|
+
if (!reachable) {
|
|
76
|
+
const fix = external
|
|
77
|
+
? `The team's SonarQube at ${host} is not answering (sonarqube.external: kj does not start a local one): check the network or VPN, or ask whoever runs it.`
|
|
78
|
+
: withDocLink("Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.", "sonar_docker");
|
|
79
|
+
return { ok: false, severity: "fail", detail: `Not reachable at ${host}`, fix, extra: { host, reachable } };
|
|
80
|
+
}
|
|
81
|
+
const profile = config.sonarqube?.quality_profile;
|
|
82
|
+
if (!profile) return { ok: true, severity: "fail", detail: `Reachable at ${host}`, extra: { host, reachable } };
|
|
83
|
+
return teamProfileResult(config, { host, profile });
|
|
66
84
|
},
|
|
67
85
|
};
|
|
68
86
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1059: the supply-chain cooldown outside the review. kj check reads
|
|
3
|
+
* what the branch's lockfile installs against the merge base with the base
|
|
4
|
+
* branch; kj release check reads how old the long-lived npm tokens are.
|
|
5
|
+
* Both warn, never block: they are signals, and a signal never stays silent.
|
|
6
|
+
*/
|
|
7
|
+
import { runCommand } from "../utils/process.js";
|
|
8
|
+
import { cooldownAdvisories, DEFAULT_COOLDOWN_HOURS, formatCooldown } from "../review/dep-cooldown.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_TOKEN_MAX_DAYS = 90;
|
|
11
|
+
const DAY_MS = 86_400_000;
|
|
12
|
+
|
|
13
|
+
/** The cooldown warnings for what the branch brings in, committed or not. */
|
|
14
|
+
export async function branchCooldownLines({ projectDir = process.cwd(), base = "main", thresholdHours = DEFAULT_COOLDOWN_HOURS, run = runCommand, now = Date.now() } = {}) {
|
|
15
|
+
let from = null;
|
|
16
|
+
for (const ref of [`origin/${base}`, base]) {
|
|
17
|
+
const res = await run("git", ["-C", projectDir, "merge-base", "HEAD", ref]);
|
|
18
|
+
if (res.exitCode === 0 && res.stdout.trim()) { from = res.stdout.trim(); break; }
|
|
19
|
+
}
|
|
20
|
+
if (!from) return [`⚠ supply-chain cooldown not checked: no merge base with ${base}`];
|
|
21
|
+
const diff = await run("git", ["-C", projectDir, "diff", from, "--", "package-lock.json", "**/package-lock.json"]);
|
|
22
|
+
if (diff.exitCode !== 0) return [`⚠ supply-chain cooldown not checked: git diff failed (${(diff.stderr || "").trim()})`];
|
|
23
|
+
return formatCooldown(await cooldownAdvisories(diff.stdout, { thresholdHours, run, now }), thresholdHours).lines;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A token without an expiry is a long-lived (classic) one: the older it is,
|
|
28
|
+
* the longer a leaked copy keeps working. Each one over maxDays is named.
|
|
29
|
+
*/
|
|
30
|
+
export async function npmTokenAgeCheck({ run = runCommand, now = Date.now(), maxDays = DEFAULT_TOKEN_MAX_DAYS } = {}) {
|
|
31
|
+
const res = await run("npm", ["token", "list", "--json"]);
|
|
32
|
+
if (res.exitCode !== 0) {
|
|
33
|
+
return { name: "npm-tokens", ok: true, warn: true, detail: `could not list npm tokens (${(res.stderr || res.stdout || "").trim()}): their age is unverified` };
|
|
34
|
+
}
|
|
35
|
+
let tokens;
|
|
36
|
+
try { tokens = JSON.parse(res.stdout || "[]"); } catch (err) {
|
|
37
|
+
return { name: "npm-tokens", ok: true, warn: true, detail: `npm token list gave no JSON (${err.message}): their age is unverified` };
|
|
38
|
+
}
|
|
39
|
+
const old = tokens
|
|
40
|
+
.filter((t) => !t.expires && t.created)
|
|
41
|
+
// Never the token field, masked or not: only a short prefix of its id reaches a log.
|
|
42
|
+
.map((t) => ({ label: `id ${String(t.key ?? "?").slice(0, 6)}…`, days: Math.floor((now - Date.parse(t.created)) / DAY_MS) }))
|
|
43
|
+
.filter((t) => t.days > maxDays);
|
|
44
|
+
if (old.length === 0) return { name: "npm-tokens", ok: true, warn: false, detail: `no long-lived npm token older than ${maxDays} days` };
|
|
45
|
+
const list = old.map((t) => `${t.label} (${t.days} days)`).join(", ");
|
|
46
|
+
return { name: "npm-tokens", ok: true, warn: true, detail: `${old.length} long-lived npm token(s) older than ${maxDays} days: ${list}. Rotate them, or move to granular tokens with an expiry` };
|
|
47
|
+
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -334,6 +334,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
334
334
|
// KJC-BUG-0257: the valid states, said before the call fails.
|
|
335
335
|
.description(`Move a card; <status> is one of: ${HU_STATUSES.join(", ")}`)
|
|
336
336
|
.option("--json", "Machine-readable output")
|
|
337
|
+
.option("--release", "Take over a running card reserved by someone else (the explicit act)")
|
|
337
338
|
.action(async (id, status, flags) => {
|
|
338
339
|
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
339
340
|
await huCommand({ config, action: "move", args: [id, status], flags });
|
|
@@ -515,7 +516,11 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
515
516
|
const res = await runReleaseCheck({ projectDir: config?.projectDir || process.cwd(), config, forCommand: flags.forCommand ?? null });
|
|
516
517
|
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
517
518
|
else {
|
|
518
|
-
|
|
519
|
+
const mark = (c) => {
|
|
520
|
+
if (!c.ok) return "✗";
|
|
521
|
+
return c.warn ? "⚠" : "✓";
|
|
522
|
+
};
|
|
523
|
+
for (const c of res.checks) console.log(` ${mark(c)} ${c.name}: ${c.detail}`);
|
|
519
524
|
if (res.lifted?.length) console.log(` ℓ no bloquean este comando porque es su remedio: ${res.lifted.join(", ")}`);
|
|
520
525
|
console.log(res.ok ? `release check: ready to release ${res.version ?? ""}`.trim() : "release check: NOT ready — fix the red items first");
|
|
521
526
|
}
|
|
@@ -14,6 +14,7 @@ import { runCommandHandler } from "../commands/run.js";
|
|
|
14
14
|
import { statusCommand } from "../commands/status.js";
|
|
15
15
|
import { loadPlan } from "../plan/plan-store.js";
|
|
16
16
|
import { withConfig } from "./_shared.js";
|
|
17
|
+
import { publishPrReview } from "../review/publish-pr.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Register the "core pipeline" commands: everything the user runs day-to-day
|
|
@@ -66,6 +67,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
66
67
|
.option("--coder-model <name>")
|
|
67
68
|
.option("--reviewer-model <name>")
|
|
68
69
|
.option("--refactorer-model <name>")
|
|
70
|
+
.option("--profile <name>", "Model profile for every role without an explicit model: frugal | standard | deep (the reviewer never goes below standard)")
|
|
69
71
|
.option("--enable-planner")
|
|
70
72
|
.option("--enable-reviewer")
|
|
71
73
|
.option("--enable-refactorer")
|
|
@@ -254,8 +256,15 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
254
256
|
// this the rule could only complain, which is the gate with no exit.
|
|
255
257
|
.option("--walked <route>", "Recorrido comprobado como lo haría una persona (repetible) — queda en el veredicto", (v, prev) => [...(prev || []), v], [])
|
|
256
258
|
.option("--walked-with <tool>", "Con qué se comprobó el recorrido (por ejemplo chrome-devtools)")
|
|
259
|
+
.option("--publish-pr <n>", "Publish the cross-AI verdicts of a PR's commits on it as one GitHub review")
|
|
257
260
|
.action(async (task, flags) => {
|
|
258
261
|
await withConfig(pkgVersion, "review", flags, async ({ config, logger }) => {
|
|
262
|
+
// KJC-TSK-0913: the verdicts kj already holds, published on the PR as one review.
|
|
263
|
+
if (flags.publishPr) {
|
|
264
|
+
const r = await publishPrReview({ projectDir: config.projectDir || process.cwd(), pr: flags.publishPr });
|
|
265
|
+
logger.info(r.published ? `✓ PR #${flags.publishPr}: review published (${r.commits} commit(s))` : `PR #${flags.publishPr}: ${r.reason}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
259
268
|
// ENV-B1 (KJC-TSK-0637): the gate mode records a verdict tied to
|
|
260
269
|
// the exact diff so the pre-commit hook can enforce cross-AI review.
|
|
261
270
|
if (flags.staged || flags.check || flags.range || flags.installGate || flags.prune) {
|
package/src/cli/register-plan.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
planShareCommand,
|
|
13
13
|
planUnshareCommand,
|
|
14
14
|
} from "../commands/plan.js";
|
|
15
|
+
import { planLicenseCommand } from "../commands/plan/license.js";
|
|
15
16
|
import { parseCanvasMarkdown } from "../canvas/parse-md.js";
|
|
16
17
|
import { validateCanvas } from "../canvas/validate.js";
|
|
17
18
|
import { canvasToTask } from "../canvas/to-flat.js";
|
|
@@ -195,6 +196,15 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
195
196
|
});
|
|
196
197
|
});
|
|
197
198
|
|
|
199
|
+
// KJC-TSK-0717 (SPD-A): the REWORK licence, from the module's mutation score.
|
|
200
|
+
plan.command("license").description("Score a module with kj mutate and record the REWORK licence on an HU")
|
|
201
|
+
.argument("<planId>").argument("<huId>").requiredOption("--module <path>", "The module the REWORK regenerates")
|
|
202
|
+
.action(async (planId, huId, flags) => {
|
|
203
|
+
await withConfig(pkgVersion, "plan", flags, async ({ config }) => {
|
|
204
|
+
process.exitCode = await planLicenseCommand({ config, planId, huId, module: flags.module });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
198
208
|
// KJC-TSK-0402: re-ejecuta reviewer + self-fix + structural pass sobre
|
|
199
209
|
// un plan existente. Acepta --prompt opcional con feedback humano que
|
|
200
210
|
// el reviewer inyecta como contexto extra antes de revisar.
|
package/src/commands/check.js
CHANGED
|
@@ -11,6 +11,8 @@ import { checkAiSurface, formatAiSurface } from "../checks/ai-surface.js";
|
|
|
11
11
|
import { detectObservedAgents } from "../utils/agent-detect.js";
|
|
12
12
|
import { panelSummary } from "../environment/panel.js";
|
|
13
13
|
import { loadConfig } from "../config.js";
|
|
14
|
+
import { branchCooldownLines } from "../checks/supply-chain-check.js";
|
|
15
|
+
import { auditCycle } from "../checks/audit-cycle.js";
|
|
14
16
|
|
|
15
17
|
export async function checkCommand({ projectDir = process.cwd(), profile = "standard", json = false, logger = console } = {}) {
|
|
16
18
|
const result = await checkHarden({ projectDir, profile });
|
|
@@ -34,9 +36,15 @@ export async function checkCommand({ projectDir = process.cwd(), profile = "stan
|
|
|
34
36
|
aiSurface = checkAiSurface({ projectDir, extraSurface: clis });
|
|
35
37
|
} catch { /* inventory is best-effort */ }
|
|
36
38
|
|
|
39
|
+
// KJC-TSK-1059: young dependencies the branch brings in, a warning outside the review.
|
|
40
|
+
const depCooldown = await branchCooldownLines({ projectDir, base: config?.base_branch ?? "main", thresholdHours: config?.supply_chain?.cooldown_hours })
|
|
41
|
+
.catch((err) => [`⚠ supply-chain cooldown not checked: ${err.message}`]);
|
|
42
|
+
// KJC-TSK-0719 (SPD-C): the audit is due by work cycles too; visibility, never the exit code.
|
|
43
|
+
const audit = await auditCycle({ projectDir, config }).catch((err) => ({ due: false, line: `⚠ audit cycle not checked: ${err.message}` }));
|
|
44
|
+
|
|
37
45
|
const ok = result.ok && ragCoverage.ok;
|
|
38
46
|
if (json) {
|
|
39
|
-
logger.info?.(JSON.stringify({ ...result, ok, method, ragCoverage, aiSurface }));
|
|
47
|
+
logger.info?.(JSON.stringify({ ...result, ok, method, ragCoverage, aiSurface, depCooldown, auditCycle: audit }));
|
|
40
48
|
return ok ? 0 : 1;
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -45,6 +53,8 @@ export async function checkCommand({ projectDir = process.cwd(), profile = "stan
|
|
|
45
53
|
if (method) logger.info?.(` method: ${formatMethodStats(method)}`);
|
|
46
54
|
logger.info?.(` ${ragCoverage.ok ? "✓" : "✗"} rag-coverage: ${ragCoverage.detail}`);
|
|
47
55
|
if (aiSurface) logger.info?.(` ${formatAiSurface(aiSurface)}`);
|
|
56
|
+
for (const line of depCooldown) logger.info?.(` ${line}`);
|
|
57
|
+
logger.info?.(` ${audit.line}`);
|
|
48
58
|
// KJC-TSK-0865: who writes and who reviews is the user's choice, so it is
|
|
49
59
|
// said out loud next to everything else that describes the project. Never
|
|
50
60
|
// a check: it has no ok/fail, it informs.
|
package/src/commands/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
4
|
import { loadConfig, validateConfig, getConfigPath, writeConfig } from "../config.js";
|
|
5
5
|
import { ensureDir } from "../utils/fs.js";
|
|
6
|
+
import { formatModelProfile } from "../utils/model-selector.js";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
|
|
8
9
|
function resolveEditor() {
|
|
@@ -70,5 +71,6 @@ export async function configCommand({ json = false, edit = false }) {
|
|
|
70
71
|
|
|
71
72
|
console.log(`Config path: ${cfgPath}`);
|
|
72
73
|
console.log(`Exists: ${exists}`);
|
|
74
|
+
console.log(formatModelProfile(config));
|
|
73
75
|
console.log(JSON.stringify(config, null, 2));
|
|
74
76
|
}
|
package/src/commands/harden.js
CHANGED
|
@@ -24,6 +24,7 @@ import { installHarnessHooks } from "../harden/harness-hooks.js";
|
|
|
24
24
|
import { installSentinelHooks } from "../harden/sentinel-hooks.js";
|
|
25
25
|
import { detectStackRoots } from "../harden/stack-roots.js";
|
|
26
26
|
import { installWorkflows } from "../harden/workflow-engine.js";
|
|
27
|
+
import { adviseBranchProtection } from "../harden/branch-protection.js";
|
|
27
28
|
import { detectTestFramework } from "../utils/project-detect.js";
|
|
28
29
|
import { ensureIdentity } from "../identity/bootstrap.js";
|
|
29
30
|
import { commitContract, contractChanges } from "../environment/contract-commit.js";
|
|
@@ -250,6 +251,8 @@ export async function hardenCommand({
|
|
|
250
251
|
out.identityCommand = id.command;
|
|
251
252
|
}
|
|
252
253
|
}
|
|
254
|
+
// KJC-TSK-0928: the method's checks only stop a merge with branch protection; kj says the exact command.
|
|
255
|
+
if (!dryRun) out.branchProtection = await adviseBranchProtection({ projectDir, branch: await resolveBaseBranch(projectDir), logger });
|
|
253
256
|
// KJC-BUG-0161 / ADR 0009 (opción A): --commit versiona la regeneración
|
|
254
257
|
// del supervisor con procedencia sellada. Acto humano — el guard vive en
|
|
255
258
|
// commitSupervisorRegeneration y rechaza sesiones de agente.
|