karajan-code 3.15.1 → 3.15.3
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 +3 -2
- package/scripts/quickstart-gate.mjs +97 -0
- package/scripts/verify-pack.mjs +38 -0
- package/src/brain/agent-error-classifier.js +5 -1
- package/src/commands/report.js +9 -0
- package/src/git/automation.js +17 -4
- package/src/orchestrator/config-init.js +8 -2
- package/src/orchestrator/drivers/init-context.js +14 -15
- package/src/orchestrator/drivers/iteration-loop.js +51 -2
- package/src/orchestrator/solomon-escalation.js +5 -1
- package/src/orchestrator/stages/coder-stage.js +7 -4
- package/src/roles/commiter-role.js +10 -3
- package/src/utils/agent-detect.js +4 -1
- package/src/utils/git.js +10 -0
- package/src/utils/role-assigner.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.3",
|
|
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",
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"mcp": "node src/mcp/server.js",
|
|
86
86
|
"audit:test-diet": "node scripts/audit-test-diet.mjs",
|
|
87
87
|
"verify-pack": "node scripts/verify-pack.mjs",
|
|
88
|
-
"prepublishOnly": "node scripts/verify-pack.mjs"
|
|
88
|
+
"prepublishOnly": "node scripts/verify-pack.mjs",
|
|
89
|
+
"verify-quickstart": "node scripts/quickstart-gate.mjs"
|
|
89
90
|
},
|
|
90
91
|
"simple-git-hooks": {
|
|
91
92
|
"pre-commit": "npx lint-staged"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quickstart gate (KJC-TSK-0635) — the landing's Quick Start, end to end,
|
|
4
|
+
* against the PACKED tarball, with a REAL coder run.
|
|
5
|
+
*
|
|
6
|
+
* Run manually before a release (`npm run verify-quickstart`). Opt-in
|
|
7
|
+
* because the coder run spends real subscription quota (~$0.5-1.5, 5-8
|
|
8
|
+
* minutes). What it catches that unit tests and verify-pack cannot:
|
|
9
|
+
* the exact first-contact sequence a new user follows — install → init →
|
|
10
|
+
* `kj run` on a no-remote repo → playable artifact (2026-07-19: that
|
|
11
|
+
* sequence surfaced KJC-BUG-0111/0112/0113 in one afternoon).
|
|
12
|
+
*
|
|
13
|
+
* Asserts: run exits 0 · report says approved · index.html exists with
|
|
14
|
+
* game markers · "skipping push/PR" logged · ZERO Solomon escalations.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
22
|
+
const TASK = "Create a tic-tac-toe game in a single self-contained index.html — vanilla HTML, CSS and JavaScript, no server and no build step. The human plays X, the bot plays O and blocks or takes a winning move when it can. Detect wins and draws, and add a New Game button. Opening index.html in a browser must be enough to play.";
|
|
23
|
+
|
|
24
|
+
function fail(msg, detail) {
|
|
25
|
+
console.error(`\n✗ quickstart-gate: ${msg}`);
|
|
26
|
+
if (detail) console.error(String(detail).slice(-1200));
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
throw new Error(msg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let work = null;
|
|
32
|
+
try {
|
|
33
|
+
console.log("quickstart-gate: packing…");
|
|
34
|
+
const packOut = execFileSync("npm", ["pack", "--json", "--silent"], { cwd: repoRoot, encoding: "utf8" });
|
|
35
|
+
const tgz = path.join(repoRoot, JSON.parse(packOut)[0].filename);
|
|
36
|
+
|
|
37
|
+
work = fs.mkdtempSync(path.join(os.tmpdir(), "kj-qs-gate-"));
|
|
38
|
+
const prefix = path.join(work, "npm");
|
|
39
|
+
const project = path.join(work, "project");
|
|
40
|
+
fs.mkdirSync(project, { recursive: true });
|
|
41
|
+
|
|
42
|
+
console.log("quickstart-gate: installing the tarball (isolated prefix)…");
|
|
43
|
+
execFileSync("npm", ["install", "-g", tgz, "--no-audit", "--no-fund", "--silent", "--prefix", prefix], { encoding: "utf8" });
|
|
44
|
+
const kj = path.join(prefix, "bin", "kj");
|
|
45
|
+
fs.rmSync(tgz, { force: true });
|
|
46
|
+
|
|
47
|
+
// Isolated KARAJAN_HOME: faithful to a brand-new user (no global
|
|
48
|
+
// config) and never touches the maintainer's real ~/.karajan.
|
|
49
|
+
const env = { ...process.env, KARAJAN_HOME: path.join(work, "karajan-home") };
|
|
50
|
+
delete env.CLAUDECODE;
|
|
51
|
+
// If the maintainer's machine has a SonarQube server running, preflight
|
|
52
|
+
// (correctly) demands a token the isolated home doesn't have. Forward the
|
|
53
|
+
// maintainer's token so the gate exercises the sonar stage instead of
|
|
54
|
+
// dying in preflight. A machine with no sonar server is unaffected.
|
|
55
|
+
if (!env.KJ_SONAR_TOKEN) {
|
|
56
|
+
try {
|
|
57
|
+
const cfg = fs.readFileSync(path.join(os.homedir(), ".karajan", "kj.config.yml"), "utf8");
|
|
58
|
+
const m = cfg.match(/^\s*token:\s*(sqa_[A-Za-z0-9]+|squ_[A-Za-z0-9]+)\s*$/m);
|
|
59
|
+
if (m) env.KJ_SONAR_TOKEN = m[1];
|
|
60
|
+
} catch { /* no global config — brand-new machine, nothing to forward */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("quickstart-gate: git init + kj init (unattended)…");
|
|
64
|
+
execFileSync("git", ["init", "-q", "-b", "main"], { cwd: project });
|
|
65
|
+
const init = spawnSync(kj, ["init", "--no-interactive", "--no-ollama", "--no-rtk", "--no-squeezr", "--no-qmd"], {
|
|
66
|
+
cwd: project, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 300000,
|
|
67
|
+
});
|
|
68
|
+
if (init.status !== 0) fail("kj init failed", init.stderr || init.stdout);
|
|
69
|
+
|
|
70
|
+
console.log("quickstart-gate: kj run (REAL coder — this spends quota, ~5-8 min)…");
|
|
71
|
+
const runRes = spawnSync(kj, ["run", "-y", TASK], {
|
|
72
|
+
cwd: project, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 20 * 60 * 1000,
|
|
73
|
+
});
|
|
74
|
+
const runOut = `${runRes.stdout || ""}${runRes.stderr || ""}`;
|
|
75
|
+
fs.writeFileSync(path.join(work, "run.log"), runOut);
|
|
76
|
+
|
|
77
|
+
if (runRes.status !== 0) fail(`kj run exited ${runRes.status} (log: ${work}/run.log)`, runOut);
|
|
78
|
+
if (/escalating to Solomon/i.test(runOut)) fail(`Solomon escalation detected — the happy path must not consult Solomon (log: ${work}/run.log)`);
|
|
79
|
+
// KJC-BUG-0112 regression: a remote-less repo must never attempt (and
|
|
80
|
+
// fail) push/fetch automation. With init's defaults (auto_push: false)
|
|
81
|
+
// the "skipping push/PR" line is legitimately absent, so assert the
|
|
82
|
+
// absence of failure symptoms rather than the presence of the skip line.
|
|
83
|
+
if (/failed to push|fatal: .*origin|couldn't find remote/i.test(runOut)) fail(`push/fetch against a missing remote detected (log: ${work}/run.log)`);
|
|
84
|
+
|
|
85
|
+
const html = path.join(project, "index.html");
|
|
86
|
+
if (!fs.existsSync(html)) fail("index.html was not created");
|
|
87
|
+
const htmlText = fs.readFileSync(html, "utf8");
|
|
88
|
+
if (!/new game/i.test(htmlText)) fail("index.html has no New Game control — not the requested game");
|
|
89
|
+
|
|
90
|
+
const report = spawnSync(kj, ["report"], { cwd: project, env, encoding: "utf8", timeout: 60000 });
|
|
91
|
+
if (!/approved/i.test(`${report.stdout || ""}`)) fail("kj report does not say approved", report.stdout);
|
|
92
|
+
|
|
93
|
+
console.log("\n✓ quickstart-gate: install → init → run → playable index.html → approved. Ship it.");
|
|
94
|
+
} finally {
|
|
95
|
+
if (work && process.exitCode !== 1 && fs.existsSync(work)) fs.rmSync(work, { recursive: true, force: true });
|
|
96
|
+
if (work && process.exitCode === 1) console.error(`quickstart-gate: workdir kept for inspection: ${work}`);
|
|
97
|
+
}
|
package/scripts/verify-pack.mjs
CHANGED
|
@@ -52,6 +52,7 @@ let tgzPath = null;
|
|
|
52
52
|
let tmpDir = null;
|
|
53
53
|
let gTmp = null;
|
|
54
54
|
let pnpmTmp = null;
|
|
55
|
+
let qsTmp = null;
|
|
55
56
|
try {
|
|
56
57
|
console.log(`verify-pack: packing karajan-code@${expectedVersion}…`);
|
|
57
58
|
// --json gives us the exact filename without parsing human output.
|
|
@@ -146,6 +147,42 @@ try {
|
|
|
146
147
|
}
|
|
147
148
|
console.log(`verify-pack: global install + kj --version → ${gVersion} ✓`);
|
|
148
149
|
|
|
150
|
+
// 5.5 Quickstart smoke (KJC-TSK-0635) — the landing's Getting Started
|
|
151
|
+
// sequence against the INSTALLED tarball, no LLM involved. Born from
|
|
152
|
+
// 2026-07-19: following the docs to the letter caught 3 field bugs
|
|
153
|
+
// (silent 2.34.0 install, Solomon loop on no-remote repos, retired
|
|
154
|
+
// gemini CLI) that 6000 unit tests never saw. Aux bootstraps (ollama,
|
|
155
|
+
// rtk, squeezr, qmd, harden) are skipped — this validates kj's own
|
|
156
|
+
// init + config on the canonical no-remote scenario, not the network.
|
|
157
|
+
qsTmp = fs.mkdtempSync(path.join(os.tmpdir(), "kj-verify-qs-"));
|
|
158
|
+
console.log(`verify-pack: quickstart smoke (init on a no-remote repo) in ${qsTmp}…`);
|
|
159
|
+
run("git", ["init", "-q", "-b", "main"], { cwd: qsTmp });
|
|
160
|
+
// KARAJAN_HOME points at an isolated dir: faithful to a brand-new user
|
|
161
|
+
// (no pre-existing global config — `--local` without one is rejected by
|
|
162
|
+
// design, which is exactly what this smoke caught on its first CI run)
|
|
163
|
+
// AND hermetic (never touches the real ~/.karajan).
|
|
164
|
+
const qsEnv = { ...childEnv, KARAJAN_HOME: path.join(qsTmp, "karajan-home") };
|
|
165
|
+
delete qsEnv.CLAUDECODE;
|
|
166
|
+
const initRes = spawnSync(gBin, ["init", "--no-interactive", "--no-ollama", "--no-rtk", "--no-squeezr", "--no-qmd", "--no-harden"], {
|
|
167
|
+
encoding: "utf8", cwd: qsTmp, env: qsEnv, timeout: 180000,
|
|
168
|
+
});
|
|
169
|
+
if (initRes.status !== 0) {
|
|
170
|
+
fail("`kj init --no-interactive` failed on a fresh no-remote repo", (initRes.stderr || initRes.stdout || "").slice(-800));
|
|
171
|
+
}
|
|
172
|
+
const qsCfgLocal = path.join(qsTmp, ".karajan", "kj.config.yml");
|
|
173
|
+
const qsCfgGlobal = path.join(qsTmp, "karajan-home", "kj.config.yml");
|
|
174
|
+
const qsCfg = fs.existsSync(qsCfgLocal) ? qsCfgLocal : qsCfgGlobal;
|
|
175
|
+
if (!fs.existsSync(qsCfg)) fail(`kj init did not write ${qsCfg}`);
|
|
176
|
+
if (!/coder:\s*\S+/.test(fs.readFileSync(qsCfg, "utf8"))) {
|
|
177
|
+
fail("generated kj.config.yml has no coder assignment");
|
|
178
|
+
}
|
|
179
|
+
const reportRes = spawnSync(gBin, ["report"], { encoding: "utf8", cwd: qsTmp, env: qsEnv, timeout: 60000 });
|
|
180
|
+
const reportOut = `${reportRes.stdout || ""}${reportRes.stderr || ""}`;
|
|
181
|
+
if (reportRes.status !== 0 && !/no session|sin sesi|not found/i.test(reportOut)) {
|
|
182
|
+
fail("`kj report` crashed on a project with no sessions", reportOut.slice(-400));
|
|
183
|
+
}
|
|
184
|
+
console.log("verify-pack: quickstart smoke (init + report, no-remote repo) ✓");
|
|
185
|
+
|
|
149
186
|
// 6. pnpm install smoke (KJC-TSK-0580). pnpm's layout differs from npm's
|
|
150
187
|
// (a symlinked virtual store), so it can break resolution of the bundled
|
|
151
188
|
// karajan-core the way npm packaging breakage did before (KJC-BUG-0082/0086).
|
|
@@ -195,4 +232,5 @@ try {
|
|
|
195
232
|
if (tmpDir && fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
196
233
|
if (gTmp && fs.existsSync(gTmp)) fs.rmSync(gTmp, { recursive: true, force: true });
|
|
197
234
|
if (pnpmTmp && fs.existsSync(pnpmTmp)) fs.rmSync(pnpmTmp, { recursive: true, force: true });
|
|
235
|
+
if (qsTmp && fs.existsSync(qsTmp)) fs.rmSync(qsTmp, { recursive: true, force: true });
|
|
198
236
|
}
|
|
@@ -31,7 +31,11 @@ export const ERROR_CLASS = Object.freeze({
|
|
|
31
31
|
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
32
32
|
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// `error\s+authenticating` + `IneligibleTierError` (KJC-BUG-0113): the
|
|
35
|
+
// retired Gemini Code Assist CLI fails with "Error authenticating:
|
|
36
|
+
// IneligibleTierError … migrate to Antigravity" — it classified as
|
|
37
|
+
// UNKNOWN_FATAL and left the run without the AUTH escalation message.
|
|
38
|
+
const AUTH_PATTERNS = /\b401\b|\b403\b|unauthorized|invalid\s+api\s+key|authentication\s+failed|expired\s+token|error\s+authenticating|IneligibleTierError/i;
|
|
35
39
|
const SILENCED_PATTERNS = /killed\s+after\s+\d+\s*ms|silence\s*timeout|no\s+output\s+for\s+\d+/i;
|
|
36
40
|
// `session\s+limit` / `weekly\s+limit` cover Claude Code's usage caps
|
|
37
41
|
// ("You've hit your session limit · resets 10:10pm"). Without them the
|
package/src/commands/report.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { exists } from "../utils/fs.js";
|
|
4
4
|
import { getSessionRoot } from "../utils/paths.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
|
+
import { DEFAULTS } from "../config/defaults.js";
|
|
6
7
|
|
|
7
8
|
function parseBudgetFromActivityLog(logText) {
|
|
8
9
|
if (!logText) {
|
|
@@ -131,6 +132,14 @@ async function buildReport(dir, sessionId) {
|
|
|
131
132
|
|
|
132
133
|
const sonar = summarizeSonar(checkpoints);
|
|
133
134
|
const budget = parseBudgetFromActivityLog(activityLog);
|
|
135
|
+
// KJC-BUG-0114: activity logs written when max_budget_usd was null carry
|
|
136
|
+
// a phantom "$0.00" ceiling (Number(null) === 0). Resolve the effective
|
|
137
|
+
// ceiling from the session's config snapshot, falling back to the
|
|
138
|
+
// shipped default; an explicit 0 means "no ceiling" and drops the suffix.
|
|
139
|
+
if (budget.limit_usd === 0) {
|
|
140
|
+
const snapBudget = session.config_snapshot?.max_budget_usd;
|
|
141
|
+
budget.limit_usd = snapBudget == null ? DEFAULTS.max_budget_usd : (Number(snapBudget) || null);
|
|
142
|
+
}
|
|
134
143
|
const commits = summarizeCommits(session, checkpoints);
|
|
135
144
|
|
|
136
145
|
const budgetTrace = Array.isArray(session.budget?.trace) ? session.budget.trace : [];
|
package/src/git/automation.js
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
commitAll,
|
|
17
17
|
pushBranch,
|
|
18
18
|
createPullRequest,
|
|
19
|
-
listPendingPaths
|
|
19
|
+
listPendingPaths,
|
|
20
|
+
hasRemote
|
|
20
21
|
} from "../utils/git.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -276,7 +277,19 @@ export async function finalizeGitAutomation({ config, gitCtx, task, logger, sess
|
|
|
276
277
|
logger.info(committed ? "Committed changes" : "No changes to commit");
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
|
|
280
|
+
// KJC-BUG-0112: without an `origin` remote (the quickstart scenario:
|
|
281
|
+
// fresh folder + git init) push/PR automation has nothing to talk to —
|
|
282
|
+
// fetchBase used to throw here and escalate to Solomon on EVERY
|
|
283
|
+
// iteration, burning the whole budget on an expected condition.
|
|
284
|
+
const remoteAvailable = (config.git.auto_push || config.git.auto_pr)
|
|
285
|
+
? await hasRemote()
|
|
286
|
+
: false;
|
|
287
|
+
if ((config.git.auto_push || config.git.auto_pr) && !remoteAvailable) {
|
|
288
|
+
logger.info("No remote configured — skipping push/PR automation");
|
|
289
|
+
await addCheckpoint(session, { stage: "git-push", skipped: "no-remote" });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (remoteAvailable && (config.git.auto_push || config.git.auto_pr)) {
|
|
280
293
|
await fetchBase(gitCtx.baseBranch);
|
|
281
294
|
await ensureBranchUpToDateWithBase({
|
|
282
295
|
branch: gitCtx.branch,
|
|
@@ -286,14 +299,14 @@ export async function finalizeGitAutomation({ config, gitCtx, task, logger, sess
|
|
|
286
299
|
await addCheckpoint(session, { stage: "git-rebase-check", branch: gitCtx.branch });
|
|
287
300
|
}
|
|
288
301
|
|
|
289
|
-
if (config.git.auto_push || config.git.auto_pr) {
|
|
302
|
+
if (remoteAvailable && (config.git.auto_push || config.git.auto_pr)) {
|
|
290
303
|
await pushBranch(gitCtx.branch);
|
|
291
304
|
await addCheckpoint(session, { stage: "git-push", branch: gitCtx.branch });
|
|
292
305
|
logger.info(`Pushed branch: ${gitCtx.branch}`);
|
|
293
306
|
}
|
|
294
307
|
|
|
295
308
|
let prUrl = session.ci_pr_url || null;
|
|
296
|
-
if (config.git.auto_pr && !prUrl) {
|
|
309
|
+
if (remoteAvailable && config.git.auto_pr && !prUrl) {
|
|
297
310
|
const body = buildPrBody({ task, stageResults });
|
|
298
311
|
prUrl = await createPullRequest({
|
|
299
312
|
baseBranch: gitCtx.baseBranch,
|
|
@@ -12,6 +12,7 @@ import { resolveRole } from "../config.js";
|
|
|
12
12
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
13
13
|
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
14
14
|
import { BudgetTracker, extractUsageMetrics } from "../utils/budget.js";
|
|
15
|
+
import { DEFAULTS } from "../config/defaults.js";
|
|
15
16
|
import { computeKjComparison } from "../budget/comparison.js";
|
|
16
17
|
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
17
18
|
import { projectSlug } from "../plan/plan-store.js";
|
|
@@ -303,8 +304,13 @@ export async function handleDryRun({ task, config, flags, emitter, pipelineFlags
|
|
|
303
304
|
|
|
304
305
|
export function createBudgetManager({ config, emitter, eventBase, getCompressionStats = null }) {
|
|
305
306
|
const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
// KJC-BUG-0114: Number(null) === 0, so a config that reached us with
|
|
308
|
+
// max_budget_usd: null produced a phantom "$X / $0.00" ceiling (and a
|
|
309
|
+
// permanent warn state). null/undefined fall back to the shipped
|
|
310
|
+
// default ceiling; an explicit 0 means "no ceiling".
|
|
311
|
+
const rawBudget = config?.max_budget_usd;
|
|
312
|
+
const budgetLimit = rawBudget == null ? DEFAULTS.max_budget_usd : Number(rawBudget);
|
|
313
|
+
const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit > 0;
|
|
308
314
|
const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
|
|
309
315
|
let stageCounter = 0;
|
|
310
316
|
|
|
@@ -21,9 +21,8 @@ import { prepareGitAutomation } from "../../git/automation.js";
|
|
|
21
21
|
import { CoderRole } from "../../roles/coder-role.js";
|
|
22
22
|
import { PipelineContext } from "../pipeline-context.js";
|
|
23
23
|
import { detectRtk } from "../../utils/rtk-detect.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { setRunner as setGitRunner } from "../../utils/git.js";
|
|
24
|
+
import { RtkSavingsTracker } from "../../utils/rtk-wrapper.js";
|
|
25
|
+
import { setProjectDir as setDiffProjectDir } from "../../review/diff-generator.js";
|
|
27
26
|
import {
|
|
28
27
|
loadProductContext, resolvePipelineFlags, createBudgetManager,
|
|
29
28
|
initializeSession, autoInit,
|
|
@@ -116,19 +115,19 @@ export async function initFlowContext({ task, config, logger, emitter, askQuesti
|
|
|
116
115
|
const rtkResult = await detectRtk();
|
|
117
116
|
if (rtkResult.available) {
|
|
118
117
|
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
ctx.rtkTracker =
|
|
129
|
-
logger.info(`RTK detected (${rtkResult.version}) —
|
|
118
|
+
// KJC-BUG-0115: rtk MUST NOT wrap the pipeline's internal git/diff
|
|
119
|
+
// commands. `rtk git diff` emits a compressed summary without
|
|
120
|
+
// `diff --git` headers, so every consumer that parses the output
|
|
121
|
+
// (tdd-policy extractChangedFiles, reviewer diffs, status checks)
|
|
122
|
+
// sees an empty change set — in the field this made the TDD gate
|
|
123
|
+
// fail forever with "(2 src, 0 test)" while real tests existed.
|
|
124
|
+
// RTK still saves tokens where it belongs: inside the coder agent's
|
|
125
|
+
// own shell, via its Claude Code hook. Detection is kept so the
|
|
126
|
+
// config advertises availability to agents.
|
|
127
|
+
ctx.rtkTracker = new RtkSavingsTracker();
|
|
128
|
+
logger.info(`RTK detected (${rtkResult.version}) — available to agents (internal git/diff stay unwrapped)`);
|
|
130
129
|
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
131
|
-
message: "RTK detected —
|
|
130
|
+
message: "RTK detected — available to agents",
|
|
132
131
|
detail: { version: rtkResult.version, executorType: "local" }
|
|
133
132
|
}));
|
|
134
133
|
}
|
|
@@ -253,6 +253,7 @@ export async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitt
|
|
|
253
253
|
}
|
|
254
254
|
if (iterResult.action === "retry") { i -= 1; }
|
|
255
255
|
else {
|
|
256
|
+
await ensureIterationRecorded(ctx, i, logger);
|
|
256
257
|
// Iteration gate (KJC-TSK-0628): opt-in pause with a report before the
|
|
257
258
|
// next iteration; free-text answers become directives for the coder.
|
|
258
259
|
const gate = await handleIterationGate({
|
|
@@ -306,6 +307,7 @@ export async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitt
|
|
|
306
307
|
if (iterResult.action === "return") return iterResult.result;
|
|
307
308
|
if (iterResult.action === "retry") { i -= 1; }
|
|
308
309
|
else {
|
|
310
|
+
await ensureIterationRecorded(ctx, i, logger);
|
|
309
311
|
// Same iteration gate on Solomon-extended iterations (KJC-TSK-0628).
|
|
310
312
|
const gate = await handleIterationGate({
|
|
311
313
|
enabled: ctx.config.session?.iteration_gate === true,
|
|
@@ -323,9 +325,56 @@ export async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitt
|
|
|
323
325
|
|
|
324
326
|
// Extended iterations also exhausted — final Solomon call
|
|
325
327
|
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
|
|
326
|
-
return finalResult;
|
|
328
|
+
return finalizeMaxIterationsApproval(ctx, finalResult, { task: loopTask, askQuestion, emitter, logger, i });
|
|
327
329
|
}
|
|
328
330
|
|
|
329
|
-
return maxIterResult;
|
|
331
|
+
return finalizeMaxIterationsApproval(ctx, maxIterResult, { task: loopTask, askQuestion, emitter, logger, i });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// KJC-BUG-0117: iterations that end before the reviewer (TDD/sonar
|
|
335
|
+
// "continue", guard retries) never reached the recordIteration call at
|
|
336
|
+
// the end of runSingleIteration, so session._journalIterations stayed
|
|
337
|
+
// empty and the journal read "Iterations: 0" after a 5-iteration run.
|
|
338
|
+
export async function ensureIterationRecorded(ctx, i, logger) {
|
|
339
|
+
if (!ctx.journalIterations) return;
|
|
340
|
+
if ((ctx.session._journalIterations?.length || 0) >= i) return;
|
|
341
|
+
try {
|
|
342
|
+
const { recordIteration, extractIterationData } = await import("../../session/journal/iteration-logger.js");
|
|
343
|
+
recordIteration(ctx.session, extractIterationData({
|
|
344
|
+
iteration: i, durationMs: 0, stageResults: ctx.stageResults, session: ctx.session,
|
|
345
|
+
}));
|
|
346
|
+
} catch (err) {
|
|
347
|
+
logger.warn(`Iteration journal record failed (non-blocking): ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// KJC-BUG-0116: an "approved" verdict at max_iterations (brain_approved,
|
|
352
|
+
// brain_solomon_approved, solomon_approved) used to be returned as-is,
|
|
353
|
+
// skipping the entire post-loop (tester/security/audit) AND git
|
|
354
|
+
// automation (push/PR) that every reviewer-approved session gets — the
|
|
355
|
+
// session read "approved" with zero verification and zero push. Route it
|
|
356
|
+
// through the same handleApprovedReview path instead.
|
|
357
|
+
export async function finalizeMaxIterationsApproval(ctx, maxIterResult, { task, askQuestion, emitter, logger, i }) {
|
|
358
|
+
if (maxIterResult?.approved !== true) return maxIterResult;
|
|
359
|
+
|
|
360
|
+
const review = {
|
|
361
|
+
approved: true,
|
|
362
|
+
raw_summary: `Finalized at max_iterations (${maxIterResult.reason || "approved"})`,
|
|
363
|
+
blocking_issues: []
|
|
364
|
+
};
|
|
365
|
+
const fin = await handleApprovedReview({
|
|
366
|
+
config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase,
|
|
367
|
+
coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
368
|
+
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion, logger,
|
|
369
|
+
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject,
|
|
370
|
+
review, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
|
|
371
|
+
});
|
|
372
|
+
if (fin.action === "return") return fin.result;
|
|
373
|
+
|
|
374
|
+
// Post-loop demanded another coder pass but iterations are exhausted —
|
|
375
|
+
// report honestly instead of a false green.
|
|
376
|
+
logger.warn("Post-loop stages rejected the work at max_iterations — session NOT approved");
|
|
377
|
+
await markSessionStatus(ctx.session, "failed");
|
|
378
|
+
return { approved: false, sessionId: ctx.session.id, reason: "post_loop_rejected_at_max_iterations" };
|
|
330
379
|
}
|
|
331
380
|
|
|
@@ -123,7 +123,11 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
123
123
|
return escalateToHuman({ askQuestion, session, emitter, eventBase, stage, conflict, iteration });
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
// KJC-BUG-0113: the fallback used to be "gemini" — Google retired the
|
|
127
|
+
// Gemini Code Assist CLI for individuals (IneligibleTierError), so an
|
|
128
|
+
// unconfigured Solomon died on every escalation. Claude matches the
|
|
129
|
+
// brain.provider default.
|
|
130
|
+
const solomonProvider = config?.roles?.solomon?.provider || "claude";
|
|
127
131
|
emitProgress(
|
|
128
132
|
emitter,
|
|
129
133
|
makeEvent("solomon:start", { ...eventBase, stage: "solomon" }, {
|
|
@@ -389,16 +389,19 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
|
|
|
389
389
|
return { action: "continue" };
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
// Brain:
|
|
392
|
+
// Brain: at the sub-loop limit the TDD gate must stop eating iterations.
|
|
393
|
+
// KJC-BUG-0115: returning "continue" here short-circuited runSingleIteration
|
|
394
|
+
// before the reviewer gate — 5/5 iterations ended without any review. The
|
|
395
|
+
// failure is already queued as feedback; "proceed" lets the reviewer run.
|
|
393
396
|
if (brainCtx?.enabled) {
|
|
394
|
-
logger.info("Brain: TDD sub-loop limit reached —
|
|
397
|
+
logger.info("Brain: TDD sub-loop limit reached — proceeding to reviewer with TDD failure as pending feedback");
|
|
395
398
|
emitProgress(emitter, makeEvent("brain:tdd-retry-limit", { ...eventBase, stage: "tdd" }, {
|
|
396
|
-
message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats}) —
|
|
399
|
+
message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats}) — proceeding to reviewer`,
|
|
397
400
|
detail: { subloop: "tdd", retryCount: session.repeated_issue_count, reason: tddEval.reason }
|
|
398
401
|
}));
|
|
399
402
|
resetRetryCount(session, "repeated_issue");
|
|
400
403
|
await saveSession(session);
|
|
401
|
-
return { action: "
|
|
404
|
+
return { action: "proceed" };
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
emitProgress(
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
commitAll,
|
|
9
9
|
pushBranch,
|
|
10
10
|
createPullRequest,
|
|
11
|
-
revParse
|
|
11
|
+
revParse,
|
|
12
|
+
hasRemote
|
|
12
13
|
} from "../utils/git.js";
|
|
13
14
|
|
|
14
15
|
function buildCommitMessage(task) {
|
|
@@ -59,7 +60,13 @@ export class CommiterRole extends BaseRole {
|
|
|
59
60
|
await commitAll(msg);
|
|
60
61
|
const commitHash = await revParse("HEAD");
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
// KJC-BUG-0112: no `origin` remote (quickstart scenario) → skip
|
|
64
|
+
// push/PR instead of throwing on fetch.
|
|
65
|
+
const remoteAvailable = push ? await hasRemote() : false;
|
|
66
|
+
if (push && !remoteAvailable) {
|
|
67
|
+
this.logger?.info?.("No remote configured — skipping push/PR");
|
|
68
|
+
}
|
|
69
|
+
if (push && remoteAvailable) {
|
|
63
70
|
const baseBranch = this.config.base_branch || "main";
|
|
64
71
|
await fetchBase(baseBranch);
|
|
65
72
|
await ensureBranchUpToDateWithBase({
|
|
@@ -71,7 +78,7 @@ export class CommiterRole extends BaseRole {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
let prUrl = null;
|
|
74
|
-
if (push && createPr) {
|
|
81
|
+
if (push && createPr && remoteAvailable) {
|
|
75
82
|
const baseBranch = this.config.base_branch || "main";
|
|
76
83
|
prUrl = await createPullRequest({
|
|
77
84
|
baseBranch,
|
|
@@ -12,7 +12,10 @@ const KNOWN_AGENTS = [
|
|
|
12
12
|
|
|
13
13
|
export async function checkBinary(name, versionArg = "--version") {
|
|
14
14
|
const resolved = resolveBin(name);
|
|
15
|
-
|
|
15
|
+
// KJC-BUG-0113: a zombie CLI that hangs on --version (seen with the
|
|
16
|
+
// retired Gemini Code Assist binary) must not hang init/preflight —
|
|
17
|
+
// 5s is generous for a version print.
|
|
18
|
+
const res = await runCommand(resolved, [versionArg], { timeout: 5000 });
|
|
16
19
|
const version = (res.stdout || res.stderr || "").split("\n")[0].trim();
|
|
17
20
|
return { ok: res.exitCode === 0, version, path: resolved };
|
|
18
21
|
}
|
package/src/utils/git.js
CHANGED
|
@@ -251,6 +251,16 @@ export async function commitAll(message, cwd = null) {
|
|
|
251
251
|
return { committed: true, commit: { hash, message: commitMessage } };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* KJC-BUG-0112: whether the repo has an `origin` remote. The quickstart
|
|
256
|
+
* scenario (fresh folder + git init) has none — post-approval push/PR
|
|
257
|
+
* automation must skip instead of failing and escalating to Solomon.
|
|
258
|
+
*/
|
|
259
|
+
export async function hasRemote(cwd = null) {
|
|
260
|
+
const res = await run("git", ["remote", "get-url", "origin"], cwd ? { cwd } : {});
|
|
261
|
+
return res.exitCode === 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
254
264
|
export async function pushBranch(branch, cwd = null) {
|
|
255
265
|
await runGit(["push", "-u", "origin", branch], cwd ? { cwd } : {});
|
|
256
266
|
}
|
|
@@ -15,7 +15,13 @@ const CAPABILITY_TIERS = {
|
|
|
15
15
|
// Role requirements: what capability level is ideal for each role
|
|
16
16
|
const ROLE_PREFERENCES = {
|
|
17
17
|
brain: { minTier: 4, prefer: "claude", description: "Karajan Brain (orchestrator)" },
|
|
18
|
-
|
|
18
|
+
// KJC-BUG-0113: Solomon preferred gemini, but Google retired the Gemini
|
|
19
|
+
// Code Assist CLI for individuals — a binary that answers --version yet
|
|
20
|
+
// dies with IneligibleTierError on every real call. Any machine with the
|
|
21
|
+
// stale CLI on PATH got a judge that could never rule. Claude matches
|
|
22
|
+
// the brain default; diversifyReviewer still picks a different agent
|
|
23
|
+
// when more than one healthy option exists.
|
|
24
|
+
solomon: { minTier: 3, prefer: "claude", description: "Solomon (judge/arbiter)" },
|
|
19
25
|
coder: { minTier: 2, prefer: "claude", description: "Coder" },
|
|
20
26
|
reviewer: { minTier: 3, prefer: "codex", description: "Reviewer" },
|
|
21
27
|
planner: { minTier: 4, prefer: "claude", description: "Planner" },
|