karajan-code 4.70.3 → 5.0.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 (388) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +34 -3
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -1,642 +0,0 @@
1
- /**
2
- * HU Sub-Pipeline — executes each certified HU as an independent sub-pipeline
3
- * with per-HU state tracking, topological ordering, and dependency-aware blocking.
4
- */
5
- import { assertNoCycle, topologicalSort } from "../hu/graph.js";
6
- import { updateStoryStatus, loadHuBatch, saveHuBatch, HU_STATUS } from "../hu/store.js";
7
- import { emitProgress, makeEvent } from "../utils/events.js";
8
- import { refineHuWithContext } from "../hu/lazy-planner.js";
9
- import { join } from "node:path";
10
- import { acquireSlot, releaseSlot } from "karajan-core/slot-registry";
11
- import { getKarajanHome } from "karajan-core/paths";
12
- import { findParallelGroups, createWorktree, mergeWorktree, removeWorktree } from "../hu/parallel-executor.js";
13
- import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
14
- import { partitionConflictFree } from "./hu-scheduler.js";
15
- import { createParallelLimiter, planBudgetUsd } from "./parallel-limiter.js";
16
-
17
- /**
18
- * Determine whether the HU reviewer result needs the sub-pipeline path.
19
- *
20
- * Pre-fix this returned true only when there were > 1 certified stories.
21
- * That broke single-HU runs (`kj run --plan <id> --hu <huId>`, the path
22
- * the board's per-card ▶ button drives): with exactly one HU certified
23
- * the flow fell through to the standard single-task pipeline, which
24
- * does NOT call updateStoryStatus / saveHuBatch / emit hu:* events.
25
- * Result: the HU stayed forever in `coding` even after the run finished
26
- * because nobody persisted "done"/"failed" anywhere — board kanban
27
- * showed "1 running…" indefinitely, plan JSON kept the stale status.
28
- *
29
- * The sub-pipeline already handles a 1-HU batch as a degenerate case
30
- * (no dependency graph to resolve, no parallel batching) — so the fix
31
- * is to take that path for any non-empty batch, not just >1.
32
- *
33
- * @param {object|null} huReviewerResult - stageResults.huReviewer
34
- * @returns {boolean}
35
- */
36
- export function needsSubPipeline(huReviewerResult) {
37
- if (!huReviewerResult?.ok) return false;
38
- const certified = (huReviewerResult.stories || []).filter(
39
- s => s.status === "certified"
40
- );
41
- return certified.length >= 1;
42
- }
43
-
44
- /**
45
- * Mark all stories that depend (directly or transitively) on a failed story as "blocked".
46
- * @param {object} batch - The HU batch object.
47
- * @param {string} failedId - The ID of the failed story.
48
- * @returns {string[]} IDs of newly blocked stories.
49
- */
50
- export function blockDependents(batch, failedId) {
51
- const blocked = [];
52
- const failedSet = new Set([failedId]);
53
- // Iterate until no new blocked stories are found (handles transitive deps)
54
- let changed = true;
55
- while (changed) {
56
- changed = false;
57
- for (const story of batch.stories) {
58
- if (failedSet.has(story.id)) continue;
59
- if (story.status === HU_STATUS.BLOCKED) continue;
60
- const deps = story.blocked_by || [];
61
- if (deps.some(dep => failedSet.has(dep))) {
62
- updateStoryStatus(batch, story.id, HU_STATUS.BLOCKED);
63
- failedSet.add(story.id);
64
- blocked.push(story.id);
65
- changed = true;
66
- }
67
- }
68
- }
69
- return blocked;
70
- }
71
-
72
- /**
73
- * Build task description for the coder from a certified HU story.
74
- * @param {object} story - A story object from the batch.
75
- * @returns {string}
76
- */
77
- function buildHuTask(story) {
78
- if (story.certified?.text) return story.certified.text;
79
- if (story.original?.text) return story.original.text;
80
- return `Implement HU ${story.id}`;
81
- }
82
-
83
- /**
84
- * Sum the cost_usd of a slice of BudgetTracker entries. Returns null
85
- * (not 0) when the slice is empty or contains no usable cost data —
86
- * the board uses null as "unmeasured" and renders no badge, while 0
87
- * means "really free run". Mixing the two would silently hide free
88
- * runs and falsely show "$0.00" for runs with no telemetry.
89
- *
90
- * Cost D (KJC-TSK-0515): the orchestrator calls this once per HU at
91
- * runSingleHu exit, passing the slice of entries recorded during that
92
- * HU (snapshotted via the entries.length cursor before the iteration
93
- * starts). The resulting number flows through buildHuOutcome →
94
- * notifyOutcome → setStoryCost in inject-loaded-plan.js.
95
- *
96
- * @param {Array<{cost_usd?: number}>} entries
97
- * @returns {number|null}
98
- */
99
- function sumCostUsd(entries) {
100
- if (!Array.isArray(entries) || entries.length === 0) return null;
101
- let total = 0;
102
- let hasAny = false;
103
- for (const e of entries) {
104
- const n = Number(e?.cost_usd);
105
- if (Number.isFinite(n)) {
106
- total += n;
107
- hasAny = true;
108
- }
109
- }
110
- if (!hasAny) return null;
111
- return Math.round(total * 1e6) / 1e6;
112
- }
113
-
114
- /**
115
- * Sum an integer token-counter field across a slice of BudgetTracker
116
- * entries (Φ0-G, KJC-TSK-0525). Same null-vs-0 semantics as sumCostUsd:
117
- * returns null when nothing measurable was recorded so the board can
118
- * distinguish "unmeasured" from "really zero".
119
- *
120
- * @param {Array<object>} entries
121
- * @param {string} field — entry key to sum, e.g. "cached_tokens"
122
- * @returns {number|null}
123
- */
124
- function sumTokenField(entries, field) {
125
- if (!Array.isArray(entries) || entries.length === 0) return null;
126
- let total = 0;
127
- let hasAny = false;
128
- for (const e of entries) {
129
- const n = Number(e?.[field]);
130
- if (Number.isFinite(n) && n >= 0) {
131
- total += n;
132
- hasAny = true;
133
- }
134
- }
135
- if (!hasAny) return null;
136
- return Math.round(total);
137
- }
138
-
139
- /**
140
- * Build the per-HU outcome blob from whatever metadata the iteration
141
- * loop returned. Tolerant by design — most fields are optional and we
142
- * default to safe values so the board can render something useful
143
- * even on weird failure modes (HU crashed before producing summary,
144
- * runIterationFn missing the commits array, etc.).
145
- *
146
- * Plain-Spanish summary lives at the bottom of the priority order so
147
- * we surface what the run actually said before falling back to
148
- * generic phrases.
149
- *
150
- * @param {object} args
151
- * @param {object} args.story
152
- * @param {object} args.iterResult — whatever runIterationFn returned
153
- * @param {string} args.status — HU_STATUS.* terminal value
154
- * @param {number} args.startedAt — Date.now() when runSingleHu started
155
- * @param {Array} [args.huBudgetEntries] — slice of BudgetTracker
156
- * entries recorded during this HU. Cost D (KJC-TSK-0515).
157
- * @returns {object}
158
- */
159
- function buildHuOutcome({ story, iterResult, status, startedAt, huBudgetEntries }) {
160
- const r = iterResult || {};
161
- const git = r.git || {};
162
- // Iterations are tracked in the standard pipeline as
163
- // `result.iterations`, in the tests-first pipeline as the loop
164
- // counter — fall back to whatever we can find.
165
- const iterations = r.iterations ?? r.iteration ?? r.attempts ?? null;
166
- const commits = Array.isArray(git.commits)
167
- ? git.commits.map((c) => (typeof c === "string" ? c : c.hash || c.sha || "")).filter(Boolean)
168
- : (Array.isArray(r.commits) ? r.commits : []);
169
- const blockers = [];
170
- if (r.error) blockers.push(String(r.error));
171
- if (r.reason && r.reason !== "approved") blockers.push(String(r.reason));
172
- if (Array.isArray(r.deferredIssues)) {
173
- for (const d of r.deferredIssues) blockers.push(typeof d === "string" ? d : d.message || JSON.stringify(d));
174
- }
175
- // KJC-BUG-0248: a push or PR that failed is a blocker the board shows.
176
- for (const e of git.errors ?? []) blockers.push(`${e.step} failed: ${e.message}. ${e.remedy}`);
177
- // Plain-Spanish, one-or-two sentences.
178
- let summary;
179
- if (r.review?.summary) summary = String(r.review.summary);
180
- else if (status === "done") summary = `HU completada${commits.length ? ` con ${commits.length} commit(s)` : ""}.`;
181
- else if (status === "failed") summary = `HU falló${blockers.length ? `: ${blockers[0]}` : ""}.`;
182
- else summary = `HU ${status}.`;
183
- return {
184
- status,
185
- iterations: typeof iterations === "number" ? iterations : null,
186
- duration_ms: typeof startedAt === "number" ? Date.now() - startedAt : null,
187
- branch: r.branch || git.branch || null,
188
- commits,
189
- pr_url: r.pr_url || r.prUrl || git.pr_url || null,
190
- blockers,
191
- summary,
192
- cost_usd: sumCostUsd(huBudgetEntries),
193
- // Φ0-G (KJC-TSK-0525): cached/input tokens drive the "🎯 N% cached"
194
- // board badge. Null when nothing measurable was recorded so the UI
195
- // can hide the chip on cold runs or providers without cache.
196
- cached_tokens: sumTokenField(huBudgetEntries, 'cached_tokens'),
197
- tokens_in: sumTokenField(huBudgetEntries, 'tokens_in'),
198
- // KJC-TSK-0702 (MR-E): the baseline model routing is measured against.
199
- tokens_out: sumTokenField(huBudgetEntries, 'tokens_out'),
200
- ...routingBaseline(huBudgetEntries, status),
201
- // KJC-TSK-1071 (MR-D3): the routing decision applyCoderRoute left on the HU.
202
- coder_routing: story?.coder_routing ?? null,
203
- finishedAt: new Date().toISOString(),
204
- };
205
- }
206
-
207
- /**
208
- * KJC-TSK-0702 (MR-E): which model the coder used and how many times the
209
- * reviewer sent the HU back: its rounds, minus the approval that closed a done HU.
210
- * Null when nothing was recorded, as "unmeasured".
211
- */
212
- function routingBaseline(entries, status) {
213
- const list = Array.isArray(entries) ? entries : [];
214
- const coder = list.findLast((e) => e?.role === "coder");
215
- const reviewerRounds = list.filter((e) => e?.role === "reviewer").length;
216
- const approvalRound = status === "done" ? 1 : 0;
217
- return {
218
- coder: coder ? { provider: coder.provider ?? null, model: coder.model ?? null } : null,
219
- reviewer_rejections: reviewerRounds === 0 ? null : reviewerRounds - approvalRound,
220
- };
221
- }
222
-
223
- /**
224
- * Execute a single HU through the coder→sonar→reviewer sub-pipeline.
225
- * Extracted to support both sequential and parallel execution paths.
226
- *
227
- * @param {object} params
228
- * @returns {Promise<{huId: string, approved: boolean, result?: object, error?: string, blockedDependents?: string[]}>}
229
- */
230
- async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, laneSlot = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
231
- // PR1 (live HU status): every saveHuBatch should also notify the
232
- // plan JSON so the board reflects state in real time. Defined as a
233
- // local helper so we don't repeat the try/null-check boilerplate.
234
- const notifyStatus = async (huId, status) => {
235
- if (typeof onStatusChange !== "function") return;
236
- try { await onStatusChange(huId, status); }
237
- catch (err) { logger?.warn?.(`onStatusChange(${huId}, ${status}) failed: ${err.message}`); }
238
- };
239
- // PR3 (per-HU outcome): single notification at the end of the HU
240
- // with the rich outcome blob. Plan JSON gets it stamped before
241
- // we return, so the board's chokidar fires once and the card
242
- // shows its summary indicator.
243
- const notifyOutcome = async (huId, outcome) => {
244
- if (typeof onOutcome !== "function") return;
245
- try { await onOutcome(huId, outcome); }
246
- catch (err) { logger?.warn?.(`onOutcome(${huId}) failed: ${err.message}`); }
247
- };
248
-
249
- // PR3: capture the start time so duration can be reported in the
250
- // outcome (wall-clock per HU is what the user needs in the board,
251
- // not internal stage timings).
252
- const huStartedAt = Date.now();
253
- // Cost D (KJC-TSK-0515): snapshot the budget cursor so we can slice
254
- // out only the entries this HU produced. BudgetTracker is session-
255
- // scoped (it accumulates across all HUs of the run), so without this
256
- // cursor every HU after the first would inherit the previous ones'
257
- // cost too. Null-safe — older callers without a tracker still work.
258
- const huBudgetStartIdx = budgetTracker?.entries?.length ?? 0;
259
- const sliceHuBudget = () => {
260
- if (!budgetTracker?.entries) return [];
261
- return budgetTracker.entries.slice(huBudgetStartIdx);
262
- };
263
- const story = batch.stories.find(s => s.id === storyId);
264
-
265
- // --- Lazy refinement ---
266
- if (story.needsRefinement && config) {
267
- const completedHus = results
268
- .filter(r => r.approved)
269
- .map(r => {
270
- const completedStory = batch.stories.find(s => s.id === r.huId);
271
- return {
272
- ...completedStory,
273
- resultSummary: r.result?.summary || r.result?.reason || null
274
- };
275
- });
276
-
277
- emitProgress(emitter, makeEvent("hu:refine-start", { ...eventBase, stage: "hu-sub-pipeline" }, {
278
- message: `Refining HU ${story.id} with context from ${completedHus.length} completed HU(s)`,
279
- detail: { huId: story.id, completedCount: completedHus.length }
280
- }));
281
-
282
- try {
283
- const refined = await refineHuWithContext(story, completedHus, config, logger);
284
- Object.assign(story, refined);
285
- story.needsRefinement = false;
286
- await saveHuBatch(batchSessionId, batch);
287
-
288
- emitProgress(emitter, makeEvent("hu:refine-end", { ...eventBase, stage: "hu-sub-pipeline" }, {
289
- message: `HU ${story.id} refined successfully`,
290
- detail: { huId: story.id }
291
- }));
292
- } catch (err) {
293
- logger.warn(`Lazy refinement failed for HU ${story.id}: ${err.message} — proceeding with original`);
294
- story.needsRefinement = false;
295
- }
296
- }
297
-
298
- const huTask = buildHuTask(story);
299
-
300
- // --- hu:start ---
301
- emitProgress(emitter, makeEvent("hu:start", { ...eventBase, stage: "hu-sub-pipeline" }, {
302
- message: `Starting HU ${story.id}`,
303
- detail: { huId: story.id, title: story.certified?.title || story.id, worktreePath: worktreePath || null }
304
- }));
305
-
306
- updateStoryStatus(batch, story.id, HU_STATUS.CODING);
307
- await saveHuBatch(batchSessionId, batch);
308
- await notifyStatus(story.id, HU_STATUS.CODING);
309
- emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
310
- message: `HU ${story.id} status → coding`,
311
- detail: { huId: story.id, status: HU_STATUS.CODING, timestamp: new Date().toISOString() }
312
- }));
313
-
314
- try {
315
- // PAR-E2 (KJC-TSK-0629): lanes handed a worktree must aim every git and
316
- // filesystem touchpoint at it — laneOpts carries that path to the runner.
317
- const iterResult = await runIterationFn(huTask, story, { worktreePath: worktreePath || null, laneSlot });
318
- const approved = Boolean(iterResult?.approved);
319
-
320
- // --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
321
- updateStoryStatus(batch, story.id, HU_STATUS.REVIEWING);
322
- await saveHuBatch(batchSessionId, batch);
323
- await notifyStatus(story.id, HU_STATUS.REVIEWING);
324
- emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
325
- message: `HU ${story.id} status → reviewing`,
326
- detail: { huId: story.id, status: HU_STATUS.REVIEWING, timestamp: new Date().toISOString() }
327
- }));
328
-
329
- if (approved) {
330
- updateStoryStatus(batch, story.id, HU_STATUS.DONE);
331
- await saveHuBatch(batchSessionId, batch);
332
- await notifyStatus(story.id, HU_STATUS.DONE);
333
- // PR3: build + persist the per-HU outcome.
334
- const okOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.DONE, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
335
- await notifyOutcome(story.id, okOutcome);
336
- emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
337
- message: `HU ${story.id} status → done`,
338
- detail: { huId: story.id, status: HU_STATUS.DONE, timestamp: new Date().toISOString() }
339
- }));
340
- emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
341
- status: "ok",
342
- message: `HU ${story.id} completed successfully`,
343
- detail: { huId: story.id, approved: true, outcome: okOutcome }
344
- }));
345
- return { huId: story.id, approved: true, result: iterResult };
346
- } else {
347
- updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
348
- await saveHuBatch(batchSessionId, batch);
349
- await notifyStatus(story.id, HU_STATUS.FAILED);
350
- const failOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
351
- await notifyOutcome(story.id, failOutcome);
352
- emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
353
- message: `HU ${story.id} status → failed`,
354
- detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
355
- }));
356
- emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
357
- status: "fail",
358
- message: `HU ${story.id} failed`,
359
- detail: { huId: story.id, approved: false, reason: iterResult?.reason, outcome: failOutcome }
360
- }));
361
- return { huId: story.id, approved: false, result: iterResult };
362
- }
363
- } catch (err) {
364
- updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
365
- await saveHuBatch(batchSessionId, batch);
366
- await notifyStatus(story.id, HU_STATUS.FAILED);
367
- const errOutcome = buildHuOutcome({ story, iterResult: { error: err.message }, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
368
- await notifyOutcome(story.id, errOutcome);
369
- emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
370
- message: `HU ${story.id} status → failed`,
371
- detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
372
- }));
373
- emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
374
- status: "fail",
375
- message: `HU ${story.id} threw: ${err.message}`,
376
- detail: { huId: story.id, approved: false, error: err.message }
377
- }));
378
- return { huId: story.id, approved: false, error: err.message };
379
- }
380
- }
381
-
382
- /**
383
- * Run all certified HUs as sub-pipelines in topological order.
384
- *
385
- * @param {object} params
386
- * @param {object} params.huReviewerResult - stageResults.huReviewer (from pre-loop)
387
- * @param {Function} params.runIterationFn - async (task) => { approved, result, ... }
388
- * Callback that runs the full coder→sonar→reviewer iteration loop for a given task.
389
- * Returns an object with at least { approved: boolean }.
390
- * @param {object} params.emitter - Event emitter
391
- * @param {object} params.eventBase - Base event fields
392
- * @param {object} params.logger - Logger instance
393
- * @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
394
- */
395
- export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
396
- const batchSessionId = huReviewerResult.batchSessionId;
397
- let batch;
398
- try {
399
- batch = await loadHuBatch(batchSessionId);
400
- } catch {
401
- logger.warn("HU sub-pipeline: could not load batch, falling back to in-memory stories");
402
- // Build a minimal batch from the stage result
403
- batch = {
404
- session_id: batchSessionId,
405
- stories: huReviewerResult.stories || []
406
- };
407
- }
408
-
409
- // Plan-driven runs: the plan JSON is the source of truth for the
410
- // ENTIRE HU, NOT just status, NOT the on-disk hu-stories batch.
411
- //
412
- // PR #537 reconciled `status` and fixed the "All HUs completed
413
- // successfully for zero work" bug. But status is only ONE field of
414
- // the HU. The 2026-04-29 incident hit a different field — a user
415
- // edited the plan JSON to fix a broken jq query inside the HU's
416
- // `acceptance_tests`, but the persisted batch kept the broken
417
- // version, so the acceptance runner used the broken test, the coder
418
- // burned every iteration trying to make an irreparable test pass,
419
- // and the HU failed.
420
- //
421
- // Treat the plan as authoritative for ALL content fields the user
422
- // might edit between runs (acceptance_tests, title, scope,
423
- // blocked_by, spec_section, original.text). In-flight runtime
424
- // context that doesn't live in the plan (worktree path, refinement
425
- // state, certification quality scores, context_requests) is
426
- // preserved.
427
- if (huReviewerResult.source?.plan === true && Array.isArray(huReviewerResult.stories)) {
428
- // Fields the plan owns. Anything NOT in this set is preserved
429
- // from the persisted batch (in-flight runtime state).
430
- const PLAN_OWNED_FIELDS = [
431
- "status",
432
- "title",
433
- "scope",
434
- "blocked_by",
435
- "acceptance_tests",
436
- "acceptance_criteria",
437
- "spec_section",
438
- "task_type",
439
- "certified", // canonical HU body
440
- "original", // original text reference
441
- ];
442
- const planById = new Map(huReviewerResult.stories.map((s) => [s.id, s]));
443
- let statusChanges = 0;
444
- let contentChanges = 0;
445
- for (const persisted of batch.stories) {
446
- const fromPlan = planById.get(persisted.id);
447
- if (!fromPlan) continue;
448
- for (const field of PLAN_OWNED_FIELDS) {
449
- if (!(field in fromPlan)) continue;
450
- const before = JSON.stringify(persisted[field]);
451
- const after = JSON.stringify(fromPlan[field]);
452
- if (before === after) continue;
453
- persisted[field] = fromPlan[field];
454
- if (field === "status") statusChanges += 1;
455
- else contentChanges += 1;
456
- }
457
- }
458
- // Stories present in the plan but missing from disk → append.
459
- const persistedIds = new Set(batch.stories.map((s) => s.id));
460
- let appended = 0;
461
- for (const fromPlan of huReviewerResult.stories) {
462
- if (!persistedIds.has(fromPlan.id)) {
463
- batch.stories.push(fromPlan);
464
- appended += 1;
465
- }
466
- }
467
- const totalChanges = statusChanges + contentChanges + appended;
468
- if (totalChanges > 0) {
469
- const parts = [];
470
- if (statusChanges > 0) parts.push(`${statusChanges} status`);
471
- if (contentChanges > 0) parts.push(`${contentChanges} content field(s)`);
472
- if (appended > 0) parts.push(`${appended} appended`);
473
- logger.info(`HU sub-pipeline: reconciled from plan — ${parts.join(", ")} (disk batch was stale)`);
474
- await saveHuBatch(batchSessionId, batch);
475
- }
476
- }
477
-
478
- const certifiedStories = batch.stories.filter(s => s.status === HU_STATUS.CERTIFIED);
479
- // KJC-BUG-0315 (#2065): a cycle has no valid order. The catch below fell back to
480
- // the list order and ran it anyway; a cycle now stops the run and is named.
481
- assertNoCycle(certifiedStories);
482
- let orderedIds;
483
- try {
484
- orderedIds = topologicalSort(certifiedStories);
485
- } catch { /* a dependency outside the certified batch (done or pending): keep the list order */
486
- orderedIds = certifiedStories.map(s => s.id);
487
- }
488
-
489
- const results = [];
490
- const blockedIds = [];
491
- let allApproved = true;
492
-
493
- // --- Group HUs into parallel batches ---
494
- const parallelBatches = findParallelGroups(certifiedStories, orderedIds);
495
-
496
- // Governance (KJC-TSK-0626): the dependency groups above used to launch
497
- // UNBOUNDED via Promise.all — a plan with N independent HUs ran N full
498
- // pipelines at once, with no cap, no shared budget and no scope checks.
499
- // Now each group is partitioned into conflict-free chunks of at most
500
- // max_parallel_hus (default 1 = fully sequential, today's safe path) and
501
- // a shared limiter gates every launch against the PLAN budget.
502
- const maxParallel = Math.max(1, Number(config?.session?.max_parallel_hus ?? 1));
503
- const limiter = createParallelLimiter({
504
- maxParallel,
505
- budgetTracker,
506
- planBudgetUsd: planBudgetUsd({ maxBudgetUsd: config?.max_budget_usd, maxParallel }),
507
- });
508
- let stopReason = null;
509
-
510
- outer: for (const group of parallelBatches) {
511
- for (const chunk of partitionConflictFree(batch.stories, group, maxParallel)) {
512
- // Filter out HUs that were blocked by a failed dependency in a previous batch
513
- const runnableIds = chunk.filter(id => {
514
- const story = batch.stories.find(s => s.id === id);
515
- return story && story.status !== HU_STATUS.BLOCKED;
516
- });
517
-
518
- if (runnableIds.length === 0) continue;
519
-
520
- const blockReason = limiter.launchBlockReason();
521
- if (blockReason) {
522
- stopReason = blockReason;
523
- allApproved = false;
524
- logger.warn(`HU sub-pipeline stopped before launching ${runnableIds.join(", ")}: ${blockReason}`);
525
- break outer;
526
- }
527
-
528
- // Emit parallel batch start event
529
- emitProgress(emitter, makeEvent("hu:parallel-start", { ...eventBase, stage: "hu-sub-pipeline" }, {
530
- message: `Starting parallel batch of ${runnableIds.length} HU(s): ${runnableIds.join(", ")}`,
531
- detail: { batchIds: runnableIds, parallel: runnableIds.length > 1 }
532
- }));
533
-
534
- if (runnableIds.length === 1) {
535
- // Single HU: run as before, no worktree needed
536
- const singleResult = await runSingleHu({
537
- storyId: runnableIds[0], batch, batchSessionId, runIterationFn,
538
- emitter, eventBase, logger, config, results, onStatusChange, onOutcome, budgetTracker
539
- });
540
- results.push(singleResult);
541
- if (!singleResult.approved) {
542
- allApproved = false;
543
- const newlyBlocked = blockDependents(batch, singleResult.huId);
544
- blockedIds.push(...newlyBlocked);
545
- // PR1 (live HU status): notify the plan JSON of every newly
546
- // BLOCKED HU so the board moves them into the blocked column
547
- // without waiting for the run to finish.
548
- if (typeof onStatusChange === "function") {
549
- for (const blockedHuId of newlyBlocked) {
550
- try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
551
- catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
552
- }
553
- }
554
- }
555
- await saveHuBatch(batchSessionId, batch);
556
- } else {
557
- // Multiple HUs: create worktrees, run in parallel
558
- const projectDir = config?.projectDir || process.cwd();
559
- const worktrees = new Map();
560
- // PAR-H (KJC-TSK-0631): every lane gets a stable numeric slot so
561
- // services it starts can offset their ports (KJ_LANE_SLOT /
562
- // KJ_PORT_OFFSET reach the coder and the acceptance tests).
563
- const slotRegistryPath = join(getKarajanHome(), "worktree-slots.json");
564
- const laneSlots = new Map();
565
-
566
- // Create worktrees for each HU in the batch
567
- for (const id of runnableIds) {
568
- try {
569
- const wtPath = await createWorktree(projectDir, id);
570
- worktrees.set(id, wtPath);
571
- // PAR-G (KJC-TSK-0630): a fresh worktree has no node_modules and
572
- // no initialized submodules — make the lane operative before the
573
- // coder lands. Best-effort: warnings never block the lane.
574
- await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null, logger });
575
- try {
576
- const { slot } = await acquireSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${id}` });
577
- laneSlots.set(id, slot);
578
- } catch (err) {
579
- logger.warn(`Failed to acquire lane slot for HU ${id}: ${err.message} — lane runs without port offset`);
580
- }
581
- } catch (err) {
582
- logger.warn(`Failed to create worktree for HU ${id}: ${err.message} — will run sequentially`);
583
- }
584
- }
585
-
586
- // Run all HUs in the batch concurrently, each holding a limiter slot
587
- // (belt-and-braces: chunks are already ≤ maxParallel).
588
- const batchPromises = runnableIds.map(async (storyId) => {
589
- await limiter.acquire();
590
- try {
591
- return await runSingleHu({
592
- storyId, batch, batchSessionId, runIterationFn,
593
- emitter, eventBase, logger, config, results,
594
- worktreePath: worktrees.get(storyId),
595
- laneSlot: laneSlots.get(storyId) ?? null,
596
- onStatusChange, onOutcome, budgetTracker
597
- });
598
- } finally {
599
- limiter.release();
600
- }
601
- });
602
-
603
- const batchResults = await Promise.all(batchPromises);
604
-
605
- // Process results: merge successful worktrees sequentially, clean up failed ones
606
- for (const res of batchResults) {
607
- results.push(res);
608
- if (res.approved && worktrees.has(res.huId)) {
609
- try {
610
- await mergeWorktree(projectDir, res.huId);
611
- await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
612
- } catch (err) {
613
- logger.warn(`Failed to merge worktree for HU ${res.huId}: ${err.message}`);
614
- }
615
- } else if (!res.approved) {
616
- allApproved = false;
617
- const newlyBlocked = blockDependents(batch, res.huId);
618
- blockedIds.push(...newlyBlocked);
619
- // PR1 (live HU status): same as the single-HU path — push
620
- // BLOCKED transitions to the plan JSON immediately so the
621
- // board shows them moving to the blocked column live.
622
- if (typeof onStatusChange === "function") {
623
- for (const blockedHuId of newlyBlocked) {
624
- try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
625
- catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
626
- }
627
- }
628
- // Clean up failed worktree
629
- if (worktrees.has(res.huId)) {
630
- try { await removeWorktree(projectDir, res.huId); } catch { /* ignore */ }
631
- await releaseSlot({ registryPath: slotRegistryPath, id: `${projectDir}::${res.huId}` }).catch(() => {});
632
- }
633
- }
634
- }
635
-
636
- await saveHuBatch(batchSessionId, batch);
637
- }
638
- }
639
- }
640
-
641
- return { approved: allApproved, results, blockedIds, ...(stopReason ? { stopReason } : {}) };
642
- }