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.
Files changed (167) hide show
  1. package/README.md +4 -4
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/bootstrap.js +26 -1
  10. package/src/brain/decision-tracker.js +3 -1
  11. package/src/brain/decisor.js +1 -1
  12. package/src/canvas/parse-md.js +238 -0
  13. package/src/canvas/schema.js +101 -0
  14. package/src/canvas/to-flat.js +85 -0
  15. package/src/canvas/validate.js +163 -0
  16. package/src/checks/binaries.js +1 -1
  17. package/src/checks/ports.js +97 -27
  18. package/src/checks/runner.js +26 -10
  19. package/src/checks/sonar.js +1 -1
  20. package/src/checks/system.js +1 -1
  21. package/src/checks/tokens.js +77 -24
  22. package/src/cli/_shared.js +30 -0
  23. package/src/cli/register-meta.js +204 -0
  24. package/src/cli/register-pipeline.js +212 -0
  25. package/src/cli/register-plan.js +152 -0
  26. package/src/cli/register-roles-skills.js +38 -0
  27. package/src/cli/register-sonar.js +28 -0
  28. package/src/cli.js +83 -468
  29. package/src/commands/agents.js +18 -6
  30. package/src/commands/architect.js +29 -2
  31. package/src/commands/audit.js +27 -3
  32. package/src/commands/board.js +160 -30
  33. package/src/commands/clean.js +83 -0
  34. package/src/commands/code.js +7 -2
  35. package/src/commands/discover.js +7 -2
  36. package/src/commands/init.js +2 -2
  37. package/src/commands/plan/_shared.js +40 -0
  38. package/src/commands/plan/add-hu.js +21 -0
  39. package/src/commands/plan/delete.js +9 -0
  40. package/src/commands/plan/generate.js +284 -0
  41. package/src/commands/plan/index.js +14 -0
  42. package/src/commands/plan/list.js +21 -0
  43. package/src/commands/plan/ready.js +46 -0
  44. package/src/commands/plan/remove-hu.js +17 -0
  45. package/src/commands/plan/show.js +81 -0
  46. package/src/commands/plan/validate.js +20 -0
  47. package/src/commands/plan.js +14 -291
  48. package/src/commands/researcher.js +7 -2
  49. package/src/commands/resume.js +24 -1
  50. package/src/commands/review.js +7 -2
  51. package/src/commands/run.js +52 -1
  52. package/src/commands/status.js +1 -1
  53. package/src/commands/sync.js +75 -0
  54. package/src/commands/triage.js +7 -2
  55. package/src/commands/undo.js +1 -1
  56. package/src/config/defaults.js +213 -0
  57. package/src/config/loader.js +137 -0
  58. package/src/config/overrides.js +199 -0
  59. package/src/config/role-resolver.js +159 -0
  60. package/src/config/test-harness.js +79 -0
  61. package/src/config.js +30 -609
  62. package/src/git/automation.js +2 -2
  63. package/src/guards/output-guard.js +0 -2
  64. package/src/hu/acceptance-runner.js +52 -10
  65. package/src/hu/store.js +11 -0
  66. package/src/mcp/handlers/direct-handlers.js +2 -5
  67. package/src/mcp/handlers/management-handlers.js +48 -4
  68. package/src/mcp/handlers/run-handler.js +9 -8
  69. package/src/mcp/preflight.js +32 -13
  70. package/src/mcp/server-handlers.js +4 -3
  71. package/src/mcp/suggest-handler.js +3 -2
  72. package/src/mcp/tools.js +15 -0
  73. package/src/orchestrator/agent-fallback.js +1 -1
  74. package/src/orchestrator/ci-integration.js +10 -10
  75. package/src/orchestrator/config-init.js +25 -7
  76. package/src/orchestrator/direct-actions.js +6 -2
  77. package/src/orchestrator/drivers/error-recovery.js +287 -0
  78. package/src/orchestrator/drivers/init-context.js +222 -0
  79. package/src/orchestrator/drivers/iteration-loop.js +311 -0
  80. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  81. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  82. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  83. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  84. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  85. package/src/orchestrator/drivers/post-loop.js +370 -0
  86. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  87. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  88. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  89. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  90. package/src/orchestrator/drivers/pre-loop.js +434 -0
  91. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  92. package/src/orchestrator/flow-control.js +4 -3
  93. package/src/orchestrator/flow-runner.js +121 -2019
  94. package/src/orchestrator/fs-leak-detector.js +156 -0
  95. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  96. package/src/orchestrator/integrations.js +88 -0
  97. package/src/orchestrator/post-loop-stages.js +35 -8
  98. package/src/orchestrator/preflight-checks.js +30 -3
  99. package/src/orchestrator/repair/repair-detector.js +117 -0
  100. package/src/orchestrator/repair/repair-runner.js +142 -0
  101. package/src/orchestrator/reviewer-fallback.js +1 -1
  102. package/src/orchestrator/solomon-escalation.js +3 -2
  103. package/src/orchestrator/solomon-rules.js +7 -3
  104. package/src/orchestrator/stages/architect-stage.js +12 -11
  105. package/src/orchestrator/stages/coder-stage.js +56 -7
  106. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  107. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  108. package/src/orchestrator/stages/planner-stage.js +1 -1
  109. package/src/orchestrator/stages/research-stage.js +1 -1
  110. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  111. package/src/orchestrator/stages/sonar-stage.js +94 -10
  112. package/src/orchestrator/stages/stage-classes.js +86 -0
  113. package/src/orchestrator/stages/stage-executor.js +1 -1
  114. package/src/orchestrator/stages/triage-stage.js +1 -1
  115. package/src/orchestrator/standby.js +3 -2
  116. package/src/orchestrator/verification-gate.js +20 -12
  117. package/src/plan/adr-generator.js +170 -0
  118. package/src/plan/adr-loader.js +60 -0
  119. package/src/plan/plan-executor.js +19 -1
  120. package/src/plan/plan-hu-ops.js +170 -1
  121. package/src/plan/plan-reviewer.js +199 -0
  122. package/src/plan/plan-schema.js +69 -1
  123. package/src/plan/plan-store.js +40 -3
  124. package/src/plan/spec-detector.js +61 -0
  125. package/src/plan/tests-synthesizer.js +116 -0
  126. package/src/planning-game/client.js +3 -14
  127. package/src/planning-game/decomposition.js +1 -1
  128. package/src/planning-game/pipeline-adapter.js +36 -5
  129. package/src/prompts/coder.js +172 -1
  130. package/src/prompts/hu-reviewer.js +0 -1
  131. package/src/prompts/planner.js +81 -4
  132. package/src/review/diff-generator.js +35 -22
  133. package/src/review/snapshot-diff.js +1 -1
  134. package/src/roles/coder-role.js +25 -3
  135. package/src/roles/planner-role.js +42 -1
  136. package/src/roles/repairer-role.js +181 -0
  137. package/src/roles/tester-role.js +70 -4
  138. package/src/session/mutators.js +304 -0
  139. package/src/session/runtime-overrides.js +59 -0
  140. package/src/{session-store.js → session/store.js} +8 -2
  141. package/src/skills/semantic-detector.js +10 -5
  142. package/src/skills/skill-detector.js +1 -1
  143. package/src/sonar/cloud-scanner.js +0 -1
  144. package/src/sonar/credentials.js +14 -6
  145. package/src/sonar/discovery.js +123 -0
  146. package/src/sonar/manager.js +18 -0
  147. package/src/sync/detect-drift.js +113 -0
  148. package/src/tracker-bootstrap.js +44 -0
  149. package/src/utils/agent-detect.js +15 -11
  150. package/src/utils/board-prompt-bridge.js +109 -0
  151. package/src/utils/cli-progress.js +161 -0
  152. package/src/utils/cwd-confirm.js +39 -0
  153. package/src/utils/derive-project-name-from-cwd.js +119 -0
  154. package/src/utils/garbage-collector.js +396 -0
  155. package/src/utils/git.js +16 -4
  156. package/src/utils/locale.js +1 -1
  157. package/src/utils/paths.js +21 -1
  158. package/src/utils/process.js +1 -1
  159. package/src/utils/prompt-project-name.js +35 -0
  160. package/src/utils/run-context.js +75 -0
  161. package/src/utils/run-log.js +12 -0
  162. package/src/utils/task-file.js +1 -1
  163. package/src/utils/telemetry.js +13 -1
  164. package/src/webperf/devtools-detect.js +1 -1
  165. package/templates/coder-rules.md +13 -0
  166. package/templates/roles/coder.md +34 -0
  167. package/templates/roles/repairer.md +94 -0
