karajan-code 2.7.4 → 2.9.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 +1 -1
- 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/audit/basal-cost.js +92 -9
- package/src/audit/deterministic-summary.js +200 -0
- package/src/audit/osv-findings.js +162 -0
- package/src/audit/semgrep-findings.js +117 -0
- package/src/audit/sonar-findings.js +81 -0
- package/src/audit/webperf-input.js +90 -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 +3 -3
- package/src/checks/ci.js +2 -2
- package/src/checks/config-files.js +6 -6
- package/src/checks/dir-setup.js +4 -0
- package/src/checks/mcp-health.js +6 -0
- package/src/checks/ports.js +91 -29
- package/src/checks/rtk.js +1 -1
- package/src/checks/skills.js +5 -1
- package/src/checks/sonar.js +2 -2
- package/src/checks/system.js +3 -3
- package/src/checks/tokens.js +1 -1
- package/src/checks/types.js +0 -12
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +218 -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 +265 -30
- 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/doctor.js +1 -1
- 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 +78 -0
- package/src/config.js +30 -632
- 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 +11 -11
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +7 -3
- 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 +301 -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 +431 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/feedback-queue.js +0 -1
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +81 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- 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/role-output-compressor.js +0 -1
- package/src/orchestrator/session-journal.js +4 -41
- 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 +84 -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 -17
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -28
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +37 -6
- package/src/prompts/audit.js +206 -6
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/prompts/rtk-snippet.js +1 -1
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/audit-role.js +125 -13
- 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} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/api.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/display/event-handlers.js +1 -1
- package/src/utils/display/header.js +1 -1
- package/src/utils/display/session-summary.js +3 -3
- package/src/utils/display/solomon.js +1 -1
- 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/role-assigner.js +1 -1
- 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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj clean` — manual garbage collector.
|
|
3
|
+
*
|
|
4
|
+
* Shows or deletes accumulated state under `~/.kj/plans/` and
|
|
5
|
+
* `~/.karajan/` (sessions, HU story batches). Dry-run by default so
|
|
6
|
+
* the user can eyeball the plan before committing to it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* kj clean # dry-run, shows what would be removed
|
|
10
|
+
* kj clean --yes # actually delete
|
|
11
|
+
* kj clean --plan-days 60 # keep finalised plans for 60 days instead of 30
|
|
12
|
+
* kj clean --draft-days 90 # keep drafts for 90 days instead of 60
|
|
13
|
+
* kj clean --session-days 14 # keep finalised sessions for 14 days instead of 7
|
|
14
|
+
* kj clean --hu-days 30 # keep HU batches for 30 days instead of 14
|
|
15
|
+
*
|
|
16
|
+
* The auto-GC that runs at the start of every `kj run` / `kj plan`
|
|
17
|
+
* uses the same retention defaults; tweaking them here lets the user
|
|
18
|
+
* sweep more or less aggressively without editing config.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { runManualGC, summarizeGC, nukeBoardDb } from "../utils/garbage-collector.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {boolean} [opts.yes=false] - when false, dry-run only
|
|
26
|
+
* @param {boolean} [opts.nuke=false] - retention=0 for every bucket + wipe board DB
|
|
27
|
+
* @param {number} [opts.planDays=30]
|
|
28
|
+
* @param {number} [opts.draftDays=60]
|
|
29
|
+
* @param {number} [opts.sessionDays=7]
|
|
30
|
+
* @param {number} [opts.huDays=14]
|
|
31
|
+
* @param {Object} [opts.logger]
|
|
32
|
+
*/
|
|
33
|
+
export async function cleanCommand(opts = {}) {
|
|
34
|
+
const dryRun = !opts.yes;
|
|
35
|
+
// --nuke forces retention=0 everywhere so drafts / sessions / plans of
|
|
36
|
+
// any age are swept. Combined with wipeBoardDb this is the "I want it
|
|
37
|
+
// all gone right now" button.
|
|
38
|
+
const retention = opts.nuke
|
|
39
|
+
? { planRetentionDays: 0, draftRetentionDays: 0, sessionRetentionDays: 0, huRetentionDays: 0 }
|
|
40
|
+
: {
|
|
41
|
+
planRetentionDays: opts.planDays,
|
|
42
|
+
draftRetentionDays: opts.draftDays,
|
|
43
|
+
sessionRetentionDays: opts.sessionDays,
|
|
44
|
+
huRetentionDays: opts.huDays,
|
|
45
|
+
};
|
|
46
|
+
const result = await runManualGC({ ...retention, dryRun });
|
|
47
|
+
|
|
48
|
+
if (opts.nuke) {
|
|
49
|
+
const boardResult = await nukeBoardDb({ dryRun });
|
|
50
|
+
result.removed.push(...boardResult.removed);
|
|
51
|
+
result.bytesFreed += boardResult.bytesFreed;
|
|
52
|
+
result.errors.push(...boardResult.errors);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!result.removed.length) {
|
|
56
|
+
console.log("[clean] nothing to clean — ~/.kj/ and ~/.karajan/ are tidy.");
|
|
57
|
+
return { ok: true, removed: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
61
|
+
console.log(`[clean] ${verb} ${result.removed.length} item(s):`);
|
|
62
|
+
for (const r of result.removed) {
|
|
63
|
+
console.log(` - ${r.kind.padEnd(10)} ${r.path}`);
|
|
64
|
+
console.log(` ${r.reason}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const kb = Math.round(result.bytesFreed / 1024);
|
|
68
|
+
if (kb) console.log(`[clean] ${verb} ${kb} KiB total`);
|
|
69
|
+
|
|
70
|
+
if (result.errors.length) {
|
|
71
|
+
console.log(`[clean] ${result.errors.length} error(s) (non-blocking):`);
|
|
72
|
+
for (const e of result.errors) console.log(` - ${e.path}: ${e.error}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("[clean] DRY-RUN — no files touched. Re-run with --yes to actually delete.");
|
|
78
|
+
} else {
|
|
79
|
+
console.log(summarizeGC(result));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { ok: true, removed: result.removed.length, bytesFreed: result.bytesFreed };
|
|
83
|
+
}
|
package/src/commands/code.js
CHANGED
|
@@ -4,6 +4,7 @@ import { assertAgentsAvailable } from "../agents/availability.js";
|
|
|
4
4
|
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
5
5
|
import { resolveRole } from "../config.js";
|
|
6
6
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
7
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
7
8
|
|
|
8
9
|
export async function codeCommand({ task, config, logger }) {
|
|
9
10
|
return withCliRunLog("code", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
@@ -21,8 +22,12 @@ export async function codeCommand({ task, config, logger }) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const progress = createCliProgressReporter({ role: "coder" });
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = await coder.runTask({ prompt, onOutput: progress.onOutput, role: "coder" });
|
|
29
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
30
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
26
31
|
if (!result.ok) {
|
|
27
32
|
if (result.error) logger.error(result.error);
|
|
28
33
|
throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
|
package/src/commands/discover.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgent } from "../agents/index.js";
|
|
2
2
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
3
|
import { resolveRole } from "../config.js";
|
|
4
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
4
5
|
import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
|
|
5
6
|
|
|
6
7
|
function formatGaps(gaps, lines) {
|
|
@@ -71,8 +72,12 @@ export async function discoverCommand({ task, config, logger, mode, json }) {
|
|
|
71
72
|
|
|
72
73
|
const agent = createAgent(discoverRole.provider, config, logger);
|
|
73
74
|
const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const progress = createCliProgressReporter({ role: "discover" });
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "discover" });
|
|
79
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
80
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
76
81
|
|
|
77
82
|
if (!result.ok) {
|
|
78
83
|
throw new Error(result.error || result.output || "Discover failed");
|
package/src/commands/doctor.js
CHANGED
|
@@ -77,7 +77,7 @@ function confirmViaTty(describe) {
|
|
|
77
77
|
* @param {{ config: Object, checkOnly?: boolean, yes?: boolean, onConfirm?: Function, logger?: Object }} args
|
|
78
78
|
* @returns {Promise<import("../checks/types.js").RunReport>}
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
|
|
81
81
|
const checks = buildChecks(config);
|
|
82
82
|
return runCheckPipeline(checks, { config }, {
|
|
83
83
|
mode: checkOnly ? "check-only" : "fix",
|
package/src/commands/init.js
CHANGED
|
@@ -408,10 +408,10 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
408
408
|
// Telemetry: anonymous install event (non-blocking)
|
|
409
409
|
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
410
410
|
const { readFileSync } = await import("node:fs");
|
|
411
|
-
const { resolve, dirname } = await import("node:path");
|
|
412
411
|
const { fileURLToPath } = await import("node:url");
|
|
413
412
|
try {
|
|
414
|
-
|
|
413
|
+
// TSK-0340: use the already-static `path` default import for resolve/dirname.
|
|
414
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
415
415
|
const version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
416
416
|
sendTelemetryEvent("install", { version }, config).catch(() => {});
|
|
417
417
|
} catch { /* non-blocking */ }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ---- Formatting helpers shared across plan sub-commands ----
|
|
2
|
+
|
|
3
|
+
export function formatPlan(plan) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
if (plan.approach) lines.push("## Approach", plan.approach, "");
|
|
6
|
+
if (plan.steps?.length) {
|
|
7
|
+
lines.push("## Steps");
|
|
8
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
9
|
+
const step = plan.steps[i];
|
|
10
|
+
const commit = step.commit ? ` → \`${step.commit}\`` : "";
|
|
11
|
+
lines.push(`${i + 1}. ${step.description}${commit}`);
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
}
|
|
15
|
+
if (plan.risks?.length) {
|
|
16
|
+
lines.push("## Risks");
|
|
17
|
+
for (const risk of plan.risks) lines.push(`- ${risk}`);
|
|
18
|
+
lines.push("");
|
|
19
|
+
}
|
|
20
|
+
if (plan.outOfScope?.length) {
|
|
21
|
+
lines.push("## Out of scope");
|
|
22
|
+
for (const item of plan.outOfScope) lines.push(`- ${item}`);
|
|
23
|
+
lines.push("");
|
|
24
|
+
}
|
|
25
|
+
return lines.join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatHuTable(hus) {
|
|
29
|
+
if (!hus?.length) return " (no HUs)";
|
|
30
|
+
const lines = [];
|
|
31
|
+
const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
|
|
32
|
+
lines.push(` ${"ID".padEnd(30)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
|
|
33
|
+
lines.push(` ${"─".repeat(30)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
|
|
34
|
+
for (const hu of hus) {
|
|
35
|
+
const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
|
|
36
|
+
const deps = (hu.blocked_by || []).join(", ") || "—";
|
|
37
|
+
lines.push(` ${hu.id.padEnd(30)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan add-hu <planId> --title "..." --type sw --deps hu_xxx
|
|
3
|
+
*/
|
|
4
|
+
export async function planAddHuCommand({ config, planId, title, type, deps, scope }) {
|
|
5
|
+
const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const { addHu } = await import("../../plan/plan-hu-ops.js");
|
|
7
|
+
const projectDir = config.projectDir || process.cwd();
|
|
8
|
+
|
|
9
|
+
const plan = await loadPlan(projectDir, planId);
|
|
10
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
11
|
+
|
|
12
|
+
const hu = addHu(plan, {
|
|
13
|
+
title: title || "New HU",
|
|
14
|
+
task_type: type || "sw",
|
|
15
|
+
scope: scope || null,
|
|
16
|
+
blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await savePlan(projectDir, plan);
|
|
20
|
+
console.log(`Added: ${hu.id} — ${hu.title}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan delete <planId>
|
|
3
|
+
*/
|
|
4
|
+
export async function planDeleteCommand({ config, planId }) {
|
|
5
|
+
const { deletePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const projectDir = config.projectDir || process.cwd();
|
|
7
|
+
const ok = await deletePlan(projectDir, planId);
|
|
8
|
+
console.log(ok ? `Deleted: ${planId}` : `Not found: ${planId}`);
|
|
9
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createAgent } from "../../agents/index.js";
|
|
2
|
+
import { assertAgentsAvailable } from "../../agents/availability.js";
|
|
3
|
+
import { resolveRole } from "../../config.js";
|
|
4
|
+
import { buildPlannerPrompt } from "../../prompts/planner.js";
|
|
5
|
+
import { parseMaybeJsonString } from "../../review/parser.js";
|
|
6
|
+
import { createCliProgressReporter } from "../../utils/cli-progress.js";
|
|
7
|
+
import { runAutoGC, summarizeGC } from "../../utils/garbage-collector.js";
|
|
8
|
+
import { promptProjectName } from "../../utils/prompt-project-name.js";
|
|
9
|
+
import { deriveProjectNameFromCwd } from "../../utils/derive-project-name-from-cwd.js";
|
|
10
|
+
import { formatPlan, formatHuTable } from "./_shared.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* kj plan "task" — generate plan + HUs
|
|
14
|
+
*/
|
|
15
|
+
export async function planGenerateCommand({ task, config, logger, json, context, flags = {} }) {
|
|
16
|
+
const { withCliRunLog } = await import("../../utils/cli-run-log.js");
|
|
17
|
+
return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
18
|
+
return planGenerateImpl({ task, config, logger, json, context, runLog, flags });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {} }) {
|
|
23
|
+
// Auto-GC: prune orphan plans (project dir gone) + old finalised plans/
|
|
24
|
+
// sessions/HU batches before we start. Silent unless something was
|
|
25
|
+
// removed; one-liner summary on stderr in that case so --json stays
|
|
26
|
+
// untouched. Skipped in test env.
|
|
27
|
+
if (!process.env.VITEST && process.env.NODE_ENV !== "test") {
|
|
28
|
+
try {
|
|
29
|
+
const gcResult = await runAutoGC();
|
|
30
|
+
const line = summarizeGC(gcResult);
|
|
31
|
+
if (line && !json) process.stderr.write(`${line}\n`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger?.warn?.(`Auto-GC failed (non-blocking): ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const plannerRole = resolveRole(config, "planner");
|
|
38
|
+
await assertAgentsAvailable([plannerRole.provider]);
|
|
39
|
+
runLog.logText(`[planner] provider=${plannerRole.provider}`);
|
|
40
|
+
|
|
41
|
+
const planner = createAgent(plannerRole.provider, config, logger);
|
|
42
|
+
const prompt = buildPlannerPrompt({ task, context });
|
|
43
|
+
const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
|
|
44
|
+
? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
|
|
45
|
+
: undefined;
|
|
46
|
+
const timeoutMs = Number(config?.session?.max_planner_minutes) > 0
|
|
47
|
+
? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000)
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
// Live progress to stderr so the user sees what the planner is doing
|
|
51
|
+
// instead of staring at a silent shell for 30 s – 3 min. `--json` mode
|
|
52
|
+
// stays quiet so stdout remains a pure JSON document.
|
|
53
|
+
const progress = createCliProgressReporter({ role: "planner", quiet: Boolean(json) });
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await planner.runTask({
|
|
57
|
+
prompt, role: "planner", silenceTimeoutMs, timeoutMs,
|
|
58
|
+
onOutput: progress.onOutput,
|
|
59
|
+
});
|
|
60
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
61
|
+
} catch (err) {
|
|
62
|
+
progress.finish("failed");
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!result.ok) throw new Error(result.error || result.output || "Planner failed");
|
|
67
|
+
|
|
68
|
+
const parsed = parseMaybeJsonString(result.output);
|
|
69
|
+
|
|
70
|
+
// Create v2 plan with HUs from planner steps
|
|
71
|
+
const { createPlanV2 } = await import("../../plan/plan-schema.js");
|
|
72
|
+
const { addHu } = await import("../../plan/plan-hu-ops.js");
|
|
73
|
+
const { savePlan } = await import("../../plan/plan-store.js");
|
|
74
|
+
const { deriveProjectName, classifyTaskType } = await import("../../hu/auto-generator.js");
|
|
75
|
+
|
|
76
|
+
const projectDir = config.projectDir || process.cwd();
|
|
77
|
+
const plan = createPlanV2(task);
|
|
78
|
+
// Default project name resolution, in priority order:
|
|
79
|
+
// 1. cwd signals — package.json "name" → git remote basename →
|
|
80
|
+
// directory basename. This is what the user actually expects:
|
|
81
|
+
// if they ran `git init` in `~/ws_demo/weather-dashboard` and
|
|
82
|
+
// then asked Karajan to plan, the default name should reflect
|
|
83
|
+
// THAT directory, not the words of the SPEC document.
|
|
84
|
+
// 2. Fallback: derive from the task text (legacy heuristic). Used
|
|
85
|
+
// when none of the cwd signals yields anything (e.g. running
|
|
86
|
+
// from a tmp dir, no git, no package.json — typical for
|
|
87
|
+
// ad-hoc planning sessions).
|
|
88
|
+
// The user is still prompted to confirm / override on TTY runs;
|
|
89
|
+
// non-interactive runs (--json, --yes, board-spawned subprocesses)
|
|
90
|
+
// accept the default silently.
|
|
91
|
+
const defaultName = deriveProjectNameFromCwd(projectDir) || deriveProjectName(task);
|
|
92
|
+
const canPrompt = process.stdin.isTTY && process.stdout.isTTY
|
|
93
|
+
&& !json && !flags?.yes && flags?.interactive !== false;
|
|
94
|
+
plan.name = canPrompt ? await promptProjectName(defaultName) : defaultName;
|
|
95
|
+
plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
|
|
96
|
+
plan.risks = parsed?.risks || [];
|
|
97
|
+
plan.outOfScope = parsed?.outOfScope || [];
|
|
98
|
+
|
|
99
|
+
// Convert planner steps → HUs, carrying the structured acceptance_tests
|
|
100
|
+
// the LLM was asked to emit. If the LLM fell back to legacy behaviour
|
|
101
|
+
// (omitted tests or returned an empty array) we DO NOT inject a fake
|
|
102
|
+
// suite-wide placeholder — that was the pre-v2.7.5 lie. Instead we
|
|
103
|
+
// stamp `acceptance_tests: []` and the board / CLI flag the HU as
|
|
104
|
+
// "missing test contract" so the user can either regenerate or edit
|
|
105
|
+
// the tests manually before executing.
|
|
106
|
+
const { normaliseAcceptanceTests } = await import("../../plan/plan-schema.js");
|
|
107
|
+
const steps = parsed?.steps || [];
|
|
108
|
+
let prevId = null;
|
|
109
|
+
let stepsMissingTests = 0;
|
|
110
|
+
for (const step of steps) {
|
|
111
|
+
const desc = typeof step === "string" ? step : step.description || step.title || JSON.stringify(step);
|
|
112
|
+
const rawTests = typeof step === "object" ? step.acceptance_tests : null;
|
|
113
|
+
const tests = normaliseAcceptanceTests(rawTests);
|
|
114
|
+
if (tests.length === 0) stepsMissingTests += 1;
|
|
115
|
+
const hu = addHu(plan, {
|
|
116
|
+
title: desc.slice(0, 80),
|
|
117
|
+
task_type: classifyTaskType(desc),
|
|
118
|
+
scope: desc,
|
|
119
|
+
blocked_by: prevId ? [prevId] : [],
|
|
120
|
+
acceptance_tests: tests,
|
|
121
|
+
// Spec mapping (PR C): preserve the planner's section citation
|
|
122
|
+
// so the coder/reviewer / board can show "implements §5.3".
|
|
123
|
+
spec_section: typeof step === "object" && typeof step.spec_section === "string" ? step.spec_section.trim() || null : null,
|
|
124
|
+
});
|
|
125
|
+
prevId = hu.id;
|
|
126
|
+
}
|
|
127
|
+
// Tests-synthesizer pass (v2.7.5): the first planner call asks for
|
|
128
|
+
// acceptance_tests inline, but Claude / Codex frequently skip that
|
|
129
|
+
// sub-instruction on dense specs. If any HU came back without
|
|
130
|
+
// tests, run a focused second pass that ONLY synthesizes tests for
|
|
131
|
+
// those HUs. This is ON by default — the user is paying a CLI
|
|
132
|
+
// subscription, not per-token, so we optimise for plan quality, not
|
|
133
|
+
// round-trip cost. If `--no-tests-synth` (or `--quick`) is set, we
|
|
134
|
+
// skip and warn loudly that `kj run --plan` will refuse to execute.
|
|
135
|
+
const skipSynth = Boolean(flags?.noTestsSynth || flags?.quick);
|
|
136
|
+
if (stepsMissingTests > 0 && !skipSynth) {
|
|
137
|
+
runLog.logText(
|
|
138
|
+
`[planner] ${stepsMissingTests}/${steps.length} HUs missing acceptance_tests — running tests-synthesizer pass`
|
|
139
|
+
);
|
|
140
|
+
progress.onOutput?.({ line: `${stepsMissingTests} HU(s) missing tests — running synthesizer pass`, kind: "text" });
|
|
141
|
+
const { synthesizeMissingTests } = await import("../../plan/tests-synthesizer.js");
|
|
142
|
+
const husNeedingTests = plan.hus
|
|
143
|
+
.filter((h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0)
|
|
144
|
+
.map((h) => ({ id: h.id, title: h.title, scope: h.scope }));
|
|
145
|
+
const synthResult = await synthesizeMissingTests({
|
|
146
|
+
agent: planner,
|
|
147
|
+
task,
|
|
148
|
+
husNeedingTests,
|
|
149
|
+
onOutput: progress.onOutput,
|
|
150
|
+
silenceTimeoutMs,
|
|
151
|
+
timeoutMs,
|
|
152
|
+
});
|
|
153
|
+
if (synthResult.ok) {
|
|
154
|
+
let patched = 0;
|
|
155
|
+
for (const hu of plan.hus) {
|
|
156
|
+
const tests = synthResult.tests[hu.id];
|
|
157
|
+
if (tests && tests.length > 0) {
|
|
158
|
+
hu.acceptance_tests = tests;
|
|
159
|
+
hu.updatedAt = new Date().toISOString();
|
|
160
|
+
patched += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
stepsMissingTests = plan.hus.filter((h) => !h.acceptance_tests?.length).length;
|
|
164
|
+
runLog.logText(
|
|
165
|
+
`[planner] tests-synthesizer patched ${patched}/${husNeedingTests.length} HUs (${stepsMissingTests} still missing)`
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
runLog.logText(`[planner] tests-synthesizer failed: ${synthResult.error}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (stepsMissingTests > 0) {
|
|
173
|
+
runLog.logText(
|
|
174
|
+
`[planner] WARN: ${stepsMissingTests}/${steps.length} HUs still have no acceptance_tests after the synthesizer pass. `
|
|
175
|
+
+ `Edit them on the board (✎ Edit) or re-run \`kj plan\`. \`kj run --plan\` will refuse to execute these HUs.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Plan reviewer pass (PR D). Asks five closed-ended questions
|
|
180
|
+
// about the just-generated plan: missing HUs, missing deps, scope
|
|
181
|
+
// overlap, parallelisable groups, order issues. Output is purely
|
|
182
|
+
// advisory — we surface the findings to the user and they decide
|
|
183
|
+
// whether to apply them. Skipped when --no-plan-review or --quick.
|
|
184
|
+
const skipReview = Boolean(flags?.noPlanReview || flags?.quick);
|
|
185
|
+
if (plan.hus.length > 0 && !skipReview) {
|
|
186
|
+
runLog.logText("[planner] running plan reviewer pass");
|
|
187
|
+
progress.onOutput?.({ line: "Reviewing the plan for gaps / overlaps / order issues", kind: "text" });
|
|
188
|
+
const { reviewPlan, findingsCount } = await import("../../plan/plan-reviewer.js");
|
|
189
|
+
const review = await reviewPlan({
|
|
190
|
+
agent: planner,
|
|
191
|
+
task,
|
|
192
|
+
hus: plan.hus,
|
|
193
|
+
onOutput: progress.onOutput,
|
|
194
|
+
silenceTimeoutMs,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
});
|
|
197
|
+
if (review.ok) {
|
|
198
|
+
plan.review = review.findings;
|
|
199
|
+
const count = findingsCount(review.findings);
|
|
200
|
+
runLog.logText(
|
|
201
|
+
count > 0
|
|
202
|
+
? `[planner] reviewer surfaced ${count} item(s): ${review.findings.summary || "(see plan.review for details)"}`
|
|
203
|
+
: `[planner] reviewer found nothing actionable: ${review.findings.summary || "OK"}`
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
runLog.logText(`[planner] reviewer pass failed (non-blocking): ${review.error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const planId = await savePlan(projectDir, plan);
|
|
211
|
+
const { plansDir } = await import("../../plan/plan-store.js");
|
|
212
|
+
const path = await import("node:path");
|
|
213
|
+
const planPath = path.join(plansDir(projectDir), `${planId}.json`);
|
|
214
|
+
|
|
215
|
+
runLog.logText(`[planner] finished — hus=${plan.hus.length} plan=${planId}`);
|
|
216
|
+
|
|
217
|
+
if (json) {
|
|
218
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
219
|
+
return { ok: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (parsed?.approach) {
|
|
223
|
+
console.log(formatPlan(parsed));
|
|
224
|
+
} else if (plan.approach) {
|
|
225
|
+
console.log(plan.approach);
|
|
226
|
+
}
|
|
227
|
+
console.log(`\n## HUs (${plan.hus.length})`);
|
|
228
|
+
console.log(formatHuTable(plan.hus));
|
|
229
|
+
// Tell the user what just happened and what their two real options are.
|
|
230
|
+
// Pre-v2.7.5 we printed Review / Approve / Execute as three lines, which
|
|
231
|
+
// confused everyone:
|
|
232
|
+
// - "Review" sounded like an action; `kj plan show` is read-only.
|
|
233
|
+
// - "Approve" exposed the internal `kj plan ready` gate that the board
|
|
234
|
+
// workflow now bypasses (the board's "Run plan" button calls
|
|
235
|
+
// `kj run --plan` directly and the tests-first gate is enforced there).
|
|
236
|
+
// Now we just say: inspect, then run. The board is the recommended UI.
|
|
237
|
+
console.log(`\nPlan saved: ${planId} (${plan.hus.length} HUs, status: ${plan.status})`);
|
|
238
|
+
console.log(` → ${planPath}`);
|
|
239
|
+
// Reviewer findings: print a short summary so the user knows
|
|
240
|
+
// at-a-glance whether the plan needs attention. Full details live
|
|
241
|
+
// in `plan.review` and `kj plan show <id>` renders them.
|
|
242
|
+
if (plan.review) {
|
|
243
|
+
const r = plan.review;
|
|
244
|
+
const counts = [
|
|
245
|
+
r.missing_hus?.length ? `${r.missing_hus.length} missing HU(s)` : null,
|
|
246
|
+
r.missing_dependencies?.length ? `${r.missing_dependencies.length} missing dep(s)` : null,
|
|
247
|
+
r.scope_overlaps?.length ? `${r.scope_overlaps.length} overlap(s)` : null,
|
|
248
|
+
r.order_issues?.length ? `${r.order_issues.length} order issue(s)` : null,
|
|
249
|
+
].filter(Boolean);
|
|
250
|
+
if (counts.length === 0) {
|
|
251
|
+
console.log(`Reviewer: ✓ ${r.summary || "no issues"}`);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(`Reviewer flagged: ${counts.join(", ")} — see \`kj plan show ${planId}\``);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log(`Inspect: kj plan show ${planId}`);
|
|
258
|
+
console.log(`Run: kj run --plan ${planId} "${task.slice(0, 40)}..."`);
|
|
259
|
+
console.log(` (or click ▶ Run plan on the board, if running)`);
|
|
260
|
+
|
|
261
|
+
// UX boost for HU-bearing plans: auto-start the HU Board (same pattern as
|
|
262
|
+
// `kj run` auto-HU generation) so the user can inspect the 12-HU plan in a
|
|
263
|
+
// browser instead of squinting at the ASCII table. The board reads
|
|
264
|
+
// ~/.kj/plans/ directly, so this plan is visible immediately. Never in
|
|
265
|
+
// vitest / test mode, and never when `--json` is active.
|
|
266
|
+
if (plan.hus.length > 0 && !process.env.VITEST && process.env.NODE_ENV !== "test") {
|
|
267
|
+
try {
|
|
268
|
+
const { startBoard, renderBoardBanner } = await import("../board.js");
|
|
269
|
+
const { projectSlug: slugFor } = await import("../../plan/plan-store.js");
|
|
270
|
+
const boardPort = config?.hu_board?.port ?? 4000;
|
|
271
|
+
// `projectSlug` scopes the board URL to this project's view
|
|
272
|
+
// (`/p/<slug>`) instead of the global "All projects" dashboard. The
|
|
273
|
+
// board UI hides multi-project chrome when it sees that prefix.
|
|
274
|
+
const boardResult = await startBoard(boardPort, { projectSlug: slugFor(projectDir) });
|
|
275
|
+
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
276
|
+
console.log(renderBoardBanner({ url: boardResult.url, status, projectName: plan.name }));
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// Non-blocking: plan is saved, only the visualiser failed.
|
|
279
|
+
logger?.warn?.(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
280
|
+
console.log(`\nVisualize: kj board start (http://localhost:${config?.hu_board?.port ?? 4000})`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { ok: true };
|
|
284
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Barrel: re-export all plan sub-command handlers.
|
|
2
|
+
import { planGenerateCommand } from "./generate.js";
|
|
3
|
+
|
|
4
|
+
export { planGenerateCommand } from "./generate.js";
|
|
5
|
+
export { planListCommand } from "./list.js";
|
|
6
|
+
export { planShowCommand } from "./show.js";
|
|
7
|
+
export { planReadyCommand } from "./ready.js";
|
|
8
|
+
export { planValidateCommand } from "./validate.js";
|
|
9
|
+
export { planDeleteCommand } from "./delete.js";
|
|
10
|
+
export { planAddHuCommand } from "./add-hu.js";
|
|
11
|
+
export { planRemoveHuCommand } from "./remove-hu.js";
|
|
12
|
+
|
|
13
|
+
// Keep backward compat — old callers import planCommand
|
|
14
|
+
export const planCommand = planGenerateCommand;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan list
|
|
3
|
+
*/
|
|
4
|
+
export async function planListCommand({ config }) {
|
|
5
|
+
const { listPlans } = await import("../../plan/plan-store.js");
|
|
6
|
+
const projectDir = config.projectDir || process.cwd();
|
|
7
|
+
const plans = await listPlans(projectDir);
|
|
8
|
+
|
|
9
|
+
if (plans.length === 0) {
|
|
10
|
+
console.log("No plans found. Create one with: kj plan \"your task\"");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(`Plans for ${projectDir}:\n`);
|
|
15
|
+
for (const p of plans) {
|
|
16
|
+
const husInfo = p.huCount > 0 ? ` (${p.huCount} HUs)` : "";
|
|
17
|
+
const status = p.status !== "draft" ? ` [${p.status}]` : "";
|
|
18
|
+
console.log(` ${p.planId} ${(p.name || p.task || "").slice(0, 50)}${husInfo}${status}`);
|
|
19
|
+
console.log(` Created: ${p.createdAt}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan ready <planId>
|
|
3
|
+
*/
|
|
4
|
+
export async function planReadyCommand({ config, planId }) {
|
|
5
|
+
const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const { certifyAllHus } = await import("../../plan/plan-hu-ops.js");
|
|
7
|
+
const { validatePlan } = await import("../../plan/plan-schema.js");
|
|
8
|
+
const projectDir = config.projectDir || process.cwd();
|
|
9
|
+
|
|
10
|
+
const plan = await loadPlan(projectDir, planId);
|
|
11
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
12
|
+
|
|
13
|
+
const validation = validatePlan(plan);
|
|
14
|
+
if (!validation.valid) {
|
|
15
|
+
console.error("Plan validation failed:");
|
|
16
|
+
for (const err of validation.errors) console.error(` - ${err}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (plan.hus.length === 0) {
|
|
22
|
+
console.error("Cannot approve a plan with 0 HUs.");
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Tests-first gate: refuse to certify HUs that have no acceptance_tests.
|
|
28
|
+
// A "ready" plan must have an executable test contract per HU, otherwise
|
|
29
|
+
// the coder has nothing to aim for. Flag each offender, then bail so the
|
|
30
|
+
// user can edit the plan (board ✎ Edit or `kj plan add-hu`) and retry.
|
|
31
|
+
const missing = plan.hus.filter((h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0);
|
|
32
|
+
if (missing.length > 0) {
|
|
33
|
+
console.error(`Cannot mark ready: ${missing.length}/${plan.hus.length} HU(s) have no acceptance_tests:`);
|
|
34
|
+
for (const h of missing) console.error(` - ${h.id} ${(h.title || "").slice(0, 60)}`);
|
|
35
|
+
console.error("");
|
|
36
|
+
console.error("Tests-first flow requires a test contract per HU. Edit each HU on the board");
|
|
37
|
+
console.error("(http://localhost:4000) via the ✎ Edit button, or re-run `kj plan` to regenerate.");
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const count = certifyAllHus(plan);
|
|
43
|
+
await savePlan(projectDir, plan);
|
|
44
|
+
console.log(`✅ ${count} HUs certified. Plan status: ready`);
|
|
45
|
+
console.log(`Run: kj run --plan ${planId} "${(plan.task || "").slice(0, 40)}..."`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan remove-hu <planId> <huId>
|
|
3
|
+
*/
|
|
4
|
+
export async function planRemoveHuCommand({ config, planId, huId }) {
|
|
5
|
+
const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const { removeHu } = await import("../../plan/plan-hu-ops.js");
|
|
7
|
+
const projectDir = config.projectDir || process.cwd();
|
|
8
|
+
|
|
9
|
+
const plan = await loadPlan(projectDir, planId);
|
|
10
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
11
|
+
|
|
12
|
+
const ok = removeHu(plan, huId);
|
|
13
|
+
if (!ok) { console.error(`HU not found: ${huId}`); process.exitCode = 1; return; }
|
|
14
|
+
|
|
15
|
+
await savePlan(projectDir, plan);
|
|
16
|
+
console.log(`Removed: ${huId}`);
|
|
17
|
+
}
|