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.
Files changed (35) hide show
  1. package/README.md +10 -0
  2. package/package.json +1 -1
  3. package/packages/hu-board/src/routes/api.js +7 -3
  4. package/scripts/verify-pack.mjs +5 -1
  5. package/src/agents/claude-agent.js +9 -4
  6. package/src/agents/model-registry.js +23 -0
  7. package/src/config/defaults.js +4 -0
  8. package/src/git/hu-automation.js +6 -5
  9. package/src/hu/acceptance-runner.js +8 -4
  10. package/src/hu/worktree-bootstrap.js +59 -0
  11. package/src/orchestrator/drivers/iteration-phases/guards.js +1 -1
  12. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +1 -1
  13. package/src/orchestrator/drivers/run-hu-batch.js +82 -52
  14. package/src/orchestrator/hu-sub-pipeline.js +26 -2
  15. package/src/orchestrator/stages/coder-stage.js +10 -2
  16. package/src/orchestrator/stages/reviewer-stage.js +3 -3
  17. package/src/plan/plan-executor.js +4 -0
  18. package/src/plan/preflight-hu.js +3 -0
  19. package/src/rag/embedder.js +2 -81
  20. package/src/rag/embedders/_cloud-base.js +2 -24
  21. package/src/rag/embedders/cohere.js +2 -28
  22. package/src/rag/embedders/factory.js +2 -29
  23. package/src/rag/embedders/mistral.js +2 -26
  24. package/src/rag/embedders/onnx.js +2 -63
  25. package/src/rag/embedders/openai.js +2 -22
  26. package/src/rag/embedders/voyage.js +2 -18
  27. package/src/rag/rerank.js +4 -74
  28. package/src/rag/retriever.js +2 -127
  29. package/src/rag/where-parser.js +4 -54
  30. package/src/review/diff-generator.js +2 -2
  31. package/src/roles/agent-role.js +4 -0
  32. package/src/roles/coder-role.js +8 -2
  33. package/src/spec-review/run-spec-review.js +5 -1
  34. package/src/utils/cli-ask-question.js +9 -0
  35. package/src/utils/git.js +12 -11
@@ -1,54 +1,4 @@
1
- // KJC-TSK-0448 — minimal --where filter parser for metadata-aware RAG
2
- // queries. Grammar (intentionally small):
3
- //
4
- // clause := pair ( "AND" pair )*
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
  }
@@ -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
@@ -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
- { detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 } },
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
- await runGit(["add", "-A"]);
234
- const changed = await hasChanges();
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
  }