@karajan-family/code 4.21.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 (746) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +98 -0
  3. package/bin/karajan-mcp.js +2 -0
  4. package/bin/kj-rag-mcp.js +75 -0
  5. package/bin/kj-tail +361 -0
  6. package/bin/kj.js +2 -0
  7. package/docs/README.es.md +80 -0
  8. package/docs/karajan-code-logo-small.png +0 -0
  9. package/library/logical-clocks-lamport.md +40 -0
  10. package/package.json +140 -0
  11. package/packages/ai-trash/README.md +37 -0
  12. package/packages/ai-trash/bin/kj-trash.js +10 -0
  13. package/packages/ai-trash/package.json +43 -0
  14. package/packages/ai-trash/src/cli.js +200 -0
  15. package/packages/ai-trash/src/destructive-parser.js +74 -0
  16. package/packages/ai-trash/src/git-snapshot.js +128 -0
  17. package/packages/ai-trash/src/hook.js +137 -0
  18. package/packages/ai-trash/src/index.js +14 -0
  19. package/packages/ai-trash/src/install.js +73 -0
  20. package/packages/ai-trash/src/logger.js +60 -0
  21. package/packages/ai-trash/src/manifest.js +91 -0
  22. package/packages/ai-trash/src/permissions.js +26 -0
  23. package/packages/ai-trash/src/snapshot.js +66 -0
  24. package/packages/ai-trash/src/ulid.js +66 -0
  25. package/packages/hu-board/README.md +69 -0
  26. package/packages/hu-board/package.json +28 -0
  27. package/packages/hu-board/public/app.js +259 -0
  28. package/packages/hu-board/public/index.html +77 -0
  29. package/packages/hu-board/public/pipeline.css +132 -0
  30. package/packages/hu-board/public/pipeline.html +48 -0
  31. package/packages/hu-board/public/pipeline.js +200 -0
  32. package/packages/hu-board/public/rag.html +21 -0
  33. package/packages/hu-board/public/rag.js +39 -0
  34. package/packages/hu-board/public/styles.css +973 -0
  35. package/packages/hu-board/public/utils/api.js +139 -0
  36. package/packages/hu-board/public/utils/board-view.js +473 -0
  37. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  38. package/packages/hu-board/public/utils/config-editor.js +181 -0
  39. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  40. package/packages/hu-board/public/utils/formatters.js +300 -0
  41. package/packages/hu-board/public/utils/graph-view.js +216 -0
  42. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  43. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  44. package/packages/hu-board/public/utils/log-panel.js +222 -0
  45. package/packages/hu-board/public/utils/modals.js +309 -0
  46. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  47. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  48. package/packages/hu-board/public/utils/project-actions.js +143 -0
  49. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  50. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  51. package/packages/hu-board/public/utils/server-push.js +252 -0
  52. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  53. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  54. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  55. package/packages/hu-board/public/wiki.html +32 -0
  56. package/packages/hu-board/public/wiki.js +53 -0
  57. package/packages/hu-board/src/auth.js +92 -0
  58. package/packages/hu-board/src/boot-info.js +16 -0
  59. package/packages/hu-board/src/cleanup-zombies.js +145 -0
  60. package/packages/hu-board/src/command-runner.js +133 -0
  61. package/packages/hu-board/src/config-yaml.js +531 -0
  62. package/packages/hu-board/src/db.js +963 -0
  63. package/packages/hu-board/src/ephemeral-cleaner.js +196 -0
  64. package/packages/hu-board/src/event-bus.js +58 -0
  65. package/packages/hu-board/src/format.js +207 -0
  66. package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
  67. package/packages/hu-board/src/journal-parser.js +180 -0
  68. package/packages/hu-board/src/plan-mutations.js +424 -0
  69. package/packages/hu-board/src/preflight.js +286 -0
  70. package/packages/hu-board/src/routes/api.js +1374 -0
  71. package/packages/hu-board/src/routes/pipeline.js +45 -0
  72. package/packages/hu-board/src/routes/rag.js +42 -0
  73. package/packages/hu-board/src/routes/wiki.js +43 -0
  74. package/packages/hu-board/src/run-tracker.js +121 -0
  75. package/packages/hu-board/src/server.js +438 -0
  76. package/packages/hu-board/src/sync.js +732 -0
  77. package/packages/hu-board/src/token-store.js +56 -0
  78. package/packages/hu-board/src/zombie-reaper.js +206 -0
  79. package/scripts/ai-attribution-guard.yml +84 -0
  80. package/scripts/audit-test-diet.mjs +151 -0
  81. package/scripts/build-sea.mjs +150 -0
  82. package/scripts/diet-baseline.mjs +170 -0
  83. package/scripts/dual-publish.mjs +67 -0
  84. package/scripts/esbuild-sea.config.mjs +273 -0
  85. package/scripts/git-hooks/post-merge +18 -0
  86. package/scripts/install-binary.ps1 +174 -0
  87. package/scripts/install-binary.sh +159 -0
  88. package/scripts/install-guard-all-repos.sh +85 -0
  89. package/scripts/install-kj.sh +193 -0
  90. package/scripts/install.js +901 -0
  91. package/scripts/install.sh +7 -0
  92. package/scripts/postinstall.js +117 -0
  93. package/scripts/protect-all-repos.sh +88 -0
  94. package/scripts/quickstart-gate.mjs +97 -0
  95. package/scripts/regen-arch-stats.sh +71 -0
  96. package/scripts/setup-multi-instance.sh +150 -0
  97. package/scripts/verify-pack.mjs +278 -0
  98. package/src/activity-log.js +59 -0
  99. package/src/agents/agy-agent.js +62 -0
  100. package/src/agents/aider-agent.js +78 -0
  101. package/src/agents/availability.js +52 -0
  102. package/src/agents/base-agent.js +134 -0
  103. package/src/agents/claude-agent.js +413 -0
  104. package/src/agents/codex-agent.js +112 -0
  105. package/src/agents/copilot-agent.js +65 -0
  106. package/src/agents/gemini-agent.js +85 -0
  107. package/src/agents/host-agent.js +38 -0
  108. package/src/agents/index.js +59 -0
  109. package/src/agents/kimi-agent.js +45 -0
  110. package/src/agents/model-registry.js +137 -0
  111. package/src/agents/opencode-agent.js +72 -0
  112. package/src/agents/qwen-agent.js +20 -0
  113. package/src/agents/resolve-bin.js +73 -0
  114. package/src/audit/agent-readiness.js +259 -0
  115. package/src/audit/ai-slop-findings.js +102 -0
  116. package/src/audit/audit-fallback.js +66 -0
  117. package/src/audit/audit-history-display.js +61 -0
  118. package/src/audit/audit-history.js +102 -0
  119. package/src/audit/basal-cost.js +281 -0
  120. package/src/audit/circular-deps.js +144 -0
  121. package/src/audit/dead-exports.js +186 -0
  122. package/src/audit/deterministic-summary.js +327 -0
  123. package/src/audit/harness-scorecard.js +100 -0
  124. package/src/audit/harness-section.js +81 -0
  125. package/src/audit/infra-findings.js +100 -0
  126. package/src/audit/injection-findings.js +114 -0
  127. package/src/audit/issue-filter.js +133 -0
  128. package/src/audit/osv-findings.js +167 -0
  129. package/src/audit/plan-adherence.js +224 -0
  130. package/src/audit/semgrep-findings.js +129 -0
  131. package/src/audit/sonar-findings.js +93 -0
  132. package/src/audit/webperf-input.js +133 -0
  133. package/src/autonomy/arbiter-role.js +71 -0
  134. package/src/autonomy/arbiter.js +22 -0
  135. package/src/autonomy/decider.js +18 -0
  136. package/src/autonomy/decision-resolver.js +58 -0
  137. package/src/autonomy/outcome-report.js +55 -0
  138. package/src/autonomy/policy.js +41 -0
  139. package/src/autonomy/stage-ask.js +30 -0
  140. package/src/bootstrap.js +276 -0
  141. package/src/brain/agent-error-classifier.js +116 -0
  142. package/src/brain/decision-tracker.js +125 -0
  143. package/src/brain/decisor.js +243 -0
  144. package/src/brain/solomon-consult.js +171 -0
  145. package/src/brain/standby-scheduler.js +8 -0
  146. package/src/brain/standby-store.js +12 -0
  147. package/src/brain/with-brain-recovery.js +235 -0
  148. package/src/budget/comparison.js +96 -0
  149. package/src/canvas/parse-md.js +238 -0
  150. package/src/canvas/schema.js +101 -0
  151. package/src/canvas/to-flat.js +85 -0
  152. package/src/canvas/validate.js +163 -0
  153. package/src/checks/ai-surface.js +79 -0
  154. package/src/checks/ai-trash.js +34 -0
  155. package/src/checks/binaries.js +207 -0
  156. package/src/checks/ci.js +105 -0
  157. package/src/checks/config-files.js +217 -0
  158. package/src/checks/dir-setup.js +103 -0
  159. package/src/checks/guarantee-level.js +38 -0
  160. package/src/checks/hardware.js +150 -0
  161. package/src/checks/harness-scorecard.js +41 -0
  162. package/src/checks/mcp-health.js +169 -0
  163. package/src/checks/method.js +73 -0
  164. package/src/checks/native-build.js +67 -0
  165. package/src/checks/node.js +70 -0
  166. package/src/checks/ollama.js +41 -0
  167. package/src/checks/orphans.js +77 -0
  168. package/src/checks/ports.js +188 -0
  169. package/src/checks/project-checks.js +245 -0
  170. package/src/checks/qmd.js +41 -0
  171. package/src/checks/rag-hooks.js +54 -0
  172. package/src/checks/release-check.js +102 -0
  173. package/src/checks/rtk.js +36 -0
  174. package/src/checks/runner.js +326 -0
  175. package/src/checks/skills.js +53 -0
  176. package/src/checks/sonar.js +67 -0
  177. package/src/checks/squeezr.js +36 -0
  178. package/src/checks/system.js +53 -0
  179. package/src/checks/tokens.js +172 -0
  180. package/src/checks/types.js +103 -0
  181. package/src/ci/dispatch.js +99 -0
  182. package/src/ci/index.js +3 -0
  183. package/src/ci/pr-diff.js +26 -0
  184. package/src/ci/repo.js +45 -0
  185. package/src/cli/_shared.js +30 -0
  186. package/src/cli/advanced-commands.js +61 -0
  187. package/src/cli/register-meta.js +772 -0
  188. package/src/cli/register-pipeline.js +335 -0
  189. package/src/cli/register-plan.js +224 -0
  190. package/src/cli/register-roles-skills.js +54 -0
  191. package/src/cli/register-sonar.js +28 -0
  192. package/src/cli/register-standby.js +26 -0
  193. package/src/cli.js +155 -0
  194. package/src/commands/advanced.js +31 -0
  195. package/src/commands/agent-run.js +63 -0
  196. package/src/commands/agents.js +105 -0
  197. package/src/commands/architect.js +123 -0
  198. package/src/commands/audit.js +430 -0
  199. package/src/commands/autorun.js +49 -0
  200. package/src/commands/board.js +458 -0
  201. package/src/commands/check.js +40 -0
  202. package/src/commands/clean.js +145 -0
  203. package/src/commands/code.js +45 -0
  204. package/src/commands/config.js +74 -0
  205. package/src/commands/discover.js +105 -0
  206. package/src/commands/doctor.js +251 -0
  207. package/src/commands/env.js +207 -0
  208. package/src/commands/harden.js +203 -0
  209. package/src/commands/hu.js +210 -0
  210. package/src/commands/identity.js +60 -0
  211. package/src/commands/init.js +950 -0
  212. package/src/commands/install-tools.js +432 -0
  213. package/src/commands/mutate.js +94 -0
  214. package/src/commands/ollama.js +44 -0
  215. package/src/commands/onboard.js +45 -0
  216. package/src/commands/plan/_shared.js +55 -0
  217. package/src/commands/plan/add-hu.js +23 -0
  218. package/src/commands/plan/delete.js +9 -0
  219. package/src/commands/plan/fix.js +99 -0
  220. package/src/commands/plan/generate.js +532 -0
  221. package/src/commands/plan/index.js +13 -0
  222. package/src/commands/plan/list.js +21 -0
  223. package/src/commands/plan/migrate-result.js +41 -0
  224. package/src/commands/plan/ready.js +46 -0
  225. package/src/commands/plan/remove-hu.js +17 -0
  226. package/src/commands/plan/share.js +85 -0
  227. package/src/commands/plan/show.js +81 -0
  228. package/src/commands/plan/unshare.js +41 -0
  229. package/src/commands/plan/validate.js +20 -0
  230. package/src/commands/plan.js +17 -0
  231. package/src/commands/policy.js +164 -0
  232. package/src/commands/privacy.js +45 -0
  233. package/src/commands/qmd.js +86 -0
  234. package/src/commands/rag-mcp.js +18 -0
  235. package/src/commands/rag.js +173 -0
  236. package/src/commands/report-issue.js +77 -0
  237. package/src/commands/report.js +469 -0
  238. package/src/commands/researcher.js +54 -0
  239. package/src/commands/resume.js +68 -0
  240. package/src/commands/review-gate.js +355 -0
  241. package/src/commands/review.js +89 -0
  242. package/src/commands/roles.js +116 -0
  243. package/src/commands/run.js +185 -0
  244. package/src/commands/scan.js +18 -0
  245. package/src/commands/skills.js +86 -0
  246. package/src/commands/sonar.js +53 -0
  247. package/src/commands/standby.js +117 -0
  248. package/src/commands/start.js +121 -0
  249. package/src/commands/status.js +36 -0
  250. package/src/commands/sync.js +103 -0
  251. package/src/commands/telemetry.js +70 -0
  252. package/src/commands/tournament.js +85 -0
  253. package/src/commands/triage.js +78 -0
  254. package/src/commands/undo.js +62 -0
  255. package/src/commands/watch.js +46 -0
  256. package/src/commands/webperf.js +162 -0
  257. package/src/commands/worktree.js +76 -0
  258. package/src/config/defaults.js +270 -0
  259. package/src/config/loader.js +204 -0
  260. package/src/config/overrides.js +220 -0
  261. package/src/config/role-resolver.js +171 -0
  262. package/src/config/schema.js +299 -0
  263. package/src/config/test-harness.js +78 -0
  264. package/src/config.js +34 -0
  265. package/src/domains/domain-loader.js +245 -0
  266. package/src/domains/domain-registry.js +140 -0
  267. package/src/domains/domain-synthesizer.js +129 -0
  268. package/src/environment/adr.js +45 -0
  269. package/src/environment/board-access.js +97 -0
  270. package/src/environment/board-select.js +59 -0
  271. package/src/environment/briefs.js +129 -0
  272. package/src/environment/issue-report.js +72 -0
  273. package/src/environment/mcp-wiring.js +44 -0
  274. package/src/environment/playbook.js +127 -0
  275. package/src/git/automation.js +334 -0
  276. package/src/git/hu-automation.js +171 -0
  277. package/src/git/hu-snapshot.js +10 -0
  278. package/src/golden/asserter.js +92 -0
  279. package/src/golden/loader.js +52 -0
  280. package/src/golden/runner.js +92 -0
  281. package/src/golden/schema.js +41 -0
  282. package/src/guards/intent-guard.js +161 -0
  283. package/src/guards/output-guard.js +149 -0
  284. package/src/guards/perf-guard.js +126 -0
  285. package/src/guards/policy-guard.js +90 -0
  286. package/src/guards/policy-resolver.js +85 -0
  287. package/src/guards/secret-redactor.js +71 -0
  288. package/src/harden/advisory.js +203 -0
  289. package/src/harden/alternatives.js +50 -0
  290. package/src/harden/check.js +81 -0
  291. package/src/harden/config-engine.js +128 -0
  292. package/src/harden/config-templates.js +122 -0
  293. package/src/harden/guidelines-engine.js +48 -0
  294. package/src/harden/guidelines-templates.js +36 -0
  295. package/src/harden/harden-engine.js +80 -0
  296. package/src/harden/harness-hooks.js +107 -0
  297. package/src/harden/hook-commands.js +25 -0
  298. package/src/harden/hook-templates.js +183 -0
  299. package/src/harden/interactive.js +76 -0
  300. package/src/harden/sentinel-hooks.js +721 -0
  301. package/src/harden/stack-roots.js +105 -0
  302. package/src/harden/test-on-push.js +38 -0
  303. package/src/harden/workflow-engine.js +116 -0
  304. package/src/harden/workflow-templates.js +285 -0
  305. package/src/hu/acceptance-runner.js +138 -0
  306. package/src/hu/auto-generator.js +358 -0
  307. package/src/hu/graph.js +71 -0
  308. package/src/hu/lazy-planner.js +109 -0
  309. package/src/hu/model-router.js +108 -0
  310. package/src/hu/parallel-executor.js +94 -0
  311. package/src/hu/split-validator.js +143 -0
  312. package/src/hu/splitting-detector.js +158 -0
  313. package/src/hu/splitting-generator.js +175 -0
  314. package/src/hu/store.js +392 -0
  315. package/src/hu/worktree-bootstrap.js +59 -0
  316. package/src/identity/bootstrap.js +48 -0
  317. package/src/identity/compare.js +38 -0
  318. package/src/identity/detect.js +51 -0
  319. package/src/identity/store.js +66 -0
  320. package/src/infrastructure/command-runner.js +44 -0
  321. package/src/infrastructure/environment.js +38 -0
  322. package/src/infrastructure/file-system-service.js +63 -0
  323. package/src/infrastructure/mocks.js +182 -0
  324. package/src/lang/chunk-go.js +94 -0
  325. package/src/lang/chunk-java.js +90 -0
  326. package/src/lang/chunk-python.js +88 -0
  327. package/src/lang/chunk-rust.js +102 -0
  328. package/src/lang/go.js +14 -0
  329. package/src/lang/java.js +16 -0
  330. package/src/lang/js.js +8 -0
  331. package/src/lang/python.js +14 -0
  332. package/src/lang/registry.js +79 -0
  333. package/src/lang/rust.js +14 -0
  334. package/src/lang/tree-sitter-loader.js +45 -0
  335. package/src/mcp/direct-role-runner.js +112 -0
  336. package/src/mcp/error-classifiers.js +66 -0
  337. package/src/mcp/handlers/direct-handlers.js +486 -0
  338. package/src/mcp/handlers/hu-handlers.js +93 -0
  339. package/src/mcp/handlers/management-handlers.js +265 -0
  340. package/src/mcp/handlers/rag-handler.js +52 -0
  341. package/src/mcp/handlers/run-handler.js +313 -0
  342. package/src/mcp/orphan-guard.js +131 -0
  343. package/src/mcp/preflight.js +47 -0
  344. package/src/mcp/progress.js +149 -0
  345. package/src/mcp/response-compressor.js +81 -0
  346. package/src/mcp/run-kj.js +135 -0
  347. package/src/mcp/server-handlers.js +79 -0
  348. package/src/mcp/server.js +66 -0
  349. package/src/mcp/shared-helpers.js +297 -0
  350. package/src/mcp/sovereignty-guard.js +112 -0
  351. package/src/mcp/suggest-handler.js +92 -0
  352. package/src/mcp/tool-arg-normalizers.js +16 -0
  353. package/src/mcp/tools.js +432 -0
  354. package/src/mutate/diff-scope.js +105 -0
  355. package/src/mutate/reviewer-signal.js +76 -0
  356. package/src/mutate/runner.js +72 -0
  357. package/src/mutate/tool-registry.js +90 -0
  358. package/src/onboarder/cache.js +24 -0
  359. package/src/onboarder/collectors/framework-multilang.js +64 -0
  360. package/src/onboarder/collectors/index.js +136 -0
  361. package/src/orchestrator/agent-fallback.js +81 -0
  362. package/src/orchestrator/brain-coordinator.js +186 -0
  363. package/src/orchestrator/ci-integration.js +139 -0
  364. package/src/orchestrator/config-init.js +510 -0
  365. package/src/orchestrator/direct-actions.js +217 -0
  366. package/src/orchestrator/drivers/error-recovery.js +287 -0
  367. package/src/orchestrator/drivers/init-context.js +227 -0
  368. package/src/orchestrator/drivers/iteration-loop.js +380 -0
  369. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +69 -0
  370. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  371. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  372. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +114 -0
  373. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  374. package/src/orchestrator/drivers/post-loop.js +383 -0
  375. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +136 -0
  376. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  377. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  378. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +391 -0
  379. package/src/orchestrator/drivers/pre-loop.js +507 -0
  380. package/src/orchestrator/drivers/run-hu-batch.js +427 -0
  381. package/src/orchestrator/feedback-enrichment.js +140 -0
  382. package/src/orchestrator/feedback-queue.js +151 -0
  383. package/src/orchestrator/flow-control.js +170 -0
  384. package/src/orchestrator/flow-runner.js +364 -0
  385. package/src/orchestrator/fs-leak-detector.js +210 -0
  386. package/src/orchestrator/hu-scheduler.js +114 -0
  387. package/src/orchestrator/hu-sub-pipeline.js +616 -0
  388. package/src/orchestrator/integrations.js +81 -0
  389. package/src/orchestrator/iteration-gate.js +73 -0
  390. package/src/orchestrator/iteration-stages.js +9 -0
  391. package/src/orchestrator/parallel-limiter.js +74 -0
  392. package/src/orchestrator/pipeline-context.js +72 -0
  393. package/src/orchestrator/post-loop-stages.js +468 -0
  394. package/src/orchestrator/pre-loop-stages.js +11 -0
  395. package/src/orchestrator/preflight-checks.js +462 -0
  396. package/src/orchestrator/repair/repair-detector.js +117 -0
  397. package/src/orchestrator/repair/repair-runner.js +142 -0
  398. package/src/orchestrator/reviewer-fallback.js +45 -0
  399. package/src/orchestrator/role-output-compressor.js +189 -0
  400. package/src/orchestrator/session-journal.js +308 -0
  401. package/src/orchestrator/solomon-escalation.js +305 -0
  402. package/src/orchestrator/solomon-rules.js +212 -0
  403. package/src/orchestrator/stages/architect-stage.js +172 -0
  404. package/src/orchestrator/stages/coder-stage.js +480 -0
  405. package/src/orchestrator/stages/domain-curator-stage.js +82 -0
  406. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  407. package/src/orchestrator/stages/hu-reviewer-stage.js +394 -0
  408. package/src/orchestrator/stages/perf-stage.js +140 -0
  409. package/src/orchestrator/stages/planner-stage.js +85 -0
  410. package/src/orchestrator/stages/rag-context-stage.js +77 -0
  411. package/src/orchestrator/stages/rag-preload-decisor.js +58 -0
  412. package/src/orchestrator/stages/research-stage.js +69 -0
  413. package/src/orchestrator/stages/reviewer-stage.js +422 -0
  414. package/src/orchestrator/stages/sonar-stage.js +421 -0
  415. package/src/orchestrator/stages/stage-classes.js +84 -0
  416. package/src/orchestrator/stages/stage-executor.js +155 -0
  417. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  418. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  419. package/src/orchestrator/stages/triage-stage.js +172 -0
  420. package/src/orchestrator/standby.js +71 -0
  421. package/src/orchestrator/verification-gate.js +183 -0
  422. package/src/orchestrator.js +22 -0
  423. package/src/plan/adr-generator.js +170 -0
  424. package/src/plan/adr-loader.js +60 -0
  425. package/src/plan/plan-executor.js +100 -0
  426. package/src/plan/plan-fixer.js +192 -0
  427. package/src/plan/plan-hu-ops.js +16 -0
  428. package/src/plan/plan-id.js +4 -0
  429. package/src/plan/plan-migrate-result.js +24 -0
  430. package/src/plan/plan-reviewer.js +236 -0
  431. package/src/plan/plan-schema.js +334 -0
  432. package/src/plan/plan-store.js +260 -0
  433. package/src/plan/plan-structural-pass.js +122 -0
  434. package/src/plan/plan-validation.js +4 -0
  435. package/src/plan/preflight-hu.js +105 -0
  436. package/src/plan/spec-detector.js +61 -0
  437. package/src/plan/tests-synthesizer.js +131 -0
  438. package/src/planning-game/adapter.js +108 -0
  439. package/src/planning-game/architect-adrs.js +45 -0
  440. package/src/planning-game/client.js +108 -0
  441. package/src/planning-game/decomposition.js +87 -0
  442. package/src/planning-game/pipeline-adapter.js +251 -0
  443. package/src/plugins/loader.js +67 -0
  444. package/src/policy/add.js +111 -0
  445. package/src/policy/decisions.js +42 -0
  446. package/src/policy/engine.js +82 -0
  447. package/src/policy/exceptions.js +60 -0
  448. package/src/privacy/onboarding.js +35 -0
  449. package/src/privacy/scan.js +120 -0
  450. package/src/prompts/arbiter.js +38 -0
  451. package/src/prompts/architect.js +103 -0
  452. package/src/prompts/audit.js +474 -0
  453. package/src/prompts/coder.js +287 -0
  454. package/src/prompts/diff-clip.js +27 -0
  455. package/src/prompts/discover.js +222 -0
  456. package/src/prompts/hu-reviewer.js +219 -0
  457. package/src/prompts/planner.js +312 -0
  458. package/src/prompts/prompt-layout.js +80 -0
  459. package/src/prompts/prompt-resolver.js +120 -0
  460. package/src/prompts/reviewer.js +75 -0
  461. package/src/prompts/rtk-snippet.js +25 -0
  462. package/src/prompts/spec-reviewer.js +66 -0
  463. package/src/prompts/split-signal.js +82 -0
  464. package/src/prompts/start-decision.js +52 -0
  465. package/src/prompts/tool-correctness-judge.js +31 -0
  466. package/src/prompts/triage.js +57 -0
  467. package/src/rag/auto-update.js +72 -0
  468. package/src/rag/chunker-ast.js +56 -0
  469. package/src/rag/chunker.js +132 -0
  470. package/src/rag/embedder.js +2 -0
  471. package/src/rag/embedders/_cloud-base.js +2 -0
  472. package/src/rag/embedders/cohere.js +2 -0
  473. package/src/rag/embedders/factory.js +2 -0
  474. package/src/rag/embedders/mistral.js +2 -0
  475. package/src/rag/embedders/onnx.js +2 -0
  476. package/src/rag/embedders/openai.js +2 -0
  477. package/src/rag/embedders/voyage.js +2 -0
  478. package/src/rag/eval.js +61 -0
  479. package/src/rag/governed-embedder.js +39 -0
  480. package/src/rag/indexer.js +199 -0
  481. package/src/rag/library.js +62 -0
  482. package/src/rag/ollama-capability.js +59 -0
  483. package/src/rag/ollama-manager.js +98 -0
  484. package/src/rag/onnx-fallback.js +69 -0
  485. package/src/rag/rerank.js +4 -0
  486. package/src/rag/retriever.js +2 -0
  487. package/src/rag/vec-store.js +17 -0
  488. package/src/rag/watcher.js +62 -0
  489. package/src/rag/where-parser.js +4 -0
  490. package/src/repeat-detector.js +88 -0
  491. package/src/review/card-first.js +99 -0
  492. package/src/review/card-verify.js +83 -0
  493. package/src/review/diff-generator.js +112 -0
  494. package/src/review/gate-gitignore.js +77 -0
  495. package/src/review/git-stash-helper.js +104 -0
  496. package/src/review/mutation-pregate.js +48 -0
  497. package/src/review/one-shot-review.js +122 -0
  498. package/src/review/parser.js +80 -0
  499. package/src/review/policy-gate.js +26 -0
  500. package/src/review/profiles.js +66 -0
  501. package/src/review/reviewer-fallback.js +125 -0
  502. package/src/review/schema.js +31 -0
  503. package/src/review/scope-filter.js +152 -0
  504. package/src/review/snapshot-diff.js +122 -0
  505. package/src/review/solomon-arbitration.js +113 -0
  506. package/src/review/sonar-pregate.js +63 -0
  507. package/src/review/tdd-discipline.js +90 -0
  508. package/src/review/tdd-policy.js +69 -0
  509. package/src/review/tests-with-code.js +33 -0
  510. package/src/review/verdict-store.js +87 -0
  511. package/src/review/workspace.js +21 -0
  512. package/src/roles/agent-role.js +194 -0
  513. package/src/roles/architect-role.js +77 -0
  514. package/src/roles/audit-role.js +258 -0
  515. package/src/roles/base-role.js +165 -0
  516. package/src/roles/coder-role.js +107 -0
  517. package/src/roles/commiter-role.js +110 -0
  518. package/src/roles/discover-role.js +81 -0
  519. package/src/roles/domain-curator-role.js +141 -0
  520. package/src/roles/hu-reviewer-role.js +64 -0
  521. package/src/roles/impeccable-role.js +85 -0
  522. package/src/roles/index.js +18 -0
  523. package/src/roles/karajan-brain-role.js +134 -0
  524. package/src/roles/onboarder-role.js +61 -0
  525. package/src/roles/perf-role.js +122 -0
  526. package/src/roles/planner-role.js +151 -0
  527. package/src/roles/refactorer-role.js +30 -0
  528. package/src/roles/repairer-role.js +181 -0
  529. package/src/roles/researcher-role.js +70 -0
  530. package/src/roles/reviewer-role.js +104 -0
  531. package/src/roles/security-role.js +73 -0
  532. package/src/roles/solomon-role.js +265 -0
  533. package/src/roles/sonar-role.js +70 -0
  534. package/src/roles/spec-reviewer-role.js +106 -0
  535. package/src/roles/tester-role.js +235 -0
  536. package/src/roles/tool-correctness-judge-role.js +78 -0
  537. package/src/roles/triage-role.js +99 -0
  538. package/src/session/journal/decisions-writer.js +200 -0
  539. package/src/session/journal/iteration-logger.js +194 -0
  540. package/src/session/journal/summary-writer.js +320 -0
  541. package/src/session/journal/tree-writer.js +164 -0
  542. package/src/session/journal/write-all.js +89 -0
  543. package/src/session/mutators.js +339 -0
  544. package/src/session/runtime-overrides.js +59 -0
  545. package/src/session/store.js +106 -0
  546. package/src/session-cleanup.js +116 -0
  547. package/src/skills/addyosmani-catalog.js +315 -0
  548. package/src/skills/addyosmani-role-map.js +139 -0
  549. package/src/skills/openskills-client.js +157 -0
  550. package/src/skills/semantic-detector.js +167 -0
  551. package/src/skills/skill-detector.js +385 -0
  552. package/src/skills/skill-loader.js +219 -0
  553. package/src/skills/skills-cache.js +128 -0
  554. package/src/sonar/api.js +128 -0
  555. package/src/sonar/cloud-scanner.js +78 -0
  556. package/src/sonar/config-resolver.js +88 -0
  557. package/src/sonar/credentials.js +60 -0
  558. package/src/sonar/discovery.js +124 -0
  559. package/src/sonar/enforcer.js +19 -0
  560. package/src/sonar/issue-filter.js +12 -0
  561. package/src/sonar/manager.js +181 -0
  562. package/src/sonar/project-key.js +120 -0
  563. package/src/sonar/scanner.js +307 -0
  564. package/src/sonar/token-bootstrap.js +205 -0
  565. package/src/sonar/token-recovery.js +67 -0
  566. package/src/spec-review/refine-loop.js +58 -0
  567. package/src/spec-review/run-spec-review.js +119 -0
  568. package/src/start/assessment.js +74 -0
  569. package/src/start/maturity.js +73 -0
  570. package/src/start/start-decider-role.js +79 -0
  571. package/src/start/sweep.js +157 -0
  572. package/src/sync/apply-canvas-patch.js +55 -0
  573. package/src/sync/detect-drift.js +113 -0
  574. package/src/tournament/crown.js +88 -0
  575. package/src/tournament/judge.js +150 -0
  576. package/src/tournament/run.js +142 -0
  577. package/src/tournament/scoreboard.js +124 -0
  578. package/src/tracker-bootstrap.js +44 -0
  579. package/src/types/agent.js +55 -0
  580. package/src/types/config.js +114 -0
  581. package/src/types/hu.js +47 -0
  582. package/src/types/index.js +38 -0
  583. package/src/types/policy.js +43 -0
  584. package/src/types/session.js +75 -0
  585. package/src/types/solomon.js +87 -0
  586. package/src/types/stage.js +68 -0
  587. package/src/utils/agent-detect.js +93 -0
  588. package/src/utils/ai-trash.js +48 -0
  589. package/src/utils/async-lock.js +31 -0
  590. package/src/utils/atomic-write.js +6 -0
  591. package/src/utils/banner.js +66 -0
  592. package/src/utils/binary-sources.js +70 -0
  593. package/src/utils/board-prompt-bridge.js +108 -0
  594. package/src/utils/budget.js +229 -0
  595. package/src/utils/cli-ask-question.js +93 -0
  596. package/src/utils/cli-progress.js +220 -0
  597. package/src/utils/cli-run-log.js +83 -0
  598. package/src/utils/cwd-confirm.js +39 -0
  599. package/src/utils/derive-project-name-from-cwd.js +119 -0
  600. package/src/utils/display/event-handlers.js +403 -0
  601. package/src/utils/display/formatters.js +132 -0
  602. package/src/utils/display/header.js +47 -0
  603. package/src/utils/display/resume-hint.js +41 -0
  604. package/src/utils/display/session-summary.js +118 -0
  605. package/src/utils/display/solomon.js +42 -0
  606. package/src/utils/display/spec-findings.js +44 -0
  607. package/src/utils/doc-links.js +29 -0
  608. package/src/utils/docker-install.js +34 -0
  609. package/src/utils/escape-regexp.js +15 -0
  610. package/src/utils/events.js +35 -0
  611. package/src/utils/fs.js +19 -0
  612. package/src/utils/garbage-collector.js +481 -0
  613. package/src/utils/git.js +279 -0
  614. package/src/utils/home-migration.js +87 -0
  615. package/src/utils/injection-guard.js +186 -0
  616. package/src/utils/install-hints.js +180 -0
  617. package/src/utils/json-extract.js +64 -0
  618. package/src/utils/locale.js +59 -0
  619. package/src/utils/logger.js +96 -0
  620. package/src/utils/managed-markers.js +93 -0
  621. package/src/utils/manifest-sniff.js +82 -0
  622. package/src/utils/messages.js +61 -0
  623. package/src/utils/model-selector.js +107 -0
  624. package/src/utils/os-detect.js +79 -0
  625. package/src/utils/paths.js +13 -0
  626. package/src/utils/pending-user-action.js +95 -0
  627. package/src/utils/port-check.js +6 -0
  628. package/src/utils/port-occupant.js +96 -0
  629. package/src/utils/pricing.js +29 -0
  630. package/src/utils/process.js +4 -0
  631. package/src/utils/project-detect.js +160 -0
  632. package/src/utils/prompt-project-name.js +35 -0
  633. package/src/utils/provider-env.js +116 -0
  634. package/src/utils/qmd-collection.js +72 -0
  635. package/src/utils/qmd-detect.js +18 -0
  636. package/src/utils/qmd-install.js +37 -0
  637. package/src/utils/quota-simulator.js +56 -0
  638. package/src/utils/rate-limit-detector.js +181 -0
  639. package/src/utils/repo-cleaner.js +79 -0
  640. package/src/utils/retry.js +88 -0
  641. package/src/utils/role-assigner.js +137 -0
  642. package/src/utils/role-env.js +58 -0
  643. package/src/utils/rtk-detect.js +18 -0
  644. package/src/utils/rtk-install.js +38 -0
  645. package/src/utils/rtk-wrapper.js +87 -0
  646. package/src/utils/run-context.js +75 -0
  647. package/src/utils/run-log.js +217 -0
  648. package/src/utils/run-registry.js +9 -0
  649. package/src/utils/shared-paths.js +12 -0
  650. package/src/utils/squeezr-detect.js +21 -0
  651. package/src/utils/squeezr-install.js +37 -0
  652. package/src/utils/stack-detect.js +135 -0
  653. package/src/utils/stall-detector.js +159 -0
  654. package/src/utils/status-dashboard.js +254 -0
  655. package/src/utils/task-file.js +109 -0
  656. package/src/utils/telemetry.js +124 -0
  657. package/src/utils/templates-root.js +61 -0
  658. package/src/utils/tool-call-extractor.js +56 -0
  659. package/src/utils/tool-governor.js +116 -0
  660. package/src/utils/tool-installer.js +93 -0
  661. package/src/utils/update-check.js +280 -0
  662. package/src/utils/vector-store-orphans.js +89 -0
  663. package/src/utils/welcome.js +47 -0
  664. package/src/utils/with-timeout.js +42 -0
  665. package/src/utils/wizard.js +47 -0
  666. package/src/webperf/cwv-gate.js +94 -0
  667. package/src/webperf/devtools-detect.js +112 -0
  668. package/src/webperf/scanner.js +209 -0
  669. package/src/webperf/webperf-role-integration.js +66 -0
  670. package/templates/coder-rules.md +45 -0
  671. package/templates/docker-compose.sonar.yml +60 -0
  672. package/templates/domains/example-dental.md +57 -0
  673. package/templates/kj.config.yml +128 -0
  674. package/templates/review-rules.md +17 -0
  675. package/templates/roles/architect/anthropic.md +74 -0
  676. package/templates/roles/architect/default.md +72 -0
  677. package/templates/roles/architect/google.md +61 -0
  678. package/templates/roles/architect/openai.md +60 -0
  679. package/templates/roles/architect/opencode.md +59 -0
  680. package/templates/roles/architect.md +76 -0
  681. package/templates/roles/audit.md +68 -0
  682. package/templates/roles/coder/anthropic.md +81 -0
  683. package/templates/roles/coder/default.md +78 -0
  684. package/templates/roles/coder/google.md +87 -0
  685. package/templates/roles/coder/openai.md +68 -0
  686. package/templates/roles/coder/opencode.md +58 -0
  687. package/templates/roles/coder.md +152 -0
  688. package/templates/roles/commiter.md +44 -0
  689. package/templates/roles/discover.md +167 -0
  690. package/templates/roles/domain-curator.md +52 -0
  691. package/templates/roles/hu-reviewer.md +236 -0
  692. package/templates/roles/impeccable-design.md +87 -0
  693. package/templates/roles/impeccable.md +125 -0
  694. package/templates/roles/karajan-brain.md +95 -0
  695. package/templates/roles/onboarder.md +45 -0
  696. package/templates/roles/planner/anthropic.md +59 -0
  697. package/templates/roles/planner/default.md +57 -0
  698. package/templates/roles/planner/google.md +75 -0
  699. package/templates/roles/planner/openai.md +55 -0
  700. package/templates/roles/planner/opencode.md +62 -0
  701. package/templates/roles/planner.md +61 -0
  702. package/templates/roles/refactorer.md +48 -0
  703. package/templates/roles/repairer.md +103 -0
  704. package/templates/roles/researcher.md +41 -0
  705. package/templates/roles/reviewer/anthropic.md +61 -0
  706. package/templates/roles/reviewer/default.md +62 -0
  707. package/templates/roles/reviewer/google.md +63 -0
  708. package/templates/roles/reviewer/openai.md +57 -0
  709. package/templates/roles/reviewer/opencode.md +42 -0
  710. package/templates/roles/reviewer-paranoid.md +38 -0
  711. package/templates/roles/reviewer-relaxed.md +34 -0
  712. package/templates/roles/reviewer-strict.md +37 -0
  713. package/templates/roles/reviewer.md +62 -0
  714. package/templates/roles/security.md +54 -0
  715. package/templates/roles/solomon/anthropic.md +71 -0
  716. package/templates/roles/solomon/default.md +83 -0
  717. package/templates/roles/solomon/google.md +61 -0
  718. package/templates/roles/solomon/openai.md +53 -0
  719. package/templates/roles/solomon/opencode.md +55 -0
  720. package/templates/roles/solomon.md +83 -0
  721. package/templates/roles/sonar.md +49 -0
  722. package/templates/roles/spec-reviewer.md +47 -0
  723. package/templates/roles/tester.md +50 -0
  724. package/templates/roles/triage.md +69 -0
  725. package/templates/skills/kj-architect.md +45 -0
  726. package/templates/skills/kj-audit.md +63 -0
  727. package/templates/skills/kj-board.md +16 -0
  728. package/templates/skills/kj-code.md +51 -0
  729. package/templates/skills/kj-csv-transform.md +40 -0
  730. package/templates/skills/kj-data-report.md +44 -0
  731. package/templates/skills/kj-discover.md +24 -0
  732. package/templates/skills/kj-rag-query.md +50 -0
  733. package/templates/skills/kj-review.md +47 -0
  734. package/templates/skills/kj-run.md +73 -0
  735. package/templates/skills/kj-security.md +49 -0
  736. package/templates/skills/kj-sonar.md +41 -0
  737. package/templates/skills/kj-sql-analysis.md +40 -0
  738. package/templates/skills/kj-test.md +40 -0
  739. package/templates/workflows/automerge.yml +30 -0
  740. package/templates/workflows/houston-override.yml +46 -0
  741. package/templates/workflows/kj-ci-gateway.yml +58 -0
  742. package/vendor/tree-sitter-grammars/README.md +21 -0
  743. package/vendor/tree-sitter-grammars/go.wasm +0 -0
  744. package/vendor/tree-sitter-grammars/java.wasm +0 -0
  745. package/vendor/tree-sitter-grammars/python.wasm +0 -0
  746. package/vendor/tree-sitter-grammars/rust.wasm +0 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Plan persistence — save, load, list, update, delete plans.
