karajan-code 3.14.0 → 3.15.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/README.md +10 -0
- package/package.json +1 -1
- package/packages/hu-board/src/routes/api.js +7 -3
- package/scripts/verify-pack.mjs +5 -1
- package/src/agents/claude-agent.js +9 -4
- package/src/agents/model-registry.js +23 -0
- package/src/config/defaults.js +4 -0
- package/src/git/hu-automation.js +6 -5
- package/src/hu/acceptance-runner.js +8 -4
- package/src/hu/worktree-bootstrap.js +59 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +1 -1
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +1 -1
- package/src/orchestrator/drivers/run-hu-batch.js +82 -52
- package/src/orchestrator/hu-sub-pipeline.js +26 -2
- package/src/orchestrator/stages/coder-stage.js +10 -2
- package/src/orchestrator/stages/reviewer-stage.js +3 -3
- package/src/plan/plan-executor.js +4 -0
- package/src/plan/preflight-hu.js +3 -0
- package/src/rag/embedder.js +2 -81
- package/src/rag/embedders/_cloud-base.js +2 -24
- package/src/rag/embedders/cohere.js +2 -28
- package/src/rag/embedders/factory.js +2 -29
- package/src/rag/embedders/mistral.js +2 -26
- package/src/rag/embedders/onnx.js +2 -63
- package/src/rag/embedders/openai.js +2 -22
- package/src/rag/embedders/voyage.js +2 -18
- package/src/rag/rerank.js +4 -74
- package/src/rag/retriever.js +2 -127
- package/src/rag/where-parser.js +4 -54
- package/src/review/diff-generator.js +2 -2
- package/src/roles/agent-role.js +4 -0
- package/src/roles/coder-role.js +8 -2
- package/src/spec-review/run-spec-review.js +5 -1
- package/src/utils/cli-ask-question.js +9 -0
- package/src/utils/git.js +12 -11
package/src/rag/where-parser.js
CHANGED
|
@@ -1,54 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// pair := IDENT "=" VALUE
|
|
6
|
-
// IDENT := [A-Za-z_][A-Za-z0-9_.-]*
|
|
7
|
-
// VALUE := unquoted token | "quoted string"
|
|
8
|
-
//
|
|
9
|
-
// Examples:
|
|
10
|
-
// symbol=loadConfig
|
|
11
|
-
// hu_id=HU-003 AND kind=plan
|
|
12
|
-
// "headingPath.0"=Architecture
|
|
13
|
-
//
|
|
14
|
-
// Returns { ok: true, clauses: [["symbol","loadConfig"], ...] } or
|
|
15
|
-
// { ok: false, error: "...explanation..." }. The retriever turns each
|
|
16
|
-
// clause into a `json_extract(c.metadata, '$.<key>') = ?` SQL fragment;
|
|
17
|
-
// the `kind` key is special-cased to filter the column directly.
|
|
18
|
-
|
|
19
|
-
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
20
|
-
|
|
21
|
-
export function parseWhere(input) {
|
|
22
|
-
if (input == null) return { ok: true, clauses: [] };
|
|
23
|
-
if (typeof input !== "string") return { ok: false, error: "where: input must be a string" };
|
|
24
|
-
const s = input.trim();
|
|
25
|
-
if (!s) return { ok: true, clauses: [] };
|
|
26
|
-
const parts = s.split(/\s+AND\s+/i);
|
|
27
|
-
const clauses = [];
|
|
28
|
-
for (const p of parts) {
|
|
29
|
-
const eq = p.indexOf("=");
|
|
30
|
-
if (eq <= 0) return { ok: false, error: `where: bad pair "${p}" — expected key=value` };
|
|
31
|
-
const key = p.slice(0, eq).trim().replace(/^"|"$/g, "");
|
|
32
|
-
let val = p.slice(eq + 1).trim();
|
|
33
|
-
if (!IDENT_RE.test(key)) return { ok: false, error: `where: invalid key "${key}"` };
|
|
34
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
|
|
35
|
-
if (val.length === 0) return { ok: false, error: `where: empty value for key "${key}"` };
|
|
36
|
-
clauses.push([key, val]);
|
|
37
|
-
}
|
|
38
|
-
return { ok: true, clauses };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Build a `(sqlFragment, params)` pair to append to a chunks-table query.
|
|
43
|
-
* `kind` filters the column directly; everything else goes through json_extract.
|
|
44
|
-
*/
|
|
45
|
-
export function buildWhereSql(clauses) {
|
|
46
|
-
if (!clauses?.length) return { sql: "", params: [] };
|
|
47
|
-
const fragments = [];
|
|
48
|
-
const params = [];
|
|
49
|
-
for (const [k, v] of clauses) {
|
|
50
|
-
if (k === "kind") { fragments.push("c.kind = ?"); params.push(v); }
|
|
51
|
-
else { fragments.push(`json_extract(c.metadata, '$.${k}') = ?`); params.push(v); }
|
|
52
|
-
}
|
|
53
|
-
return { sql: ` AND ${fragments.join(" AND ")}`, params };
|
|
54
|
-
}
|
|
1
|
+
// Shim: where-parser now lives in karajan-core/rag so the hu-board
|
|
2
|
+
// workspace can consume it without a relative dep on the CLI src tree.
|
|
3
|
+
// KJC-TSK-0632 PR2.
|
|
4
|
+
export { parseWhere, buildWhereSql } from "karajan-core/rag/where-parser";
|
|
@@ -105,8 +105,8 @@ export async function generateDiff({ baseRef, stageNewFiles = false, projectDir
|
|
|
105
105
|
return generateSnapshotDiff(emptySnapshot, null, dir);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export async function getUntrackedFiles() {
|
|
109
|
-
const result = await run("git", ["ls-files", "--others", "--exclude-standard"]);
|
|
108
|
+
export async function getUntrackedFiles(cwd = null) {
|
|
109
|
+
const result = await run("git", ["ls-files", "--others", "--exclude-standard"], cwd ? { cwd } : {});
|
|
110
110
|
if (result.exitCode !== 0) return [];
|
|
111
111
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
112
112
|
}
|
package/src/roles/agent-role.js
CHANGED
|
@@ -129,6 +129,10 @@ export class AgentRole extends BaseRole {
|
|
|
129
129
|
|
|
130
130
|
const runArgs = { prompt, role: this.name };
|
|
131
131
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
132
|
+
// PAR-H (KJC-TSK-0631): per-call env additions for the agent
|
|
133
|
+
// subprocess (lane slot vars). Agents that ignore task.env simply
|
|
134
|
+
// run without them.
|
|
135
|
+
if (extracted.env) runArgs.env = extracted.env;
|
|
132
136
|
// Φ1-D (KJC-PCS-0057): roles that build their prompt as a
|
|
133
137
|
// stable/volatile layout forward both buckets so cache-aware agents
|
|
134
138
|
// (ClaudeAgent) can ship the stable block as system prompt. Agents
|
package/src/roles/coder-role.js
CHANGED
|
@@ -31,6 +31,7 @@ export class CoderRole extends AgentRole {
|
|
|
31
31
|
reviewerFeedback: null, sonarSummary: null, deferredContext: null,
|
|
32
32
|
acceptanceTests: null,
|
|
33
33
|
adrs: null, specSection: null, reviewerFindings: null, huId: null,
|
|
34
|
+
projectDir: null,
|
|
34
35
|
onOutput: null
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -52,12 +53,17 @@ export class CoderRole extends AgentRole {
|
|
|
52
53
|
specSection: input?.specSection || null,
|
|
53
54
|
reviewerFindings: input?.reviewerFindings || null,
|
|
54
55
|
huId: input?.huId || null,
|
|
56
|
+
// PAR-E2 (KJC-TSK-0629): per-call project root. Worktree lanes pass
|
|
57
|
+
// their lane dir here; the shared role instance keeps its own config.
|
|
58
|
+
projectDir: input?.projectDir || null,
|
|
59
|
+
// PAR-H (KJC-TSK-0631): per-call subprocess env (lane slot vars).
|
|
60
|
+
env: input?.env || null,
|
|
55
61
|
onOutput: input?.onOutput || null
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId }) {
|
|
60
|
-
const projectDir = this.config?.projectDir || null;
|
|
65
|
+
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId, projectDir: laneProjectDir }) {
|
|
66
|
+
const projectDir = laneProjectDir || this.config?.projectDir || null;
|
|
61
67
|
// KJC sesgo-de-stack: pasar stack/testFramework al prompt para que
|
|
62
68
|
// el coder no asuma JS/vitest en proyectos Python / Go / Rust /
|
|
63
69
|
// Ruby. audit-role.js y tester-role.js ya lo hacen así.
|
|
@@ -70,7 +70,11 @@ export async function runSpecReview({ spec, config, logger, askQuestion, flags =
|
|
|
70
70
|
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
71
71
|
const answer = await askQuestion(
|
|
72
72
|
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: continue)`,
|
|
73
|
-
{
|
|
73
|
+
{
|
|
74
|
+
detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 },
|
|
75
|
+
// KJC-BUG-0109: mirrors the interactive default — --yes presses Enter.
|
|
76
|
+
defaultAnswer: "continue",
|
|
77
|
+
},
|
|
74
78
|
);
|
|
75
79
|
if (answer === null) return { proceed: false, cancelled: true, severity, findings };
|
|
76
80
|
const n = String(answer).trim().toLowerCase();
|
|
@@ -30,6 +30,15 @@ export function createCliAskQuestion(opts = {}) {
|
|
|
30
30
|
const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
|
|
31
31
|
if (!isInteractive) {
|
|
32
32
|
if (flags?.yes) {
|
|
33
|
+
// KJC-BUG-0109: a question that declares a safe default is answered
|
|
34
|
+
// with it — --yes means "press Enter for me", not "abort on any
|
|
35
|
+
// gate". Questions without a declared default keep failing loud.
|
|
36
|
+
const fallback = typeof context?.defaultAnswer === "string" && context.defaultAnswer.trim()
|
|
37
|
+
? context.defaultAnswer.trim() : null;
|
|
38
|
+
if (fallback) {
|
|
39
|
+
process.stderr.write(`\n[non-interactive --yes] Auto-answering with declared default "${fallback}": ${question}\n`);
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
33
42
|
process.stderr.write(
|
|
34
43
|
`\n[non-interactive --yes] Pipeline asked a question that cannot be auto-answered:\n`
|
|
35
44
|
+ ` ❓ ${question}\n`
|
package/src/utils/git.js
CHANGED
|
@@ -167,8 +167,8 @@ export function buildBranchName(prefix, task) {
|
|
|
167
167
|
return `${prefix}${slugifyTask(task) || "task"}-${stamp}`;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
export async function hasChanges() {
|
|
171
|
-
const status = await runGit(["status", "--porcelain"]);
|
|
170
|
+
export async function hasChanges(cwd = null) {
|
|
171
|
+
const status = await runGit(["status", "--porcelain"], cwd ? { cwd } : {});
|
|
172
172
|
return status.length > 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -229,12 +229,13 @@ function isNothingToCommit(message) {
|
|
|
229
229
|
return NOTHING_TO_COMMIT_PATTERNS.some((re) => re.test(m));
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export async function commitAll(message) {
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
export async function commitAll(message, cwd = null) {
|
|
233
|
+
const opts = cwd ? { cwd } : {};
|
|
234
|
+
await runGit(["add", "-A"], opts);
|
|
235
|
+
const changed = await hasChanges(cwd);
|
|
235
236
|
if (!changed) return { committed: false };
|
|
236
237
|
try {
|
|
237
|
-
await runGit(["commit", "-m", message]);
|
|
238
|
+
await runGit(["commit", "-m", message], opts);
|
|
238
239
|
} catch (err) {
|
|
239
240
|
// `git status --porcelain` and `git commit` disagreed about whether
|
|
240
241
|
// there was anything to commit. Don't escalate — the only outcome
|
|
@@ -245,18 +246,18 @@ export async function commitAll(message) {
|
|
|
245
246
|
}
|
|
246
247
|
throw err;
|
|
247
248
|
}
|
|
248
|
-
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"]);
|
|
249
|
+
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"], opts);
|
|
249
250
|
const [hash, commitMessage] = raw.split("\x1f");
|
|
250
251
|
return { committed: true, commit: { hash, message: commitMessage } };
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
export async function pushBranch(branch) {
|
|
254
|
-
await runGit(["push", "-u", "origin", branch]);
|
|
254
|
+
export async function pushBranch(branch, cwd = null) {
|
|
255
|
+
await runGit(["push", "-u", "origin", branch], cwd ? { cwd } : {});
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
export async function createPullRequest({ baseBranch, branch, title, body }) {
|
|
258
|
+
export async function createPullRequest({ baseBranch, branch, title, body, cwd = null }) {
|
|
258
259
|
const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
|
|
259
|
-
const res = await runCommand("gh", args);
|
|
260
|
+
const res = await runCommand("gh", args, cwd ? { cwd } : {});
|
|
260
261
|
if (res.exitCode !== 0) {
|
|
261
262
|
throw new Error(`gh ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
262
263
|
}
|