karajan-code 2.7.3 → 2.8.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/README.md +4 -4
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -609
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2019
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +30 -3
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +8 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj audit --agent-readiness — score a repo for AI-agent readability.
|
|
3
|
+
*
|
|
4
|
+
* Static, deterministic, NO LLM call. Inspired by addyosmani/agentic-seo.
|
|
5
|
+
* Catches the failure modes that make repos invisible to Cursor /
|
|
6
|
+
* Cline / Aider / Claude Code:
|
|
7
|
+
* - no llms.txt → agents have to crawl the README
|
|
8
|
+
* - oversized doc pages → agents truncate or drop them
|
|
9
|
+
* - JS-only docs → agents can't render
|
|
10
|
+
* - broken heading hierarchy → agents misinfer structure
|
|
11
|
+
* - no robots allowlist → some agents skip silently
|
|
12
|
+
*
|
|
13
|
+
* Returns a 0-100 score + per-check detail + top remediation hints.
|
|
14
|
+
* Pure data transformation: filesystem reads in, analysis out.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const PAGE_BYTE_LIMIT = 32 * 1024; // ≈ 8K tokens, the typical agent budget per fetch
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Catalogue of checks. Each check has:
|
|
24
|
+
* - id — stable identifier
|
|
25
|
+
* - weight — points contributed when ok
|
|
26
|
+
* - run — (rootDir) => { ok, hint, details }
|
|
27
|
+
*/
|
|
28
|
+
const CHECKS = [
|
|
29
|
+
{
|
|
30
|
+
id: "llms-txt-present",
|
|
31
|
+
weight: 25,
|
|
32
|
+
description: "llms.txt at repo root",
|
|
33
|
+
run(rootDir) {
|
|
34
|
+
const p = path.join(rootDir, "llms.txt");
|
|
35
|
+
const ok = fs.existsSync(p) && fs.statSync(p).isFile();
|
|
36
|
+
return ok
|
|
37
|
+
? { ok: true, hint: null, details: { path: p } }
|
|
38
|
+
: { ok: false, hint: "Add llms.txt at repo root with a single-fetch index of docs + commands.", details: {} };
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "llms-txt-valid",
|
|
43
|
+
weight: 10,
|
|
44
|
+
description: "llms.txt has at least one section heading and one link",
|
|
45
|
+
run(rootDir) {
|
|
46
|
+
const p = path.join(rootDir, "llms.txt");
|
|
47
|
+
if (!fs.existsSync(p)) return { ok: false, hint: "llms.txt missing — see llms-txt-present.", details: {} };
|
|
48
|
+
const text = safeRead(p);
|
|
49
|
+
const hasSection = /^##\s+/m.test(text);
|
|
50
|
+
const hasLink = /\[[^\]]+\]\([^)]+\)/.test(text);
|
|
51
|
+
const ok = hasSection && hasLink;
|
|
52
|
+
return ok
|
|
53
|
+
? { ok: true, hint: null, details: { hasSection, hasLink } }
|
|
54
|
+
: { ok: false, hint: "Add `## Section` headings and `[label](path)` links to llms.txt.", details: { hasSection, hasLink } };
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "robots-allowlist",
|
|
59
|
+
weight: 10,
|
|
60
|
+
description: "robots.txt explicitly Allows AI crawlers",
|
|
61
|
+
run(rootDir) {
|
|
62
|
+
// Check root or public/.
|
|
63
|
+
const candidates = ["robots.txt", "public/robots.txt", "static/robots.txt"];
|
|
64
|
+
const found = candidates.find((c) => fs.existsSync(path.join(rootDir, c)));
|
|
65
|
+
if (!found) return { ok: false, hint: "Add robots.txt with `User-agent: GPTBot|ClaudeBot|PerplexityBot\\nAllow: /` blocks.", details: {} };
|
|
66
|
+
const text = safeRead(path.join(rootDir, found));
|
|
67
|
+
const aiBots = ["GPTBot", "ClaudeBot", "PerplexityBot", "anthropic-ai", "Google-Extended"];
|
|
68
|
+
const allowed = aiBots.filter((bot) => new RegExp(`User-agent:\\s*${bot}[\\s\\S]*?Allow:\\s*/`, "i").test(text));
|
|
69
|
+
const ok = allowed.length > 0;
|
|
70
|
+
return ok
|
|
71
|
+
? { ok: true, hint: null, details: { found, allowed } }
|
|
72
|
+
: { ok: false, hint: `robots.txt found at ${found} but no AI crawler explicitly Allowed. Add explicit User-agent: ClaudeBot blocks.`, details: { found, allowed } };
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "page-token-budget",
|
|
77
|
+
weight: 15,
|
|
78
|
+
description: "every .md doc fits within ≈ 8K tokens (32 KB raw)",
|
|
79
|
+
run(rootDir) {
|
|
80
|
+
const docsDir = path.join(rootDir, "docs");
|
|
81
|
+
if (!fs.existsSync(docsDir)) {
|
|
82
|
+
// No docs/ at all is its own kind of agent-unfriendly, but
|
|
83
|
+
// not strictly a budget violation. Pass with a hint.
|
|
84
|
+
return { ok: true, hint: "No docs/ directory found. Consider adding one.", details: { docsDirPresent: false } };
|
|
85
|
+
}
|
|
86
|
+
const oversized = listMarkdownFiles(docsDir).filter((f) => fs.statSync(f).size > PAGE_BYTE_LIMIT);
|
|
87
|
+
if (oversized.length === 0) {
|
|
88
|
+
return { ok: true, hint: null, details: { oversizedCount: 0 } };
|
|
89
|
+
}
|
|
90
|
+
const sample = oversized.slice(0, 3).map((f) => `${path.relative(rootDir, f)} (${(fs.statSync(f).size / 1024).toFixed(1)} KB)`);
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
hint: `Split or trim ${oversized.length} oversized doc(s): ${sample.join(", ")}.`,
|
|
94
|
+
details: { oversizedCount: oversized.length, sample },
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "heading-hierarchy",
|
|
100
|
+
weight: 10,
|
|
101
|
+
description: "each .md has exactly one H1 and no skipped levels",
|
|
102
|
+
run(rootDir) {
|
|
103
|
+
const docsDir = path.join(rootDir, "docs");
|
|
104
|
+
if (!fs.existsSync(docsDir)) return { ok: true, hint: null, details: { docsDirPresent: false } };
|
|
105
|
+
const violations = [];
|
|
106
|
+
for (const f of listMarkdownFiles(docsDir)) {
|
|
107
|
+
const text = safeRead(f);
|
|
108
|
+
const h1Count = (text.match(/^#\s+/gm) || []).length;
|
|
109
|
+
if (h1Count !== 1) {
|
|
110
|
+
violations.push({ file: path.relative(rootDir, f), h1Count });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Check for skipped levels (#### without preceding ###, etc.)
|
|
114
|
+
const headings = [...text.matchAll(/^(#{1,6})\s/gm)].map((m) => m[1].length);
|
|
115
|
+
let prev = 0;
|
|
116
|
+
for (const lvl of headings) {
|
|
117
|
+
if (prev > 0 && lvl > prev + 1) {
|
|
118
|
+
violations.push({ file: path.relative(rootDir, f), reason: `H${prev}→H${lvl} skip` });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
prev = lvl;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (violations.length === 0) {
|
|
125
|
+
return { ok: true, hint: null, details: { violationsCount: 0 } };
|
|
126
|
+
}
|
|
127
|
+
const sample = violations.slice(0, 3);
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
hint: `Fix heading hierarchy in ${violations.length} doc(s): ${sample.map((v) => v.file).join(", ")}.`,
|
|
131
|
+
details: { violationsCount: violations.length, sample },
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "agents-readme",
|
|
137
|
+
weight: 10,
|
|
138
|
+
description: "docs/agents/README.md as agent entry point",
|
|
139
|
+
run(rootDir) {
|
|
140
|
+
const p = path.join(rootDir, "docs/agents/README.md");
|
|
141
|
+
const ok = fs.existsSync(p);
|
|
142
|
+
return ok
|
|
143
|
+
? { ok: true, hint: null, details: { path: p } }
|
|
144
|
+
: { ok: false, hint: "Create docs/agents/README.md as the agent-facing index (links from llms.txt).", details: {} };
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "skill-md-coverage",
|
|
149
|
+
weight: 20,
|
|
150
|
+
description: "at least one SKILL.md per declared CLI capability",
|
|
151
|
+
run(rootDir) {
|
|
152
|
+
const skillDir = path.join(rootDir, "docs/agents");
|
|
153
|
+
if (!fs.existsSync(skillDir)) return { ok: false, hint: "Create docs/agents/ with one SKILL.<command>.md per CLI capability.", details: {} };
|
|
154
|
+
const skillFiles = fs.readdirSync(skillDir).filter((f) => /^SKILL\..+\.md$/.test(f));
|
|
155
|
+
if (skillFiles.length === 0) {
|
|
156
|
+
return { ok: false, hint: "Add SKILL.<command>.md files documenting each CLI capability.", details: { skillFilesCount: 0 } };
|
|
157
|
+
}
|
|
158
|
+
// We don't enumerate the user's own CLI here (out of scope for a
|
|
159
|
+
// generic auditor). At least 1 SKILL.md is the minimum signal.
|
|
160
|
+
return { ok: true, hint: null, details: { skillFilesCount: skillFiles.length } };
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
function safeRead(p) {
|
|
166
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return ""; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function listMarkdownFiles(dir, out = []) {
|
|
170
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
171
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
172
|
+
const p = path.join(dir, e.name);
|
|
173
|
+
if (e.isDirectory()) listMarkdownFiles(p, out);
|
|
174
|
+
else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run every agent-readiness check against `rootDir`. Returns a
|
|
181
|
+
* report with score, per-check results, and ranked remediation hints.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} rootDir — absolute path to the repo root to audit
|
|
184
|
+
* @returns {{ score: number, checks: object[], topFixes: object[] }}
|
|
185
|
+
*/
|
|
186
|
+
export function runAgentReadiness(rootDir) {
|
|
187
|
+
const checks = [];
|
|
188
|
+
let earned = 0;
|
|
189
|
+
let possible = 0;
|
|
190
|
+
for (const c of CHECKS) {
|
|
191
|
+
possible += c.weight;
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = c.run(rootDir);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
result = { ok: false, hint: `Check threw: ${err.message}`, details: { error: err.message } };
|
|
197
|
+
}
|
|
198
|
+
if (result.ok) earned += c.weight;
|
|
199
|
+
checks.push({
|
|
200
|
+
id: c.id,
|
|
201
|
+
description: c.description,
|
|
202
|
+
weight: c.weight,
|
|
203
|
+
score: result.ok ? c.weight : 0,
|
|
204
|
+
ok: result.ok,
|
|
205
|
+
hint: result.hint,
|
|
206
|
+
details: result.details || {},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const score = possible === 0 ? 0 : Math.round((earned / possible) * 100);
|
|
210
|
+
const topFixes = checks
|
|
211
|
+
.filter((c) => !c.ok && c.hint)
|
|
212
|
+
.sort((a, b) => b.weight - a.weight)
|
|
213
|
+
.slice(0, 5)
|
|
214
|
+
.map((c, i) => ({
|
|
215
|
+
priority: i + 1,
|
|
216
|
+
checkId: c.id,
|
|
217
|
+
weightLost: c.weight,
|
|
218
|
+
action: c.hint,
|
|
219
|
+
}));
|
|
220
|
+
return { score, possible, earned, checks, topFixes };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Render the report in plain text for the human terminal.
|
|
225
|
+
*/
|
|
226
|
+
export function formatAgentReadinessReport({ score, checks, topFixes }, rootDir) {
|
|
227
|
+
const lines = [`Agent Readiness — ${rootDir}: ${score}/100`, ""];
|
|
228
|
+
for (const c of checks) {
|
|
229
|
+
const tick = c.ok ? "✓" : "✗";
|
|
230
|
+
const pts = `(+${c.score})`.padStart(6);
|
|
231
|
+
lines.push(` ${tick} ${c.description.padEnd(50)} ${pts}${c.hint ? " " + c.hint : ""}`);
|
|
232
|
+
}
|
|
233
|
+
if (topFixes.length > 0) {
|
|
234
|
+
lines.push("", "Top fixes:");
|
|
235
|
+
for (const f of topFixes) {
|
|
236
|
+
lines.push(` ${f.priority}. [${f.checkId}] ${f.action}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
package/src/bootstrap.js
CHANGED
|
@@ -36,7 +36,7 @@ function getPackageVersion() {
|
|
|
36
36
|
// ── Individual checks ────────────────────────────────────────────────
|
|
37
37
|
|
|
38
38
|
async function checkGitRepo() {
|
|
39
|
-
let ok
|
|
39
|
+
let ok;
|
|
40
40
|
try {
|
|
41
41
|
ok = await ensureGitRepo();
|
|
42
42
|
} catch { /* git may not be installed */
|
|
@@ -219,6 +219,31 @@ function formatBootstrapFailure(failures) {
|
|
|
219
219
|
* Throws Error with actionable message if any prerequisite fails.
|
|
220
220
|
*/
|
|
221
221
|
export async function ensureBootstrap(projectDir, config) {
|
|
222
|
+
// Plugin discovery runs unconditionally on every bootstrap, regardless
|
|
223
|
+
// of whether the bootstrap cache says environment is valid. A user
|
|
224
|
+
// dropping a new file into `.karajan/plugins/` expects it to be picked
|
|
225
|
+
// up without running `kj init` again.
|
|
226
|
+
//
|
|
227
|
+
// Pre-v2.7.5 this was never called — `loadPlugins()` was exported but
|
|
228
|
+
// never imported from production code, so any plugin the user added
|
|
229
|
+
// was silently ignored. Fixed in TSK-0329.
|
|
230
|
+
try {
|
|
231
|
+
const { loadPlugins } = await import("./plugins/loader.js");
|
|
232
|
+
await loadPlugins({ projectDir });
|
|
233
|
+
} catch { /* plugin system failed — non-blocking, continue bootstrap */ }
|
|
234
|
+
|
|
235
|
+
// TSK-0339: register the Planning Game adapter in the integrations registry
|
|
236
|
+
// when enabled. The orchestrator no longer imports `src/planning-game/*`
|
|
237
|
+
// directly — it calls `getIntegration("tracker")?.hookName(...)` instead.
|
|
238
|
+
// Other trackers (GitHub Projects, Jira, Linear, …) drop in by writing
|
|
239
|
+
// a sibling adapter that calls `registerIntegration("tracker", …)`.
|
|
240
|
+
if (config?.planning_game?.enabled) {
|
|
241
|
+
try {
|
|
242
|
+
const { registerPgAdapter } = await import("./planning-game/pipeline-adapter.js");
|
|
243
|
+
await registerPgAdapter();
|
|
244
|
+
} catch { /* PG registration failed — non-blocking, continue bootstrap */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
222
247
|
const cached = await readBootstrapFile(projectDir);
|
|
223
248
|
if (isBootstrapValid(cached, projectDir)) {
|
|
224
249
|
return; // Environment already validated
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* the session for human input.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { setBrainDecisions } from "../session/mutators.js";
|
|
15
|
+
|
|
14
16
|
export const DEFAULT_MAX_DECISIONS = 20;
|
|
15
17
|
export const INDECISION_REPEAT_THRESHOLD = 3; // 3 identical classifications in a row
|
|
16
18
|
|
|
@@ -42,7 +44,7 @@ export const INDECISION_REPEAT_THRESHOLD = 3; // 3 identical classifications in
|
|
|
42
44
|
*/
|
|
43
45
|
export function createTracker(session, { max = DEFAULT_MAX_DECISIONS } = {}) {
|
|
44
46
|
if (!Array.isArray(session.brainDecisions)) {
|
|
45
|
-
session
|
|
47
|
+
setBrainDecisions(session, []);
|
|
46
48
|
}
|
|
47
49
|
return { session, max };
|
|
48
50
|
}
|
package/src/brain/decisor.js
CHANGED
|
@@ -125,7 +125,7 @@ function applyOverrides(roles, overrides) {
|
|
|
125
125
|
* @param {{ forceRoles?: string[], skipRoles?: string[] }} [args.overrides]
|
|
126
126
|
* @returns {Decision}
|
|
127
127
|
*/
|
|
128
|
-
export function buildDecision({ triage, task, config = {}, overrides = {} } = {}) {
|
|
128
|
+
export function buildDecision({ triage, task: _task, config = {}, overrides = {} } = {}) {
|
|
129
129
|
const level = triage?.level || "simple";
|
|
130
130
|
const taskType = triage?.taskType || "sw";
|
|
131
131
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REASONS Canvas markdown parser.
|
|
3
|
+
*
|
|
4
|
+
* Reads a `SPEC.canvas.md` file and returns a Canvas object that
|
|
5
|
+
* validates against canvas/schema.js.
|
|
6
|
+
*
|
|
7
|
+
* Format:
|
|
8
|
+
*
|
|
9
|
+
* # <title>
|
|
10
|
+
*
|
|
11
|
+
* ## REASONS:Requirements
|
|
12
|
+
* <problem statement, free text>
|
|
13
|
+
*
|
|
14
|
+
* Definition of done:
|
|
15
|
+
* - bullet 1
|
|
16
|
+
* - bullet 2
|
|
17
|
+
*
|
|
18
|
+
* ## REASONS:Entities
|
|
19
|
+
* - `User` — fields: id, email. has-many Order.
|
|
20
|
+
* - `Order` — ...
|
|
21
|
+
*
|
|
22
|
+
* ## REASONS:Approach
|
|
23
|
+
* <free text>
|
|
24
|
+
*
|
|
25
|
+
* ## REASONS:Structure
|
|
26
|
+
* - touches: src/auth/*, src/orders/*
|
|
27
|
+
* - depends_on: TokenService
|
|
28
|
+
* - contracts: POST /orders, GET /orders/:id
|
|
29
|
+
*
|
|
30
|
+
* ## REASONS:Operations
|
|
31
|
+
* 1. <title>
|
|
32
|
+
* Acceptance:
|
|
33
|
+
* - shell: `cmd here`
|
|
34
|
+
* - gherkin: ```Given … When … Then …```
|
|
35
|
+
* 2. ...
|
|
36
|
+
*
|
|
37
|
+
* ## REASONS:Norms
|
|
38
|
+
* - bullet
|
|
39
|
+
*
|
|
40
|
+
* ## REASONS:Safeguards
|
|
41
|
+
* - bullet
|
|
42
|
+
*
|
|
43
|
+
* Permissive on whitespace and bullet style ("- ", "* ", "1. ").
|
|
44
|
+
* Strict on structure: a missing required section ⇒ exception with a
|
|
45
|
+
* pointer to which section is missing.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const SECTION_HEADER_RE = /^##\s+REASONS:(\w+)\s*$/i;
|
|
49
|
+
const TITLE_RE = /^#\s+(.+?)\s*$/;
|
|
50
|
+
const BULLET_RE = /^\s*(?:[-*]|\d+\.)\s+(.+?)\s*$/;
|
|
51
|
+
const ACCEPTANCE_HEADER_RE = /^\s*Acceptance(?:\s+tests?)?\s*:\s*$/i;
|
|
52
|
+
const TEST_LINE_RE = /^\s*(?:[-*])\s+(shell|gherkin)\s*:\s*(.+?)\s*$/i;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a SPEC.canvas.md string into a Canvas object.
|
|
56
|
+
* Throws { name: "CanvasParseError", message } on missing required sections.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} source — markdown text
|
|
59
|
+
* @returns {object} Canvas object (un-validated against schema; caller validates)
|
|
60
|
+
*/
|
|
61
|
+
export function parseCanvasMarkdown(source) {
|
|
62
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
63
|
+
throw makeError("Canvas markdown is empty or not a string.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lines = source.split(/\r?\n/);
|
|
67
|
+
const sections = {};
|
|
68
|
+
let title = null;
|
|
69
|
+
let currentName = null;
|
|
70
|
+
let currentBuf = [];
|
|
71
|
+
|
|
72
|
+
// First pass: split into sections.
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const titleMatch = line.match(TITLE_RE);
|
|
75
|
+
if (titleMatch && currentName === null && title === null) {
|
|
76
|
+
title = titleMatch[1].trim();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const sectionMatch = line.match(SECTION_HEADER_RE);
|
|
80
|
+
if (sectionMatch) {
|
|
81
|
+
// flush previous
|
|
82
|
+
if (currentName) {
|
|
83
|
+
sections[currentName] = currentBuf.join("\n");
|
|
84
|
+
}
|
|
85
|
+
currentName = sectionMatch[1].toLowerCase();
|
|
86
|
+
currentBuf = [];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (currentName) currentBuf.push(line);
|
|
90
|
+
}
|
|
91
|
+
if (currentName) sections[currentName] = currentBuf.join("\n");
|
|
92
|
+
|
|
93
|
+
if (!title) throw makeError("Missing title — first line must be `# <title>`.");
|
|
94
|
+
|
|
95
|
+
// Required sections — must be PRESENT (header exists), but content
|
|
96
|
+
// may be empty for sections like Structure where every key is
|
|
97
|
+
// optional. Use `in` so an empty-body section is accepted.
|
|
98
|
+
for (const required of ["requirements", "approach", "structure", "operations"]) {
|
|
99
|
+
if (!(required in sections)) {
|
|
100
|
+
throw makeError(`Missing required section: ## REASONS:${capitalize(required)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
title,
|
|
106
|
+
requirements: parseRequirements(sections.requirements),
|
|
107
|
+
entities: sections.entities ? parseEntities(sections.entities) : [],
|
|
108
|
+
approach: sections.approach.trim(),
|
|
109
|
+
structure: parseStructure(sections.structure),
|
|
110
|
+
operations: parseOperations(sections.operations),
|
|
111
|
+
norms: sections.norms ? parseBullets(sections.norms) : [],
|
|
112
|
+
safeguards: sections.safeguards ? parseBullets(sections.safeguards) : [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function capitalize(s) {
|
|
117
|
+
return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function makeError(msg) {
|
|
121
|
+
const e = new Error(msg);
|
|
122
|
+
e.name = "CanvasParseError";
|
|
123
|
+
return e;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Requirements: a paragraph (problem) + optional "Definition of done:" bullets.
|
|
128
|
+
*/
|
|
129
|
+
function parseRequirements(text) {
|
|
130
|
+
const lines = text.split("\n");
|
|
131
|
+
const problemLines = [];
|
|
132
|
+
const done = [];
|
|
133
|
+
let inDone = false;
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (/^\s*Definition of done:?\s*$/i.test(line)) { inDone = true; continue; }
|
|
136
|
+
if (inDone) {
|
|
137
|
+
const m = line.match(BULLET_RE);
|
|
138
|
+
if (m) done.push(m[1].trim());
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (line.trim()) problemLines.push(line.trim());
|
|
142
|
+
}
|
|
143
|
+
const problem = problemLines.join(" ").trim();
|
|
144
|
+
if (!problem) throw makeError("REASONS:Requirements is empty or has no problem statement.");
|
|
145
|
+
return { problem, done };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Entities: each bullet is "Name — fields: f1, f2. notes." (loose).
|
|
150
|
+
* We parse the first word as the entity name and capture the rest as notes.
|
|
151
|
+
*/
|
|
152
|
+
function parseEntities(text) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const raw of text.split("\n")) {
|
|
155
|
+
const m = raw.match(BULLET_RE);
|
|
156
|
+
if (!m) continue;
|
|
157
|
+
const body = m[1].trim();
|
|
158
|
+
// First token (possibly `quoted`) is the name; rest is notes.
|
|
159
|
+
const nameMatch = body.match(/^[`"']?([A-Za-z][\w-]*)[`"']?\s*[—-]?\s*(.*)$/);
|
|
160
|
+
if (!nameMatch) continue;
|
|
161
|
+
const [, name, rest] = nameMatch;
|
|
162
|
+
const fields = [];
|
|
163
|
+
const fm = rest.match(/fields?:\s*([^.]+)/i);
|
|
164
|
+
if (fm) fields.push(...fm[1].split(",").map((s) => s.trim()).filter(Boolean));
|
|
165
|
+
out.push({ name, fields, notes: rest });
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseStructure(text) {
|
|
171
|
+
const out = { touches: [], depends_on: [], contracts: [] };
|
|
172
|
+
for (const raw of text.split("\n")) {
|
|
173
|
+
const m = raw.match(BULLET_RE);
|
|
174
|
+
if (!m) continue;
|
|
175
|
+
const body = m[1].trim();
|
|
176
|
+
const kvMatch = body.match(/^(touches|depends_on|contracts)\s*:\s*(.+)$/i);
|
|
177
|
+
if (!kvMatch) continue;
|
|
178
|
+
const key = kvMatch[1].toLowerCase();
|
|
179
|
+
const values = kvMatch[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
180
|
+
out[key] = values;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseBullets(text) {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const raw of text.split("\n")) {
|
|
188
|
+
const m = raw.match(BULLET_RE);
|
|
189
|
+
if (m) out.push(m[1].trim());
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Operations: each top-level numbered/bulleted entry is one Operation.
|
|
196
|
+
* Inside, an "Acceptance:" sub-line introduces nested bullets where each
|
|
197
|
+
* bullet is "shell: <cmd>" or "gherkin: <scenario>".
|
|
198
|
+
*/
|
|
199
|
+
function parseOperations(text) {
|
|
200
|
+
const ops = [];
|
|
201
|
+
const lines = text.split("\n");
|
|
202
|
+
let current = null;
|
|
203
|
+
let inAcceptance = false;
|
|
204
|
+
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
if (!line.trim()) continue;
|
|
207
|
+
// Top-level operation: numbered list or single dash at left margin.
|
|
208
|
+
const topMatch = line.match(/^(\d+\.\s+|[-*]\s+)(.+)$/);
|
|
209
|
+
if (topMatch && !line.startsWith(" ") && !line.startsWith("\t")) {
|
|
210
|
+
if (current) ops.push(current);
|
|
211
|
+
current = { title: topMatch[2].trim(), acceptance: [] };
|
|
212
|
+
inAcceptance = false;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (current) {
|
|
216
|
+
if (ACCEPTANCE_HEADER_RE.test(line)) { inAcceptance = true; continue; }
|
|
217
|
+
const testMatch = line.match(TEST_LINE_RE);
|
|
218
|
+
if (testMatch && inAcceptance) {
|
|
219
|
+
current.acceptance.push({
|
|
220
|
+
type: testMatch[1].toLowerCase(),
|
|
221
|
+
content: testMatch[2].replace(/^`+|`+$/g, "").trim(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (current) ops.push(current);
|
|
227
|
+
|
|
228
|
+
if (ops.length === 0) {
|
|
229
|
+
throw makeError("REASONS:Operations has no operations. Each operation must be a numbered or bulleted item with at least one acceptance test.");
|
|
230
|
+
}
|
|
231
|
+
for (const op of ops) {
|
|
232
|
+
if (op.acceptance.length === 0) {
|
|
233
|
+
throw makeError(`Operation "${op.title}" has no acceptance tests. Add at least one ` +
|
|
234
|
+
'`- shell: <cmd>` or `- gherkin: <scenario>` under an "Acceptance:" line.');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return ops;
|
|
238
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REASONS Canvas — Valibot schema for the structured SPEC format.
|
|
3
|
+
*
|
|
4
|
+
* Issue #539. Per Martin Fowler's Structured Prompt-Driven Development
|
|
5
|
+
* (https://martinfowler.com/articles/structured-prompt-driven/), a
|
|
6
|
+
* Canvas captures intent in 7 named sections so the planner doesn't
|
|
7
|
+
* have to improvise. The shape:
|
|
8
|
+
*
|
|
9
|
+
* Requirements — problem + done-when bullets
|
|
10
|
+
* Entities — domain nouns + relationships
|
|
11
|
+
* Approach — strategy + trade-offs
|
|
12
|
+
* Structure — system fit, dependencies, contracts
|
|
13
|
+
* Operations — concrete numbered steps. Each = one HU.
|
|
14
|
+
* Norms — task-level engineering standards
|
|
15
|
+
* Safeguards — non-negotiables: invariants, perf, security
|
|
16
|
+
*
|
|
17
|
+
* Operations carry their acceptance tests inline so the planner-time
|
|
18
|
+
* validator (validate.js) can check them parse BEFORE any LLM call.
|
|
19
|
+
* That's the bug-class fix this whole feature exists for: the
|
|
20
|
+
* 2026-04-29 incident where the planner emitted a syntactically-broken
|
|
21
|
+
* jq query and the coder iterated 3× against an irreparable test.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as v from "valibot";
|
|
25
|
+
|
|
26
|
+
const NonEmptyString = v.pipe(v.string(), v.minLength(1));
|
|
27
|
+
|
|
28
|
+
const Entity = v.looseObject({
|
|
29
|
+
name: NonEmptyString,
|
|
30
|
+
fields: v.optional(v.array(v.string())),
|
|
31
|
+
relationships: v.optional(v.array(v.unknown())),
|
|
32
|
+
notes: v.optional(v.string()),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const AcceptanceTest = v.looseObject({
|
|
36
|
+
type: v.picklist(["shell", "gherkin"]),
|
|
37
|
+
content: NonEmptyString,
|
|
38
|
+
// optional: file hint for tester translation of gherkin
|
|
39
|
+
file: v.optional(v.string()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const Operation = v.looseObject({
|
|
43
|
+
// Either a free-form title or numbered step text.
|
|
44
|
+
title: NonEmptyString,
|
|
45
|
+
// Optional: the file or module the operation produces / touches.
|
|
46
|
+
file_hint: v.optional(v.string()),
|
|
47
|
+
// Optional: dependencies on other operation titles or HU ids.
|
|
48
|
+
blocked_by: v.optional(v.array(v.string())),
|
|
49
|
+
// Required: at least ONE acceptance test per operation. Empty array
|
|
50
|
+
// is rejected at plan-time so we never ship an HU without a contract.
|
|
51
|
+
acceptance: v.pipe(v.array(AcceptanceTest), v.minLength(1)),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const CanvasSchema = v.looseObject({
|
|
55
|
+
// Title is the project / feature name. The planner uses it as the
|
|
56
|
+
// plan name unless the user overrides on prompt.
|
|
57
|
+
title: NonEmptyString,
|
|
58
|
+
requirements: v.looseObject({
|
|
59
|
+
problem: NonEmptyString,
|
|
60
|
+
// Definition-of-done bullets. Empty array allowed — sometimes the
|
|
61
|
+
// problem statement IS the contract — but a non-empty array
|
|
62
|
+
// discourages woolly specs.
|
|
63
|
+
done: v.optional(v.array(NonEmptyString)),
|
|
64
|
+
}),
|
|
65
|
+
entities: v.optional(v.array(Entity)),
|
|
66
|
+
approach: NonEmptyString,
|
|
67
|
+
structure: v.looseObject({
|
|
68
|
+
touches: v.optional(v.array(v.string())),
|
|
69
|
+
depends_on: v.optional(v.array(v.string())),
|
|
70
|
+
contracts: v.optional(v.array(v.string())),
|
|
71
|
+
}),
|
|
72
|
+
operations: v.pipe(v.array(Operation), v.minLength(1)),
|
|
73
|
+
norms: v.optional(v.array(NonEmptyString)),
|
|
74
|
+
safeguards: v.optional(v.array(NonEmptyString)),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate-only entry point. Throws an Error with the offending path
|
|
79
|
+
* + message when the canvas doesn't match the schema. The validator
|
|
80
|
+
* (validate.js) uses this AND adds parse-checks for acceptance tests.
|
|
81
|
+
*
|
|
82
|
+
* @param {unknown} canvas
|
|
83
|
+
* @returns {import("valibot").InferOutput<typeof CanvasSchema>}
|
|
84
|
+
*/
|
|
85
|
+
export function parseCanvas(canvas) {
|
|
86
|
+
return v.parse(CanvasSchema, canvas);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Soft check: returns the validation result without throwing. Useful
|
|
91
|
+
* for surfacing every schema error at once instead of bailing on the
|
|
92
|
+
* first.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} canvas
|
|
95
|
+
* @returns {{ success: true, output: object } | { success: false, issues: object[] }}
|
|
96
|
+
*/
|
|
97
|
+
export function safeParseCanvas(canvas) {
|
|
98
|
+
const r = v.safeParse(CanvasSchema, canvas);
|
|
99
|
+
if (r.success) return { success: true, output: r.output };
|
|
100
|
+
return { success: false, issues: r.issues };
|
|
101
|
+
}
|