3
+ * Plans are stored at ~/.kj/plans/<projectSlug>/<planId>.json
4
+ * Supports v1 (text-only) and v2 (plans + HUs) schemas.
5
+ */
6
+
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { writeJsonAtomic } from "../utils/atomic-write.js";
10
+ import { resolveHome } from "../utils/paths.js";
11
+ import { getSharedPlansDir } from "../utils/shared-paths.js";
12
+ import { generatePlanId, normaliseAlias } from "./plan-id.js";
13
+
14
+ /**
15
+ * A plan JSON file failed to parse — typically an interrupted write
16
+ * leaves it truncated. Previously every read site swallowed this with
17
+ * a silent catch and the plan vanished from `kj plan list` and
18
+ * `kj plan load` as if it had never existed.
19
+ *
20
+ * Now: warn loudly so the user knows their plan is corrupt, and rename
21
+ * the file to `<name>.corrupt-<ts>` so it stops blocking subsequent
22
+ * reads of the same alias / planId (without destroying evidence).
23
+ * Best-effort: if the rename itself fails (read-only FS, permissions),
24
+ * we log and move on — never escalate to a throw, that would block the
25
+ * caller from reading the rest of the directory.
26
+ */
27
+ async function handleCorruptPlanFile(filePath, error) {
28
+ const corruptPath = `${filePath}.corrupt-${Date.now()}`;
29
+ // eslint-disable-next-line no-console
30
+ console.warn(`[plan-store] Corrupt plan JSON at ${filePath}: ${error.message}`);
31
+ try {
32
+ await fs.rename(filePath, corruptPath);
33
+ // eslint-disable-next-line no-console
34
+ console.warn(`[plan-store] Renamed to ${path.basename(corruptPath)} so it stops blocking subsequent reads.`);
35
+ } catch (renameErr) {
36
+ // eslint-disable-next-line no-console
37
+ console.warn(`[plan-store] Could not rename ${filePath}: ${renameErr.message}`);
38
+ }
39
+ }
40
+ import { isPlanV2, migratePlanV1toV2 } from "./plan-schema.js";
41
+
42
+ // Delegates to the unified resolver in src/utils/paths.js. PR 3 of
43
+ // KJC-PCS-0047 flipped the default from `.kj` to `.karajan` — plans
44
+ // now live under `~/.karajan/plans/`. The migrator (src/utils/
45
+ // home-migration.js) auto-moves legacy `~/.kj/plans/` content on the
46
+ // next `kj` invocation, so existing users are not stranded.
47
+ export function getKjHome() {
48
+ return resolveHome({ defaultSegment: ".karajan" });
49
+ }
50
+
51
+ /**
52
+ * Deterministic slug for a project path — stable across plan-store writes,
53
+ * HU-board syncs and board URL routing. Keep in lockstep with
54
+ * `packages/hu-board/src/sync.js::deriveProjectIdFromDir`.
55
+ * @param {string} projectDir - absolute project path
56
+ * @returns {string}
57
+ */
58
+ export function projectSlug(projectDir) {
59
+ return projectDir
60
+ .replace(/^\//, "")
61
+ .replace(/[/\\]/g, "_")
62
+ .replaceAll(/[^a-zA-Z0-9_.-]/g, "")
63
+ .slice(0, 120);
64
+ }
65
+
66
+ export function plansDir(projectDir) {
67
+ return path.join(getKjHome(), "plans", projectSlug(projectDir));
68
+ }
69
+
70
+ /**
71
+ * Save a plan to disk. Accepts both v1 and v2 schemas.
72
+ * @param {string} projectDir
73
+ * @param {object} planResult - v1 { task, plan, raw } or v2 { planId, version, hus, ... }
74
+ * @returns {Promise<string>} planId
75
+ */
76
+ export async function savePlan(projectDir, planResult) {
77
+ const dir = plansDir(projectDir);
78
+ await fs.mkdir(dir, { recursive: true });
79
+
80
+ // v2 plans already have planId and full schema
81
+ if (isPlanV2(planResult)) {
82
+ const filePath = path.join(dir, `${planResult.planId}.json`);
83
+ planResult.updatedAt = new Date().toISOString();
84
+ // TSK-0341 / garbage-collector: stamp the absolute projectDir on every
85
+ // plan so the GC can check `fs.exists(projectDir)` reliably. Reverse-
86
+ // engineering it from the slug is lossy (underscores in the original
87
+ // name collide with underscores introduced by slash-substitution).
88
+ if (!planResult.projectDir && projectDir) planResult.projectDir = projectDir;
89
+ // Humanización IDs: si el plan no tiene alias todavía, derívalo de
90
+ // plan.name (lo que el planner pidió al usuario). Resuelve
91
+ // colisiones con sufijo -2, -3… leyendo el dir antes.
92
+ if (!planResult.alias && planResult.name) {
93
+ planResult.alias = await resolveUniqueAlias(dir, planResult.planId, normaliseAlias(planResult.name));
94
+ }
95
+ await writeJsonAtomic(filePath, planResult);
96
+ return planResult.planId;
97
+ }
98
+
99
+ // v1: generate planId and wrap
100
+ const planId = planResult.planId || generatePlanId();
101
+ const record = {
102
+ planId,
103
+ task: planResult.task || null,
104
+ researchContext: planResult.researchContext || null,
105
+ architectContext: planResult.architectContext || null,
106
+ plan: planResult.plan || null,
107
+ raw: planResult.raw || null,
108
+ createdAt: new Date().toISOString()
109
+ };
110
+
111
+ const filePath = path.join(dir, `${planId}.json`);
112
+ await writeJsonAtomic(filePath, record);
113
+ return planId;
114
+ }
115
+
116
+ /**
117
+ * Resuelve un alias único en `dir`. Si `base` ya está en uso por otro
118
+ * plan, intenta `base-2`, `base-3`… hasta encontrar libre.
119
+ * @param {string} dir
120
+ * @param {string} ownPlanId - planId del plan que se está guardando (su propio archivo se ignora en el lookup).
121
+ * @param {string|null} base
122
+ * @returns {Promise<string|null>}
123
+ */
124
+ async function resolveUniqueAlias(dir, ownPlanId, base) {
125
+ if (!base) return null;
126
+ let files;
127
+ try { files = await fs.readdir(dir); } catch { return base; }
128
+ const taken = new Set();
129
+ for (const f of files) {
130
+ if (!f.endsWith(".json")) continue;
131
+ const filePath = path.join(dir, f);
132
+ try {
133
+ const record = JSON.parse(await fs.readFile(filePath, "utf8"));
134
+ if (record.planId === ownPlanId) continue;
135
+ if (record.alias) taken.add(record.alias);
136
+ } catch (err) {
137
+ if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
138
+ }
139
+ }
140
+ if (!taken.has(base)) return base;
141
+ for (let i = 2; i < 1000; i += 1) {
142
+ const candidate = `${base}-${i}`;
143
+ if (!taken.has(candidate)) return candidate;
144
+ }
145
+ return `${base}-${ownPlanId.slice(-4)}`; // fallback improbable
146
+ }
147
+
148
+ /**
149
+ * Load a plan by ID or alias. Auto-migrates v1 to v2 on read (lazy).
150
+ * Acepta tanto el planId canónico (plan-XXX-XX) como el alias humano
151
+ * (greta-app). Primero busca por filename exacto (planId.json), luego
152
+ * escanea el dir y matchea por alias.
153
+ *
154
+ * @returns {Promise<object|null>} v2 plan o null
155
+ */
156
+ export async function loadPlan(projectDir, ref) {
157
+ if (!ref) return null;
158
+ // Local plans take precedence — if a teammate's shared copy and your
159
+ // working copy share a planId, your local edits win until you re-share.
160
+ const local = await loadPlanFromDir(plansDir(projectDir), ref);
161
+ if (local) return local;
162
+ // Fallback: shared plans committed by a teammate. KJC-PRP-0002 PR1.
163
+ return loadPlanFromDir(getSharedPlansDir(projectDir), ref);
164
+ }
165
+
166
+ async function loadPlanFromDir(dir, ref) {
167
+ // 1. Match exacto por planId.json (camino caliente).
168
+ const direct = path.join(dir, `${ref}.json`);
169
+ try {
170
+ const data = await fs.readFile(direct, "utf8");
171
+ const plan = JSON.parse(data);
172
+ if (!isPlanV2(plan)) return migratePlanV1toV2(plan);
173
+ return plan;
174
+ } catch (err) {
175
+ // ENOENT is expected (caller may have passed an alias, not a planId)
176
+ // → fall through to the alias scan. Anything else means the file is
177
+ // there but unreadable / corrupt; surface it instead of silently
178
+ // hiding the plan.
179
+ if (err.code !== "ENOENT") await handleCorruptPlanFile(direct, err);
180
+ }
181
+ // 2. Escaneo del dir por alias. Coste O(N) pero N suele ser <10.
182
+ let files;
183
+ try { files = await fs.readdir(dir); } catch { return null; }
184
+ for (const f of files) {
185
+ if (!f.endsWith(".json")) continue;
186
+ const filePath = path.join(dir, f);
187
+ try {
188
+ const record = JSON.parse(await fs.readFile(filePath, "utf8"));
189
+ if (record.alias === ref || record.planId === ref) {
190
+ if (!isPlanV2(record)) return migratePlanV1toV2(record);
191
+ return record;
192
+ }
193
+ } catch (err) {
194
+ if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * Delete a plan from disk.
202
+ * @returns {Promise<boolean>}
203
+ */
204
+ export async function deletePlan(projectDir, planId) {
205
+ const filePath = path.join(plansDir(projectDir), `${planId}.json`);
206
+ try {
207
+ await fs.unlink(filePath);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * List all plans for a project, sorted newest first.
216
+ */
217
+ export async function listPlans(projectDir) {
218
+ const dir = plansDir(projectDir);
219
+ let files;
220
+ try {
221
+ files = await fs.readdir(dir);
222
+ } catch {
223
+ return [];
224
+ }
225
+
226
+ const plans = [];
227
+ for (const file of files) {
228
+ if (!file.endsWith(".json")) continue;
229
+ if (file.includes(".corrupt-")) continue; // own renamed-aside files
230
+ const filePath = path.join(dir, file);
231
+ try {
232
+ const data = await fs.readFile(filePath, "utf8");
233
+ const record = JSON.parse(data);
234
+ plans.push({
235
+ planId: record.planId,
236
+ alias: record.alias || null,
237
+ task: record.task,
238
+ name: record.name || null,
239
+ status: record.status || "draft",
240
+ version: record.version || 1,
241
+ huCount: Array.isArray(record.hus) ? record.hus.length : 0,
242
+ createdAt: record.createdAt
243
+ });
244
+ } catch (err) {
245
+ if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
246
+ }
247
+ }
248
+
249
+ plans.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
250
+ return plans;
251
+ }
252
+
253
+ /**
254
+ * Get the most recent plan.
255
+ */
256
+ export async function getLatestPlan(projectDir) {
257
+ const all = await listPlans(projectDir);
258
+ if (all.length === 0) return null;
259
+ return loadPlan(projectDir, all[0].planId);
260
+ }
@@ -0,0 +1,122 @@
1
+ // KJC-TSK-0399: deterministic post-review pass — cycles, orphan refs,
2
+ // missing short_id. No LLM. Fix shape: { kind, huId?, ...details };
3
+ // stored in plan.review.structural_fixes.
4
+
5
+ export function runStructuralPass(plan) {
6
+ const fixes = [];
7
+ if (!plan || !Array.isArray(plan.hus) || plan.hus.length === 0) {
8
+ return { fixes, modified: false };
9
+ }
10
+ fixes.push(...removeOrphanRefs(plan));
11
+ fixes.push(...breakCycles(plan));
12
+ fixes.push(...assignMissingShortIds(plan));
13
+ const modified = fixes.length > 0;
14
+ if (modified) plan.updatedAt = new Date().toISOString();
15
+ return { fixes, modified };
16
+ }
17
+
18
+ export function removeOrphanRefs(plan) {
19
+ const fixes = [];
20
+ const known = new Set(plan.hus.map((h) => h.id));
21
+ for (const hu of plan.hus) {
22
+ const removed = [];
23
+ if (Array.isArray(hu.blocked_by) && hu.blocked_by.length > 0) {
24
+ hu.blocked_by = hu.blocked_by.filter((d) => {
25
+ if (known.has(d)) return true;
26
+ removed.push(d); return false;
27
+ });
28
+ }
29
+ if (removed.length > 0) fixes.push({ kind: "orphan_ref_removed", huId: hu.id, removed });
30
+ }
31
+ return fixes;
32
+ }
33
+
34
+ function breakCycles(plan) {
35
+ const fixes = [];
36
+ let cycles = findCycles(plan.hus);
37
+ let budget = plan.hus.length;
38
+ while (cycles.length > 0 && budget-- > 0) {
39
+ const cycle = cycles[0];
40
+ const dropped = breakCycle(plan, cycle);
41
+ if (!dropped) break;
42
+ fixes.push({ kind: "cycle_break", cycle: [...cycle], droppedEdge: dropped });
43
+ cycles = findCycles(plan.hus);
44
+ }
45
+ return fixes;
46
+ }
47
+
48
+ export function findCycles(hus) {
49
+ const graph = new Map();
50
+ for (const hu of hus) graph.set(hu.id, Array.isArray(hu.blocked_by) ? hu.blocked_by : []);
51
+ const cycles = [];
52
+ const seen = new Set();
53
+ const onStack = new Set();
54
+ const stack = [];
55
+ function dfs(node) {
56
+ onStack.add(node); stack.push(node); seen.add(node);
57
+ for (const dep of graph.get(node) || []) {
58
+ if (!graph.has(dep)) continue;
59
+ if (onStack.has(dep)) {
60
+ const idx = stack.indexOf(dep);
61
+ if (idx >= 0) {
62
+ const cycle = stack.slice(idx);
63
+ if (!cycles.some((c) => sameCycle(c, cycle))) cycles.push(cycle);
64
+ }
65
+ } else if (!seen.has(dep)) dfs(dep);
66
+ }
67
+ stack.pop(); onStack.delete(node);
68
+ }
69
+ for (const id of graph.keys()) if (!seen.has(id)) dfs(id);
70
+ return cycles;
71
+ }
72
+
73
+ function sameCycle(a, b) {
74
+ if (a.length !== b.length) return false;
75
+ return [...a].sort().join("|") === [...b].sort().join("|");
76
+ }
77
+
78
+ // Heurística: dropea arista cuyo origen tiene menos incoming. Tiebreak edge.to.
79
+ export function breakCycle(plan, cycle) {
80
+ const incoming = new Map();
81
+ for (const hu of plan.hus) {
82
+ for (const dep of hu.blocked_by || []) incoming.set(dep, (incoming.get(dep) || 0) + 1);
83
+ }
84
+ const edges = [];
85
+ for (let i = 0; i < cycle.length; i++) {
86
+ const from = cycle[i];
87
+ const to = cycle[(i + 1) % cycle.length];
88
+ const hu = plan.hus.find((h) => h.id === from);
89
+ if (hu && Array.isArray(hu.blocked_by) && hu.blocked_by.includes(to)) edges.push({ from, to });
90
+ }
91
+ if (edges.length === 0) return null;
92
+ edges.sort((a, b) => {
93
+ const d = (incoming.get(a.from) || 0) - (incoming.get(b.from) || 0);
94
+ return d !== 0 ? d : a.to.localeCompare(b.to);
95
+ });
96
+ const drop = edges[0];
97
+ const hu = plan.hus.find((h) => h.id === drop.from);
98
+ hu.blocked_by = hu.blocked_by.filter((d) => d !== drop.to);
99
+ return drop;
100
+ }
101
+
102
+ export function assignMissingShortIds(plan) {
103
+ const fixes = [];
104
+ const used = new Set();
105
+ let maxAuto = 0;
106
+ for (const hu of plan.hus) {
107
+ if (!hu.short_id) continue;
108
+ used.add(hu.short_id);
109
+ const m = String(hu.short_id).match(/^AUTOFIX-(\d+)$/);
110
+ if (m) maxAuto = Math.max(maxAuto, Number(m[1]));
111
+ }
112
+ for (const hu of plan.hus) {
113
+ if (hu.short_id) continue;
114
+ maxAuto += 1;
115
+ const candidate = `AUTOFIX-${String(maxAuto).padStart(3, "0")}`;
116
+ if (used.has(candidate)) continue;
117
+ hu.short_id = candidate;
118
+ used.add(candidate);
119
+ fixes.push({ kind: "short_id_assigned", huId: hu.id, assigned: candidate });
120
+ }
121
+ return fixes;
122
+ }
@@ -0,0 +1,4 @@
1
+ // Shim: plan-validation now lives in karajan-core/plan-validation so
2
+ // the hu-board workspace can consume it without a relative dep on the
3
+ // CLI src tree. KJC-TSK-0511 PR6.
4
+ export { validateBlockedByChange } from "karajan-core/plan-validation";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * KJC-TSK-0397: auto-inject a [PREFLIGHT-000] HU at the top of every
3
+ * plan generated by `kj plan generate` so the sub-pipeline gates on a
4
+ * clean environment before burning tokens on functional HUs.
5
+ *
6
+ * Design points:
7
+ * - Pure shell tests — no Karajan-specific runtime, just commands
8
+ * that exit 0 when the environment is ready.
9
+ * - Stack-aware: Node, Python and Firebase signals come straight
10
+ * from detectProjectStack() + on-disk file probes. Adding a new
11
+ * stack is one entry in `composePreflightTests`.
12
+ * - Idempotent: re-running over a plan that already has a HU with
13
+ * id `PREFLIGHT-000` or a title containing 'preflight' / 'verificar
14
+ * entorno' returns the plan unchanged. Required because the same
15
+ * planner output may flow through structural-pass + plan-fixer
16
+ * before reaching savePlan.
17
+ * - All other HUs' `blocked_by` get the PREFLIGHT-000 id appended
18
+ * (unless they already have it, again idempotent). Their own
19
+ * dependencies are preserved.
20
+ */
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+
24
+ import { detectProjectStack } from "../utils/stack-detect.js";
25
+
26
+ const PREFLIGHT_ID = "PREFLIGHT-000";
27
+
28
+ /**
29
+ * Compose stack-aware shell acceptance tests. Always includes a
30
+ * `git status --porcelain` clean-tree check; everything else is
31
+ * additive based on what the project actually uses.
32
+ */
33
+ export function composePreflightTests(stack, projectDir = ".") {
34
+ const tests = [
35
+ { type: "shell", content: "git status --porcelain | (! grep -q .)" },
36
+ ];
37
+ const lang = stack?.language;
38
+ if (lang === "javascript" || lang === "typescript") {
39
+ tests.push({ type: "shell", content: "node --version | grep -qE '^v(2[0-9]|[3-9][0-9])'" });
40
+ if (existsSync(join(projectDir, "package.json"))) {
41
+ tests.push({ type: "shell", content: "npm install --no-audit --no-fund" });
42
+ tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ test$')\" || npm test" });
43
+ tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ lint$')\" || npm run lint" });
44
+ }
45
+ } else if (lang === "python") {
46
+ tests.push({ type: "shell", content: "python --version | grep -qE 'Python 3\\.(1[0-9]|[2-9][0-9])'" });
47
+ if (existsSync(join(projectDir, "requirements.txt"))) {
48
+ tests.push({ type: "shell", content: "pip install -r requirements.txt" });
49
+ } else if (existsSync(join(projectDir, "pyproject.toml"))) {
50
+ tests.push({ type: "shell", content: "test -f poetry.lock && poetry install || pip install -e ." });
51
+ }
52
+ tests.push({ type: "shell", content: "command -v pytest && pytest --collect-only || true" });
53
+ }
54
+ if (existsSync(join(projectDir, "firebase.json"))) {
55
+ tests.push({ type: "shell", content: "firebase projects:list" });
56
+ }
57
+ if (existsSync(join(projectDir, ".gcloudignore"))) {
58
+ tests.push({ type: "shell", content: "gcloud auth list --filter=status:ACTIVE --format='value(account)' | (! test -z \"$(cat)\")" });
59
+ }
60
+ return tests;
61
+ }
62
+
63
+ /**
64
+ * Return true when the plan already has a preflight-style HU (by id or
65
+ * title). Pattern-matches conservatively to avoid duplicate injection.
66
+ */
67
+ export function hasPreflightHu(plan) {
68
+ if (!plan?.hus?.length) return false;
69
+ return plan.hus.some((hu) => {
70
+ if (hu.id === PREFLIGHT_ID) return true;
71
+ const title = (hu.title || hu.description || "").toLowerCase();
72
+ return title.includes("preflight-000") || title.includes("verificar entorno") || title.includes("preflight check");
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Prepend a [PREFLIGHT-000] HU to the plan and append its id to the
78
+ * `blocked_by` of every other HU. Returns the mutated plan reference
79
+ * for convenience; mutation is in place so callers don't need to
80
+ * reassign. No-op when the plan already has a preflight HU.
81
+ */
82
+ export async function prependPreflightHu(plan, projectDir) {
83
+ if (!plan?.hus) return plan;
84
+ if (hasPreflightHu(plan)) return plan;
85
+ const stack = await detectProjectStack(projectDir);
86
+ const preflight = {
87
+ id: PREFLIGHT_ID,
88
+ title: "[PREFLIGHT-000] Verificar entorno de desarrollo listo",
89
+ description: "[PREFLIGHT-000] Block functional HUs until the project's environment is reproducible: deps installed, build/test/lint passing, git tree clean, cloud auth ready when applicable.",
90
+ task_type: "infra",
91
+ // KJC-BUG-0108: without an explicit status, `kj plan ready` rejects the
92
+ // whole plan with "invalid status undefined".
93
+ status: "pending",
94
+ blocked_by: [],
95
+ reuse: [],
96
+ acceptance_tests: composePreflightTests(stack, projectDir),
97
+ spec_section: null,
98
+ };
99
+ for (const hu of plan.hus) {
100
+ if (!Array.isArray(hu.blocked_by)) hu.blocked_by = [];
101
+ if (!hu.blocked_by.includes(PREFLIGHT_ID)) hu.blocked_by.push(PREFLIGHT_ID);
102
+ }
103
+ plan.hus.unshift(preflight);
104
+ return plan;
105
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Detect numbered/sectioned headings in a task description so the
3
+ * planner prompt can demand `spec_section` per HU instead of leaving
4
+ * it as a soft "preferred" hint that LLMs ignore.
5
+ *
6
+ * Recognises three common shapes (kept intentionally narrow — false
7
+ * positives turn the planner prompt into noise):
8
+ *
9
+ * ## 1. Overview → section "1", title "Overview"
10
+ * ### 2.1 add → section "2.1", title "add"
11
+ * ## §5 Initial Scope → section "5", title "Initial Scope"
12
+ *
13
+ * The matcher is anchored to markdown heading lines (`^#+`) so prose
14
+ * mentions of a number ("see version 4.2") never trigger detection.
15
+ *
16
+ * Returns `{ hasSpec, sections, sectionList }`:
17
+ * - `hasSpec` boolean — true when at least one heading matched.
18
+ * Prompt builders use this to switch language from
19
+ * "preferred" to "MUST".
20
+ * - `sections` string[] — bare section refs, dedup, in document
21
+ * order. e.g. ["1", "2", "2.1", "2.2", "3"].
22
+ * - `sectionList` Array<{section, title}> — full list with titles
23
+ * so the prompt can echo the checklist back at the
24
+ * LLM. Keeps order, dedup by section ref.
25
+ *
26
+ * @param {string} text
27
+ * @returns {{ hasSpec: boolean, sections: string[], sectionList: Array<{section: string, title: string}> }}
28
+ */
29
+ export function detectSpecSections(text) {
30
+ const empty = { hasSpec: false, sections: [], sectionList: [] };
31
+ if (!text || typeof text !== "string") return empty;
32
+
33
+ // Anchored to heading lines. Two regex shapes — numbered sections
34
+ // and §-prefixed sections — are both common in the wild.
35
+ //
36
+ // ^#{1,6}\s* ← markdown heading marker (any depth)
37
+ // §?\s* ← optional "§"
38
+ // (\d+(?:\.\d+){0,4}) ← section number, up to 5 levels deep
39
+ // [.)]?\s+ ← optional trailing "." or ")", then space
40
+ // (.+?)\s*$ ← title (lazy, trim trailing whitespace)
41
+ const headingRe = /^#{1,6}\s*§?\s*(\d+(?:\.\d+){0,4})[.)]?\s+(.+?)\s*$/;
42
+
43
+ const seen = new Set();
44
+ const sectionList = [];
45
+ for (const rawLine of text.split(/\r?\n/)) {
46
+ const line = rawLine.trimEnd();
47
+ const m = headingRe.exec(line);
48
+ if (!m) continue;
49
+ const section = m[1];
50
+ if (seen.has(section)) continue;
51
+ seen.add(section);
52
+ sectionList.push({ section, title: m[2].trim() });
53
+ }
54
+
55
+ if (sectionList.length === 0) return empty;
56
+ return {
57
+ hasSpec: true,
58
+ sections: sectionList.map((s) => s.section),
59
+ sectionList,
60
+ };
61
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Tests-first second pass for `kj plan` (v2.7.5).
3
+ *
4
+ * The first planner call asks for steps + acceptance_tests in one
5
+ * shot. Modern LLMs (Claude, Codex) reliably produce the steps but
6
+ * frequently skip the acceptance_tests sub-instruction when the
7
+ * surrounding spec is dense — a known weakness of long single-shot
8
+ * prompts. Pre-v2.7.5 we papered over this with a generic `npx
9
+ * vitest run` placeholder that wasn't actually a per-HU contract.
10
+ *
11
+ * This module runs a focused SECOND pass that asks the LLM ONLY to
12
+ * fill in tests for the HUs that came back without them. Because the
13
+ * prompt is shorter and has a single deliverable, the hit rate is
14
+ * effectively 100% on the HUs that slipped through the first time.
15
+ *
16
+ * Returns a map `{ huId → acceptance_tests[] }` so the caller can
17
+ * patch the plan in place. The patch always normalises through
18
+ * `plan-schema::normaliseAcceptanceTests`, so any malformed entries
19
+ * the LLM produces are silently dropped (better than corrupting the
20
+ * plan with junk).
21
+ */
22
+
23
+ import { extractFirstJson } from "../utils/json-extract.js";
24
+ import { normaliseAcceptanceTests } from "./plan-schema.js";
25
+ import { withBrainRecovery } from "../brain/with-brain-recovery.js";
26
+
27
+ /**
28
+ * Build the focused-pass prompt asking ONLY for tests for the given
29
+ * HUs. Exported for unit-testing the prompt shape without an LLM.
30
+ *
31
+ * @param {object} args
32
+ * @param {string} args.task
33
+ * @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
34
+ * @returns {string}
35
+ */
36
+ export function buildSynthesizerPrompt({ task, husNeedingTests, stack = null, testFramework = null }) {
37
+ const lines = [
38
+ "You previously generated an implementation plan for the task below, but some HUs were left without acceptance_tests.",
39
+ "Your ONLY job in this call is to fill that gap.",
40
+ "",
41
+ "Output MUST be a JSON object with this exact shape:",
42
+ "",
43
+ "```json",
44
+ "{",
45
+ ' "tests": {',
46
+ ' "<huId>": [',
47
+ ' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
48
+ ' { "type": "shell", "content": "<bash command that must exit 0>" }',
49
+ " ]",
50
+ " }",
51
+ "}",
52
+ "```",
53
+ "",
54
+ "Rules:",
55
+ "- The keys of `tests` are the HU ids EXACTLY as listed below.",
56
+ "- 2-4 tests per HU. At least one happy-path and one edge case.",
57
+ "- gherkin = behaviour spec; shell = concrete bash command exiting 0 on pass.",
58
+ "- Tests must be specific to the HU. Do NOT use generic fallbacks like `npx vitest run`.",
59
+ "- Tests must be writable BEFORE the HU is implemented (they should fail until done).",
60
+ "- Return ONLY the JSON — no prose, no markdown fences around it.",
61
+ "",
62
+ ];
63
+ // Stack-aware hint: tell the LLM what test framework / language to
64
+ // emit shell commands for. Without this it defaulted to vitest /
65
+ // npm even on Python / Go / Rust projects (sesgo-de-stack audit).
66
+ if (stack || testFramework) {
67
+ lines.push("## Project Stack — generate shell tests for THIS stack, not generic JS");
68
+ if (stack?.language) lines.push(`- Language: **${stack.language}**`);
69
+ if (stack?.frameworks?.length) lines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
70
+ if (testFramework?.hasTests && testFramework?.framework) {
71
+ lines.push(`- Test framework already in use: **${testFramework.framework}** — use it for the shell commands.`);
72
+ } else if (stack?.language && stack.language !== "javascript" && stack.language !== "typescript") {
73
+ lines.push(`- Use the canonical test runner for ${stack.language} (pytest / go test / cargo test / rspec). Do NOT emit vitest, jest, or npm commands.`);
74
+ }
75
+ lines.push("");
76
+ }
77
+ lines.push("## Task", task, "", "## HUs that need acceptance_tests", "");
78
+ for (const hu of husNeedingTests) {
79
+ lines.push(`### ${hu.id}: ${hu.title || ""}`);
80
+ lines.push(hu.scope ? `Scope: ${hu.scope}` : "(no scope)");
81
+ lines.push("");
82
+ }
83
+ return lines.join("\n");
84
+ }
85
+
86
+ /**
87
+ * Run the focused second pass. Pass an `agent` with a `runTask`
88
+ * method (Claude / Codex / Gemini wrappers all expose it).
89
+ *
90
+ * @param {object} args
91
+ * @param {object} args.agent
92
+ * @param {string} args.task
93
+ * @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
94
+ * @param {Function} [args.onOutput]
95
+ * @param {number} [args.silenceTimeoutMs]
96
+ * @param {number} [args.timeoutMs]
97
+ * @returns {Promise<{ ok: boolean, tests?: Record<string, object[]>, error?: string }>}
98
+ */
99
+ export async function synthesizeMissingTests({ agent, task, husNeedingTests, onOutput, silenceTimeoutMs, timeoutMs, emitter, eventBase, logger }) {
100
+ if (!Array.isArray(husNeedingTests) || husNeedingTests.length === 0) {
101
+ return { ok: true, tests: {} };
102
+ }
103
+ const prompt = buildSynthesizerPrompt({ task, husNeedingTests });
104
+ const runArgs = { prompt, role: "planner" };
105
+ if (onOutput) runArgs.onOutput = onOutput;
106
+ if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
107
+ if (timeoutMs) runArgs.timeoutMs = timeoutMs;
108
+
109
+ // KJC-TSK-0413: Brain Recovery wraps el agent call.
110
+ const result = await withBrainRecovery({
111
+ agent, taskArgs: runArgs, role: "tests-synthesizer",
112
+ emitter, eventBase, logger,
113
+ });
114
+ if (!result.ok) {
115
+ return { ok: false, error: result.error || `synthesizer agent failed (${result.recovery?.class || "unknown"})`, recovery: result.recovery, action: result.action };
116
+ }
117
+
118
+ const parsed = extractFirstJson(result.output || "");
119
+ const rawTests = parsed?.tests;
120
+ if (!rawTests || typeof rawTests !== "object") {
121
+ return { ok: false, error: "synthesizer returned no `tests` object" };
122
+ }
123
+
124
+ // Normalise per HU so junk entries don't slip into the plan.
125
+ const tests = {};
126
+ for (const [huId, list] of Object.entries(rawTests)) {
127
+ const cleaned = normaliseAcceptanceTests(list);
128
+ if (cleaned.length > 0) tests[huId] = cleaned;
129
+ }
130
+ return { ok: true, tests };
131
+ }