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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Verification gate: checks whether the coder actually produced changes.
|
|
2
2
|
// Prevents advancing the pipeline when coder iterations produce 0 file changes.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} VerificationResult
|
|
@@ -16,14 +16,20 @@ import { execSync } from "node:child_process";
|
|
|
16
16
|
/**
|
|
17
17
|
* Count files and lines changed since a base reference.
|
|
18
18
|
* Uses git diff --numstat to get both file and line counts.
|
|
19
|
+
*
|
|
20
|
+
* Audit follow-up: was using execSync with template-string interpolation
|
|
21
|
+
* of `baseRef` and `projectDir`. Both come ultimately from session
|
|
22
|
+
* state / config — not user shell input — but the shape was a known
|
|
23
|
+
* shell-injection vector. Switched to execFileSync with arg arrays so
|
|
24
|
+
* no interpolation reaches /bin/sh.
|
|
19
25
|
*/
|
|
20
26
|
export function countChangesSince(baseRef, projectDir = null) {
|
|
21
27
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
).trim();
|
|
28
|
+
const args = ["diff", "--numstat", String(baseRef)];
|
|
29
|
+
if (projectDir) args.push("--", String(projectDir));
|
|
30
|
+
const output = execFileSync("git", args, {
|
|
31
|
+
encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
+
}).trim();
|
|
27
33
|
|
|
28
34
|
if (!output) return { files: [], filesChanged: 0, linesAdded: 0, linesDeleted: 0 };
|
|
29
35
|
|
|
@@ -48,14 +54,16 @@ export function countChangesSince(baseRef, projectDir = null) {
|
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* Also count untracked files (new files not yet added to git).
|
|
57
|
+
*
|
|
58
|
+
* Audit follow-up: same execSync→execFileSync change as countChangesSince.
|
|
51
59
|
*/
|
|
52
60
|
export function countUntrackedFiles(projectDir = null) {
|
|
53
61
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
).trim();
|
|
62
|
+
const args = ["ls-files", "--others", "--exclude-standard"];
|
|
63
|
+
if (projectDir) args.push(String(projectDir));
|
|
64
|
+
const output = execFileSync("git", args, {
|
|
65
|
+
encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
}).trim();
|
|
59
67
|
|
|
60
68
|
if (!output) return [];
|
|
61
69
|
return output.split("\n").filter(Boolean);
|
|
@@ -111,7 +119,7 @@ export function verifyCoderOutput({ baseRef, projectDir = null, minFiles = 1, mi
|
|
|
111
119
|
/**
|
|
112
120
|
* Build a retry strategy based on the verification failure.
|
|
113
121
|
*/
|
|
114
|
-
function buildRetryStrategy(filesChanged,
|
|
122
|
+
function buildRetryStrategy(filesChanged, _linesChanged) {
|
|
115
123
|
if (filesChanged === 0) {
|
|
116
124
|
return "Rephrase the feedback with explicit file paths. List specific files to create/modify. Include example code snippets.";
|
|
117
125
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture Decision Records (ADRs) — distill the architect role's
|
|
3
|
+
* tradeoffs[] output into the canonical "Context · Decision ·
|
|
4
|
+
* Consequences · Status" markdown format.
|
|
5
|
+
*
|
|
6
|
+
* Why ADRs as a first-class artifact (v2.7.5):
|
|
7
|
+
* - The architect already analyses the task and produces a
|
|
8
|
+
* `tradeoffs[]` list with `{ decision, rationale, alternatives }`.
|
|
9
|
+
* That data is exactly what an ADR captures, just unformatted.
|
|
10
|
+
* - An ADR persists the WHY of an architectural choice. Coder and
|
|
11
|
+
* reviewer in the next `kj run` can pull the active ADRs into
|
|
12
|
+
* their prompt for context.
|
|
13
|
+
* - It's also a deliverable in its own right — projects benefit
|
|
14
|
+
* from a `docs/adr/` history regardless of Karajan.
|
|
15
|
+
*
|
|
16
|
+
* Storage: per-plan under `~/.kj/plans/<slug>/adrs/<adr-id>.md` (so
|
|
17
|
+
* an ADR's lifecycle matches its plan's). When the orchestrator runs
|
|
18
|
+
* a plan, the coder gets the ADRs as context. When a plan is removed
|
|
19
|
+
* (`kj plan delete`) the ADRs go with it.
|
|
20
|
+
*
|
|
21
|
+
* Format follows Michael Nygard's classic structure (the most widely
|
|
22
|
+
* adopted in the industry, used by GitHub, Spotify, AWS docs, etc.).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { plansDir } from "./plan-store.js";
|
|
28
|
+
|
|
29
|
+
const VALID_ADR_STATUSES = new Set(["proposed", "accepted", "deprecated", "superseded"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Slugify a decision title into a filesystem-safe ADR id fragment.
|
|
33
|
+
* "Use SQLite for plan storage" → "use-sqlite-for-plan-storage".
|
|
34
|
+
*/
|
|
35
|
+
function slugifyTitle(title) {
|
|
36
|
+
return String(title || "")
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
// Decompose accents (NFD) then strip combining marks so "versión"
|
|
39
|
+
// → "version", keeping the ADR id ASCII-only and filesystem-safe.
|
|
40
|
+
.normalize("NFD")
|
|
41
|
+
.replace(/[̀-ͯ]/g, "")
|
|
42
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
43
|
+
.replace(/\s+/g, "-")
|
|
44
|
+
.replace(/-+/g, "-")
|
|
45
|
+
.replace(/^-|-$/g, "")
|
|
46
|
+
.slice(0, 80) || "untitled";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Turn the architect's `architecture.tradeoffs[]` into an array of
|
|
51
|
+
* ADR objects. Each tradeoff carries `{ decision, rationale,
|
|
52
|
+
* alternatives }`; we map them to the four ADR sections plus
|
|
53
|
+
* metadata.
|
|
54
|
+
*
|
|
55
|
+
* Numbering: 1-based, zero-padded to 4 digits ("0001-…"). Stable per
|
|
56
|
+
* call — callers should treat `id` as opaque (filename) and `number`
|
|
57
|
+
* as ordinal.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} architecture - parsed architect output
|
|
60
|
+
* @param {object} [opts]
|
|
61
|
+
* @param {string} [opts.status] - default "accepted" (architect runs imply the decision is taken)
|
|
62
|
+
* @returns {Array<{ number: number, id: string, title: string, status: string,
|
|
63
|
+
* context: string, decision: string, consequences: string,
|
|
64
|
+
* alternatives: string[] }>}
|
|
65
|
+
*/
|
|
66
|
+
export function extractAdrsFromArchitecture(architecture, opts = {}) {
|
|
67
|
+
const tradeoffs = Array.isArray(architecture?.tradeoffs) ? architecture.tradeoffs : [];
|
|
68
|
+
const status = VALID_ADR_STATUSES.has(opts.status) ? opts.status : "accepted";
|
|
69
|
+
const adrs = [];
|
|
70
|
+
let n = 0;
|
|
71
|
+
for (const t of tradeoffs) {
|
|
72
|
+
if (!t || (typeof t !== "object" && typeof t !== "string")) continue;
|
|
73
|
+
const title = typeof t === "string" ? t : (t.decision || t.title || "");
|
|
74
|
+
if (!title.trim()) continue;
|
|
75
|
+
n += 1;
|
|
76
|
+
const number = n;
|
|
77
|
+
const slug = slugifyTitle(title);
|
|
78
|
+
const id = `${String(number).padStart(4, "0")}-${slug}`;
|
|
79
|
+
adrs.push({
|
|
80
|
+
number,
|
|
81
|
+
id,
|
|
82
|
+
title: title.trim(),
|
|
83
|
+
status,
|
|
84
|
+
context: typeof t === "object" && t.context ? String(t.context) : "",
|
|
85
|
+
decision: typeof t === "object" && t.decision ? String(t.decision) : title.trim(),
|
|
86
|
+
consequences: typeof t === "object" && t.rationale ? String(t.rationale) : "",
|
|
87
|
+
alternatives: typeof t === "object" && Array.isArray(t.alternatives) ? t.alternatives.map(String) : [],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return adrs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Render an ADR to markdown using Nygard's format. Newline-stable so
|
|
95
|
+
* tests can do exact-match assertions.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} adr
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {Date} [opts.date]
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function renderAdrMarkdown(adr, opts = {}) {
|
|
103
|
+
const date = (opts.date || new Date()).toISOString().slice(0, 10); // YYYY-MM-DD
|
|
104
|
+
const lines = [
|
|
105
|
+
`# ${String(adr.number).padStart(4, "0")}. ${adr.title}`,
|
|
106
|
+
"",
|
|
107
|
+
`**Status:** ${adr.status} `,
|
|
108
|
+
`**Date:** ${date} `,
|
|
109
|
+
"",
|
|
110
|
+
"## Context",
|
|
111
|
+
"",
|
|
112
|
+
adr.context || "_(architect role did not provide an explicit context — see Decision below.)_",
|
|
113
|
+
"",
|
|
114
|
+
"## Decision",
|
|
115
|
+
"",
|
|
116
|
+
adr.decision,
|
|
117
|
+
"",
|
|
118
|
+
];
|
|
119
|
+
if (adr.alternatives?.length > 0) {
|
|
120
|
+
lines.push("**Alternatives considered:**");
|
|
121
|
+
lines.push("");
|
|
122
|
+
for (const alt of adr.alternatives) lines.push(`- ${alt}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
lines.push(
|
|
126
|
+
"## Consequences",
|
|
127
|
+
"",
|
|
128
|
+
adr.consequences || "_(no consequences captured by the architect role.)_",
|
|
129
|
+
""
|
|
130
|
+
);
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write the ADRs to disk under the plan's adrs/ directory. Returns
|
|
136
|
+
* the list of absolute paths written so callers can surface them in
|
|
137
|
+
* the CLI output.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} args
|
|
140
|
+
* @param {string} args.projectDir
|
|
141
|
+
* @param {string} args.planId - used to scope ADRs to a plan; falls
|
|
142
|
+
* back to "_loose" when this came
|
|
143
|
+
* from `kj architect` standalone
|
|
144
|
+
* @param {Array} args.adrs - output of extractAdrsFromArchitecture
|
|
145
|
+
* @returns {Promise<string[]>} absolute paths written
|
|
146
|
+
*/
|
|
147
|
+
export async function writeAdrsToDisk({ projectDir, planId, adrs }) {
|
|
148
|
+
if (!Array.isArray(adrs) || adrs.length === 0) return [];
|
|
149
|
+
if (!projectDir) throw new Error("writeAdrsToDisk: projectDir is required");
|
|
150
|
+
const planSlug = planId || "_loose";
|
|
151
|
+
const dir = join(plansDir(projectDir), planSlug, "adrs");
|
|
152
|
+
await mkdir(dir, { recursive: true });
|
|
153
|
+
const paths = [];
|
|
154
|
+
for (const adr of adrs) {
|
|
155
|
+
const filePath = join(dir, `${adr.id}.md`);
|
|
156
|
+
await writeFile(filePath, renderAdrMarkdown(adr), "utf-8");
|
|
157
|
+
paths.push(filePath);
|
|
158
|
+
}
|
|
159
|
+
return paths;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Convenience: extract + write in one call. Used by `kj architect`
|
|
164
|
+
* and (later) the orchestrator's architect stage.
|
|
165
|
+
*/
|
|
166
|
+
export async function persistAdrsFromArchitecture({ projectDir, planId, architecture }) {
|
|
167
|
+
const adrs = extractAdrsFromArchitecture(architecture);
|
|
168
|
+
const paths = await writeAdrsToDisk({ projectDir, planId, adrs });
|
|
169
|
+
return { adrs, paths };
|
|
170
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counterpart to `adr-generator.js`: reads back the persisted ADRs
|
|
3
|
+
* for a given plan so the coder / reviewer can pull them into their
|
|
4
|
+
* prompt context. PR F of the v2.7.5 multi-step planning roadmap.
|
|
5
|
+
*
|
|
6
|
+
* ADRs live at `~/.kj/plans/<slug>/<planId>/adrs/<id>.md` (per
|
|
7
|
+
* `adr-generator.js::writeAdrsToDisk`). On read we surface the raw
|
|
8
|
+
* markdown to the caller — they decide how much of it to render.
|
|
9
|
+
*
|
|
10
|
+
* Returns an empty array (not an error) when the directory doesn't
|
|
11
|
+
* exist: not every plan has ADRs (architect role is optional).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { plansDir } from "./plan-store.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} Adr
|
|
20
|
+
* @property {string} id - "0001-use-vitest"
|
|
21
|
+
* @property {string} markdown - full file contents
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} projectDir
|
|
26
|
+
* @param {string|null} planId - null falls back to the "_loose" bucket
|
|
27
|
+
* used by `kj architect` standalone
|
|
28
|
+
* @returns {Promise<Adr[]>}
|
|
29
|
+
*/
|
|
30
|
+
export async function loadActiveAdrs(projectDir, planId) {
|
|
31
|
+
if (!projectDir) return [];
|
|
32
|
+
const slug = planId || "_loose";
|
|
33
|
+
const dir = join(plansDir(projectDir), slug, "adrs");
|
|
34
|
+
let files;
|
|
35
|
+
try {
|
|
36
|
+
const s = await stat(dir);
|
|
37
|
+
if (!s.isDirectory()) return [];
|
|
38
|
+
files = await readdir(dir);
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
// Audit follow-up: was a sequential `for ... await readFile(...)`. ADRs
|
|
43
|
+
// are independent files; the loop has no inter-iteration state. Parallel
|
|
44
|
+
// reads cut typical 5-ADR loads from ~5 ms (sequential FS RTT) to a
|
|
45
|
+
// single ~1 ms burst, and the cost grows with ADR count. Filter the
|
|
46
|
+
// markdown files first so we don't fire reads against junk.
|
|
47
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
48
|
+
const settled = await Promise.all(
|
|
49
|
+
mdFiles.map(async (file) => {
|
|
50
|
+
try {
|
|
51
|
+
const md = await readFile(join(dir, file), "utf-8");
|
|
52
|
+
return { id: file.replace(/\.md$/, ""), markdown: md };
|
|
53
|
+
} catch { return null; /* skip unreadable file */ }
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
const adrs = settled.filter(Boolean);
|
|
57
|
+
// Numeric prefix in id ("0001-…") sorts naturally.
|
|
58
|
+
adrs.sort((a, b) => a.id.localeCompare(b.id));
|
|
59
|
+
return adrs;
|
|
60
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { isPlanV2 } from "./plan-schema.js";
|
|
8
|
-
import { updateHuStatus } from "./plan-hu-ops.js";
|
|
8
|
+
import { updateHuStatus, computePlanOutcome, setPlanOutcome } from "./plan-hu-ops.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Convert a v2 plan's HUs into the stageResults.huReviewer format
|
|
@@ -27,6 +27,10 @@ export function planToHuBatch(plan) {
|
|
|
27
27
|
certified: { text: hu.scope || hu.title },
|
|
28
28
|
acceptance_criteria: hu.acceptance_criteria || [],
|
|
29
29
|
acceptance_tests: hu.acceptance_tests || [],
|
|
30
|
+
// PR F (v2.7.5): forward the SPEC section pointer into the story
|
|
31
|
+
// so the per-HU loop in flow-runner can hand it to the coder
|
|
32
|
+
// prompt without having to re-load the plan from disk.
|
|
33
|
+
spec_section: hu.spec_section || null,
|
|
30
34
|
original: { text: hu.scope || hu.title }
|
|
31
35
|
}));
|
|
32
36
|
|
|
@@ -39,6 +43,12 @@ export function planToHuBatch(plan) {
|
|
|
39
43
|
certified,
|
|
40
44
|
batchSessionId: `plan-${plan.planId}`,
|
|
41
45
|
planId: plan.planId,
|
|
46
|
+
// PR F (v2.7.5): plan-wide reviewer findings ride along the batch
|
|
47
|
+
// so the coder can be told about issues that touch its HU. The
|
|
48
|
+
// shape mirrors plan-reviewer.js output (missing_dependencies,
|
|
49
|
+
// scope_overlaps, order_issues, …) — null when the reviewer
|
|
50
|
+
// never ran for this plan.
|
|
51
|
+
review: plan.review || null,
|
|
42
52
|
auto_generated: false,
|
|
43
53
|
source: { plan: true, planId: plan.planId }
|
|
44
54
|
};
|
|
@@ -66,4 +76,12 @@ export function syncResultsToPlan(plan, subPipelineResult) {
|
|
|
66
76
|
const anyFailed = plan.hus.some(h => h.status === "failed");
|
|
67
77
|
plan.status = allDone ? "done" : anyFailed ? "failed" : "running";
|
|
68
78
|
plan.updatedAt = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
// PR3: stamp the plan-level rollup so the board can show the
|
|
81
|
+
// "Plan finalizado · X done · Y failed · Z blocked · D min" banner
|
|
82
|
+
// without recomputing it on every render. Built from the per-HU
|
|
83
|
+
// outcomes that hu-sub-pipeline already wrote on the way through.
|
|
84
|
+
const duration_ms = subPipelineResult.duration_ms ?? null;
|
|
85
|
+
const rollup = computePlanOutcome(plan, { duration_ms });
|
|
86
|
+
setPlanOutcome(plan, rollup);
|
|
69
87
|
}
|
package/src/plan/plan-hu-ops.js
CHANGED
|
@@ -37,6 +37,12 @@ export function addHu(plan, huData) {
|
|
|
37
37
|
scope: huData.scope || null,
|
|
38
38
|
acceptance_criteria: huData.acceptance_criteria || [],
|
|
39
39
|
acceptance_tests: huData.acceptance_tests || [],
|
|
40
|
+
// Spec mapping (v2.7.5 PR C): the SPEC.md section / heading this
|
|
41
|
+
// HU implements. Lets the coder cite "this implements SPEC §5.3"
|
|
42
|
+
// in PRs and the reviewer trace coverage gaps when the SPEC is
|
|
43
|
+
// re-read. Free-form string — typically "5.3", "§5.3 Initial
|
|
44
|
+
// Scope", or whatever the planner extracted from the source spec.
|
|
45
|
+
spec_section: huData.spec_section || null,
|
|
40
46
|
createdAt: new Date().toISOString(),
|
|
41
47
|
updatedAt: new Date().toISOString()
|
|
42
48
|
};
|
|
@@ -71,7 +77,7 @@ export function removeHu(plan, huId) {
|
|
|
71
77
|
export function updateHu(plan, huId, patch) {
|
|
72
78
|
const hu = plan.hus.find(h => h.id === huId);
|
|
73
79
|
if (!hu) return null;
|
|
74
|
-
const allowed = ["title", "task_type", "scope", "acceptance_criteria", "acceptance_tests", "blocked_by"];
|
|
80
|
+
const allowed = ["title", "task_type", "scope", "acceptance_criteria", "acceptance_tests", "blocked_by", "spec_section"];
|
|
75
81
|
for (const key of allowed) {
|
|
76
82
|
if (patch[key] !== undefined) hu[key] = patch[key];
|
|
77
83
|
}
|
|
@@ -93,6 +99,169 @@ export function updateHuStatus(plan, huId, status) {
|
|
|
93
99
|
return true;
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Stamp the per-HU outcome — what actually happened during execution.
|
|
104
|
+
* Written ONCE per HU, at the end of runSingleHu in hu-sub-pipeline.
|
|
105
|
+
* Consumed by the board to render the per-HU summary indicator and
|
|
106
|
+
* by `kj report` for retrospective inspection.
|
|
107
|
+
*
|
|
108
|
+
* Shape (every field optional except status + finishedAt):
|
|
109
|
+
* {
|
|
110
|
+
* status: "done" | "failed" | "blocked",
|
|
111
|
+
* iterations: number, // how many coder→reviewer rounds
|
|
112
|
+
* duration_ms: number, // wall-clock for this HU
|
|
113
|
+
* branch: string|null, // git branch the coder used
|
|
114
|
+
* commits: string[], // SHA list captured during the run
|
|
115
|
+
* pr_url: string|null,
|
|
116
|
+
* blockers: string[], // why it failed/was blocked, plain
|
|
117
|
+
* summary: string, // 1-2 sentence human summary
|
|
118
|
+
* finishedAt: ISO timestamp
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* @returns {boolean} true if the HU was found and the outcome stored
|
|
122
|
+
*/
|
|
123
|
+
export function setHuOutcome(plan, huId, outcome) {
|
|
124
|
+
const hu = plan.hus.find(h => h.id === huId);
|
|
125
|
+
if (!hu) return false;
|
|
126
|
+
hu.outcome = {
|
|
127
|
+
status: outcome.status || hu.status || null,
|
|
128
|
+
iterations: outcome.iterations ?? null,
|
|
129
|
+
duration_ms: outcome.duration_ms ?? null,
|
|
130
|
+
branch: outcome.branch ?? null,
|
|
131
|
+
commits: Array.isArray(outcome.commits) ? outcome.commits : [],
|
|
132
|
+
pr_url: outcome.pr_url ?? null,
|
|
133
|
+
blockers: Array.isArray(outcome.blockers) ? outcome.blockers : [],
|
|
134
|
+
summary: outcome.summary || "",
|
|
135
|
+
finishedAt: outcome.finishedAt || new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
hu.updatedAt = new Date().toISOString();
|
|
138
|
+
plan.updatedAt = new Date().toISOString();
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Stamp the plan-level rollup once every HU has finished. Built from
|
|
144
|
+
* the per-HU outcomes so the board can show "Plan finished · 8 done
|
|
145
|
+
* · 2 failed · 1 blocked · 15 min" in one banner.
|
|
146
|
+
*
|
|
147
|
+
* {
|
|
148
|
+
* status: "done" | "partial" | "failed",
|
|
149
|
+
* total: number,
|
|
150
|
+
* counts: { done, failed, blocked, pending },
|
|
151
|
+
* duration_ms: number,
|
|
152
|
+
* prs: string[], // unique pr_url across HUs
|
|
153
|
+
* blockers: string[], // dedup'd blockers
|
|
154
|
+
* finishedAt: ISO timestamp,
|
|
155
|
+
* }
|
|
156
|
+
*/
|
|
157
|
+
export function setPlanOutcome(plan, outcome) {
|
|
158
|
+
plan.outcome = {
|
|
159
|
+
status: outcome.status || "partial",
|
|
160
|
+
total: outcome.total ?? (plan.hus?.length || 0),
|
|
161
|
+
counts: outcome.counts || { done: 0, failed: 0, blocked: 0, pending: 0 },
|
|
162
|
+
duration_ms: outcome.duration_ms ?? null,
|
|
163
|
+
prs: Array.isArray(outcome.prs) ? [...new Set(outcome.prs.filter(Boolean))] : [],
|
|
164
|
+
blockers: Array.isArray(outcome.blockers) ? [...new Set(outcome.blockers.filter(Boolean))] : [],
|
|
165
|
+
finishedAt: outcome.finishedAt || new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
plan.updatedAt = new Date().toISOString();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* PR-E: auto-promote every pending HU with at least one acceptance
|
|
172
|
+
* test to "certified". The intermediate "certified" state was an
|
|
173
|
+
* invisible wall — there's no UI button for it and the user had no
|
|
174
|
+
* way to discover `kj plan ready`. Treating pending-with-tests as
|
|
175
|
+
* runnable removes the wall while keeping the test contract gate.
|
|
176
|
+
*
|
|
177
|
+
* Mutates plan in place. HUs in done / failed / blocked are left
|
|
178
|
+
* alone so re-runs stay explicit (use --hu <id>).
|
|
179
|
+
*
|
|
180
|
+
* @returns {number} how many HUs were promoted (0 if nothing changed).
|
|
181
|
+
*/
|
|
182
|
+
export function autoCertifyPendingHus(plan) {
|
|
183
|
+
if (!plan?.hus?.length) return 0;
|
|
184
|
+
let promoted = 0;
|
|
185
|
+
const now = new Date().toISOString();
|
|
186
|
+
for (const hu of plan.hus) {
|
|
187
|
+
if (hu.status === "pending" && Array.isArray(hu.acceptance_tests) && hu.acceptance_tests.length > 0) {
|
|
188
|
+
hu.status = "certified";
|
|
189
|
+
hu.updatedAt = now;
|
|
190
|
+
promoted += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (promoted > 0) plan.updatedAt = now;
|
|
194
|
+
return promoted;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* PR-E: assert that the plan has at least one HU that's actually
|
|
199
|
+
* going to run. Throws with a plain-Spanish message otherwise so
|
|
200
|
+
* `kj run --plan` can never silently fall back to single-task mode
|
|
201
|
+
* and burn tokens for nothing.
|
|
202
|
+
*
|
|
203
|
+
* The set of "runnable" statuses matches the sub-pipeline filter
|
|
204
|
+
* (today: certified). The caller should run autoCertifyPendingHus()
|
|
205
|
+
* BEFORE this so pending-with-tests get a chance.
|
|
206
|
+
*
|
|
207
|
+
* @throws {Error} when no HU is runnable.
|
|
208
|
+
*/
|
|
209
|
+
export function assertPlanRunnable(plan, planId) {
|
|
210
|
+
if (!plan?.hus?.length) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`El plan ${planId} no contiene ninguna HU. `
|
|
213
|
+
+ `Edita el plan o vuelve a generarlo con \`kj plan\`. `
|
|
214
|
+
+ `Aborto para no malgastar tokens en un fallback de "single task".`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const certified = plan.hus.filter((h) => h.status === "certified").length;
|
|
218
|
+
if (certified > 0) return;
|
|
219
|
+
const counts = plan.hus.reduce((acc, h) => {
|
|
220
|
+
const k = h.status || "pending";
|
|
221
|
+
acc[k] = (acc[k] || 0) + 1;
|
|
222
|
+
return acc;
|
|
223
|
+
}, {});
|
|
224
|
+
const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Plan ${planId}: 0 HU(s) listas para ejecutar (estados: ${summary}). `
|
|
227
|
+
+ `Si las HU(s) están en done/failed/blocked y quieres relanzarlas, usa --hu <id>. `
|
|
228
|
+
+ `Aborto para no malgastar tokens en un fallback de "single task".`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Compute the rollup from the per-HU outcomes already stamped on the
|
|
234
|
+
* plan. Invoked by syncResultsToPlan in plan-executor when the run
|
|
235
|
+
* ends so the caller doesn't have to assemble the counts by hand.
|
|
236
|
+
*/
|
|
237
|
+
export function computePlanOutcome(plan, { duration_ms = null } = {}) {
|
|
238
|
+
const hus = plan.hus || [];
|
|
239
|
+
const counts = { done: 0, failed: 0, blocked: 0, pending: 0 };
|
|
240
|
+
const prs = [];
|
|
241
|
+
const blockers = [];
|
|
242
|
+
for (const hu of hus) {
|
|
243
|
+
const status = hu.outcome?.status || hu.status || "pending";
|
|
244
|
+
if (counts[status] !== undefined) counts[status] += 1;
|
|
245
|
+
else counts.pending += 1;
|
|
246
|
+
if (hu.outcome?.pr_url) prs.push(hu.outcome.pr_url);
|
|
247
|
+
if (Array.isArray(hu.outcome?.blockers)) blockers.push(...hu.outcome.blockers);
|
|
248
|
+
}
|
|
249
|
+
let status = "done";
|
|
250
|
+
if (counts.failed > 0 || counts.blocked > 0) status = counts.done > 0 ? "partial" : "failed";
|
|
251
|
+
if (counts.done === 0 && counts.failed === 0 && counts.blocked === 0) status = "pending";
|
|
252
|
+
return {
|
|
253
|
+
status,
|
|
254
|
+
total: hus.length,
|
|
255
|
+
counts,
|
|
256
|
+
duration_ms,
|
|
257
|
+
// Dedupe at the rollup boundary — same PR / blocker reported by
|
|
258
|
+
// multiple HUs is noise in the banner.
|
|
259
|
+
prs: [...new Set(prs.filter(Boolean))],
|
|
260
|
+
blockers: [...new Set(blockers.filter(Boolean))],
|
|
261
|
+
finishedAt: new Date().toISOString(),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
96
265
|
/**
|
|
97
266
|
* Mark all pending HUs as certified (ready to execute).
|
|
98
267
|
* Sets plan status to "ready".
|