karajan-code 2.19.0 → 2.19.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/README.md +2 -0
- package/package.json +9 -1
- package/packages/hu-board/README.md +69 -0
- package/packages/hu-board/package.json +23 -0
- package/packages/hu-board/public/app.js +4038 -0
- package/packages/hu-board/public/index.html +55 -0
- package/packages/hu-board/public/pipeline.css +132 -0
- package/packages/hu-board/public/pipeline.html +48 -0
- package/packages/hu-board/public/pipeline.js +200 -0
- package/packages/hu-board/public/styles.css +918 -0
- package/packages/hu-board/src/auth.js +92 -0
- package/packages/hu-board/src/boot-info.js +16 -0
- package/packages/hu-board/src/cleanup-zombies.js +142 -0
- package/packages/hu-board/src/command-runner.js +134 -0
- package/packages/hu-board/src/config-yaml.js +285 -0
- package/packages/hu-board/src/db.js +739 -0
- package/packages/hu-board/src/ephemeral-cleaner.js +198 -0
- package/packages/hu-board/src/event-bus.js +58 -0
- package/packages/hu-board/src/format.js +83 -0
- package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
- package/packages/hu-board/src/journal-parser.js +180 -0
- package/packages/hu-board/src/plan-mutations.js +422 -0
- package/packages/hu-board/src/preflight.js +285 -0
- package/packages/hu-board/src/routes/api.js +1271 -0
- package/packages/hu-board/src/routes/pipeline.js +45 -0
- package/packages/hu-board/src/run-tracker.js +121 -0
- package/packages/hu-board/src/server.js +435 -0
- package/packages/hu-board/src/sync.js +626 -0
- package/packages/hu-board/src/token-store.js +57 -0
- package/packages/hu-board/src/zombie-reaper.js +207 -0
- package/src/audit/webperf-input.js +4 -7
- package/src/checks/dir-setup.js +5 -3
- package/src/cli/register-pipeline.js +1 -0
- package/src/cli/register-plan.js +1 -0
- package/src/commands/plan/generate.js +15 -0
- package/src/commands/run.js +19 -1
- package/src/commands/webperf.js +4 -6
- package/src/config/role-resolver.js +3 -1
- package/src/mcp/handlers/direct-handlers.js +3 -0
- package/src/mcp/handlers/run-handler.js +5 -0
- package/src/mcp/tools.js +4 -2
- package/src/prompts/spec-reviewer.js +66 -0
- package/src/roles/perf-role.js +4 -4
- package/src/roles/spec-reviewer-role.js +106 -0
- package/src/spec-review/refine-loop.js +58 -0
- package/src/spec-review/run-spec-review.js +70 -0
- package/src/utils/board-prompt-bridge.js +4 -5
- package/src/utils/display/spec-findings.js +44 -0
- package/src/utils/paths.js +15 -0
- package/src/utils/run-registry.js +4 -4
- package/templates/roles/spec-reviewer.md +38 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Refine loop for the spec-reviewer (KJC-PCS-0048 PR 3). Called from
|
|
2
|
+
// run-spec-review.js when the user picks [r]efine. Asks the role for a
|
|
3
|
+
// rewritten v2, persists v1.md / v2.md, opens $EDITOR for the user to
|
|
4
|
+
// tweak, uses a SHA-256 hash diff to flag whether the v2 needs a fresh
|
|
5
|
+
// review pass (user modified it) or can proceed as-is.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const sha256 = (s) => crypto.createHash("sha256").update(String(s || ""), "utf8").digest("hex");
|
|
13
|
+
|
|
14
|
+
function openEditor(filePath, { logger, openEditorFn = null } = {}) {
|
|
15
|
+
if (openEditorFn) return openEditorFn(filePath);
|
|
16
|
+
const raw = process.env.VISUAL || process.env.EDITOR || "vi"; // same precedence as src/commands/config.js
|
|
17
|
+
const parts = raw.split(/\s+/);
|
|
18
|
+
logger?.info?.(`Opening ${parts[0]} on ${filePath}`);
|
|
19
|
+
return spawnSync(parts[0], [...parts.slice(1), filePath], { stdio: "inherit" }).status === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Single refine iteration. The outer loop in run-spec-review.js decides
|
|
24
|
+
* whether to re-invoke us based on the returned `hashChanged` flag
|
|
25
|
+
* (re-review the v2 because the user modified it) or accept the v2 as
|
|
26
|
+
* the final spec (`hashChanged === false`).
|
|
27
|
+
*
|
|
28
|
+
* @returns {Promise<{ok, refinedSpec?, hashChanged, error?, v2Path?}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function runRefinePass({ spec, findings, role, projectDir, taskFile, logger, fs: fsOverride, openEditorFn }) {
|
|
31
|
+
const _fs = fsOverride || fs;
|
|
32
|
+
const refineRes = await role.refineSpec({ spec, findings });
|
|
33
|
+
if (!refineRes.ok) return { ok: false, error: refineRes.error || "refine failed", hashChanged: false };
|
|
34
|
+
|
|
35
|
+
let v2Path;
|
|
36
|
+
try {
|
|
37
|
+
const root = projectDir || process.cwd();
|
|
38
|
+
const ts = new Date().toISOString().replaceAll(/[:.]/g, "-");
|
|
39
|
+
const dir = path.join(root, ".reviews", `spec-review-${ts}`);
|
|
40
|
+
await _fs.mkdir(dir, { recursive: true });
|
|
41
|
+
await _fs.writeFile(path.join(dir, "spec-v1.md"), spec, "utf8");
|
|
42
|
+
v2Path = path.join(dir, "spec-v2.md");
|
|
43
|
+
await _fs.writeFile(v2Path, refineRes.refinedSpec, "utf8");
|
|
44
|
+
if (taskFile) {
|
|
45
|
+
// Mirror the v2 next to the user's --task-file so they find it
|
|
46
|
+
// without diving into .reviews/.
|
|
47
|
+
const p = path.parse(taskFile);
|
|
48
|
+
await _fs.writeFile(path.join(p.dir, `${p.name}.v2${p.ext || ".md"}`), refineRes.refinedSpec, "utf8").catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
} catch (err) { return { ok: false, error: `persist failed: ${err.message}`, hashChanged: false }; }
|
|
51
|
+
|
|
52
|
+
const before = sha256(refineRes.refinedSpec);
|
|
53
|
+
openEditor(v2Path, { logger, openEditorFn });
|
|
54
|
+
let after;
|
|
55
|
+
try { after = await _fs.readFile(v2Path, "utf8"); }
|
|
56
|
+
catch (err) { return { ok: false, error: `read v2 failed: ${err.message}`, hashChanged: false }; }
|
|
57
|
+
return { ok: true, refinedSpec: after, hashChanged: before !== sha256(after), v2Path };
|
|
58
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Pre-pipeline orchestrator for the spec-reviewer (KJC-PCS-0048 PR 2).
|
|
2
|
+
// Called from src/commands/run.js and src/commands/plan/generate.js
|
|
3
|
+
// BEFORE the actual pipeline. Decides whether to proceed based on the
|
|
4
|
+
// role's findings + the user's answer to the interactive prompt.
|
|
5
|
+
// PR 2 only supports [c]ontinue / [x]cancel; refine loop lands in PR 3.
|
|
6
|
+
|
|
7
|
+
import { SpecReviewerRole } from "../roles/spec-reviewer-role.js";
|
|
8
|
+
import { printFindings } from "../utils/display/spec-findings.js";
|
|
9
|
+
import { runRefinePass } from "./refine-loop.js";
|
|
10
|
+
|
|
11
|
+
const MAX_REFINE_ITERATIONS = 5;
|
|
12
|
+
|
|
13
|
+
export async function runSpecReview({ spec, config, logger, askQuestion, flags = {} }) {
|
|
14
|
+
// VITEST guard — pre-existing test suites (e.g. tests/commands/command-run.test.js)
|
|
15
|
+
// mock `runFlow` + `assertAgentsAvailable` but predate this role and have
|
|
16
|
+
// no need to exercise it. Auto-skip in vitest unless the test passes
|
|
17
|
+
// `flags.forceSpecReview: true` (the spec-review's own tests bypass via
|
|
18
|
+
// their vi.doMock of SpecReviewerRole and never reach this guard).
|
|
19
|
+
if (process.env.VITEST && !flags.forceSpecReview) return { proceed: true, skipped: true };
|
|
20
|
+
if (flags.skipSpecReview || config?.spec_reviewer?.enabled === false) {
|
|
21
|
+
return { proceed: true, skipped: true };
|
|
22
|
+
}
|
|
23
|
+
if (!spec || !String(spec).trim()) return { proceed: true, skipped: true };
|
|
24
|
+
|
|
25
|
+
const role = new SpecReviewerRole({ config, logger });
|
|
26
|
+
await role.init({ task: spec });
|
|
27
|
+
|
|
28
|
+
// Outer loop: review → prompt → optional refine → re-review (the
|
|
29
|
+
// refine path replaces `currentSpec` and we go around again).
|
|
30
|
+
let currentSpec = spec;
|
|
31
|
+
for (let iter = 0; iter < MAX_REFINE_ITERATIONS; iter += 1) {
|
|
32
|
+
const res = await role.execute({ spec: currentSpec });
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
logger?.warn?.(`[spec] reviewer failed: ${res.summary} — continuing without review`);
|
|
35
|
+
return { proceed: true, error: res.result?.error, finalSpec: currentSpec };
|
|
36
|
+
}
|
|
37
|
+
const { severity, findings } = res.result || {};
|
|
38
|
+
if (!findings || findings.length === 0 || severity === "ok") {
|
|
39
|
+
logger?.info?.(iter === 0 ? "✓ spec OK" : "✓ refined spec OK");
|
|
40
|
+
return { proceed: true, severity: "ok", findings: [], finalSpec: currentSpec, refined: iter > 0 };
|
|
41
|
+
}
|
|
42
|
+
printFindings(findings, severity);
|
|
43
|
+
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
44
|
+
|
|
45
|
+
const answer = await askQuestion(
|
|
46
|
+
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: continue)`,
|
|
47
|
+
{ detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 } },
|
|
48
|
+
);
|
|
49
|
+
if (answer === null) return { proceed: false, cancelled: true, severity, findings };
|
|
50
|
+
const n = String(answer).trim().toLowerCase();
|
|
51
|
+
if (n.startsWith("x") || n === "cancel") return { proceed: false, cancelled: true, severity, findings };
|
|
52
|
+
if (n.startsWith("r") || n === "refine") {
|
|
53
|
+
const refine = await runRefinePass({
|
|
54
|
+
spec: currentSpec, findings, role,
|
|
55
|
+
projectDir: config?.projectDir, taskFile: flags.taskFile, logger,
|
|
56
|
+
});
|
|
57
|
+
if (!refine.ok) {
|
|
58
|
+
logger?.warn?.(`[spec] refine failed: ${refine.error} — proceeding with original spec`);
|
|
59
|
+
return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
60
|
+
}
|
|
61
|
+
currentSpec = refine.refinedSpec;
|
|
62
|
+
if (refine.hashChanged) continue; // user tweaked v2 → re-review
|
|
63
|
+
logger?.info?.("✓ refined spec accepted by user (no edits)");
|
|
64
|
+
return { proceed: true, severity: "ok", findings: [], finalSpec: currentSpec, refined: true };
|
|
65
|
+
}
|
|
66
|
+
return { proceed: true, severity, findings, finalSpec: currentSpec }; // continue
|
|
67
|
+
}
|
|
68
|
+
logger?.warn?.(`[spec] max refine iterations (${MAX_REFINE_ITERATIONS}) reached — proceeding with last spec`);
|
|
69
|
+
return { proceed: true, finalSpec: currentSpec, refined: true };
|
|
70
|
+
}
|
|
@@ -28,16 +28,15 @@
|
|
|
28
28
|
|
|
29
29
|
import { mkdir, writeFile, readFile, unlink, stat } from "node:fs/promises";
|
|
30
30
|
import { join } from "node:path";
|
|
31
|
-
import { homedir } from "node:os";
|
|
32
31
|
import { randomUUID } from "node:crypto";
|
|
33
32
|
|
|
34
33
|
const POLL_INTERVAL_MS = 500;
|
|
35
34
|
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000; // 30 min
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
36
|
+
// Delegated to src/utils/paths.js so KARAJAN_HOME, KJ_HOME (with
|
|
37
|
+
// deprecation warning), VITEST tmp and the default all share one
|
|
38
|
+
// precedence chain — KJC-TSK-0420.
|
|
39
|
+
import { getPromptsDir } from "./paths.js";
|
|
41
40
|
|
|
42
41
|
function exists(path) {
|
|
43
42
|
return stat(path).then(() => true, () => false);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Pretty-printer for SpecReviewerRole findings (KJC-PCS-0048 PR 2).
|
|
2
|
+
// Grouped by category, colours by severity, every finding gets its
|
|
3
|
+
// `suggestion` printed below as the actionable fix-it line.
|
|
4
|
+
|
|
5
|
+
const COLOURS = {
|
|
6
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
7
|
+
cyan: "\x1b[36m", yellow: "\x1b[33m", red: "\x1b[31m", gray: "\x1b[90m",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SEV_COLOUR = { info: COLOURS.cyan, warn: COLOURS.yellow, fail: COLOURS.red };
|
|
11
|
+
const SEV_LABEL = { info: "INFO", warn: "WARN", fail: "FAIL" };
|
|
12
|
+
|
|
13
|
+
function colour(severity, text) {
|
|
14
|
+
const c = SEV_COLOUR[severity] || COLOURS.gray;
|
|
15
|
+
return `${c}${text}${COLOURS.reset}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {Array<{id,severity,category,message,suggestion?}>} findings
|
|
20
|
+
* @param {"ok"|"info"|"warn"|"fail"} severity
|
|
21
|
+
* @param {object} [opts]
|
|
22
|
+
* @param {(line:string)=>void} [opts.write] Defaults to `console.warn` (stderr).
|
|
23
|
+
*/
|
|
24
|
+
export function printFindings(findings, severity, opts = {}) {
|
|
25
|
+
const write = opts.write || ((line) => process.stderr.write(`${line}\n`));
|
|
26
|
+
if (!Array.isArray(findings) || findings.length === 0) return;
|
|
27
|
+
|
|
28
|
+
write("");
|
|
29
|
+
write(`${COLOURS.bold}Spec review — ${colour(severity, SEV_LABEL[severity] || severity.toUpperCase())}${COLOURS.reset} (${findings.length} finding${findings.length === 1 ? "" : "s"})`);
|
|
30
|
+
|
|
31
|
+
const byCat = new Map();
|
|
32
|
+
for (const f of findings) {
|
|
33
|
+
if (!byCat.has(f.category)) byCat.set(f.category, []);
|
|
34
|
+
byCat.get(f.category).push(f);
|
|
35
|
+
}
|
|
36
|
+
for (const [cat, items] of byCat) {
|
|
37
|
+
write(`${COLOURS.dim}── ${cat}${COLOURS.reset}`);
|
|
38
|
+
for (const f of items) {
|
|
39
|
+
write(` ${colour(f.severity, "●")} ${COLOURS.bold}${f.id}${COLOURS.reset} ${f.message}`);
|
|
40
|
+
if (f.suggestion) write(` ${COLOURS.dim}→${COLOURS.reset} ${f.suggestion}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
write("");
|
|
44
|
+
}
|
package/src/utils/paths.js
CHANGED
|
@@ -89,3 +89,18 @@ export function getSessionRoot() {
|
|
|
89
89
|
export function getSonarComposePath() {
|
|
90
90
|
return path.join(getKarajanHome(), "docker-compose.sonar.yml");
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
/** Webperf cache: `<karajan-home>/webperf/` — KJC-TSK-0420. */
|
|
94
|
+
export function getWebperfDir() {
|
|
95
|
+
return path.join(getKarajanHome(), "webperf");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Run-registry: `<karajan-home>/runs/` — KJC-TSK-0420. */
|
|
99
|
+
export function getRunsDir() {
|
|
100
|
+
return path.join(getKarajanHome(), "runs");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Board prompt bridge: `<karajan-home>/prompts/` — KJC-TSK-0420. */
|
|
104
|
+
export function getPromptsDir() {
|
|
105
|
+
return path.join(getKarajanHome(), "prompts");
|
|
106
|
+
}
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import os from "node:os";
|
|
23
22
|
import { writeJsonAtomicSync } from "./atomic-write.js";
|
|
23
|
+
import { getRunsDir } from "./paths.js";
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Directorio raíz donde viven los registros de run.
|
|
27
|
-
*
|
|
27
|
+
* Delega en `getRunsDir()` (KJC-TSK-0420) — respeta KARAJAN_HOME,
|
|
28
|
+
* KJ_HOME (con warning de deprecación), VITEST tmp y default.
|
|
28
29
|
*/
|
|
29
30
|
export function runsDir() {
|
|
30
|
-
|
|
31
|
-
return path.join(home, "runs");
|
|
31
|
+
return getRunsDir();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
You are the **Spec Reviewer**. You run BEFORE every other role. You audit the
|
|
2
|
+
user's spec (prompt or `.md`) for deficiencies. You do NOT execute or plan.
|
|
3
|
+
|
|
4
|
+
## Categories (every finding gets exactly one)
|
|
5
|
+
|
|
6
|
+
- **ambiguity** — words without a concrete metric ("better", "clean", "fast").
|
|
7
|
+
- **missing_scope** — no boundary of what should and should not be touched.
|
|
8
|
+
- **missing_ac** — no acceptance criteria; nothing to verify against.
|
|
9
|
+
- **contradiction** — two requirements that cannot both be satisfied.
|
|
10
|
+
- **stack** — no tech stack hint when one is required.
|
|
11
|
+
- **assumptions** — depends on unstated context.
|
|
12
|
+
- **out_of_scope** — asks for things outside what Karajan can do.
|
|
13
|
+
|
|
14
|
+
## Severity
|
|
15
|
+
|
|
16
|
+
`info` (nuance), `warn` (real gap), `fail` (unworkable). Top-level `severity`
|
|
17
|
+
is the WORST of any finding, or `ok` if findings is empty.
|
|
18
|
+
|
|
19
|
+
## Output (JSON, nothing else)
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"severity": "ok|info|warn|fail",
|
|
24
|
+
"findings": [
|
|
25
|
+
{
|
|
26
|
+
"id": "F-001",
|
|
27
|
+
"severity": "info|warn|fail",
|
|
28
|
+
"category": "ambiguity|missing_scope|missing_ac|contradiction|stack|assumptions|out_of_scope",
|
|
29
|
+
"message": "quote the offending phrase verbatim when possible",
|
|
30
|
+
"suggestion": "a sentence the user can paste into the spec"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"reasoning": "one or two sentences summarising the verdict"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Direct, accionable. No moralising. `suggestion` must be concrete (a paste-able
|
|
38
|
+
sentence), not vague advice.
|