karajan-code 4.53.1 → 4.54.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 +1 -1
- package/src/checks/qmd.js +3 -2
- package/src/cli/register-meta.js +6 -1
- package/src/commands/handoff.js +23 -5
- package/src/commands/hu.js +8 -0
- package/src/environment/mcp-wiring.js +30 -3
- package/src/environment/playbook.js +4 -1
- package/src/harden/handoff.js +54 -4
- package/src/harden/sentinel-hooks.js +8 -2
- package/src/prompts/coder.js +3 -3
- package/src/prompts/reviewer.js +3 -3
- package/src/skills/role-skills.js +53 -0
package/package.json
CHANGED
package/src/checks/qmd.js
CHANGED
|
@@ -22,14 +22,15 @@ function createQmdCheck() {
|
|
|
22
22
|
try {
|
|
23
23
|
const res = await runCommand("qmd", ["--version"]);
|
|
24
24
|
if (res.exitCode === 0) {
|
|
25
|
-
|
|
25
|
+
// KJC-TSK-1067: say what it is for, not only that it is there.
|
|
26
|
+
return { ok: true, severity: "info", detail: `${res.stdout.trim()} — answers about the project's docs, notes, ADRs and plans from the documents, with fewer tokens (kj env install wires its MCP)` };
|
|
26
27
|
}
|
|
27
28
|
} catch { /* not installed */ }
|
|
28
29
|
const installCmd = getInstallCommand("qmd");
|
|
29
30
|
return {
|
|
30
31
|
ok: false,
|
|
31
32
|
severity: "warn",
|
|
32
|
-
detail: "Not found —
|
|
33
|
+
detail: "Not found — qmd answers questions about the project's docs, notes, ADRs and plans from the documents, with fewer tokens and a source",
|
|
33
34
|
fix: withDocLink(`Install: ${installCmd}`, "qmd_install"),
|
|
34
35
|
};
|
|
35
36
|
},
|
package/src/cli/register-meta.js
CHANGED
|
@@ -197,12 +197,17 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
197
197
|
// KJC-TSK-0968 (HUM-E, ADR 0018): the agent's handoff to the human goes through kj.
|
|
198
198
|
const handoffCmd = program.command("handoff").description("Handoffs: what a gate denied the agent, registered with the gate's why, for the human to read and run by id");
|
|
199
199
|
const handoff = (action, flags) => withConfig(pkgVersion, `handoff-${action}`, flags, async ({ config }) => { process.exitCode = await handoffCommand({ action, config, flags }); });
|
|
200
|
-
handoffCmd.command("add").description("Register the EXACT command a gate denied (the why comes from the Sentinel's record, not from you)")
|
|
200
|
+
handoffCmd.command("add").description("Register the EXACT command a gate denied (the why comes from the Sentinel's record, not from you), or with --why an action only the human can take")
|
|
201
201
|
.argument("<command...>", "the denied command, as it was run").option("--cwd <dir>", "Directory the command runs in (default: the project)")
|
|
202
|
+
.option("--why <text>", "KJC-TSK-1066: what an action only the human can take is for (publish, seal); marked as the agent's")
|
|
202
203
|
.action((parts, flags) => handoff("add", { ...flags, command: parts.join(" ") }));
|
|
203
204
|
handoffCmd.command("list", { isDefault: true }).description("The handoffs registered in this project").action((flags) => handoff("list", flags));
|
|
204
205
|
handoffCmd.command("show").description("A handoff: the exact command, its sha256 and why the gate denied it")
|
|
205
206
|
.argument("<id>", "handoff id (8 hex)").option("--json", "Output the record as JSON").action((id, flags) => handoff("show", { ...flags, id }));
|
|
207
|
+
handoffCmd.command("done").description("The human did it: it leaves the pending handoffs and stays in the history")
|
|
208
|
+
.argument("<id>", "handoff id (8 hex)").action((id, flags) => handoff("done", { ...flags, id }));
|
|
209
|
+
handoffCmd.command("watch").description("Your task window: the pending handoffs, then each new one as it lands, with date and why (Ctrl+C to leave)")
|
|
210
|
+
.action((flags) => handoff("watch", flags));
|
|
206
211
|
|
|
207
212
|
// KJC-TSK-0986 (HUM-F): the invitation to enroll the phone, once per version.
|
|
208
213
|
identityCmd
|
package/src/commands/handoff.js
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
* kj handoff add|list|show (KJC-TSK-0968, HUM-E, ADR 0018): the agent registers
|
|
3
3
|
* what a gate denied it, with the gate's why; the human reads it by id.
|
|
4
4
|
*/
|
|
5
|
-
import { addHandoff, listHandoffs, readHandoff } from "../harden/handoff.js";
|
|
5
|
+
import { addHandoff, listHandoffs, markHandoffDone, readHandoff, watchHandoffs } from "../harden/handoff.js";
|
|
6
6
|
|
|
7
|
-
const line = (h) =>
|
|
7
|
+
const line = (h) => {
|
|
8
|
+
const why = h.kind === "task" ? " (" + h.why + ")" : "";
|
|
9
|
+
return `${h.id} ${h.status.padEnd(8)} ${h.createdAt} ${h.command}${why}`;
|
|
10
|
+
};
|
|
8
11
|
|
|
9
12
|
/** @returns {Promise<0|1>} */
|
|
10
13
|
export async function handoffCommand({ action = "list", config, flags = {}, log = console.log }) {
|
|
11
14
|
const projectDir = config?.projectDir || process.cwd();
|
|
12
15
|
try {
|
|
13
16
|
if (action === "add") {
|
|
14
|
-
const record = addHandoff({ projectDir, command: flags.command, cwd: flags.cwd ?? projectDir });
|
|
17
|
+
const record = addHandoff({ projectDir, command: flags.command, cwd: flags.cwd ?? projectDir, why: flags.why ?? null });
|
|
15
18
|
log(`encargo ${record.id} registrado: ${record.command}`);
|
|
16
|
-
log(`
|
|
19
|
+
if (record.kind === "task") log(`para qué (propuesto por el agente): ${record.why}`);
|
|
20
|
+
else log(`por qué lo negó el gate (${record.denial.at}):\n${record.denial.why}`);
|
|
17
21
|
log(`tu usuario lo revisa con kj handoff show ${record.id}; nadie copia y pega el comando.`);
|
|
18
22
|
return 0;
|
|
19
23
|
}
|
|
@@ -24,7 +28,21 @@ export async function handoffCommand({ action = "list", config, flags = {}, log
|
|
|
24
28
|
log(`comando ${h.command}`);
|
|
25
29
|
log(`sha256 ${h.sha256}`);
|
|
26
30
|
log(`cwd ${h.cwd}`);
|
|
27
|
-
log(`
|
|
31
|
+
if (h.kind === "task") log(`para qué ${h.why}\n (propuesto por el agente, no lo negó ningún gate)`);
|
|
32
|
+
else log(`gate ${h.denial.tool} negó el ${h.denial.at}:\n${h.denial.why}`);
|
|
33
|
+
if (h.doneAt) log(`hecho ${h.doneAt}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (action === "done") {
|
|
37
|
+
const h = markHandoffDone({ projectDir, id: flags.id });
|
|
38
|
+
log(`encargo ${h.id} hecho: ${h.command}`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (action === "watch") {
|
|
42
|
+
log("kj handoff watch: lo que te toca hacer a ti, según llega (Ctrl+C para salir)");
|
|
43
|
+
const ctl = new AbortController();
|
|
44
|
+
process.once("SIGINT", () => ctl.abort());
|
|
45
|
+
await watchHandoffs({ projectDir, log, signal: ctl.signal });
|
|
28
46
|
return 0;
|
|
29
47
|
}
|
|
30
48
|
const all = listHandoffs({ projectDir });
|
package/src/commands/hu.js
CHANGED
|
@@ -11,6 +11,7 @@ import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
|
|
|
11
11
|
import { detectHostAgent } from "../utils/agent-detect.js";
|
|
12
12
|
import { readIdentity } from "../identity/store.js";
|
|
13
13
|
import { effectiveGitEmail } from "../identity/detect.js";
|
|
14
|
+
import { roleSkills } from "../skills/role-skills.js";
|
|
14
15
|
|
|
15
16
|
// KJC-TSK-0661 (Jorge's friction): every HU records WHO created it — the
|
|
16
17
|
// board must never show work nobody can trace. Host agent env → that agent;
|
|
@@ -238,6 +239,13 @@ export async function huCommand({ config = null, action, args = [], flags = {},
|
|
|
238
239
|
if (starting || takenOver || unclaimedRunning) Object.assign(hu, { claimed_by: who ?? null, claimed_at: who ? new Date().toISOString() : null });
|
|
239
240
|
if (starting && !who) console.warn(`⚠ ${label} is not reserved: kj cannot tell who works this clone — declare it with kj identity set`);
|
|
240
241
|
await savePlan(projectDir, plan);
|
|
242
|
+
// KJC-TSK-1065: the host session starts the card here; it learns which skills apply and where to read them.
|
|
243
|
+
if (starting && !flags.json) {
|
|
244
|
+
const text = [hu.title, hu.scope, ...(Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [])].filter(Boolean).join(" ");
|
|
245
|
+
const { skills } = await (deps.skills ?? roleSkills)({ role: "coder", task: text, projectDir });
|
|
246
|
+
const where = (s) => (s.path ? ": read " + s.path : " (native)");
|
|
247
|
+
if (skills.length) console.log([`skills for ${label} (read them before you start):`, ...skills.map((s) => "- " + s.name + where(s))].join("\n"));
|
|
248
|
+
}
|
|
241
249
|
return emit({ id: hu.id, status, claimed_by: hu.claimed_by ?? null }, `✓ ${label} → ${status}`);
|
|
242
250
|
}
|
|
243
251
|
|
|
@@ -8,10 +8,28 @@
|
|
|
8
8
|
* entries; invalid JSON is left untouched (preserve, never clobber).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
11
12
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
12
14
|
import { join } from "node:path";
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
// KJC-TSK-1067: a server spec that launches `qmd mcp` (command or args, any path).
|
|
17
|
+
const launchesQmd = (s) => Boolean(s) && typeof s === "object"
|
|
18
|
+
&& [s.command, ...(Array.isArray(s.args) ? s.args : [])].some((a) => typeof a === "string" && /(^|\/)qmd$/.test(a))
|
|
19
|
+
&& Array.isArray(s.args) && s.args.includes("mcp");
|
|
20
|
+
|
|
21
|
+
const defaultQmd = {
|
|
22
|
+
installed: () => spawnSync("qmd", ["--version"], { stdio: "ignore" }).status === 0,
|
|
23
|
+
// The user's own Claude Code config: global servers and every project's.
|
|
24
|
+
wiredGlobally: () => {
|
|
25
|
+
try {
|
|
26
|
+
const c = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf8"));
|
|
27
|
+
return [c.mcpServers, ...Object.values(c.projects || {}).map((p) => p?.mcpServers)].some((m) => Object.values(m || {}).some(launchesQmd));
|
|
28
|
+
} catch { return false; }
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function installProjectMcp({ projectDir = process.cwd(), logger = console, qmd = defaultQmd } = {}) {
|
|
15
33
|
const mcpPath = join(projectDir, ".mcp.json");
|
|
16
34
|
let cfg = {};
|
|
17
35
|
if (existsSync(mcpPath)) {
|
|
@@ -37,8 +55,17 @@ export function installProjectMcp({ projectDir = process.cwd(), logger = console
|
|
|
37
55
|
const present = Object.values(cfg.mcpServers).some(launchesRagMcp);
|
|
38
56
|
if (!present) {
|
|
39
57
|
cfg.mcpServers["kj-rag"] = { command: "kj-rag-mcp", args: [] };
|
|
40
|
-
writeFileSync(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
41
58
|
logger.info?.("✓ kj_rag_query wired as a NATIVE tool (.mcp.json → kj-rag-mcp): querying the RAG is now the agent's cheapest path");
|
|
42
59
|
}
|
|
43
|
-
|
|
60
|
+
// KJC-TSK-1067: qmd answers about the project's documents with fewer tokens and a source.
|
|
61
|
+
let qmdState = "absent";
|
|
62
|
+
if (Object.values(cfg.mcpServers).some(launchesQmd)) qmdState = "present";
|
|
63
|
+
else if (qmd.installed() && qmd.wiredGlobally()) qmdState = "global";
|
|
64
|
+
else if (qmd.installed()) {
|
|
65
|
+
cfg.mcpServers.qmd = { command: "qmd", args: ["mcp"] };
|
|
66
|
+
qmdState = "added";
|
|
67
|
+
logger.info?.("✓ qmd wired as a NATIVE tool (.mcp.json → qmd mcp): questions about docs, notes, ADRs and plans answer from the documents, with fewer tokens");
|
|
68
|
+
}
|
|
69
|
+
if (!present || qmdState === "added") writeFileSync(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
70
|
+
return { wired: true, added: !present, qmd: qmdState };
|
|
44
71
|
}
|
|
@@ -61,7 +61,8 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
61
61
|
|
|
62
62
|
- The project RAG answers before you assume: \`kj rag query\` — never guess
|
|
63
63
|
what the codebase does. In MCP hosts the same index is the native
|
|
64
|
-
\`kj_rag_query\` tool: reach for it before grepping by hand.
|
|
64
|
+
\`kj_rag_query\` tool: reach for it before grepping by hand. Questions about
|
|
65
|
+
the project's docs, notes, ADRs or plans: ask qmd (its MCP) first.
|
|
65
66
|
- ${trackingLine(stateBackend, boardName)}
|
|
66
67
|
- Tests prove behavior: the failing test exists first (TDD), and the suite
|
|
67
68
|
is never left red. Green is not proof: with \`method_gates.mutation\`
|
|
@@ -103,6 +104,8 @@ Hit a kj bug or friction? Diagnose it and file it upstream with
|
|
|
103
104
|
\`kj report-issue\` (sanitized; ask your user before \`--publish\`).
|
|
104
105
|
kj announces a newer version? Tell your user what it brings and ask —
|
|
105
106
|
never run \`kj update\` on your own.
|
|
107
|
+
Something only your user can do (publish, seal, run a script)? Register it:
|
|
108
|
+
\`kj handoff add <command> --why "<what for>"\`; they follow \`kj handoff watch\`.
|
|
106
109
|
`;
|
|
107
110
|
|
|
108
111
|
export function renderPlaybook({ stateBackend = "hu-board", boardName = null, config = null } = {}) {
|
package/src/harden/handoff.js
CHANGED
|
@@ -30,19 +30,69 @@ export function findDenial({ projectDir, command }) {
|
|
|
30
30
|
* exact bytes there is nothing to hand off: the agent does not write the why.
|
|
31
31
|
* @returns {{id: string, command: string, sha256: string, why: string}}
|
|
32
32
|
*/
|
|
33
|
-
export function addHandoff({ projectDir, command, cwd = projectDir }) {
|
|
33
|
+
export function addHandoff({ projectDir, command, cwd = projectDir, why = null }) {
|
|
34
34
|
if (typeof command !== "string" || !command.trim()) throw new Error("kj handoff add: falta el comando exacto que el gate negó");
|
|
35
35
|
const denial = findDenial({ projectDir, command });
|
|
36
|
-
|
|
36
|
+
// KJC-TSK-1066: an action only the human can take (publish, seal) is a TASK, with
|
|
37
|
+
// the agent's why marked as the agent's. A command a gate denied keeps the gate's.
|
|
38
|
+
const task = !denial && typeof why === "string" && why.trim();
|
|
39
|
+
if (!denial && !task) throw new Error("kj handoff add: kj no tiene registrado ningún deny para ese comando exacto; el porqué de un encargo lo escribe el gate, no el agente (ADR 0018). Para una acción que solo puede hacer el humano, di para qué con --why");
|
|
37
40
|
const createdAt = new Date().toISOString();
|
|
38
41
|
const digest = sha256(command);
|
|
39
42
|
const id = sha256(`${digest}:${createdAt}`).slice(0, 8);
|
|
40
|
-
const
|
|
43
|
+
const base = { id, command, sha256: digest, cwd, createdAt, status: "pending" };
|
|
44
|
+
const record = task ? { ...base, kind: "task", why: why.trim(), denial: null } : { ...base, kind: "denial", denial: { at: denial.at, tool: denial.tool, why: denial.why } };
|
|
45
|
+
writeRecord(projectDir, record);
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const writeRecord = (projectDir, record) => {
|
|
41
50
|
mkdirSync(join(projectDir, HANDOFFS_DIR), { recursive: true });
|
|
42
|
-
writeFileSync(join(projectDir, HANDOFFS_DIR, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
51
|
+
writeFileSync(join(projectDir, HANDOFFS_DIR, `${record.id}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** KJC-TSK-1066: the human did it; it leaves the pending ones and stays in the history. */
|
|
55
|
+
export function markHandoffDone({ projectDir, id }) {
|
|
56
|
+
const record = { ...readHandoff({ projectDir, id }), status: "done", doneAt: new Date().toISOString() };
|
|
57
|
+
writeRecord(projectDir, record);
|
|
43
58
|
return record;
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
/** The handoff as the watch window shows it: local date and time, the command, and why. */
|
|
62
|
+
export function formatHandoff(h) {
|
|
63
|
+
const at = new Date(h.createdAt);
|
|
64
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
65
|
+
const when = `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
66
|
+
const why = h.kind === "task" ? `${h.why} (propuesto por el agente)` : `lo negó el gate: ${String(h.denial?.why ?? "").split("\n")[0]}`;
|
|
67
|
+
return `[${when}] ${h.id} ${h.command}\n ${why}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* KJC-TSK-1066: kj handoff watch. Shows the pending handoffs, then each new or
|
|
72
|
+
* changed one as it lands, until the signal aborts (Ctrl+C in the CLI).
|
|
73
|
+
*/
|
|
74
|
+
export async function watchHandoffs({ projectDir, log, intervalMs = 2000, signal }) {
|
|
75
|
+
const seen = new Map();
|
|
76
|
+
const tick = () => {
|
|
77
|
+
for (const h of listHandoffs({ projectDir })) {
|
|
78
|
+
if (seen.get(h.id) === h.status) continue;
|
|
79
|
+
const first = !seen.has(h.id);
|
|
80
|
+
seen.set(h.id, h.status);
|
|
81
|
+
if (h.status === "pending") log(formatHandoff(h));
|
|
82
|
+
else if (!first) log(`[hecho] ${h.id} ${h.command}`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
tick();
|
|
86
|
+
// One abort promise for the whole watch, raced against each pause: no listener per tick.
|
|
87
|
+
const aborted = new Promise((resolve) => signal?.addEventListener("abort", resolve, { once: true }));
|
|
88
|
+
while (!signal?.aborted) {
|
|
89
|
+
let timer;
|
|
90
|
+
await Promise.race([aborted, new Promise((resolve) => { timer = setTimeout(resolve, intervalMs); })]);
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
if (!signal?.aborted) tick();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
46
96
|
export function readHandoff({ projectDir, id }) {
|
|
47
97
|
if (!/^[0-9a-f]{8}$/.test(id ?? "")) throw new Error(`kj handoff: "${id}" no es un identificador de encargo (8 hex)`);
|
|
48
98
|
const path = join(projectDir, HANDOFFS_DIR, `${id}.json`);
|
|
@@ -216,7 +216,11 @@ process.stdin.on("end", () => {
|
|
|
216
216
|
// KJC-BUG-0293: the host may hand the answer as {content:[parts]}, as the
|
|
217
217
|
// bare array of parts, or as the text itself; all three are the same answer.
|
|
218
218
|
const parts = Array.isArray(response?.content) ? response.content : Array.isArray(response) ? response : [{ text: typeof response === "string" ? response : "" }];
|
|
219
|
-
|
|
219
|
+
let found = parts.flatMap((p) => sourcesOf(parseJson(String(p?.text || ""))));
|
|
220
|
+
// KJC-BUG-0365 (#2208): an answer cut short (whole code chunks) is no
|
|
221
|
+
// longer valid JSON, but every "source" it carries still names a hit.
|
|
222
|
+
if (found.length === 0) found = [...text.matchAll(/\\\\?"source\\\\?"\\s*:\\s*\\\\?"([^"\\\\]+)\\\\?"/g)].map((m) => m[1]);
|
|
223
|
+
recordRag(input.text, found, parts.map((p) => String(p?.text || "")).join(String.fromCharCode(10)));
|
|
220
224
|
process.exit(0);
|
|
221
225
|
}
|
|
222
226
|
if (/__update_card$/.test(String(tool)) && /^mcp__/.test(String(tool))) {
|
|
@@ -812,7 +816,9 @@ process.stdin.on("end", () => {
|
|
|
812
816
|
// it need no verifying when nothing after it writes. One mutating segment
|
|
813
817
|
// and the whole command is judged below, as before.
|
|
814
818
|
const CD_LITERAL = /^[ \\t]*(cd|pushd)([ \\t]+[^ \\t;|&<>$\`(){}\\n\\r]*)?[ \\t]*$/;
|
|
815
|
-
|
|
819
|
+
// KJC-BUG-0365: 2>&1 and 2>/dev/null only fold stderr, they write nothing.
|
|
820
|
+
const QUIET = /[ \\t]2>(?:&1|\\/dev\\/null)(?=[ \\t]|$)/g;
|
|
821
|
+
const readsOnly = (s) => s.split(/&&|\\|\\||[;|]/).every((seg) => READONLY.test(seg.replace(QUIET, "")) || CD_LITERAL.test(seg.replace(QUIET, "")));
|
|
816
822
|
// KJC-BUG-0243: operators inside inert quotes (grep -e "a|b") do not chain anything.
|
|
817
823
|
if (!readsOnly(stripInertQuotes(cmd))) {
|
|
818
824
|
// cd/pushd invalida TODO razonamiento textual de rutas posteriores
|
package/src/prompts/coder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildRtkInstructions } from "./rtk-snippet.js";
|
|
2
|
-
import {
|
|
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
5
|
|
|
@@ -215,10 +215,10 @@ export async function buildCoderPromptLayout({ task, reviewerFeedback = null, so
|
|
|
215
215
|
|
|
216
216
|
const stackRule = buildStackSection({ stack, testFramework });
|
|
217
217
|
const rtkSnippet = buildRtkInstructions({ rtkAvailable });
|
|
218
|
+
// KJC-TSK-1065: the project's skills and the catalog's for this task, up to 3.
|
|
218
219
|
let skillSection = null;
|
|
219
220
|
if (projectDir) {
|
|
220
|
-
|
|
221
|
-
skillSection = buildSkillSection(skills, { provider });
|
|
221
|
+
skillSection = buildRoleSkillSection(await roleSkills({ role: "coder", task, projectDir }), { provider }) || null;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// Volatile sections keep the legacy relative order — order matters:
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildRtkInstructions } from "./rtk-snippet.js";
|
|
2
|
-
import {
|
|
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
5
|
import { clipDiff } from "./diff-clip.js";
|
|
@@ -42,10 +42,10 @@ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode,
|
|
|
42
42
|
|
|
43
43
|
const langInstruction = getLanguageInstruction(language);
|
|
44
44
|
const rtkSnippet = buildRtkInstructions({ rtkAvailable });
|
|
45
|
+
// KJC-TSK-1065: the reviewer gets the skills of its role and of the task, up to 3.
|
|
45
46
|
let skillSection = null;
|
|
46
47
|
if (projectDir) {
|
|
47
|
-
|
|
48
|
-
skillSection = buildSkillSection(skills, { provider, role: "reviewer" });
|
|
48
|
+
skillSection = buildRoleSkillSection(await roleSkills({ role: "reviewer", task, projectDir }), { provider }) || null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
return buildPromptLayout([
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1065: the skills kj picks for a role and a task reach the prompt.
|
|
3
|
+
* Before, the addyosmani catalog was resolved and only written to the journal.
|
|
4
|
+
* Order: the project's own skills (.agent/skills and friends), then the local
|
|
5
|
+
* catalog by TASK (a UI task gets frontend-ui-engineering) and by ROLE, up to
|
|
6
|
+
* MAX_SKILLS; the rest are named, never silently lost. The maintainer's
|
|
7
|
+
* choices (2026-10-17): local catalog + project skills, 3 per role.
|
|
8
|
+
*/
|
|
9
|
+
import { loadAvailableSkills, filterSkillsForRole } from "./skill-loader.js";
|
|
10
|
+
import { listAvailableSlugs, loadSkillsBySlugs } from "./addyosmani-catalog.js";
|
|
11
|
+
import { resolveAddyosmaniSlugs } from "./addyosmani-role-map.js";
|
|
12
|
+
|
|
13
|
+
export const MAX_SKILLS = 3;
|
|
14
|
+
|
|
15
|
+
const defaultDeps = {
|
|
16
|
+
loadLocal: async (projectDir, role) => {
|
|
17
|
+
const all = projectDir ? await loadAvailableSkills(projectDir) : [];
|
|
18
|
+
return role === "coder" ? all : filterSkillsForRole(all, role);
|
|
19
|
+
},
|
|
20
|
+
listCatalog: () => listAvailableSlugs(),
|
|
21
|
+
loadCatalog: (slugs) => loadSkillsBySlugs(slugs),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** @returns {Promise<{skills: Array<{name: string, content: string, path?: string, catalog?: true}>, dropped: string[]}>} */
|
|
25
|
+
export async function roleSkills({ role, task, projectDir, max = MAX_SKILLS, deps = defaultDeps }) {
|
|
26
|
+
const local = await deps.loadLocal(projectDir, role);
|
|
27
|
+
const available = new Set(await deps.listCatalog());
|
|
28
|
+
const ordered = [...new Set([...resolveAddyosmaniSlugs({ task }), ...resolveAddyosmaniSlugs({ role })])].filter((s) => available.has(s));
|
|
29
|
+
const room = Math.max(0, max - local.length);
|
|
30
|
+
const catalog = (await deps.loadCatalog(ordered.slice(0, room))).map((s) => ({ name: s.name, content: s.content, path: s.path, catalog: true }));
|
|
31
|
+
const dropped = [...local.slice(max).map((s) => s.name), ...ordered.slice(room)];
|
|
32
|
+
return { skills: [...local.slice(0, max), ...catalog], dropped };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The prompt section. For claude, the project's skills are native to its
|
|
37
|
+
* runtime, but a catalog skill is not: it is named with the path to read.
|
|
38
|
+
* Other providers get the content inline.
|
|
39
|
+
*/
|
|
40
|
+
export function buildRoleSkillSection({ skills, dropped }, { provider } = {}) {
|
|
41
|
+
if (skills.length === 0) return "";
|
|
42
|
+
const lines = ["## Domain Skills", ""];
|
|
43
|
+
if (["claude", "anthropic"].includes(String(provider).toLowerCase())) {
|
|
44
|
+
const native = skills.filter((s) => !s.catalog).map((s) => s.name);
|
|
45
|
+
if (native.length) lines.push(`Skills available natively via your Agent Skills capability: ${native.join(", ")}.`);
|
|
46
|
+
const toRead = skills.filter((s) => s.catalog);
|
|
47
|
+
if (toRead.length) lines.push("Read these skills before you start, and follow them:", ...toRead.map((s) => `- ${s.name}: read ${s.path}`));
|
|
48
|
+
} else {
|
|
49
|
+
lines.push("The following domain-specific knowledge is available for this task:", ...skills.map((s) => `### ${s.name}\n${s.content}`));
|
|
50
|
+
}
|
|
51
|
+
if (dropped.length) lines.push("", `Left out (limit ${MAX_SKILLS}): ${dropped.join(", ")}`);
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|