karajan-code 4.63.0 → 4.64.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +32 -5
- package/src/cli/register-pipeline.js +1 -1
- package/src/cli/register-plan.js +1 -1
- package/src/commands/doc.js +26 -0
- package/src/commands/hu.js +4 -0
- package/src/commands/rag.js +26 -2
- package/src/config/defaults.js +3 -0
- package/src/docs/index-first.js +31 -0
- package/src/docs/markdown-index.js +62 -0
- package/src/hu/gh-identity.js +44 -0
- package/src/orchestrator/stages/coder-stage.js +2 -0
- package/src/plan/adr-loader.js +1 -1
- package/src/prompts/coder.js +7 -4
- package/src/utils/task-file.js +10 -3
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@ export const META_COMMANDS = ["advanced", "help"];
|
|
|
33
33
|
export const ADVANCED_GROUPS = [
|
|
34
34
|
{ title: "Pipeline (piezas sueltas)", commands: ["autorun", "code", "review", "solomon", "agent", "scan", "tournament"] },
|
|
35
35
|
{ title: "Análisis pre-run", commands: ["discover", "triage", "researcher", "architect", "onboard", "brief"] },
|
|
36
|
-
{ title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch"] },
|
|
36
|
+
{ title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch", "doc"] },
|
|
37
37
|
{ title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar", "privacy", "release", "policy", "rules", "rulesync", "claims", "steward", "pr-size"] },
|
|
38
38
|
{ title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "worktree", "undo", "standby", "sentinel", "identity", "security", "handoff"] },
|
|
39
39
|
{ title: "Infra / setup", commands: ["install-tools", "ollama", "skills", "roles", "agents", "env"] },
|
package/src/cli/register-meta.js
CHANGED
|
@@ -29,6 +29,7 @@ import { mutateCommand } from "../commands/mutate.js";
|
|
|
29
29
|
import { hardenCommand } from "../commands/harden.js";
|
|
30
30
|
import { telemetryPreviewCommand, telemetryStatusCommand } from "../commands/telemetry.js";
|
|
31
31
|
import { envInstallCommand, briefCommand } from "../commands/env.js";
|
|
32
|
+
import { docCommand } from "../commands/doc.js";
|
|
32
33
|
import { runReleaseCheck } from "../checks/release-check.js";
|
|
33
34
|
import { agentRunCommand } from "../commands/agent-run.js";
|
|
34
35
|
import { reportIssueCommand } from "../commands/report-issue.js";
|
|
@@ -75,7 +76,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
75
76
|
.action(async (task, flags) => {
|
|
76
77
|
await withConfig(pkgVersion, "discover", flags, async ({ config, logger }) => {
|
|
77
78
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
78
|
-
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
79
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
79
80
|
await discoverCommand({ task: resolvedTask, config, logger, mode: flags.mode, json: flags.json });
|
|
80
81
|
});
|
|
81
82
|
});
|
|
@@ -91,7 +92,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
91
92
|
.action(async (task, flags) => {
|
|
92
93
|
await withConfig(pkgVersion, "triage", flags, async ({ config, logger }) => {
|
|
93
94
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
94
|
-
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
95
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
95
96
|
await triageCommand({ task: resolvedTask, config, logger, json: flags.json });
|
|
96
97
|
});
|
|
97
98
|
});
|
|
@@ -106,7 +107,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
106
107
|
.action(async (task, flags) => {
|
|
107
108
|
await withConfig(pkgVersion, "researcher", flags, async ({ config, logger }) => {
|
|
108
109
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
109
|
-
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
110
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
110
111
|
await researcherCommand({ task: resolvedTask, config, logger });
|
|
111
112
|
});
|
|
112
113
|
});
|
|
@@ -123,7 +124,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
123
124
|
.action(async (task, flags) => {
|
|
124
125
|
await withConfig(pkgVersion, "architect", flags, async ({ config, logger }) => {
|
|
125
126
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
126
|
-
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
127
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
127
128
|
await architectCommand({ task: resolvedTask, config, logger, context: flags.context, json: flags.json });
|
|
128
129
|
});
|
|
129
130
|
});
|
|
@@ -542,6 +543,29 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
542
543
|
});
|
|
543
544
|
});
|
|
544
545
|
|
|
546
|
+
// KJC-TSK-1183 — a long markdown document is read by index: the headings first, then only the
|
|
547
|
+
// sections a task needs, each one whole and exactly as the file has it.
|
|
548
|
+
const doc = program.command("doc").description("Read a markdown document by index: headings first, whole sections on demand");
|
|
549
|
+
doc.command("index")
|
|
550
|
+
.description("List the headings of a markdown file with their level and line (a heading inside a code block is text)")
|
|
551
|
+
.argument("<file>", "Markdown file")
|
|
552
|
+
.option("--json", "Machine-readable output")
|
|
553
|
+
.action(async (file, flags) => {
|
|
554
|
+
await withConfig(pkgVersion, "doc-index", flags, async () => {
|
|
555
|
+
docCommand({ action: "index", file, flags });
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
doc.command("section")
|
|
559
|
+
.description("Print one section whole, as the file has it, up to the next heading of the same or a higher level; an unknown heading lists the available ones")
|
|
560
|
+
.argument("<file>", "Markdown file")
|
|
561
|
+
.argument("<heading>", "Heading title, exact or case-insensitive")
|
|
562
|
+
.option("--json", "Machine-readable output")
|
|
563
|
+
.action(async (file, heading, flags) => {
|
|
564
|
+
await withConfig(pkgVersion, "doc-section", flags, async () => {
|
|
565
|
+
docCommand({ action: "section", file, heading, flags });
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
545
569
|
// KJC-TSK-0712 — memory is the reminder, the check is the guarantee.
|
|
546
570
|
const release = program.command("release").description("Release ceremony helpers");
|
|
547
571
|
release.command("check")
|
|
@@ -798,7 +822,8 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
798
822
|
.option("--alpha <n>", "Deprecated: hybrid fusion is reciprocal rank since 4.63; tune rag.search.rrf_k (default 60) in kj.config.yml", "0.6")
|
|
799
823
|
.option("--where <clause>", "Metadata filter: 'symbol=loadConfig', 'hu_id=HU-003 AND kind=plan', or a prefix with ~= such as 'source~=src/billing' (relative to the project)")
|
|
800
824
|
.option("--library", "Query the distilled engineering canon (pattern cards shipped with kj + ~/.karajan/library, in ~/.karajan/library.db) instead of the project corpus; a project's own .karajan/library cards live in its index and answer normal queries. Cards carry problem signatures, when-NOT-to-apply and canonical citations (KJC-TSK-0697).")
|
|
801
|
-
.option("--rerank", "
|
|
825
|
+
.option("--rerank", "Re-score the top-N candidates with a cross-encoder (needs @huggingface/transformers; or rag.search.rerank.enabled in kj.config.yml)")
|
|
826
|
+
.option("--rerank-top-n <n>", "Candidates the cross-encoder re-scores before the top-K is cut (default 40, or rag.search.rerank.top_n)")
|
|
802
827
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
803
828
|
.option("--json", "Output the hits as JSON")
|
|
804
829
|
.action(async (text, flags) => {
|
|
@@ -821,6 +846,8 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
821
846
|
.option("--save-baseline <path>", "Write recall@k, MRR and latency to this file as the frozen baseline")
|
|
822
847
|
.option("--baseline <path>", "Fail (exit 1) if recall@k, MRR or p95 latency got worse than that baseline")
|
|
823
848
|
.option("--project <slug>", "Filter by project slug. Pass 'all' to query across every indexed project.")
|
|
849
|
+
.option("--rerank", "Measure with the cross-encoder rerank on (needs @huggingface/transformers)")
|
|
850
|
+
.option("--rerank-top-n <n>", "Candidates the cross-encoder re-scores before the top-K is cut (default 40)")
|
|
824
851
|
.option("--json", "Emit the full report as JSON")
|
|
825
852
|
.action(async (flags) => {
|
|
826
853
|
await withConfig(pkgVersion, "rag-eval", flags, async ({ config, logger }) => {
|
|
@@ -153,7 +153,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
153
153
|
effectiveTask = plan.task;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
const resolvedTask = await resolveTaskInput({ task: effectiveTask, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
156
|
+
const resolvedTask = await resolveTaskInput({ task: effectiveTask, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
157
157
|
const res = await runCommandHandler({ task: resolvedTask, config, logger, flags });
|
|
158
158
|
// KJC-TSK-0674 (issue #1289): an aborted run (spec-review cancel,
|
|
159
159
|
// unanswerable non-interactive gate) must be observable by CI.
|
package/src/cli/register-plan.js
CHANGED
|
@@ -52,7 +52,7 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
52
52
|
.action(async (task, flags) => {
|
|
53
53
|
await withConfig(pkgVersion, "plan", flags, async ({ config, logger }) => {
|
|
54
54
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
55
|
-
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
55
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger, indexFirstBytes: config.docs?.indexFirstBytes });
|
|
56
56
|
|
|
57
57
|
// REASONS Canvas auto-detection: a SPEC that contains
|
|
58
58
|
// `## REASONS:Section` headings is treated as a structured
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// KJC-TSK-1183 — `kj doc index <file>` and `kj doc section <file> <heading>`: a long markdown
|
|
2
|
+
// document is read by index, the headings first and only the sections a task needs, each one
|
|
3
|
+
// whole and exactly as the file has it. The headings are the index; nothing extra to keep in sync.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { indexMarkdown, sectionOf } from "../docs/markdown-index.js";
|
|
7
|
+
|
|
8
|
+
export function docCommand({ action, file, heading = null, flags = {}, cwd = process.cwd(), out = process.stdout }) {
|
|
9
|
+
if (!file) throw new Error("usage: kj doc index <file> | kj doc section <file> <heading>");
|
|
10
|
+
const path = resolve(cwd, file);
|
|
11
|
+
const text = readFileSync(path, "utf8"); // a missing file fails loud, with the path
|
|
12
|
+
if (action === "index") {
|
|
13
|
+
const headings = indexMarkdown(text);
|
|
14
|
+
if (flags.json) out.write(`${JSON.stringify({ file: path, headings })}\n`);
|
|
15
|
+
else for (const h of headings) out.write(`${" ".repeat(h.level - 1)}${"#".repeat(h.level)} ${h.title} (line ${h.line})\n`);
|
|
16
|
+
return headings;
|
|
17
|
+
}
|
|
18
|
+
if (action === "section") {
|
|
19
|
+
if (!heading) throw new Error("usage: kj doc section <file> <heading>");
|
|
20
|
+
const section = sectionOf(text, heading);
|
|
21
|
+
if (flags.json) out.write(`${JSON.stringify({ file: path, ...section })}\n`);
|
|
22
|
+
else out.write(section.text); // exactly as the file has it, nothing added
|
|
23
|
+
return section;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`unknown action "${action}": use index | section`);
|
|
26
|
+
}
|
package/src/commands/hu.js
CHANGED
|
@@ -14,6 +14,7 @@ import { effectiveGitEmail } from "../identity/detect.js";
|
|
|
14
14
|
import { roleSkills } from "../skills/role-skills.js";
|
|
15
15
|
import { readBoardConfig, setBoardBackend } from "../hu/board-config.js";
|
|
16
16
|
import { createHu, listHus, claim, setStatus, assertNotHeld, isOffline } from "../hu/github-board.js";
|
|
17
|
+
import { assertGhIdentity } from "../hu/gh-identity.js";
|
|
17
18
|
import { listWithCache, queueMove, flushQueue } from "../hu/team-board-cache.js";
|
|
18
19
|
import { writeTeamMirror } from "../hu/team-board-mirror.js";
|
|
19
20
|
|
|
@@ -196,6 +197,9 @@ export async function huCommand({ config = null, action, args = [], flags = {},
|
|
|
196
197
|
return emit(r, `✓ HU Board: ${where} — commit ${files} in a PR so the whole team inherits it`);
|
|
197
198
|
}
|
|
198
199
|
const board = await (deps.boardConfig ?? readBoardConfig)(projectDir);
|
|
200
|
+
// KJC-TSK-1186: the shared board is written as the active gh account; with an identity declared
|
|
201
|
+
// it must be that one, said with the remedy before any gh call instead of a GraphQL permission error.
|
|
202
|
+
if (board.backend === "github") (deps.assertGhIdentity ?? assertGhIdentity)({ projectDir });
|
|
199
203
|
const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
|
|
200
204
|
// KJC-TSK-1166 (TEAM-GH-E): the HUs created before the shared board, moved to it once.
|
|
201
205
|
if (action === "migrate") {
|
package/src/commands/rag.js
CHANGED
|
@@ -174,7 +174,7 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
174
174
|
// composed clause cannot leak past the kind filter; it fails loudly.
|
|
175
175
|
let where = flags.where || null;
|
|
176
176
|
if (library) where = flags.where ? `kind=library AND ${flags.where}` : "kind=library";
|
|
177
|
-
const rerankOpts =
|
|
177
|
+
const rerankOpts = rerankOptionsFor({ config, flags, logger });
|
|
178
178
|
const ranked = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, rrfK, where, whereRoot, rerankOpts });
|
|
179
179
|
const hits = library ? ranked : withPathNamedHits(pathNamedHits(db, text, { project, projectDir: config?.projectDir || process.cwd() }), ranked, topK);
|
|
180
180
|
if (flags.json) {
|
|
@@ -193,6 +193,28 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// KJC-TSK-1181 (RAG-D2): the cross-encoder, from the flag or from rag.search.rerank in the config
|
|
197
|
+
// (enabled, top_n, model). Without the library or the model the retrieval answers alone and the
|
|
198
|
+
// install is said once per process, never on every query, never in silence.
|
|
199
|
+
const rerankWarned = new Set();
|
|
200
|
+
export function rerankOptionsFor({ config, flags = {}, logger }) {
|
|
201
|
+
const cfg = config?.rag?.search?.rerank ?? {};
|
|
202
|
+
const enabled = flags.rerank === true || (flags.rerank !== false && cfg.enabled === true);
|
|
203
|
+
if (!enabled) return null;
|
|
204
|
+
const topN = flags.rerankTopN ?? cfg.top_n;
|
|
205
|
+
if (topN !== undefined && !(Number.isInteger(Number(topN)) && Number(topN) > 0)) throw new Error(`rag.search.rerank.top_n must be a positive integer, got ${JSON.stringify(topN)}`);
|
|
206
|
+
const key = config?.projectDir || "rerank";
|
|
207
|
+
return {
|
|
208
|
+
model: flags.rerankModel || cfg.model || undefined,
|
|
209
|
+
...(topN === undefined ? {} : { topN: Number(topN) }),
|
|
210
|
+
onUnavailable: (err) => {
|
|
211
|
+
if (rerankWarned.has(key)) return;
|
|
212
|
+
rerankWarned.add(key);
|
|
213
|
+
logger.warn(`[rag] rerank skipped: ${err.message}. Install it with npm install @huggingface/transformers; the retrieval answered without it`);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
196
218
|
// KJC-TSK-0483 — `kj rag eval`. Runs the retriever against a golden query
|
|
197
219
|
// set and reports recall@5, recall@10 and MRR per query and aggregated.
|
|
198
220
|
// Exit code != 0 when `--min-recall` is set and the aggregated recall@5
|
|
@@ -207,7 +229,9 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
|
207
229
|
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
208
230
|
const project = flags.project === "all" ? null : (flags.project || detected || null);
|
|
209
231
|
const embedder = makeGovernedEmbedder(config);
|
|
210
|
-
|
|
232
|
+
// KJC-TSK-1181: measured with the cross-encoder too, so its default is a number, not a hunch.
|
|
233
|
+
const rerankOpts = rerankOptionsFor({ config, flags, logger });
|
|
234
|
+
const runQuery = async (text, k) => query(db, embedder, text, { topK: k, scope: "all", project, rerankOpts });
|
|
211
235
|
const report = await runEval(queries, runQuery, { topK, ks: [5, 10] });
|
|
212
236
|
const { aggregate: agg } = report;
|
|
213
237
|
if (flags.json) {
|
package/src/config/defaults.js
CHANGED
|
@@ -274,6 +274,9 @@ const DEFAULTS = {
|
|
|
274
274
|
// safe default blocks them (local ollama/onnx always allowed).
|
|
275
275
|
sensitivity: "internal"
|
|
276
276
|
},
|
|
277
|
+
// KJC-TSK-1183 — a document over this size (task file, ADR) reaches the roles as its headings
|
|
278
|
+
// index; they fetch a section whole with `kj doc section` when the task needs it.
|
|
279
|
+
docs: { indexFirstBytes: 12000 },
|
|
277
280
|
// Opt-in. Set by the `kj init` wizard (explicit consent prompt in the OS
|
|
278
281
|
// locale). When the key is absent or false, no events are sent. See
|
|
279
282
|
// src/utils/telemetry.js and the Privacy & Telemetry section of README.md.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// KJC-TSK-1183 — Roles read long documents by index. Over the limit, a document reaches the prompt
|
|
2
|
+
// as its preamble plus its headings and the command that fetches one section whole; under it, or
|
|
3
|
+
// without headings to index, it travels as it is.
|
|
4
|
+
import { indexMarkdown } from "./markdown-index.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_INDEX_FIRST_BYTES = 12000;
|
|
7
|
+
const PREAMBLE_MAX = 1500;
|
|
8
|
+
const shellQuote = (s) => (/^[A-Za-z0-9_./:@%+=-]+$/.test(s) ? s : ["'", s.replaceAll("'", String.raw`'\''`), "'"].join(""));
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the agent reads instead of the whole document, or null when the whole fits or there is
|
|
12
|
+
* nothing to index (fewer than two headings: a single one covers it all).
|
|
13
|
+
* @param {{ path: string, text: string, limit?: number }} doc
|
|
14
|
+
*/
|
|
15
|
+
export function indexFirstView({ path, text, limit = DEFAULT_INDEX_FIRST_BYTES }) {
|
|
16
|
+
if (!Number.isInteger(limit) || limit <= 0 || text.length <= limit) return null;
|
|
17
|
+
const headings = indexMarkdown(text);
|
|
18
|
+
if (headings.length < 2) return null;
|
|
19
|
+
const preamble = text.split("\n").slice(0, headings[1].line - 1).join("\n").trim().slice(0, PREAMBLE_MAX);
|
|
20
|
+
const index = headings.map((h) => `${" ".repeat(h.level - 1)}- ${h.title}`).join("\n");
|
|
21
|
+
const file = shellQuote(path);
|
|
22
|
+
return [
|
|
23
|
+
preamble,
|
|
24
|
+
"",
|
|
25
|
+
`_This document is ${text.length} characters, over the ${limit} read-whole limit, so it is read by index. Its headings:_`,
|
|
26
|
+
"",
|
|
27
|
+
index,
|
|
28
|
+
"",
|
|
29
|
+
`Read a section whole only when the task needs it: \`kj doc section ${file} "<heading>"\` (the index again: \`kj doc index ${file}\`).`,
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1183 — Long markdown documents read by index: the headings first, then only the
|
|
3
|
+
* sections a task needs, each one whole and exactly as it is in the file. The headings are the
|
|
4
|
+
* index, so there is nothing extra to keep in sync. Independent of any spec kit's format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ATX headings only: up to three spaces, `#` to `######`, a space, the title, an optional closing run of `#`.
|
|
8
|
+
const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/;
|
|
9
|
+
// A fence opens with three or more of one character; it closes with the same character, a run at
|
|
10
|
+
// least as long, and nothing else on the line (CommonMark): ```` is not closed by ```.
|
|
11
|
+
const FENCE_OPEN = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
12
|
+
const FENCE_CLOSE = /^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
13
|
+
|
|
14
|
+
export class UnknownHeadingError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "UnknownHeadingError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The headings of the document, with level and 1-based line; a heading inside a fenced code block is text. */
|
|
22
|
+
export function indexMarkdown(text) {
|
|
23
|
+
const headings = [];
|
|
24
|
+
let fence = null; // the opening run, e.g. "````", while inside a code block
|
|
25
|
+
text.split("\n").forEach((raw, i) => {
|
|
26
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
27
|
+
if (fence !== null) {
|
|
28
|
+
const c = FENCE_CLOSE.exec(line);
|
|
29
|
+
if (c && c[1][0] === fence[0] && c[1].length >= fence.length) fence = null;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const o = FENCE_OPEN.exec(line);
|
|
33
|
+
if (o) {
|
|
34
|
+
fence = o[1];
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const m = HEADING.exec(line);
|
|
38
|
+
if (m) headings.push({ level: m[1].length, title: m[2], line: i + 1 });
|
|
39
|
+
});
|
|
40
|
+
return headings;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The section under `title`: its heading line and everything up to the next heading of the same
|
|
45
|
+
* or a higher level, as the file has it. Exact title first, case-insensitive as a fallback; with
|
|
46
|
+
* duplicates the first wins and the others are named by line. An unknown title is an error that
|
|
47
|
+
* lists the headings, never an empty section.
|
|
48
|
+
*/
|
|
49
|
+
export function sectionOf(text, title) {
|
|
50
|
+
const headings = indexMarkdown(text);
|
|
51
|
+
let hits = headings.filter((h) => h.title === title);
|
|
52
|
+
if (hits.length === 0) hits = headings.filter((h) => h.title.toLowerCase() === title.toLowerCase());
|
|
53
|
+
if (hits.length === 0) {
|
|
54
|
+
const available = headings.map((h) => h.title).join(", ") || "none";
|
|
55
|
+
throw new UnknownHeadingError(`"${title}" is not a heading of this document; the headings are: ${available}`);
|
|
56
|
+
}
|
|
57
|
+
const [first, ...others] = hits;
|
|
58
|
+
const lines = text.split("\n");
|
|
59
|
+
const next = headings.find((h) => h.line > first.line && h.level <= first.level);
|
|
60
|
+
const body = next ? `${lines.slice(first.line - 1, next.line - 1).join("\n")}\n` : lines.slice(first.line - 1).join("\n");
|
|
61
|
+
return { level: first.level, title: first.title, line: first.line, text: body, ...(others.length ? { duplicates: others.map((h) => h.line) } : {}) };
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// KJC-TSK-1186: the shared board is written as whoever gh is logged in as. With several GitHub
|
|
2
|
+
// accounts on one machine, another session switching the active one turned `kj hu move` into an
|
|
3
|
+
// opaque GraphQL permission error, three times in a day. The clone declares its account in
|
|
4
|
+
// .karajan/identity.local.yml (ADR 0005): with a declaration, the active account must be it.
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
|
+
import { readIdentity } from "../identity/store.js";
|
|
10
|
+
import { BoardRejectedError } from "./github-board.js";
|
|
11
|
+
|
|
12
|
+
/** Where gh keeps its accounts, honouring the same overrides gh does. */
|
|
13
|
+
export function ghHostsPath(env = process.env) {
|
|
14
|
+
const dir = env.GH_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "gh");
|
|
15
|
+
return join(dir, "hosts.yml");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The account gh would use now for github.com, or null when gh has none. */
|
|
19
|
+
export function activeGhUser(env = process.env) {
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = readFileSync(ghHostsPath(env), "utf8");
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code === "ENOENT") return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
const user = yaml.load(raw)?.["github.com"]?.user;
|
|
28
|
+
return typeof user === "string" && user.length > 0 ? user : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* With an identity declared, the active gh account must be it; the error carries the exact remedy.
|
|
33
|
+
* Without a declaration nothing is asserted: gh answers for itself.
|
|
34
|
+
* @returns {{ declared: string | null, active: string | null }}
|
|
35
|
+
*/
|
|
36
|
+
export function assertGhIdentity({ projectDir, env = process.env }) {
|
|
37
|
+
const declared = readIdentity(projectDir)?.gh_user ?? null;
|
|
38
|
+
if (!declared) return { declared: null, active: null };
|
|
39
|
+
const active = activeGhUser(env);
|
|
40
|
+
if (active && active !== declared) {
|
|
41
|
+
throw new BoardRejectedError(`the active gh account is ${active} and this clone declares ${declared}: run gh auth switch --user ${declared} and retry`);
|
|
42
|
+
}
|
|
43
|
+
return { declared, active };
|
|
44
|
+
}
|
|
@@ -73,6 +73,8 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
73
73
|
// the plan, the rest are scoped to this HU. All four pass through
|
|
74
74
|
// untouched when the caller omits them (legacy single-task runs).
|
|
75
75
|
adrs, specSection, reviewerFindings, huId,
|
|
76
|
+
// KJC-TSK-1183: a long ADR reaches the coder as its index, sections on demand.
|
|
77
|
+
indexFirstBytes: config?.docs?.indexFirstBytes ?? null,
|
|
76
78
|
// PAR-E2 (KJC-TSK-0629): the stage's config wins over the role's own —
|
|
77
79
|
// worktree lanes pass a laneConfig whose projectDir is the worktree.
|
|
78
80
|
projectDir: config?.projectDir || null,
|
package/src/plan/adr-loader.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function loadActiveAdrs(projectDir, planId) {
|
|
|
49
49
|
mdFiles.map(async (file) => {
|
|
50
50
|
try {
|
|
51
51
|
const md = await readFile(join(dir, file), "utf-8");
|
|
52
|
-
return { id: file.replace(/\.md$/, ""), markdown: md };
|
|
52
|
+
return { id: file.replace(/\.md$/, ""), markdown: md, path: join(dir, file) };
|
|
53
53
|
} catch { return null; /* skip unreadable file */ }
|
|
54
54
|
})
|
|
55
55
|
);
|
package/src/prompts/coder.js
CHANGED
|
@@ -2,6 +2,7 @@ import { buildRtkInstructions } from "./rtk-snippet.js";
|
|
|
2
2
|
import { roleSkills, buildRoleSkillSection } from "../skills/role-skills.js";
|
|
3
3
|
import { getLanguageInstruction } from "../utils/locale.js";
|
|
4
4
|
import { section, buildPromptLayout, joinLayout, STABLE, VOLATILE } from "./prompt-layout.js";
|
|
5
|
+
import { indexFirstView } from "../docs/index-first.js";
|
|
5
6
|
|
|
6
7
|
const SUBAGENT_PREAMBLE = [
|
|
7
8
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -95,9 +96,11 @@ function renderAcceptanceTestsSection(tests) {
|
|
|
95
96
|
* 8 ADRs (the planner shouldn't realistically emit more for one
|
|
96
97
|
* plan, but bound it just in case).
|
|
97
98
|
*/
|
|
98
|
-
function renderAdrSection(adrs) {
|
|
99
|
+
function renderAdrSection(adrs, indexFirstBytes = null) {
|
|
99
100
|
if (!Array.isArray(adrs) || adrs.length === 0) return "";
|
|
100
101
|
const capped = adrs.slice(0, 8);
|
|
102
|
+
// KJC-TSK-1183: a long ADR travels as its index; the coder fetches a section when it needs it.
|
|
103
|
+
const bodyOf = (adr) => (indexFirstBytes && adr.path ? indexFirstView({ path: adr.path, text: adr.markdown, limit: indexFirstBytes }) : null) ?? adr.markdown;
|
|
101
104
|
const lines = [
|
|
102
105
|
"## Architecture Decision Records (active)",
|
|
103
106
|
"",
|
|
@@ -105,7 +108,7 @@ function renderAdrSection(adrs) {
|
|
|
105
108
|
"",
|
|
106
109
|
];
|
|
107
110
|
for (const adr of capped) {
|
|
108
|
-
lines.push(adr.
|
|
111
|
+
lines.push(bodyOf(adr).trim(), "", "---", "");
|
|
109
112
|
}
|
|
110
113
|
if (adrs.length > capped.length) {
|
|
111
114
|
lines.push(`_(…${adrs.length - capped.length} more ADRs not shown — see ~/.kj/plans/<slug>/<planId>/adrs/.)_`, "");
|
|
@@ -189,7 +192,7 @@ function buildStackSection({ stack, testFramework }) {
|
|
|
189
192
|
* volatile: per-HU/iteration content, rendered LAST preserving the relative
|
|
190
193
|
* order the legacy prompt used (ADRs → SPEC → findings → tests → … → feedback).
|
|
191
194
|
*/
|
|
192
|
-
export async function buildCoderPromptLayout({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en", provider = null, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null, stack = null, testFramework = null }) {
|
|
195
|
+
export async function buildCoderPromptLayout({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en", provider = null, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null, stack = null, testFramework = null, indexFirstBytes = null }) {
|
|
193
196
|
const langInstruction = getLanguageInstruction(language);
|
|
194
197
|
// KJC-BUG-0032 (PR-I): hard rule about file paths. Real bug: a HU
|
|
195
198
|
// titled "Initialize project skeleton: create assistant/ directory…"
|
|
@@ -278,7 +281,7 @@ export async function buildCoderPromptLayout({ task, reviewerFeedback = null, so
|
|
|
278
281
|
section(skillSection, STABLE),
|
|
279
282
|
section(`Task:\n${task}`, VOLATILE),
|
|
280
283
|
section(plan ? `## Implementation Plan (from planner)\nFollow these steps:\n${plan}` : null, VOLATILE),
|
|
281
|
-
section(renderAdrSection(adrs) || null, VOLATILE),
|
|
284
|
+
section(renderAdrSection(adrs, indexFirstBytes) || null, VOLATILE),
|
|
282
285
|
section(renderSpecSectionLine(specSection) || null, VOLATILE),
|
|
283
286
|
section(renderReviewerFindingsSection(reviewerFindings, huId) || null, VOLATILE),
|
|
284
287
|
section(renderAcceptanceTestsSection(acceptanceTests) || null, VOLATILE),
|
package/src/utils/task-file.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import fs from "node:fs/promises";
|
|
27
27
|
import path from "node:path";
|
|
28
|
+
import { indexFirstView } from "../docs/index-first.js";
|
|
28
29
|
|
|
29
30
|
export const DEFAULT_TASK_FILE_MAX_BYTES = 256 * 1024;
|
|
30
31
|
|
|
@@ -41,7 +42,7 @@ export async function readTaskFile(filePath, opts = {}) {
|
|
|
41
42
|
if (!filePath || typeof filePath !== "string") {
|
|
42
43
|
throw new Error("readTaskFile: path must be a non-empty string");
|
|
43
44
|
}
|
|
44
|
-
const { projectDir, maxBytes = DEFAULT_TASK_FILE_MAX_BYTES } = opts;
|
|
45
|
+
const { projectDir, maxBytes = DEFAULT_TASK_FILE_MAX_BYTES, indexFirstBytes = null, logger = null } = opts;
|
|
45
46
|
const base = projectDir || process.cwd();
|
|
46
47
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath);
|
|
47
48
|
|
|
@@ -68,6 +69,12 @@ export async function readTaskFile(filePath, opts = {}) {
|
|
|
68
69
|
if (!trimmed) {
|
|
69
70
|
throw new Error(`Task file is empty (or whitespace-only): ${resolved}`);
|
|
70
71
|
}
|
|
72
|
+
// KJC-TSK-1183: over the limit the roles get the index and fetch sections on demand.
|
|
73
|
+
const view = indexFirstBytes ? indexFirstView({ path: resolved, text: trimmed, limit: indexFirstBytes }) : null;
|
|
74
|
+
if (view) {
|
|
75
|
+
logger?.info?.(`[docs] ${resolved} read by index (${trimmed.length} chars > ${indexFirstBytes}): sections on demand with kj doc section`);
|
|
76
|
+
return view;
|
|
77
|
+
}
|
|
71
78
|
return trimmed;
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -89,7 +96,7 @@ export async function readTaskFile(filePath, opts = {}) {
|
|
|
89
96
|
* @param {Object} [args.logger] - optional; uses `logger.warn(msg)` only
|
|
90
97
|
* @returns {Promise<string>}
|
|
91
98
|
*/
|
|
92
|
-
export async function resolveTaskInput({ task, taskFile, projectDir, logger } = {}) {
|
|
99
|
+
export async function resolveTaskInput({ task, taskFile, projectDir, logger, indexFirstBytes = null } = {}) {
|
|
93
100
|
const hasTask = typeof task === "string" && task.trim().length > 0;
|
|
94
101
|
const hasFile = typeof taskFile === "string" && taskFile.trim().length > 0;
|
|
95
102
|
|
|
@@ -103,7 +110,7 @@ export async function resolveTaskInput({ task, taskFile, projectDir, logger } =
|
|
|
103
110
|
return task.trim();
|
|
104
111
|
}
|
|
105
112
|
if (hasFile) {
|
|
106
|
-
return readTaskFile(taskFile, { projectDir });
|
|
113
|
+
return readTaskFile(taskFile, { projectDir, indexFirstBytes, logger });
|
|
107
114
|
}
|
|
108
115
|
return task.trim();
|
|
109
116
|
}
|