karajan-code 4.70.4 → 5.0.1

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 (390) 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/dir-setup.js +34 -1
  61. package/src/checks/native-build.js +21 -39
  62. package/src/checks/runner.js +128 -142
  63. package/src/cli/advanced-commands.js +6 -6
  64. package/src/cli/register-meta.js +42 -82
  65. package/src/cli/register-pipeline.js +8 -143
  66. package/src/cli/register-plan.js +9 -100
  67. package/src/cli/register-roles-skills.js +1 -1
  68. package/src/cli/retired-commands.js +49 -0
  69. package/src/cli.js +18 -5
  70. package/src/commands/agent-run.js +40 -26
  71. package/src/commands/audit.js +127 -124
  72. package/src/commands/board.js +5 -9
  73. package/src/commands/check.js +29 -16
  74. package/src/commands/doctor.js +2 -0
  75. package/src/commands/env.js +176 -150
  76. package/src/commands/go.js +71 -53
  77. package/src/commands/harden.js +90 -81
  78. package/src/commands/hu-start.js +52 -0
  79. package/src/commands/hu.js +288 -221
  80. package/src/commands/identity.js +111 -78
  81. package/src/commands/init-atril.js +47 -0
  82. package/src/commands/init.js +11 -40
  83. package/src/commands/install-tools.js +70 -85
  84. package/src/commands/main-guard.js +78 -0
  85. package/src/commands/policy.js +226 -184
  86. package/src/commands/qmd.js +27 -21
  87. package/src/commands/rag.js +121 -55
  88. package/src/commands/review-gate.js +421 -341
  89. package/src/commands/skills.js +4 -1
  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 +107 -26
  94. package/src/config/defaults.js +1 -0
  95. package/src/config/loader.js +34 -17
  96. package/src/config/role-resolver.js +1 -0
  97. package/src/config/schema.js +1 -0
  98. package/src/config/secrets.js +36 -0
  99. package/src/config.js +1 -0
  100. package/src/environment/adr.js +16 -2
  101. package/src/git/karajan-label.js +20 -8
  102. package/src/guards/main-guard.js +54 -0
  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 +64 -36
  106. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  107. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-templates.js +1 -1
  111. package/src/hu/board-lookup.js +29 -0
  112. package/src/hu/gh-identity.js +23 -0
  113. package/src/hu/github-board.js +4 -2
  114. package/src/hu/lane-prune.js +83 -13
  115. package/src/hu/lane-setup.js +65 -0
  116. package/src/hu/lanes.js +66 -0
  117. package/src/hu/team-board-mirror.js +3 -2
  118. package/src/infrastructure/command-runner.js +1 -1
  119. package/src/infrastructure/file-system-service.js +1 -1
  120. package/src/mcp/handlers/direct-handlers.js +2 -292
  121. package/src/mcp/handlers/hu-handlers.js +1 -35
  122. package/src/mcp/handlers/management-handlers.js +2 -42
  123. package/src/mcp/handlers/rag-handler.js +9 -1
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +1 -196
  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 +71 -44
  134. package/src/rag/bootstrap-index.js +46 -54
  135. package/src/rag/indexer.js +9 -4
  136. package/src/rag/migrate.js +1 -1
  137. package/src/rag/onnx-fallback.js +2 -2
  138. package/src/rag/pid-lease.js +181 -0
  139. package/src/rag/project-store.js +2 -12
  140. package/src/review/card-first.js +57 -46
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +12 -4
  143. package/src/review/one-shot-review.js +45 -35
  144. package/src/review/publish-pr.js +14 -2
  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 +19 -4
  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 +1 -24
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-cli.js +19 -1
  156. package/src/skills/atril-skills.js +5 -4
  157. package/src/skills/skills-search.js +72 -0
  158. package/src/skills/translate.js +62 -0
  159. package/src/sonar/scanner.js +116 -101
  160. package/src/steward/invariants.js +5 -4
  161. package/src/steward/phantom-sweep.js +66 -0
  162. package/src/tournament/judge.js +60 -53
  163. package/src/utils/gh-account.js +37 -0
  164. package/src/utils/git.js +0 -13
  165. package/src/utils/home-migration.js +7 -1
  166. package/src/utils/main-checkout.js +53 -0
  167. package/src/utils/qmd-collection.js +3 -1
  168. package/src/utils/welcome.js +3 -3
  169. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  180. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  181. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  182. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  183. package/packages/hu-board/src/run-tracker.js +0 -121
  184. package/src/activity-log.js +0 -59
  185. package/src/agents/host-agent.js +0 -38
  186. package/src/agents/model-registry.js +0 -142
  187. package/src/audit/plan-adherence.js +0 -224
  188. package/src/autonomy/arbiter-role.js +0 -71
  189. package/src/autonomy/arbiter.js +0 -22
  190. package/src/autonomy/decider.js +0 -18
  191. package/src/autonomy/decision-resolver.js +0 -58
  192. package/src/autonomy/outcome-report.js +0 -55
  193. package/src/autonomy/stage-ask.js +0 -30
  194. package/src/brain/decision-tracker.js +0 -125
  195. package/src/brain/decisor.js +0 -243
  196. package/src/brain/solomon-consult.js +0 -171
  197. package/src/brain/standby-scheduler.js +0 -8
  198. package/src/budget/comparison.js +0 -96
  199. package/src/canvas/parse-md.js +0 -238
  200. package/src/canvas/schema.js +0 -101
  201. package/src/canvas/to-flat.js +0 -85
  202. package/src/canvas/validate.js +0 -163
  203. package/src/ci/dispatch.js +0 -99
  204. package/src/ci/index.js +0 -3
  205. package/src/ci/pr-diff.js +0 -26
  206. package/src/cli/register-standby.js +0 -26
  207. package/src/commands/architect.js +0 -111
  208. package/src/commands/autorun.js +0 -49
  209. package/src/commands/researcher.js +0 -54
  210. package/src/commands/resume.js +0 -72
  211. package/src/commands/review.js +0 -89
  212. package/src/commands/run.js +0 -185
  213. package/src/commands/standby.js +0 -117
  214. package/src/commands/status.js +0 -36
  215. package/src/commands/triage.js +0 -78
  216. package/src/commands/undo.js +0 -62
  217. package/src/domains/domain-loader.js +0 -245
  218. package/src/domains/domain-registry.js +0 -140
  219. package/src/domains/domain-synthesizer.js +0 -129
  220. package/src/git/automation.js +0 -433
  221. package/src/git/coder-commit-guard.js +0 -40
  222. package/src/git/hu-automation.js +0 -243
  223. package/src/git/hu-snapshot.js +0 -10
  224. package/src/git/symlink-targets.js +0 -57
  225. package/src/golden/asserter.js +0 -92
  226. package/src/golden/loader.js +0 -52
  227. package/src/golden/runner.js +0 -92
  228. package/src/golden/schema.js +0 -41
  229. package/src/guards/duplicate-members.js +0 -86
  230. package/src/guards/intent-guard.js +0 -161
  231. package/src/guards/perf-guard.js +0 -126
  232. package/src/guards/policy-guard.js +0 -90
  233. package/src/guards/policy-resolver.js +0 -85
  234. package/src/guards/secret-redactor.js +0 -71
  235. package/src/hu/acceptance-runner.js +0 -138
  236. package/src/hu/lazy-planner.js +0 -109
  237. package/src/hu/parallel-executor.js +0 -94
  238. package/src/hu/routing-metrics.js +0 -17
  239. package/src/hu/split-validator.js +0 -143
  240. package/src/hu/splitting-detector.js +0 -158
  241. package/src/hu/splitting-generator.js +0 -175
  242. package/src/hu/store.js +0 -394
  243. package/src/infrastructure/mocks.js +0 -182
  244. package/src/mcp/handlers/run-handler.js +0 -318
  245. package/src/mcp/sovereignty-guard.js +0 -113
  246. package/src/mcp/tool-arg-normalizers.js +0 -16
  247. package/src/mutate/reviewer-signal.js +0 -76
  248. package/src/orchestrator/agent-fallback.js +0 -81
  249. package/src/orchestrator/brain-coordinator.js +0 -186
  250. package/src/orchestrator/ci-integration.js +0 -139
  251. package/src/orchestrator/config-init.js +0 -426
  252. package/src/orchestrator/direct-actions.js +0 -217
  253. package/src/orchestrator/drivers/error-recovery.js +0 -287
  254. package/src/orchestrator/drivers/init-context.js +0 -238
  255. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  256. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  257. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  258. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  259. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  260. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  261. package/src/orchestrator/drivers/post-loop.js +0 -404
  262. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  263. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  264. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  265. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  266. package/src/orchestrator/drivers/pre-loop.js +0 -524
  267. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  268. package/src/orchestrator/feedback-enrichment.js +0 -140
  269. package/src/orchestrator/feedback-queue.js +0 -151
  270. package/src/orchestrator/flow-control.js +0 -170
  271. package/src/orchestrator/flow-runner.js +0 -365
  272. package/src/orchestrator/fs-leak-detector.js +0 -228
  273. package/src/orchestrator/hu-scheduler.js +0 -114
  274. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  275. package/src/orchestrator/iteration-gate.js +0 -73
  276. package/src/orchestrator/iteration-stages.js +0 -9
  277. package/src/orchestrator/parallel-limiter.js +0 -74
  278. package/src/orchestrator/pipeline-context.js +0 -72
  279. package/src/orchestrator/post-loop-stages.js +0 -468
  280. package/src/orchestrator/pre-loop-stages.js +0 -11
  281. package/src/orchestrator/preflight-checks.js +0 -487
  282. package/src/orchestrator/repair/repair-detector.js +0 -117
  283. package/src/orchestrator/repair/repair-runner.js +0 -142
  284. package/src/orchestrator/reviewer-fallback.js +0 -45
  285. package/src/orchestrator/role-output-compressor.js +0 -189
  286. package/src/orchestrator/session-journal.js +0 -308
  287. package/src/orchestrator/solomon-escalation.js +0 -331
  288. package/src/orchestrator/solomon-rules.js +0 -212
  289. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  290. package/src/orchestrator/stages/architect-stage.js +0 -172
  291. package/src/orchestrator/stages/coder-stage.js +0 -506
  292. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  293. package/src/orchestrator/stages/format-stage.js +0 -61
  294. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  295. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  296. package/src/orchestrator/stages/perf-stage.js +0 -140
  297. package/src/orchestrator/stages/planner-stage.js +0 -85
  298. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  299. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  300. package/src/orchestrator/stages/research-stage.js +0 -69
  301. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  302. package/src/orchestrator/stages/sonar-stage.js +0 -439
  303. package/src/orchestrator/stages/stage-classes.js +0 -84
  304. package/src/orchestrator/stages/stage-executor.js +0 -155
  305. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  306. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  307. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  308. package/src/orchestrator/stages/triage-stage.js +0 -172
  309. package/src/orchestrator/standby.js +0 -71
  310. package/src/orchestrator/verification-gate.js +0 -183
  311. package/src/orchestrator.js +0 -22
  312. package/src/plan/adr-generator.js +0 -170
  313. package/src/plan/adr-loader.js +0 -60
  314. package/src/plan/plan-executor.js +0 -100
  315. package/src/plan/plan-validation.js +0 -4
  316. package/src/prompts/acceptance-coverage.js +0 -32
  317. package/src/prompts/arbiter.js +0 -38
  318. package/src/prompts/architect.js +0 -103
  319. package/src/prompts/hu-reviewer.js +0 -244
  320. package/src/prompts/tool-correctness-judge.js +0 -31
  321. package/src/prompts/triage.js +0 -57
  322. package/src/rag/embedder.js +0 -2
  323. package/src/rag/embedders/_cloud-base.js +0 -2
  324. package/src/rag/embedders/cohere.js +0 -2
  325. package/src/rag/embedders/mistral.js +0 -2
  326. package/src/rag/embedders/onnx.js +0 -2
  327. package/src/rag/embedders/openai.js +0 -2
  328. package/src/rag/embedders/voyage.js +0 -2
  329. package/src/rag/rerank.js +0 -4
  330. package/src/rag/where-parser.js +0 -4
  331. package/src/repeat-detector.js +0 -88
  332. package/src/review/diff-generator.js +0 -112
  333. package/src/review/git-stash-helper.js +0 -104
  334. package/src/review/schema.js +0 -31
  335. package/src/review/scope-filter.js +0 -152
  336. package/src/review/snapshot-diff.js +0 -122
  337. package/src/review/tdd-discipline.js +0 -90
  338. package/src/review/tdd-policy.js +0 -69
  339. package/src/roles/acceptance-coverage-role.js +0 -78
  340. package/src/roles/architect-role.js +0 -77
  341. package/src/roles/coder-role.js +0 -113
  342. package/src/roles/commiter-role.js +0 -113
  343. package/src/roles/domain-curator-role.js +0 -134
  344. package/src/roles/hu-reviewer-role.js +0 -64
  345. package/src/roles/impeccable-role.js +0 -85
  346. package/src/roles/index.js +0 -18
  347. package/src/roles/karajan-brain-role.js +0 -134
  348. package/src/roles/perf-role.js +0 -122
  349. package/src/roles/planner-role.js +0 -151
  350. package/src/roles/refactorer-role.js +0 -30
  351. package/src/roles/repairer-role.js +0 -181
  352. package/src/roles/researcher-role.js +0 -70
  353. package/src/roles/reviewer-role.js +0 -115
  354. package/src/roles/security-role.js +0 -73
  355. package/src/roles/solomon-role.js +0 -271
  356. package/src/roles/sonar-role.js +0 -72
  357. package/src/roles/tester-role.js +0 -235
  358. package/src/roles/tool-correctness-judge-role.js +0 -78
  359. package/src/roles/triage-role.js +0 -99
  360. package/src/session/journal/decisions-writer.js +0 -200
  361. package/src/session/journal/iteration-logger.js +0 -194
  362. package/src/session/journal/summary-writer.js +0 -320
  363. package/src/session/journal/tree-writer.js +0 -164
  364. package/src/session/journal/write-all.js +0 -89
  365. package/src/session-cleanup.js +0 -116
  366. package/src/skills/semantic-detector.js +0 -167
  367. package/src/skills/skill-detector.js +0 -385
  368. package/src/sonar/cloud-scanner.js +0 -78
  369. package/src/tracker-bootstrap.js +0 -44
  370. package/src/types/index.js +0 -38
  371. package/src/types/policy.js +0 -43
  372. package/src/types/solomon.js +0 -87
  373. package/src/utils/async-lock.js +0 -31
  374. package/src/utils/budget.js +0 -229
  375. package/src/utils/cwd-confirm.js +0 -39
  376. package/src/utils/display/event-handlers.js +0 -403
  377. package/src/utils/display/formatters.js +0 -132
  378. package/src/utils/display/header.js +0 -47
  379. package/src/utils/display/session-summary.js +0 -118
  380. package/src/utils/display/solomon.js +0 -42
  381. package/src/utils/pricing.js +0 -29
  382. package/src/utils/quota-simulator.js +0 -56
  383. package/src/utils/role-assigner.js +0 -137
  384. package/src/utils/rtk-wrapper.js +0 -87
  385. package/src/utils/run-registry.js +0 -9
  386. package/src/utils/status-dashboard.js +0 -254
  387. package/src/utils/tool-call-extractor.js +0 -56
  388. package/src/webperf/devtools-detect.js +0 -112
  389. package/src/webperf/webperf-role-integration.js +0 -66
  390. 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
- }