karajan-code 2.19.0 → 2.19.2
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 +4 -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/sonar/api.js +17 -3
- package/src/sonar/token-recovery.js +67 -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,67 @@
|
|
|
1
|
+
// Sonar 401 auto-recovery — KJC-BUG-0057 / v2.19.2.
|
|
2
|
+
//
|
|
3
|
+
// When the Sonar API returns 401 (token missing / invalid / expired / revoked)
|
|
4
|
+
// and we still have admin/admin (or a user/pass we saved at `kj init` time),
|
|
5
|
+
// regenerate the analysis token VIA THE REST API and persist it. The caller
|
|
6
|
+
// retries the original request once with the new token.
|
|
7
|
+
//
|
|
8
|
+
// Programmatic, no LLM involvement. The whole point of the user feedback was
|
|
9
|
+
// that this is plumbing, not a decision: Karajan has the credentials to
|
|
10
|
+
// re-bootstrap and should do it without asking.
|
|
11
|
+
|
|
12
|
+
import { bootstrapSonarToken } from "./token-bootstrap.js";
|
|
13
|
+
import { saveSonarToken } from "./credentials.js";
|
|
14
|
+
|
|
15
|
+
// Per-process latch: a single Sonar run that 401s on every endpoint must
|
|
16
|
+
// only trigger ONE bootstrap attempt — not one per endpoint. The latch
|
|
17
|
+
// resets when the process exits.
|
|
18
|
+
let _recoveryAttempted = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @internal Reset the latch between tests (mirrors __resetKjHomeWarningForTests).
|
|
22
|
+
*/
|
|
23
|
+
export function __resetSonarRecoveryLatchForTests() {
|
|
24
|
+
_recoveryAttempted = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Attempt to re-bootstrap the Sonar token after a 401. Mutates `config`
|
|
29
|
+
* in place when successful so the immediate retry picks up the new token.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} config Karajan config (will be mutated on success).
|
|
32
|
+
* @param {object} [logger]
|
|
33
|
+
* @returns {Promise<{ ok: boolean, reason?: string, newToken?: string, savedTo?: string, passwordRotated?: boolean }>}
|
|
34
|
+
*/
|
|
35
|
+
export async function recoverSonarToken(config, logger = null) {
|
|
36
|
+
if (_recoveryAttempted) {
|
|
37
|
+
return { ok: false, reason: "recovery already attempted in this process" };
|
|
38
|
+
}
|
|
39
|
+
_recoveryAttempted = true;
|
|
40
|
+
|
|
41
|
+
const host = config?.sonarqube?.host || "http://localhost:9000";
|
|
42
|
+
logger?.warn?.(`[sonar-recovery] 401 from ${host} — attempting token re-bootstrap`);
|
|
43
|
+
|
|
44
|
+
const result = await bootstrapSonarToken({ host });
|
|
45
|
+
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
logger?.warn?.(`[sonar-recovery] bootstrap failed: ${result.reason}`);
|
|
48
|
+
return { ok: false, reason: result.reason };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mutate the in-memory config so the caller's retry sees the new token.
|
|
52
|
+
config.sonarqube = config.sonarqube || {};
|
|
53
|
+
config.sonarqube.token = result.token;
|
|
54
|
+
|
|
55
|
+
// Also persist to ~/.karajan/sonar-credentials.json so future processes
|
|
56
|
+
// pick it up via the normal config-resolver chain instead of triggering
|
|
57
|
+
// recovery again. token-bootstrap.js already wrote sonar.token; this
|
|
58
|
+
// mirrors it into the structured credentials file the resolver reads.
|
|
59
|
+
try {
|
|
60
|
+
await saveSonarToken(result.token);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger?.warn?.(`[sonar-recovery] persist to sonar-credentials.json failed (non-blocking): ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger?.info?.(`[sonar-recovery] new token persisted to ${result.savedTo}${result.passwordRotated ? " (admin password rotated)" : ""} — retrying original request`);
|
|
66
|
+
return { ok: true, newToken: result.token, savedTo: result.savedTo, passwordRotated: result.passwordRotated };
|
|
67
|
+
}
|
|
@@ -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.
|