karajan-code 2.4.1 → 2.6.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.
Files changed (115) hide show
  1. package/README.md +20 -4
  2. package/package.json +5 -2
  3. package/src/agents/aider-agent.js +1 -2
  4. package/src/agents/base-agent.js +63 -1
  5. package/src/agents/claude-agent.js +3 -4
  6. package/src/agents/codex-agent.js +1 -2
  7. package/src/agents/gemini-agent.js +1 -2
  8. package/src/agents/index.js +11 -2
  9. package/src/agents/opencode-agent.js +1 -2
  10. package/src/brain/decision-tracker.js +123 -0
  11. package/src/brain/decisor.js +243 -0
  12. package/src/brain/solomon-consult.js +163 -0
  13. package/src/budget/comparison.js +96 -0
  14. package/src/checks/binaries.js +123 -0
  15. package/src/checks/ci.js +105 -0
  16. package/src/checks/config-files.js +217 -0
  17. package/src/checks/dir-setup.js +63 -0
  18. package/src/checks/mcp-health.js +157 -0
  19. package/src/checks/node.js +61 -0
  20. package/src/checks/ports.js +110 -0
  21. package/src/checks/rtk.js +36 -0
  22. package/src/checks/runner.js +267 -0
  23. package/src/checks/skills.js +49 -0
  24. package/src/checks/sonar.js +67 -0
  25. package/src/checks/system.js +53 -0
  26. package/src/checks/tokens.js +96 -0
  27. package/src/checks/types.js +105 -0
  28. package/src/cli.js +90 -7
  29. package/src/commands/board.js +23 -0
  30. package/src/commands/doctor.js +130 -362
  31. package/src/commands/plan.js +221 -37
  32. package/src/commands/resume.js +1 -1
  33. package/src/commands/run.js +2 -1
  34. package/src/commands/skills.js +49 -0
  35. package/src/config/schema.js +222 -0
  36. package/src/config.js +32 -3
  37. package/src/guards/policy-resolver.js +8 -0
  38. package/src/hu/store.js +9 -0
  39. package/src/infrastructure/command-runner.js +44 -0
  40. package/src/infrastructure/environment.js +38 -0
  41. package/src/infrastructure/file-system-service.js +63 -0
  42. package/src/infrastructure/mocks.js +182 -0
  43. package/src/mcp/handlers/management-handlers.js +7 -2
  44. package/src/mcp/tools.js +10 -3
  45. package/src/orchestrator/config-init.js +16 -1
  46. package/src/orchestrator/flow-runner.js +2085 -0
  47. package/src/orchestrator/pipeline-context.js +17 -0
  48. package/src/orchestrator/preflight-checks.js +78 -4
  49. package/src/orchestrator/solomon-escalation.js +21 -0
  50. package/src/orchestrator/stages/stage-executor.js +155 -0
  51. package/src/orchestrator.js +19 -1884
  52. package/src/plan/plan-executor.js +69 -0
  53. package/src/plan/plan-hu-ops.js +134 -0
  54. package/src/plan/plan-id.js +25 -0
  55. package/src/plan/plan-schema.js +139 -0
  56. package/src/plan/plan-store.js +58 -25
  57. package/src/prompts/architect.js +2 -2
  58. package/src/prompts/coder.js +2 -2
  59. package/src/prompts/prompt-resolver.js +127 -0
  60. package/src/prompts/reviewer.js +2 -2
  61. package/src/roles/architect-role.js +3 -1
  62. package/src/roles/base-role.js +23 -2
  63. package/src/roles/coder-role.js +3 -1
  64. package/src/roles/security-role.js +12 -0
  65. package/src/roles/tester-role.js +11 -0
  66. package/src/session/journal/decisions-writer.js +200 -0
  67. package/src/session/journal/iteration-logger.js +194 -0
  68. package/src/session/journal/summary-writer.js +176 -0
  69. package/src/session/journal/tree-writer.js +164 -0
  70. package/src/skills/semantic-detector.js +147 -0
  71. package/src/skills/skill-detector.js +186 -8
  72. package/src/skills/skill-loader.js +131 -1
  73. package/src/skills/skills-cache.js +128 -0
  74. package/src/types/agent.js +55 -0
  75. package/src/types/config.js +114 -0
  76. package/src/types/hu.js +47 -0
  77. package/src/types/index.js +34 -0
  78. package/src/types/policy.js +43 -0
  79. package/src/types/session.js +74 -0
  80. package/src/types/stage.js +68 -0
  81. package/src/utils/display/event-handlers.js +389 -0
  82. package/src/utils/display/formatters.js +132 -0
  83. package/src/utils/display/header.js +48 -0
  84. package/src/utils/display/session-summary.js +108 -0
  85. package/src/utils/display/solomon.js +42 -0
  86. package/src/utils/port-check.js +45 -0
  87. package/src/utils/port-occupant.js +95 -0
  88. package/src/utils/provider-env.js +116 -0
  89. package/src/utils/with-timeout.js +42 -0
  90. package/templates/roles/architect/anthropic.md +65 -0
  91. package/templates/roles/architect/default.md +63 -0
  92. package/templates/roles/architect/google.md +52 -0
  93. package/templates/roles/architect/openai.md +51 -0
  94. package/templates/roles/architect/opencode.md +50 -0
  95. package/templates/roles/coder/anthropic.md +72 -0
  96. package/templates/roles/coder/default.md +69 -0
  97. package/templates/roles/coder/google.md +78 -0
  98. package/templates/roles/coder/openai.md +59 -0
  99. package/templates/roles/coder/opencode.md +49 -0
  100. package/templates/roles/planner/anthropic.md +50 -0
  101. package/templates/roles/planner/default.md +48 -0
  102. package/templates/roles/planner/google.md +66 -0
  103. package/templates/roles/planner/openai.md +46 -0
  104. package/templates/roles/planner/opencode.md +53 -0
  105. package/templates/roles/reviewer/anthropic.md +61 -0
  106. package/templates/roles/reviewer/default.md +62 -0
  107. package/templates/roles/reviewer/google.md +63 -0
  108. package/templates/roles/reviewer/openai.md +57 -0
  109. package/templates/roles/reviewer/opencode.md +42 -0
  110. package/templates/roles/solomon/anthropic.md +71 -0
  111. package/templates/roles/solomon/default.md +83 -0
  112. package/templates/roles/solomon/google.md +61 -0
  113. package/templates/roles/solomon/openai.md +53 -0
  114. package/templates/roles/solomon/opencode.md +55 -0
  115. package/src/utils/display.js +0 -669