@@ -1,291 +1,14 @@
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
-
7
- // ---- Formatting helpers ----
8
-
9
- function formatPlan(plan) {
10
- const lines = [];
11
- if (plan.approach) lines.push("## Approach", plan.approach, "");
12
- if (plan.steps?.length) {
13
- lines.push("## Steps");
14
- for (let i = 0; i < plan.steps.length; i++) {
15
- const step = plan.steps[i];
16
- const commit = step.commit ? ` → \`${step.commit}\`` : "";
17
- lines.push(`${i + 1}. ${step.description}${commit}`);
18
- }
19
- lines.push("");
20
- }
21
- if (plan.risks?.length) {
22
- lines.push("## Risks");
23
- for (const risk of plan.risks) lines.push(`- ${risk}`);
24
- lines.push("");
25
- }
26
- if (plan.outOfScope?.length) {
27
- lines.push("## Out of scope");
28
- for (const item of plan.outOfScope) lines.push(`- ${item}`);
29
- lines.push("");
30
- }
31
- return lines.join("\n");
32
- }
33
-
34
- function formatHuTable(hus) {
35
- if (!hus?.length) return " (no HUs)";
36
- const lines = [];
37
- const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
38
- lines.push(` ${"ID".padEnd(30)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
39
- lines.push(` ${"─".repeat(30)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
40
- for (const hu of hus) {
41
- const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
42
- const deps = (hu.blocked_by || []).join(", ") || "—";
43
- lines.push(` ${hu.id.padEnd(30)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
44
- }
45
- return lines.join("\n");
46
- }
47
-
48
- // ---- Subcommand handlers ----
49
-
50
- /**
51
- * kj plan "task" — generate plan + HUs
52
- */
53
- export async function planGenerateCommand({ task, config, logger, json, context }) {
54
- const { withCliRunLog } = await import("../utils/cli-run-log.js");
55
- return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
56
- return planGenerateImpl({ task, config, logger, json, context, runLog });
57
- });
58
- }
59
-
60
- async function planGenerateImpl({ task, config, logger, json, context, runLog }) {
61
- const plannerRole = resolveRole(config, "planner");
62
- await assertAgentsAvailable([plannerRole.provider]);
63
- runLog.logText(`[planner] provider=${plannerRole.provider}`);
64
-
65
- const planner = createAgent(plannerRole.provider, config, logger);
66
- const prompt = buildPlannerPrompt({ task, context });
67
- const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
68
- ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
69
- : undefined;
70
- const timeoutMs = Number(config?.session?.max_planner_minutes) > 0
71
- ? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000)
72
- : undefined;
73
- const result = await planner.runTask({ prompt, role: "planner", silenceTimeoutMs, timeoutMs });
74
-
75
- if (!result.ok) throw new Error(result.error || result.output || "Planner failed");
76
-
77
- const parsed = parseMaybeJsonString(result.output);
78
-
79
- // Create v2 plan with HUs from planner steps
80
- const { createPlanV2 } = await import("../plan/plan-schema.js");
81
- const { addHu } = await import("../plan/plan-hu-ops.js");
82
- const { savePlan } = await import("../plan/plan-store.js");
83
- const { deriveProjectName, classifyTaskType } = await import("../hu/auto-generator.js");
84
-
85
- const projectDir = config.projectDir || process.cwd();
86
- const plan = createPlanV2(task);
87
- plan.name = deriveProjectName(task);
88
- plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
89
- plan.risks = parsed?.risks || [];
90
- plan.outOfScope = parsed?.outOfScope || [];
91
-
92
- // Convert planner steps → HUs
93
- const steps = parsed?.steps || [];
94
- let prevId = null;
95
- for (const step of steps) {
96
- const desc = typeof step === "string" ? step : step.description || step.title || JSON.stringify(step);
97
- const hu = addHu(plan, {
98
- title: desc.slice(0, 80),
99
- task_type: classifyTaskType(desc),
100
- scope: desc,
101
- blocked_by: prevId ? [prevId] : [],
102
- acceptance_tests: [
103
- "npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL"
104
- ]
105
- });
106
- prevId = hu.id;
107
- }
108
-
109
- const planId = await savePlan(projectDir, plan);
110
-
111
- runLog.logText(`[planner] finished — hus=${plan.hus.length} plan=${planId}`);
112
-
113
- if (json) {
114
- console.log(JSON.stringify(plan, null, 2));
115
- return { ok: true };
116
- }
117
-
118
- if (parsed?.approach) {
119
- console.log(formatPlan(parsed));
120
- } else if (plan.approach) {
121
- console.log(plan.approach);
122
- }
123
- console.log(`\n## HUs (${plan.hus.length})`);
124
- console.log(formatHuTable(plan.hus));
125
- console.log(`\nPlan saved: ${planId} (${plan.hus.length} HUs, status: ${plan.status})`);
126
- console.log(`Review: kj plan show ${planId}`);
127
- console.log(`Approve: kj plan ready ${planId}`);
128
- console.log(`Execute: kj run --plan ${planId} "${task.slice(0, 40)}..."`);
129
- return { ok: true };
130
- }
131
-
132
- /**
133
- * kj plan list
134
- */
135
- export async function planListCommand({ config }) {
136
- const { listPlans } = await import("../plan/plan-store.js");
137
- const projectDir = config.projectDir || process.cwd();
138
- const plans = await listPlans(projectDir);
139
-
140
- if (plans.length === 0) {
141
- console.log("No plans found. Create one with: kj plan \"your task\"");
142
- return;
143
- }
144
-
145
- console.log(`Plans for ${projectDir}:\n`);
146
- for (const p of plans) {
147
- const husInfo = p.huCount > 0 ? ` (${p.huCount} HUs)` : "";
148
- const status = p.status !== "draft" ? ` [${p.status}]` : "";
149
- console.log(` ${p.planId} ${(p.name || p.task || "").slice(0, 50)}${husInfo}${status}`);
150
- console.log(` Created: ${p.createdAt}`);
151
- }
152
- }
153
-
154
- /**
155
- * kj plan show <planId>
156
- */
157
- export async function planShowCommand({ config, planId }) {
158
- const { loadPlan } = await import("../plan/plan-store.js");
159
- const projectDir = config.projectDir || process.cwd();
160
- const plan = await loadPlan(projectDir, planId);
161
-
162
- if (!plan) {
163
- console.error(`Plan not found: ${planId}`);
164
- process.exitCode = 1;
165
- return;
166
- }
167
-
168
- console.log(`Plan: ${plan.planId}`);
169
- console.log(`Task: ${plan.task}`);
170
- console.log(`Name: ${plan.name || "—"}`);
171
- console.log(`Status: ${plan.status}`);
172
- console.log(`Version: ${plan.version}`);
173
- console.log(`Created: ${plan.createdAt}`);
174
-
175
- if (plan.approach) {
176
- console.log(`\n## Approach\n${plan.approach}`);
177
- }
178
-
179
- if (plan.hus?.length > 0) {
180
- console.log(`\n## HUs (${plan.hus.length})`);
181
- console.log(formatHuTable(plan.hus));
182
- } else {
183
- console.log("\nNo HUs defined. Add with: kj plan add-hu " + planId);
184
- }
185
- }
186
-
187
- /**
188
- * kj plan ready <planId>
189
- */
190
- export async function planReadyCommand({ config, planId }) {
191
- const { loadPlan, savePlan } = await import("../plan/plan-store.js");
192
- const { certifyAllHus } = await import("../plan/plan-hu-ops.js");
193
- const { validatePlan } = await import("../plan/plan-schema.js");
194
- const projectDir = config.projectDir || process.cwd();
195
-
196
- const plan = await loadPlan(projectDir, planId);
197
- if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
198
-
199
- const validation = validatePlan(plan);
200
- if (!validation.valid) {
201
- console.error("Plan validation failed:");
202
- for (const err of validation.errors) console.error(` - ${err}`);
203
- process.exitCode = 1;
204
- return;
205
- }
206
-
207
- if (plan.hus.length === 0) {
208
- console.error("Cannot approve a plan with 0 HUs.");
209
- process.exitCode = 1;
210
- return;
211
- }
212
-
213
- const count = certifyAllHus(plan);
214
- await savePlan(projectDir, plan);
215
- console.log(`✅ ${count} HUs certified. Plan status: ready`);
216
- console.log(`Execute: kj run --plan ${planId} "${(plan.task || "").slice(0, 40)}..."`);
217
- }
218
-
219
- /**
220
- * kj plan validate <planId>
221
- */
222
- export async function planValidateCommand({ config, planId }) {
223
- const { loadPlan } = await import("../plan/plan-store.js");
224
- const { validatePlan } = await import("../plan/plan-schema.js");
225
- const projectDir = config.projectDir || process.cwd();
226
-
227
- const plan = await loadPlan(projectDir, planId);
228
- if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
229
-
230
- const result = validatePlan(plan);
231
- if (result.valid) {
232
- console.log(`✅ Plan ${planId} is valid (${plan.hus.length} HUs, status: ${plan.status})`);
233
- } else {
234
- console.error(`❌ Plan ${planId} has ${result.errors.length} error(s):`);
235
- for (const err of result.errors) console.error(` - ${err}`);
236
- process.exitCode = 1;
237
- }
238
- }
239
-
240
- /**
241
- * kj plan delete <planId>
242
- */
243
- export async function planDeleteCommand({ config, planId }) {
244
- const { deletePlan } = await import("../plan/plan-store.js");
245
- const projectDir = config.projectDir || process.cwd();
246
- const ok = await deletePlan(projectDir, planId);
247
- console.log(ok ? `Deleted: ${planId}` : `Not found: ${planId}`);
248
- }
249
-
250
- /**
251
- * kj plan add-hu <planId> --title "..." --type sw --deps hu_xxx
252
- */
253
- export async function planAddHuCommand({ config, planId, title, type, deps, scope }) {
254
- const { loadPlan, savePlan } = await import("../plan/plan-store.js");
255
- const { addHu } = await import("../plan/plan-hu-ops.js");
256
- const projectDir = config.projectDir || process.cwd();
257
-
258
- const plan = await loadPlan(projectDir, planId);
259
- if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
260
-
261
- const hu = addHu(plan, {
262
- title: title || "New HU",
263
- task_type: type || "sw",
264
- scope: scope || null,
265
- blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
266
- });
267
-
268
- await savePlan(projectDir, plan);
269
- console.log(`Added: ${hu.id} — ${hu.title}`);
270
- }
271
-
272
- /**
273
- * kj plan remove-hu <planId> <huId>
274
- */
275
- export async function planRemoveHuCommand({ config, planId, huId }) {
276
- const { loadPlan, savePlan } = await import("../plan/plan-store.js");
277
- const { removeHu } = await import("../plan/plan-hu-ops.js");
278
- const projectDir = config.projectDir || process.cwd();
279
-
280
- const plan = await loadPlan(projectDir, planId);
281
- if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
282
-
283
- const ok = removeHu(plan, huId);
284
- if (!ok) { console.error(`HU not found: ${huId}`); process.exitCode = 1; return; }
285
-
286
- await savePlan(projectDir, plan);
287
- console.log(`Removed: ${huId}`);
288
- }
289
-
290
- // Keep backward compat — old callers import planCommand
291
- export const planCommand = planGenerateCommand;
1
+ // Thin shim. The plan command was split into one file per sub-command
2
+ // under `./plan/`. This file remains so existing callers that import
3
+ // `./commands/plan.js` (src/cli.js, tests) keep working without churn.
4
+ export {
5
+ planGenerateCommand,
6
+ planListCommand,
7
+ planShowCommand,
8
+ planReadyCommand,
9
+ planValidateCommand,
10
+ planDeleteCommand,
11
+ planAddHuCommand,
12
+ planRemoveHuCommand,
13
+ planCommand,
14
+ } from "./plan/index.js";
@@ -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
 
5
6
  const SUBAGENT_PREAMBLE = [
6
7
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -29,8 +30,12 @@ export async function researcherCommand({ task, config, logger }) {
29
30
 
30
31
  const agent = createAgent(researcherRole.provider, config, logger);
31
32
  const prompt = buildResearchPrompt(task);
32
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
33
- const result = await agent.runTask({ prompt, onOutput, role: "researcher" });
33
+ const progress = createCliProgressReporter({ role: "researcher" });
34
+ let result;
35
+ try {
36
+ result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "researcher" });
37
+ progress.finish(result.ok ? "done" : "failed");
38
+ } catch (err) { progress.finish("failed"); throw err; }
34
39
 
35
40
  if (!result.ok) {
36
41
  throw new Error(result.error || result.output || "Researcher failed");
@@ -4,8 +4,31 @@ import { resumeFlow } from "../orchestrator.js";
4
4
  import { createActivityLog } from "../activity-log.js";
5
5
  import { printEvent } from "../utils/display/event-handlers.js";
6
6
 
7
- function createCliAskQuestion() {
7
+ function createCliAskQuestion(opts = {}) {
8
+ const { sessionId = null } = opts;
8
9
  return async (question, context) => {
10
+ // Mirror the run.js logic: route to the board via the file-based
11
+ // bridge when there's no TTY; otherwise prompt the local terminal.
12
+ const stdinReadable = process.stdin && process.stdin.readable !== false;
13
+ const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
14
+ if (!isInteractive) {
15
+ const { askThroughBoard } = await import("../utils/board-prompt-bridge.js");
16
+ console.log(`\n\u2753 ${question}`);
17
+ if (context?.detail) {
18
+ console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
19
+ }
20
+ console.log(
21
+ "\n[non-interactive] Routing the prompt to the HU Board.\n"
22
+ + " Open http://localhost:4000 \u2014 a modal will appear asking for your answer."
23
+ );
24
+ try {
25
+ return await askThroughBoard({ sessionId, question, context });
26
+ } catch (err) {
27
+ console.log(`\n[prompt-bridge] ${err.message} \u2014 stopping the session.`);
28
+ return null;
29
+ }
30
+ }
31
+
9
32
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
33
  return new Promise((resolve) => {
11
34
  console.log(`\n\u2753 ${question}`);
@@ -5,6 +5,7 @@ import { buildReviewerPrompt } from "../prompts/reviewer.js";
5
5
  import { resolveRole } from "../config.js";
6
6
  import { resolveReviewProfile } from "../review/profiles.js";
7
7
  import { withCliRunLog } from "../utils/cli-run-log.js";
8
+ import { createCliProgressReporter } from "../utils/cli-progress.js";
8
9
 
9
10
  export async function reviewCommand({ task, config, logger, baseRef }) {
10
11
  return withCliRunLog("review", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
@@ -36,8 +37,12 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
36
37
  const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
37
38
 
38
39
  const prompt = await buildReviewerPrompt({ task, diff, reviewRules: rules, mode: config.review_mode });
39
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
40
- const result = await reviewer.reviewTask({ prompt, onOutput, role: "reviewer" });
40
+ const progress = createCliProgressReporter({ role: "reviewer" });
41
+ let result;
42
+ try {
43
+ result = await reviewer.reviewTask({ prompt, onOutput: progress.onOutput, role: "reviewer" });
44
+ progress.finish(result.ok ? "done" : "failed");
45
+ } catch (err) { progress.finish("failed"); throw err; }
41
46
  if (!result.ok) {
42
47
  if (result.error) logger.error(result.error);
43
48
  throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
@@ -8,9 +8,39 @@ import { printHeader } from "../utils/display/header.js";
8
8
  import { printEvent } from "../utils/display/event-handlers.js";
9
9
  import { resolveRole } from "../config.js";
10
10
  import { parseCardId } from "../planning-game/adapter.js";
11
+ import { confirmCwd } from "../utils/cwd-confirm.js";
11
12
 
12
- function createCliAskQuestion() {
13
+ function createCliAskQuestion(opts = {}) {
14
+ const { sessionId = null } = opts;
13
15
  return async (question, context) => {
16
+ // Two paths:
17
+ // - Interactive TTY: prompt via readline (the developer's terminal).
18
+ // - No TTY (board's \u25b6 Run plan with stdio=ignore, CI, etc.): publish
19
+ // the prompt through the file-based bridge so the HU Board can
20
+ // surface it as a modal. The runner blocks on the bridge until
21
+ // the user answers (or times out / kills the run). Pre-v2.7.5
22
+ // this path just bailed; now the question actually gets answered.
23
+ const stdinReadable = process.stdin && process.stdin.readable !== false;
24
+ const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
25
+ if (!isInteractive) {
26
+ const { askThroughBoard } = await import("../utils/board-prompt-bridge.js");
27
+ console.log(`\n\u2753 ${question}`);
28
+ if (context?.detail) {
29
+ console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
30
+ }
31
+ console.log(
32
+ "\n[non-interactive] Routing the prompt to the HU Board.\n"
33
+ + " Open http://localhost:4000 \u2014 a modal will appear asking for your answer.\n"
34
+ + " This process is now waiting; closing the board does NOT cancel the run."
35
+ );
36
+ try {
37
+ return await askThroughBoard({ sessionId, question, context });
38
+ } catch (err) {
39
+ console.log(`\n[prompt-bridge] ${err.message} \u2014 stopping the session.`);
40
+ return null;
41
+ }
42
+ }
43
+
14
44
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
45
  return new Promise((resolve) => {
16
46
  console.log(`\n\u2753 ${question}`);
@@ -30,6 +60,27 @@ function createCliAskQuestion() {
30
60
  }
31
61
 
32
62
  export async function runCommandHandler({ task, config, logger, flags }) {
63
+ // PR-F (cwd confirmation): when launching from an interactive
64
+ // terminal, surface the working directory so the user can abort if
65
+ // they typed `kj run` from the wrong place. The user's words:
66
+ // "todas las que lo lancé desde terminal te garantizo que estaba
67
+ // en la carpeta del proyecto … aún así preguntar por si acaso".
68
+ //
69
+ // Skip the prompt when:
70
+ // - --yes / -y is set (CI / scripted runs),
71
+ // - --json mode is on (machine-readable output, no TTY assumed),
72
+ // - stdin is not a TTY (piped, board-spawned subprocess, MCP).
73
+ const projectDir = config?.projectDir || process.cwd();
74
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY
75
+ && !flags?.yes && !flags?.json;
76
+ if (canPrompt) {
77
+ const ok = await confirmCwd(projectDir);
78
+ if (!ok) {
79
+ console.log("Aborted by user.");
80
+ return { ok: false, aborted: true };
81
+ }
82
+ }
83
+
33
84
  // Best-effort session cleanup before starting
34
85
  try {
35
86
  const { cleanupExpiredSessions } = await import("../session-cleanup.js");
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { readRunLog } from "../utils/run-log.js";
6
- import { loadMostRecentSession } from "../session-store.js";
6
+ import { loadMostRecentSession } from "../session/store.js";
7
7
  import { buildDashboard } from "../utils/status-dashboard.js";
8
8
 
9
9
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `kj sync` — closed-loop drift detection between code and Canvas/plan.
3
+ * Issue #540 MVP: detection + report only. No --apply yet.
4
+ */
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { listPlans, loadPlan } from "../plan/plan-store.js";
9
+ import { classifyDrift, formatDriftReport } from "../sync/detect-drift.js";
10
+
11
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".kj", "tmp", ".cache", ".astro", ".next"]);
12
+
13
+ function listSourceFiles(rootDir, out = []) {
14
+ for (const e of fs.readdirSync(rootDir, { withFileTypes: true })) {
15
+ if (e.name.startsWith(".") && !["src", "tests"].includes(e.name)) continue;
16
+ if (SKIP_DIRS.has(e.name)) continue;
17
+ const p = path.join(rootDir, e.name);
18
+ if (e.isDirectory()) {
19
+ listSourceFiles(p, out);
20
+ } else if (e.isFile()) {
21
+ try {
22
+ const s = fs.statSync(p);
23
+ out.push({
24
+ path: path.relative(rootDir, p),
25
+ mtime: s.mtime,
26
+ });
27
+ } catch { /* ignore stat errors */ }
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /**
34
+ * @param {object} args
35
+ * @param {object} args.config
36
+ * @param {object} args.logger
37
+ * @param {string} [args.planId] — when set, sync against this plan; else use the latest
38
+ * @param {boolean} [args.json]
39
+ */
40
+ export async function syncCommand({ config, logger, planId, json }) {
41
+ const projectDir = config?.projectDir || process.cwd();
42
+
43
+ // Resolve which plan to sync against.
44
+ let plan;
45
+ if (planId) {
46
+ plan = await loadPlan(projectDir, planId);
47
+ if (!plan) throw new Error(`kj sync: plan not found: ${planId}`);
48
+ } else {
49
+ const all = await listPlans(projectDir);
50
+ if (!all || all.length === 0) {
51
+ throw new Error("kj sync: no plans found for this project. Generate one with `kj plan generate --task-file SPEC.md` (or `--canvas SPEC.canvas.md` for the structured form).");
52
+ }
53
+ // Pick the most recent plan by createdAt.
54
+ plan = await loadPlan(projectDir, all.sort((a, b) =>
55
+ new Date(b.createdAt || 0) - new Date(a.createdAt || 0))[0].planId);
56
+ }
57
+
58
+ logger.info(`kj sync: comparing ${projectDir} against plan ${plan.planId}`);
59
+
60
+ const files = listSourceFiles(projectDir);
61
+ const report = classifyDrift(plan, files);
62
+
63
+ if (json) {
64
+ console.log(JSON.stringify({
65
+ planId: plan.planId,
66
+ projectDir,
67
+ ...report,
68
+ }, null, 2));
69
+ } else {
70
+ console.log(formatDriftReport(report, plan));
71
+ }
72
+
73
+ // Exit 0 always — drift is informational, not an error. CI users
74
+ // can opt-in to "exit non-zero on drift" later via a flag.
75
+ }
@@ -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 { buildTriagePrompt } from "../prompts/triage.js";
5
6
  import { parseMaybeJsonString } from "../review/parser.js";
6
7
 
@@ -43,8 +44,12 @@ export async function triageCommand({ task, config, logger, json }) {
43
44
 
44
45
  const agent = createAgent(triageRole.provider, config, logger);
45
46
  const prompt = buildTriagePrompt({ task });
46
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
47
- const result = await agent.runTask({ prompt, onOutput, role: "triage" });
47
+ const progress = createCliProgressReporter({ role: "triage" });
48
+ let result;
49
+ try {
50
+ result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "triage" });
51
+ progress.finish(result.ok ? "done" : "failed");
52
+ } catch (err) { progress.finish("failed"); throw err; }
48
53
 
49
54
  if (!result.ok) {
50
55
  throw new Error(result.error || result.output || "Triage failed");
@@ -1,4 +1,4 @@
1
- import { loadMostRecentSession } from "../session-store.js";
1
+ import { loadMostRecentSession } from "../session/store.js";
2
2
  import { runCommand } from "../utils/process.js";
3
3
 
4
4
  /**