@@ -0,0 +1,49 @@
1
+ /**
2
+ * OpenSkills CLI availability check. Only runs when skills are enabled.
3
+ * Strategy: prompt — we can offer to run `npm install -g openskills`,
4
+ * but global installs are invasive enough that they need user consent.
5
+ */
6
+
7
+ import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
8
+ import { runCommand } from "../utils/process.js";
9
+ import { STRATEGY } from "./types.js";
10
+
11
+ export function skillsEnabled(config) {
12
+ return config?.skills?.enabled !== false;
13
+ }
14
+
15
+ export function createOpenSkillsCheck() {
16
+ return {
17
+ name: "openskills",
18
+ label: "OpenSkills CLI",
19
+ strategy: STRATEGY.PROMPT,
20
+ applies: skillsEnabled,
21
+ describe: "Install OpenSkills globally: npm install -g openskills",
22
+ async detect() {
23
+ const available = await isOpenSkillsAvailable();
24
+ if (available) {
25
+ return { ok: true, severity: "info", detail: "Available via `npx openskills`" };
26
+ }
27
+ return {
28
+ ok: false,
29
+ severity: "warn", // skills enabled but missing → degrade gracefully
30
+ detail: "`npx openskills` not available — skill injection will be skipped",
31
+ fix: "Install OpenSkills: npm install -g openskills",
32
+ };
33
+ },
34
+ async remediate() {
35
+ const res = await runCommand("npm", ["install", "-g", "openskills"], { timeout: 60000 });
36
+ if (res.exitCode === 0) {
37
+ return { fixed: true, detail: "Installed openskills globally via npm" };
38
+ }
39
+ return {
40
+ fixed: false,
41
+ detail: `npm install -g openskills failed (exit ${res.exitCode}): ${res.stderr.split("\n")[0]}`,
42
+ };
43
+ },
44
+ };
45
+ }
46
+
47
+ export function getSkillsChecks() {
48
+ return [createOpenSkillsCheck()];
49
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SonarQube reachability + auth checks.
3
+ *
4
+ * Auto-remediation strategy (preserving existing behavior from
5
+ * preflight-checks.js): if Sonar is not reachable, try `sonarUp()` which
6
+ * starts the Docker container, then poll for readiness. If auth fails but
7
+ * admin credentials exist, generate a fresh token and persist it.
8
+ */
9
+
10
+ import { isSonarReachable } from "../sonar/manager.js";
11
+ import { STRATEGY } from "./types.js";
12
+ import { withDocLink } from "../utils/doc-links.js";
13
+
14
+ /**
15
+ * Whether Sonar is enabled in the active config.
16
+ */
17
+ function sonarEnabled(config) {
18
+ return config.sonarqube?.enabled !== false;
19
+ }
20
+
21
+ function sonarHost(config) {
22
+ return config.sonarqube?.host || "http://localhost:9000";
23
+ }
24
+
25
+ /**
26
+ * Informative check: Sonar enabled/disabled status.
27
+ * Produces an OK result with disabled description when Sonar is off.
28
+ */
29
+ export function createSonarStatusCheck() {
30
+ return {
31
+ name: "sonarqube",
32
+ label: "SonarQube",
33
+ strategy: STRATEGY.NONE,
34
+ async detect({ config }) {
35
+ const host = sonarHost(config);
36
+ if (!sonarEnabled(config)) {
37
+ return { ok: true, severity: "info", detail: "Disabled in config" };
38
+ }
39
+ let reachable = false;
40
+ try {
41
+ reachable = await isSonarReachable(host);
42
+ } catch {
43
+ reachable = false;
44
+ }
45
+ return {
46
+ ok: reachable,
47
+ severity: "fail",
48
+ detail: reachable ? `Reachable at ${host}` : `Not reachable at ${host}`,
49
+ fix: reachable
50
+ ? undefined
51
+ : withDocLink(
52
+ "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.",
53
+ "sonar_docker"
54
+ ),
55
+ extra: { host, reachable },
56
+ };
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * All Sonar-related checks. Keeps just the status check for commit 1;
63
+ * auth + auto-remediation are layered in commit 3.
64
+ */
65
+ export function getSonarChecks() {
66
+ return [createSonarStatusCheck()];
67
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Miscellaneous system-level checks: Karajan version, git repo presence.
3
+ * Kept in a dedicated module so they're easy to find and extend.
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ import path from "node:path";
9
+ import { ensureGitRepo } from "../utils/git.js";
10
+ import { STRATEGY } from "./types.js";
11
+
12
+ function getPackageVersion() {
13
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
14
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version;
15
+ }
16
+
17
+ export function createKarajanVersionCheck() {
18
+ return {
19
+ name: "karajan",
20
+ label: "Karajan Code",
21
+ strategy: STRATEGY.NONE,
22
+ async detect() {
23
+ const version = getPackageVersion();
24
+ return { ok: true, severity: "info", detail: `v${version}` };
25
+ },
26
+ };
27
+ }
28
+
29
+ export function createGitRepoCheck() {
30
+ return {
31
+ name: "git",
32
+ label: "Git repository",
33
+ strategy: STRATEGY.MANUAL,
34
+ async detect() {
35
+ let gitOk = false;
36
+ try {
37
+ gitOk = await ensureGitRepo();
38
+ } catch {
39
+ gitOk = false;
40
+ }
41
+ return {
42
+ ok: gitOk,
43
+ severity: "fail",
44
+ detail: gitOk ? "Inside a git repository" : "Not a git repository",
45
+ fix: gitOk ? undefined : "Run 'git init' or navigate to a git-managed project.",
46
+ };
47
+ },
48
+ };
49
+ }
50
+
51
+ export function getSystemChecks() {
52
+ return [createKarajanVersionCheck(), createGitRepoCheck()];
53
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * API token availability checks.
3
+ *
4
+ * Provider tokens (ANTHROPIC_API_KEY / OPENAI_API_KEY / ...) are LAZY:
5
+ * we only check them for providers that are actually used by at least one
6
+ * active role in the current config.
7
+ *
8
+ * Strategy:
9
+ * - Required providers with no key → FAIL, strategy manual (we can't
10
+ * fetch an API key for the user). Fix hint includes the how-to URL.
11
+ * - Optional providers (opencode, aider) without key → WARN.
12
+ * - GH_TOKEN is checked when git.auto_pr=true, strategy prompt (we can
13
+ * launch `gh auth login` with user approval).
14
+ */
15
+
16
+ import { checkBinary } from "../utils/agent-detect.js";
17
+ import { getRequiredProviderEnvs, hasEnvVar } from "../utils/provider-env.js";
18
+ import { STRATEGY } from "./types.js";
19
+
20
+ /**
21
+ * A single provider env check. One is created per active provider.
22
+ */
23
+ function createProviderTokenCheck(entry) {
24
+ return {
25
+ name: `token:${entry.provider}`,
26
+ label: `API token: ${entry.provider}`,
27
+ strategy: STRATEGY.MANUAL,
28
+ async detect() {
29
+ const ok = hasEnvVar(entry);
30
+ if (ok) {
31
+ return { ok: true, severity: "info", detail: `${entry.envVars[0]} is set` };
32
+ }
33
+ return {
34
+ ok: false,
35
+ severity: entry.optional ? "warn" : "fail",
36
+ detail: `${entry.envVars.join(" / ")} not set`,
37
+ fix: `Get an API key at ${entry.howToGetUrl} and export ${entry.envVars[0]}=...`,
38
+ extra: { provider: entry.provider, envVars: entry.envVars, howToGetUrl: entry.howToGetUrl },
39
+ };
40
+ },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * GH token check + auto-remediation via `gh auth login` when it's installed.
46
+ */
47
+ function createGhTokenCheck() {
48
+ return {
49
+ name: "token:gh",
50
+ label: "GitHub token (for auto_pr)",
51
+ strategy: STRATEGY.PROMPT,
52
+ applies: (config) => config?.git?.auto_pr === true,
53
+ describe: "Run 'gh auth login' to authenticate with GitHub",
54
+ async detect() {
55
+ const envOk = !!process.env.GH_TOKEN || !!process.env.GITHUB_TOKEN;
56
+ if (envOk) {
57
+ return { ok: true, severity: "info", detail: "GH_TOKEN or GITHUB_TOKEN is set" };
58
+ }
59
+ // If `gh` is installed and authenticated, it manages its own token.
60
+ const gh = await checkBinary("gh");
61
+ if (!gh.ok) {
62
+ return {
63
+ ok: false,
64
+ severity: "fail",
65
+ detail: "No GH_TOKEN/GITHUB_TOKEN and `gh` CLI not installed",
66
+ fix: "Install GitHub CLI (https://cli.github.com) or export GH_TOKEN=...",
67
+ extra: { hasGh: false },
68
+ };
69
+ }
70
+ return {
71
+ ok: false,
72
+ severity: "fail",
73
+ detail: "No GH_TOKEN/GITHUB_TOKEN; `gh` is installed but auth status unknown",
74
+ fix: "Run 'gh auth login' or export GH_TOKEN=...",
75
+ extra: { hasGh: true },
76
+ };
77
+ },
78
+ // remediate() intentionally omitted: launching `gh auth login` requires an
79
+ // interactive terminal session. The runner's prompt strategy will point
80
+ // the user to the command and exit with FAIL in non-TTY contexts.
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Build the list of token checks for the active config. Each active provider
86
+ * gets one, plus conditional GH token when auto_pr is on.
87
+ *
88
+ * @param {Object} config
89
+ * @returns {import("./types.js").Check[]}
90
+ */
91
+ export function getTokenChecks(config) {
92
+ const required = getRequiredProviderEnvs(config);
93
+ const checks = required.map(createProviderTokenCheck);
94
+ checks.push(createGhTokenCheck());
95
+ return checks;
96
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared check schema for doctor/preflight subsystem.
3
+ *
4
+ * A Check describes an environment requirement (binary present, port free,
5
+ * token configured, MCP reachable...). Each check can optionally remediate
6
+ * itself when it fails, using one of 4 strategies:
7
+ *
8
+ * - "auto" → fix without asking. Use for non-invasive, reversible changes
9
+ * (in-memory runtime overrides, starting existing services,
10
+ * creating files under ~/.karajan/, etc.)
11
+ * - "prompt" → ask user before fixing. Use for invasive changes that touch
12
+ * the user's system outside Karajan's scope (global npm install,
13
+ * writing kj.config.yml, `gh auth login`, etc.)
14
+ * - "manual" → cannot auto-fix, only report a hint (Node runtime upgrade,
15
+ * missing Docker, missing API key with no admin creds)
16
+ * - "none" → informational check, always OK (Karajan version, feature
17
+ * disabled by config)
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} CheckResult
22
+ * @property {boolean} ok - true if check passes
23
+ * @property {"fail"|"warn"|"info"} [severity] - relevant when !ok; default "fail"
24
+ * @property {string} detail - human-readable status
25
+ * @property {string} [fix] - remediation hint shown when manual or user declines
26
+ * @property {Object} [extra] - structured data consumed by remediate (e.g. occupant PID, host, roles)
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} RemediationContext
31
+ * @property {Object} config - resolved Karajan config
32
+ * @property {AbortSignal} [signal] - cancellation
33
+ * @property {Object} extra - check's CheckResult.extra
34
+ * @property {Object} logger - logger instance
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} RemediationResult
39
+ * @property {boolean} fixed - true if remediation succeeded
40
+ * @property {string} detail - what was done, human-readable
41
+ * @property {Object} [changes] - runtime config overrides to merge into the active session
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} Check
46
+ * @property {string} name - slug, unique across all checks (e.g. "node-version")
47
+ * @property {string} label - human label (e.g. "Node.js runtime")
48
+ * @property {"auto"|"prompt"|"manual"|"none"} strategy
49
+ * @property {(ctx: { config: Object }) => Promise<CheckResult>} detect
50
+ * @property {(ctx: RemediationContext) => Promise<RemediationResult>} [remediate]
51
+ * @property {string} [describe] - shown to user when strategy is "prompt"
52
+ * @property {(config: Object) => boolean} [applies] - if false, check is SKIPPED (lazy evaluation). Default: always applies.
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object} CheckReport
57
+ * @property {string} name
58
+ * @property {string} label
59
+ * @property {"OK"|"FIXED"|"WARN"|"FAIL"|"SKIPPED"|"TIMEOUT"} status
60
+ * @property {"auto"|"prompt"|"manual"|"none"} strategy
61
+ * @property {string} detail - detect detail or remediate detail when FIXED
62
+ * @property {string} [fix] - remediation hint for FAIL/WARN
63
+ * @property {number} runMs - total time (detect + remediate + re-verify)
64
+ * @property {boolean} [cached] - future: served from cache
65
+ */
66
+
67
+ /**
68
+ * @typedef {Object} RunReport
69
+ * @property {CheckReport[]} checks
70
+ * @property {Object} overrides - merged runtime overrides from auto-remediations
71
+ * @property {number} totalMs
72
+ * @property {{ ok: number, fixed: number, warn: number, fail: number, skipped: number, timeout: number }} summary
73
+ */
74
+
75
+ /**
76
+ * Status constants.
77
+ */
78
+ export const STATUS = Object.freeze({
79
+ OK: "OK",
80
+ FIXED: "FIXED",
81
+ WARN: "WARN",
82
+ FAIL: "FAIL",
83
+ SKIPPED: "SKIPPED",
84
+ TIMEOUT: "TIMEOUT",
85
+ });
86
+
87
+ export const STRATEGY = Object.freeze({
88
+ AUTO: "auto",
89
+ PROMPT: "prompt",
90
+ MANUAL: "manual",
91
+ NONE: "none",
92
+ });
93
+
94
+ export const SEVERITY = Object.freeze({
95
+ FAIL: "fail",
96
+ WARN: "warn",
97
+ INFO: "info",
98
+ });
99
+
100
+ /**
101
+ * Whether the final status blocks the pipeline (for preflight).
102
+ */
103
+ export function isBlocking(status) {
104
+ return status === STATUS.FAIL || status === STATUS.TIMEOUT;
105
+ }
package/src/cli.js CHANGED
@@ -128,6 +128,10 @@ program
128
128
  .option("--no-smart-models", "Disable smart model selection")
129
129
  .option("--design", "Activate design refactoring mode (impeccable role applies design changes)")
130
130
  .option("--domain <text-or-path>", "Domain knowledge: inline text or path to .md file")
131
+ .option("--skills-mode <mode>", "Skill detection mode: auto|regex|semantic|none (default: auto)")
132
+ .option("--brain <mode>", "Brain decisor: on|off (default: on). When off, only legacy flags drive routing.")
133
+ .option("--skip-role <role...>", "Force one or more roles OFF regardless of triage (e.g. --skip-role tester security)")
134
+ .option("--force-role <role...>", "Force one or more roles ON regardless of triage")
131
135
  .option("--dry-run", "Show what would be executed without running anything")
132
136
  .option("--json", "Output JSON only (no styled display)")
133
137
  .option("-q, --quiet", "Show only stage status lines, suppress raw agent output (default)")
@@ -172,9 +176,22 @@ program
172
176
 
173
177
  program
174
178
  .command("doctor")
175
- .description("Check environment requirements")
176
- .action(async () => {
177
- await withConfig("doctor", {}, doctorCommand);
179
+ .description("Check environment requirements and auto-remediate when possible")
180
+ .option("--check-only", "Detect issues without applying fixes")
181
+ .option("-y, --yes", "Auto-confirm prompts for invasive remediations (CI mode)")
182
+ .option("--json", "Emit machine-readable JSON instead of human output")
183
+ .option("--verbose", "Include fix hints and timing for every check")
184
+ .action(async (flags) => {
185
+ const exitCode = await withConfig("doctor", {}, (ctx) =>
186
+ doctorCommand({
187
+ ...ctx,
188
+ checkOnly: !!flags.checkOnly,
189
+ yes: !!flags.yes,
190
+ json: !!flags.json,
191
+ verbose: !!flags.verbose,
192
+ })
193
+ );
194
+ if (Number.isInteger(exitCode)) process.exit(exitCode);
178
195
  });
179
196
 
180
197
  program
@@ -198,6 +215,15 @@ program
198
215
  await reportCommand(flags);
199
216
  });
200
217
 
218
+ program
219
+ .command("skills [action]")
220
+ .description("Manage the local OpenSkills cache (actions: list | clear-cache)")
221
+ .action(async (action) => {
222
+ const { skillsCommand } = await import("./commands/skills.js");
223
+ const exitCode = await skillsCommand({ action: action || "list" });
224
+ if (Number.isInteger(exitCode)) process.exit(exitCode);
225
+ });
226
+
201
227
  program
202
228
  .command("roles [subcommand] [role]")
203
229
  .description("List pipeline roles or show role template instructions")
@@ -217,9 +243,11 @@ program
217
243
  });
218
244
  });
219
245
 
220
- program
221
- .command("plan")
222
- .description("Generate implementation plan")
246
+ const plan = program.command("plan").description("Plan management (generate, review, approve, execute)");
247
+
248
+ plan
249
+ .command("generate", { isDefault: true })
250
+ .description("Generate implementation plan with HUs")
223
251
  .argument("<task>")
224
252
  .option("--planner <name>")
225
253
  .option("--planner-model <name>")
@@ -227,10 +255,65 @@ program
227
255
  .option("--json", "Output raw JSON plan")
228
256
  .action(async (task, flags) => {
229
257
  await withConfig("plan", flags, async ({ config, logger }) => {
230
- await planCommand({ task, config, logger, json: flags.json, context: flags.context });
258
+ const { planGenerateCommand } = await import("./commands/plan.js");
259
+ await planGenerateCommand({ task, config, logger, json: flags.json, context: flags.context });
260
+ });
261
+ });
262
+
263
+ plan.command("list").description("List all plans").action(async (flags) => {
264
+ await withConfig("plan", flags, async ({ config }) => {
265
+ const { planListCommand } = await import("./commands/plan.js");
266
+ await planListCommand({ config });
267
+ });
268
+ });
269
+
270
+ plan.command("show").description("Show plan details + HUs").argument("<planId>").action(async (planId, flags) => {
271
+ await withConfig("plan", flags, async ({ config }) => {
272
+ const { planShowCommand } = await import("./commands/plan.js");
273
+ await planShowCommand({ config, planId });
274
+ });
275
+ });
276
+
277
+ plan.command("ready").description("Approve plan — certify all HUs").argument("<planId>").action(async (planId, flags) => {
278
+ await withConfig("plan", flags, async ({ config }) => {
279
+ const { planReadyCommand } = await import("./commands/plan.js");
280
+ await planReadyCommand({ config, planId });
281
+ });
282
+ });
283
+
284
+ plan.command("validate").description("Validate plan structure").argument("<planId>").action(async (planId, flags) => {
285
+ await withConfig("plan", flags, async ({ config }) => {
286
+ const { planValidateCommand } = await import("./commands/plan.js");
287
+ await planValidateCommand({ config, planId });
288
+ });
289
+ });
290
+
291
+ plan.command("delete").description("Delete a plan").argument("<planId>").action(async (planId, flags) => {
292
+ await withConfig("plan", flags, async ({ config }) => {
293
+ const { planDeleteCommand } = await import("./commands/plan.js");
294
+ await planDeleteCommand({ config, planId });
295
+ });
296
+ });
297
+
298
+ plan.command("add-hu").description("Add HU to plan").argument("<planId>")
299
+ .option("--title <text>", "HU title")
300
+ .option("--type <type>", "Task type: sw|infra|doc|add-tests|refactor|nocode", "sw")
301
+ .option("--deps <ids>", "Comma-separated HU IDs this depends on")
302
+ .option("--scope <text>", "Scope description")
303
+ .action(async (planId, flags) => {
304
+ await withConfig("plan", flags, async ({ config }) => {
305
+ const { planAddHuCommand } = await import("./commands/plan.js");
306
+ await planAddHuCommand({ config, planId, title: flags.title, type: flags.type, deps: flags.deps, scope: flags.scope });
231
307
  });
232
308
  });
233
309
 
310
+ plan.command("remove-hu").description("Remove HU from plan").argument("<planId>").argument("<huId>").action(async (planId, huId, flags) => {
311
+ await withConfig("plan", flags, async ({ config }) => {
312
+ const { planRemoveHuCommand } = await import("./commands/plan.js");
313
+ await planRemoveHuCommand({ config, planId, huId });
314
+ });
315
+ });
316
+
234
317
  program
235
318
  .command("discover")
236
319
  .description("Analyze task for gaps, ambiguities and missing info")
@@ -115,6 +115,29 @@ export async function boardStatus(port = 4000) {
115
115
  };
116
116
  }
117
117
 
118
+ /**
119
+ * Render a highlighted cyan box with the HU Board URL + optional project name.
120
+ * Centralizes the banner used by both the init-time auto-start (flow-runner.js)
121
+ * and the post-planner auto-HU generation safety net so they stay in sync.
122
+ *
123
+ * @param {Object} params
124
+ * @param {string} params.url - "http://localhost:4000"
125
+ * @param {string} params.status - "started" | "already running"
126
+ * @param {string} [params.projectName] - optional project label
127
+ * @returns {string} ANSI-escaped box ready for console.log
128
+ */
129
+ export function renderBoardBanner({ url, status, projectName }) {
130
+ const title = projectName ? ` Project: \u001b[1m${projectName}\u001b[0m` : null;
131
+ const lines = [
132
+ "",
133
+ "\u001b[36m\u256d\u2500 HU Board \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e",
134
+ `\u001b[36m\u2502\u001b[0m \u001b[1m${status}\u001b[0m \u001b[4m${url}\u001b[0m`,
135
+ ];
136
+ if (title) lines.push(`\u001b[36m\u2502\u001b[0m${title}`);
137
+ lines.push("\u001b[36m\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u001b[0m", "");
138
+ return lines.join("\n");
139
+ }
140
+
118
141
  export async function boardCommand({ action = "start", port = 4000, logger }) {
119
142
  switch (action) {
120
143
  case "start": {