karajan-code 4.70.4 → 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 (380) 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/retired-commands.js +49 -0
  68. package/src/cli.js +18 -5
  69. package/src/commands/agent-run.js +40 -26
  70. package/src/commands/audit.js +127 -124
  71. package/src/commands/board.js +5 -9
  72. package/src/commands/check.js +29 -16
  73. package/src/commands/doctor.js +2 -0
  74. package/src/commands/env.js +176 -150
  75. package/src/commands/go.js +71 -53
  76. package/src/commands/harden.js +90 -81
  77. package/src/commands/hu-start.js +52 -0
  78. package/src/commands/hu.js +288 -221
  79. package/src/commands/identity.js +111 -78
  80. package/src/commands/init-atril.js +47 -0
  81. package/src/commands/init.js +11 -40
  82. package/src/commands/install-tools.js +70 -85
  83. package/src/commands/main-guard.js +78 -0
  84. package/src/commands/policy.js +226 -184
  85. package/src/commands/qmd.js +27 -21
  86. package/src/commands/rag.js +75 -48
  87. package/src/commands/review-gate.js +421 -341
  88. package/src/commands/start.js +4 -5
  89. package/src/commands/steward.js +96 -47
  90. package/src/commands/tournament.js +46 -37
  91. package/src/commands/worktree.js +94 -25
  92. package/src/config/loader.js +34 -17
  93. package/src/config/secrets.js +36 -0
  94. package/src/config.js +1 -0
  95. package/src/environment/adr.js +16 -2
  96. package/src/git/karajan-label.js +20 -8
  97. package/src/guards/main-guard.js +54 -0
  98. package/src/harden/hook-templates.js +16 -1
  99. package/src/harden/sentinel/posttooluse.mjs +18 -6
  100. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  101. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  102. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  103. package/src/harden/sentinel/sessionstart.mjs +3 -2
  104. package/src/harden/sentinel/stop.mjs +5 -4
  105. package/src/harden/workflow-templates.js +1 -1
  106. package/src/hu/board-lookup.js +29 -0
  107. package/src/hu/gh-identity.js +23 -0
  108. package/src/hu/github-board.js +4 -2
  109. package/src/hu/lane-prune.js +15 -7
  110. package/src/hu/lane-setup.js +65 -0
  111. package/src/hu/lanes.js +66 -0
  112. package/src/hu/team-board-mirror.js +3 -2
  113. package/src/infrastructure/command-runner.js +1 -1
  114. package/src/infrastructure/file-system-service.js +1 -1
  115. package/src/mcp/handlers/direct-handlers.js +2 -292
  116. package/src/mcp/handlers/hu-handlers.js +1 -35
  117. package/src/mcp/handlers/management-handlers.js +2 -42
  118. package/src/mcp/server-handlers.js +16 -22
  119. package/src/mcp/tools.js +1 -196
  120. package/src/plan/lane-plans.js +54 -0
  121. package/src/plan/plan-store.js +7 -4
  122. package/src/policy/add.js +26 -12
  123. package/src/policy/adr-number-check.js +31 -0
  124. package/src/prompts/audit.js +299 -339
  125. package/src/prompts/card-context.js +10 -5
  126. package/src/prompts/reviewer.js +5 -2
  127. package/src/rag/auto-update.js +56 -43
  128. package/src/rag/bootstrap-index.js +28 -54
  129. package/src/rag/migrate.js +1 -1
  130. package/src/rag/onnx-fallback.js +2 -2
  131. package/src/rag/pid-lease.js +70 -0
  132. package/src/rag/project-store.js +2 -12
  133. package/src/review/card-first.js +57 -46
  134. package/src/review/gate-gitignore.js +23 -4
  135. package/src/review/gate-hook.js +12 -4
  136. package/src/review/one-shot-review.js +45 -35
  137. package/src/review/publish-pr.js +14 -2
  138. package/src/review/repo-docs.js +73 -0
  139. package/src/review/repo-stack.js +59 -0
  140. package/src/review/reviewer-fallback.js +19 -4
  141. package/src/review/seal-commit.js +29 -0
  142. package/src/review/sonar-pregate.js +59 -44
  143. package/src/review/third-party-review.js +90 -16
  144. package/src/review/third-party-size.js +43 -0
  145. package/src/review/third-party-skills.js +54 -0
  146. package/src/review/verify-pr.js +1 -24
  147. package/src/roles/audit-role.js +106 -125
  148. package/src/skills/atril-skills.js +5 -4
  149. package/src/sonar/scanner.js +116 -101
  150. package/src/steward/invariants.js +5 -4
  151. package/src/steward/phantom-sweep.js +66 -0
  152. package/src/tournament/judge.js +60 -53
  153. package/src/utils/gh-account.js +37 -0
  154. package/src/utils/git.js +0 -13
  155. package/src/utils/home-migration.js +7 -1
  156. package/src/utils/main-checkout.js +53 -0
  157. package/src/utils/qmd-collection.js +3 -1
  158. package/src/utils/welcome.js +3 -3
  159. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  160. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  161. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  162. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  163. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  164. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  165. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  166. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  167. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  172. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  173. package/packages/hu-board/src/run-tracker.js +0 -121
  174. package/src/activity-log.js +0 -59
  175. package/src/agents/host-agent.js +0 -38
  176. package/src/agents/model-registry.js +0 -142
  177. package/src/audit/plan-adherence.js +0 -224
  178. package/src/autonomy/arbiter-role.js +0 -71
  179. package/src/autonomy/arbiter.js +0 -22
  180. package/src/autonomy/decider.js +0 -18
  181. package/src/autonomy/decision-resolver.js +0 -58
  182. package/src/autonomy/outcome-report.js +0 -55
  183. package/src/autonomy/stage-ask.js +0 -30
  184. package/src/brain/decision-tracker.js +0 -125
  185. package/src/brain/decisor.js +0 -243
  186. package/src/brain/solomon-consult.js +0 -171
  187. package/src/brain/standby-scheduler.js +0 -8
  188. package/src/budget/comparison.js +0 -96
  189. package/src/canvas/parse-md.js +0 -238
  190. package/src/canvas/schema.js +0 -101
  191. package/src/canvas/to-flat.js +0 -85
  192. package/src/canvas/validate.js +0 -163
  193. package/src/ci/dispatch.js +0 -99
  194. package/src/ci/index.js +0 -3
  195. package/src/ci/pr-diff.js +0 -26
  196. package/src/cli/register-standby.js +0 -26
  197. package/src/commands/architect.js +0 -111
  198. package/src/commands/autorun.js +0 -49
  199. package/src/commands/researcher.js +0 -54
  200. package/src/commands/resume.js +0 -72
  201. package/src/commands/review.js +0 -89
  202. package/src/commands/run.js +0 -185
  203. package/src/commands/standby.js +0 -117
  204. package/src/commands/status.js +0 -36
  205. package/src/commands/triage.js +0 -78
  206. package/src/commands/undo.js +0 -62
  207. package/src/domains/domain-loader.js +0 -245
  208. package/src/domains/domain-registry.js +0 -140
  209. package/src/domains/domain-synthesizer.js +0 -129
  210. package/src/git/automation.js +0 -433
  211. package/src/git/coder-commit-guard.js +0 -40
  212. package/src/git/hu-automation.js +0 -243
  213. package/src/git/hu-snapshot.js +0 -10
  214. package/src/git/symlink-targets.js +0 -57
  215. package/src/golden/asserter.js +0 -92
  216. package/src/golden/loader.js +0 -52
  217. package/src/golden/runner.js +0 -92
  218. package/src/golden/schema.js +0 -41
  219. package/src/guards/duplicate-members.js +0 -86
  220. package/src/guards/intent-guard.js +0 -161
  221. package/src/guards/perf-guard.js +0 -126
  222. package/src/guards/policy-guard.js +0 -90
  223. package/src/guards/policy-resolver.js +0 -85
  224. package/src/guards/secret-redactor.js +0 -71
  225. package/src/hu/acceptance-runner.js +0 -138
  226. package/src/hu/lazy-planner.js +0 -109
  227. package/src/hu/parallel-executor.js +0 -94
  228. package/src/hu/routing-metrics.js +0 -17
  229. package/src/hu/split-validator.js +0 -143
  230. package/src/hu/splitting-detector.js +0 -158
  231. package/src/hu/splitting-generator.js +0 -175
  232. package/src/hu/store.js +0 -394
  233. package/src/infrastructure/mocks.js +0 -182
  234. package/src/mcp/handlers/run-handler.js +0 -318
  235. package/src/mcp/sovereignty-guard.js +0 -113
  236. package/src/mcp/tool-arg-normalizers.js +0 -16
  237. package/src/mutate/reviewer-signal.js +0 -76
  238. package/src/orchestrator/agent-fallback.js +0 -81
  239. package/src/orchestrator/brain-coordinator.js +0 -186
  240. package/src/orchestrator/ci-integration.js +0 -139
  241. package/src/orchestrator/config-init.js +0 -426
  242. package/src/orchestrator/direct-actions.js +0 -217
  243. package/src/orchestrator/drivers/error-recovery.js +0 -287
  244. package/src/orchestrator/drivers/init-context.js +0 -238
  245. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  246. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  247. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  248. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  249. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  250. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  251. package/src/orchestrator/drivers/post-loop.js +0 -404
  252. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  253. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  254. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  255. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  256. package/src/orchestrator/drivers/pre-loop.js +0 -524
  257. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  258. package/src/orchestrator/feedback-enrichment.js +0 -140
  259. package/src/orchestrator/feedback-queue.js +0 -151
  260. package/src/orchestrator/flow-control.js +0 -170
  261. package/src/orchestrator/flow-runner.js +0 -365
  262. package/src/orchestrator/fs-leak-detector.js +0 -228
  263. package/src/orchestrator/hu-scheduler.js +0 -114
  264. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  265. package/src/orchestrator/iteration-gate.js +0 -73
  266. package/src/orchestrator/iteration-stages.js +0 -9
  267. package/src/orchestrator/parallel-limiter.js +0 -74
  268. package/src/orchestrator/pipeline-context.js +0 -72
  269. package/src/orchestrator/post-loop-stages.js +0 -468
  270. package/src/orchestrator/pre-loop-stages.js +0 -11
  271. package/src/orchestrator/preflight-checks.js +0 -487
  272. package/src/orchestrator/repair/repair-detector.js +0 -117
  273. package/src/orchestrator/repair/repair-runner.js +0 -142
  274. package/src/orchestrator/reviewer-fallback.js +0 -45
  275. package/src/orchestrator/role-output-compressor.js +0 -189
  276. package/src/orchestrator/session-journal.js +0 -308
  277. package/src/orchestrator/solomon-escalation.js +0 -331
  278. package/src/orchestrator/solomon-rules.js +0 -212
  279. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  280. package/src/orchestrator/stages/architect-stage.js +0 -172
  281. package/src/orchestrator/stages/coder-stage.js +0 -506
  282. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  283. package/src/orchestrator/stages/format-stage.js +0 -61
  284. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  285. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  286. package/src/orchestrator/stages/perf-stage.js +0 -140
  287. package/src/orchestrator/stages/planner-stage.js +0 -85
  288. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  289. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  290. package/src/orchestrator/stages/research-stage.js +0 -69
  291. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  292. package/src/orchestrator/stages/sonar-stage.js +0 -439
  293. package/src/orchestrator/stages/stage-classes.js +0 -84
  294. package/src/orchestrator/stages/stage-executor.js +0 -155
  295. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  296. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  297. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  298. package/src/orchestrator/stages/triage-stage.js +0 -172
  299. package/src/orchestrator/standby.js +0 -71
  300. package/src/orchestrator/verification-gate.js +0 -183
  301. package/src/orchestrator.js +0 -22
  302. package/src/plan/adr-generator.js +0 -170
  303. package/src/plan/adr-loader.js +0 -60
  304. package/src/plan/plan-executor.js +0 -100
  305. package/src/plan/plan-validation.js +0 -4
  306. package/src/prompts/acceptance-coverage.js +0 -32
  307. package/src/prompts/arbiter.js +0 -38
  308. package/src/prompts/architect.js +0 -103
  309. package/src/prompts/hu-reviewer.js +0 -244
  310. package/src/prompts/tool-correctness-judge.js +0 -31
  311. package/src/prompts/triage.js +0 -57
  312. package/src/rag/embedder.js +0 -2
  313. package/src/rag/embedders/_cloud-base.js +0 -2
  314. package/src/rag/embedders/cohere.js +0 -2
  315. package/src/rag/embedders/mistral.js +0 -2
  316. package/src/rag/embedders/onnx.js +0 -2
  317. package/src/rag/embedders/openai.js +0 -2
  318. package/src/rag/embedders/voyage.js +0 -2
  319. package/src/rag/rerank.js +0 -4
  320. package/src/rag/where-parser.js +0 -4
  321. package/src/repeat-detector.js +0 -88
  322. package/src/review/diff-generator.js +0 -112
  323. package/src/review/git-stash-helper.js +0 -104
  324. package/src/review/schema.js +0 -31
  325. package/src/review/scope-filter.js +0 -152
  326. package/src/review/snapshot-diff.js +0 -122
  327. package/src/review/tdd-discipline.js +0 -90
  328. package/src/review/tdd-policy.js +0 -69
  329. package/src/roles/acceptance-coverage-role.js +0 -78
  330. package/src/roles/architect-role.js +0 -77
  331. package/src/roles/coder-role.js +0 -113
  332. package/src/roles/commiter-role.js +0 -113
  333. package/src/roles/domain-curator-role.js +0 -134
  334. package/src/roles/hu-reviewer-role.js +0 -64
  335. package/src/roles/impeccable-role.js +0 -85
  336. package/src/roles/index.js +0 -18
  337. package/src/roles/karajan-brain-role.js +0 -134
  338. package/src/roles/perf-role.js +0 -122
  339. package/src/roles/planner-role.js +0 -151
  340. package/src/roles/refactorer-role.js +0 -30
  341. package/src/roles/repairer-role.js +0 -181
  342. package/src/roles/researcher-role.js +0 -70
  343. package/src/roles/reviewer-role.js +0 -115
  344. package/src/roles/security-role.js +0 -73
  345. package/src/roles/solomon-role.js +0 -271
  346. package/src/roles/sonar-role.js +0 -72
  347. package/src/roles/tester-role.js +0 -235
  348. package/src/roles/tool-correctness-judge-role.js +0 -78
  349. package/src/roles/triage-role.js +0 -99
  350. package/src/session/journal/decisions-writer.js +0 -200
  351. package/src/session/journal/iteration-logger.js +0 -194
  352. package/src/session/journal/summary-writer.js +0 -320
  353. package/src/session/journal/tree-writer.js +0 -164
  354. package/src/session/journal/write-all.js +0 -89
  355. package/src/session-cleanup.js +0 -116
  356. package/src/skills/semantic-detector.js +0 -167
  357. package/src/skills/skill-detector.js +0 -385
  358. package/src/sonar/cloud-scanner.js +0 -78
  359. package/src/tracker-bootstrap.js +0 -44
  360. package/src/types/index.js +0 -38
  361. package/src/types/policy.js +0 -43
  362. package/src/types/solomon.js +0 -87
  363. package/src/utils/async-lock.js +0 -31
  364. package/src/utils/budget.js +0 -229
  365. package/src/utils/cwd-confirm.js +0 -39
  366. package/src/utils/display/event-handlers.js +0 -403
  367. package/src/utils/display/formatters.js +0 -132
  368. package/src/utils/display/header.js +0 -47
  369. package/src/utils/display/session-summary.js +0 -118
  370. package/src/utils/display/solomon.js +0 -42
  371. package/src/utils/pricing.js +0 -29
  372. package/src/utils/quota-simulator.js +0 -56
  373. package/src/utils/role-assigner.js +0 -137
  374. package/src/utils/rtk-wrapper.js +0 -87
  375. package/src/utils/run-registry.js +0 -9
  376. package/src/utils/status-dashboard.js +0 -254
  377. package/src/utils/tool-call-extractor.js +0 -56
  378. package/src/webperf/devtools-detect.js +0 -112
  379. package/src/webperf/webperf-role-integration.js +0 -66
  380. package/templates/skills/kj-run.md +0 -73
@@ -1,137 +0,0 @@
1
- /**
2
- * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
- * (audit recommendation #6). Previously an internal helper inside
4
- * pre-loop.js; moved here so the driver stays under 600 LOC.
5
- * Behaviour unchanged.
6
- *
7
- * Auto-generates an HU batch from triage's decomposition recommendation
8
- * when no manual `huFile` is present. Runs after researcher/architect/
9
- * planner so context is available for better HU generation. Sets
10
- * `stageResults.huReviewer` so `needsSubPipeline` picks it up later.
11
- *
12
- * Side effects:
13
- * - Persists `batch.json` to `~/.karajan/hu-stories/<batchSessionId>/`
14
- * so the sub-pipeline can update story status via `saveHuBatch`.
15
- * - Auto-starts the HU board (skipped under VITEST / NODE_ENV=test).
16
- */
17
-
18
- import { emitProgress, makeEvent } from "#utils/events.js";
19
- import { detectProjectStack } from "#utils/stack-detect.js";
20
-
21
- export async function maybeGenerateAutoHuBatch({
22
- flags, stageResults, task, logger, emitter, eventBase, projectDir, session,
23
- }) {
24
- // Skip if user passed a manual hu-file
25
- if (flags?.huFile) return;
26
- // Skip if hu-reviewer already produced a batch (manual enable + PG stories)
27
- if (stageResults.huReviewer) return;
28
- // Need triage decomposition recommendation
29
- const shouldDecompose = stageResults.triage?.shouldDecompose;
30
- const subtasks = stageResults.triage?.subtasks;
31
- if (!shouldDecompose || !Array.isArray(subtasks) || subtasks.length < 2) return;
32
-
33
- const { generateHuBatch } = await import("#hu/auto-generator.js");
34
-
35
- // Detect if project is new: empty dir or only .git/.karajan/.gitignore
36
- let isNewProject = false;
37
- try {
38
- const fs = await import("node:fs/promises");
39
- const entries = await fs.readdir(projectDir);
40
- const relevant = entries.filter(e => !e.startsWith(".git") && e !== ".karajan" && e !== ".gitignore");
41
- isNewProject = relevant.length === 0;
42
- } catch { /* ignore */ }
43
-
44
- // Stack hints from planner + architect text. Now multi-ecosystem
45
- // (was 13/16 JS keywords — Python/Go/Rust barely existed). Combined
46
- // with `detectProjectStack(projectDir)` below to override the text
47
- // heuristic with the actual filesystem reality when available.
48
- const stackHints = [];
49
- const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
50
- const stackKeywords = [
51
- "express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "nestjs", "monorepo", "workspaces",
52
- "pytest", "flask", "fastapi", "django", "numpy", "pandas",
53
- "gin", "fiber", "go",
54
- "cargo", "rust",
55
- "spring",
56
- ];
57
- for (const kw of stackKeywords) {
58
- if (combined.includes(kw)) stackHints.push(kw);
59
- }
60
-
61
- // Filesystem trumps text. detectProjectStack inspects package.json /
62
- // pyproject.toml / go.mod / Cargo.toml, so a pure-python repo gets
63
- // `language: "python"` regardless of what the task text says.
64
- let detectedLanguage = null;
65
- try {
66
- const fsStack = await detectProjectStack(projectDir);
67
- if (fsStack?.language) detectedLanguage = fsStack.language;
68
- } catch { /* best-effort */ }
69
-
70
- const batch = generateHuBatch({
71
- originalTask: task,
72
- subtasks,
73
- stackHints,
74
- isNewProject,
75
- language: detectedLanguage,
76
- researcherContext: stageResults.researcher?.summary || null,
77
- architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
78
- });
79
-
80
- // Persist batch to HU store so hu-sub-pipeline can update story status via saveHuBatch.
81
- // Use session.id as batchSessionId.
82
- const batchSessionId = `auto-${session.id}`;
83
- try {
84
- const fs = await import("node:fs/promises");
85
- const path = await import("node:path");
86
- const { getKarajanHome } = await import("#utils/paths.js");
87
- const huDir = path.join(getKarajanHome(), "hu-stories", batchSessionId);
88
- await fs.mkdir(huDir, { recursive: true });
89
- const persistBatch = {
90
- session_id: batchSessionId,
91
- project_id: batchSessionId,
92
- project_name: batch.projectName,
93
- created_at: new Date().toISOString(),
94
- updated_at: new Date().toISOString(),
95
- stories: batch.stories
96
- };
97
- await fs.writeFile(path.join(huDir, "batch.json"), JSON.stringify(persistBatch, null, 2));
98
- } catch (err) {
99
- logger.warn(`Auto-HU: failed to persist batch (${err.message}) — sub-pipeline will use in-memory fallback`);
100
- }
101
-
102
- // Wrap in format compatible with needsSubPipeline + runHuSubPipeline
103
- stageResults.huReviewer = {
104
- ok: true,
105
- stories: batch.stories,
106
- total: batch.total,
107
- certified: batch.certified,
108
- batchSessionId,
109
- auto_generated: true,
110
- source: batch.source
111
- };
112
-
113
- logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
114
- emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
115
- message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
116
- detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints, projectName: batch.projectName }
117
- }));
118
-
119
- // Auto-start the board so the user can see the generated HUs.
120
- // Always fires when auto-HU runs, independent of hu_board.auto_start flag.
121
- // Never during vitest — would race the PID file and leave a detached
122
- // node process around after the suite (TSK-0273).
123
- if (process.env.VITEST || process.env.NODE_ENV === "test") return;
124
- try {
125
- const { startBoard, renderBoardBanner, boardSlug } = await import("../../../commands/board.js");
126
- const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
127
- // KJC-TSK-0884: its project's scoped view, not the global dashboard.
128
- const boardResult = await startBoard(desiredPort, { projectSlug: projectDir ? boardSlug(projectDir) : null });
129
- const url = boardResult.url;
130
- const status = boardResult.alreadyRunning ? "already running" : "started";
131
- const projectName = batch.projectName || "Auto-generated HUs";
132
- console.log(renderBoardBanner({ url, status, projectName }));
133
- logger.info(`HU Board ${status} at ${url} (project: ${projectName})`);
134
- } catch (err) {
135
- logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
136
- }
137
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
- * (audit recommendation #6: continue pre-loop extraction). pre-loop.js
4
- * had grown to 626 LOC, over the 600-LOC ceiling that the file's own
5
- * docstring set. Splitting the deprecation warnings, addyosmani skill
6
- * bootstrap, and auto-HU batch generation into their own modules brings
7
- * the driver back to ~440 LOC.
8
- *
9
- * Behaviour unchanged. The function is exported with the same name and
10
- * signature so the move is mechanical.
11
- *
12
- * Emits one-shot warnings for config keys ignored since v2.7.4. Currently:
13
- * - `sonarqube.enabled` (kj.config.yml) → ignored, sonar is intrinsic
14
- * - `--no-sonar` CLI flag → ignored
15
- */
16
-
17
- import { emitProgress, makeEvent } from "#utils/events.js";
18
-
19
- export function emitConfigDeprecations(config, logger, emitter, eventBase) {
20
- const dep = config?._deprecated;
21
- if (!dep) return;
22
-
23
- if (dep.sonarqubeEnabledKey) {
24
- const m =
25
- "DEPRECATED: `sonarqube.enabled` in kj.config.yml is ignored since v2.7.4. " +
26
- "Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and " +
27
- "skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy. " +
28
- "Remove the key from your config to silence this warning.";
29
- logger.warn(m);
30
- emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
31
- message: m,
32
- detail: { key: "sonarqube.enabled", since: "v2.7.4" },
33
- }));
34
- }
35
-
36
- if (dep.noSonarFlag) {
37
- const m =
38
- "DEPRECATED: `--no-sonar` flag is ignored since v2.7.4. Sonar runs for code " +
39
- "tasks by policy. To skip Sonar on a one-off run, use a non-code task type " +
40
- "(e.g. `--task-type doc`) or rely on Solomon's runtime override.";
41
- logger.warn(m);
42
- emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
43
- message: m,
44
- detail: { flag: "--no-sonar", since: "v2.7.4" },
45
- }));
46
- }
47
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
- * (audit recommendation #6). Previously a 60-line export inside
4
- * pre-loop.js; moved here to keep that driver under the 600-LOC
5
- * ceiling its own docstring set. Behaviour and signature unchanged.
6
- *
7
- * Refreshes the addyosmani/agent-skills catalog (clones or pulls,
8
- * gated by `refreshDays`) and resolves which slugs are relevant to
9
- * the current task. Sets the result on the session via the
10
- * `setAddyosmaniSkills` mutator so downstream stages can read it
11
- * without re-running git.
12
- *
13
- * Failures are non-blocking: a missing git, a network blip, or an
14
- * unparseable catalog all just mark the catalog unavailable and
15
- * emit a `skills:addyosmani-unavailable` event. The pipeline keeps
16
- * going — the addyosmani sources are an optional supplement.
17
- */
18
-
19
- import {
20
- refreshIfStale as refreshAddyosmaniCatalog,
21
- listAvailableSlugs as listAddyosmaniSlugs,
22
- } from "#skills/addyosmani-catalog.js";
23
- import { resolveAddyosmaniSlugs } from "#skills/addyosmani-role-map.js";
24
- import { setAddyosmaniSkills } from "#session/mutators.js";
25
- import { emitProgress, makeEvent } from "#utils/events.js";
26
-
27
- export async function ensureAddyosmaniSkills({ task, config, logger, session, emitter, eventBase }) {
28
- const skillsConfig = config?.skills || {};
29
- const sources = Array.isArray(skillsConfig.sources) ? skillsConfig.sources : ["addyosmani", "openskills", "local"];
30
- const addyConfig = skillsConfig.addyosmani || {};
31
- // Test harness override: config.testHarness.defaultAddyosmaniEnabled=false
32
- // prevents orchestrator tests from spawning git. Tests that need the real
33
- // catalog opt in by setting config.skills.addyosmani.enabled = true.
34
- // Post-v2.7.5 no longer reads globalThis directly — config.testHarness is
35
- // populated by the loader from the global or the production default.
36
- const harnessDefault = config?.testHarness?.defaultAddyosmaniEnabled;
37
- const enabledFromConfig = addyConfig.enabled === true
38
- || (addyConfig.enabled !== false && harnessDefault !== false);
39
- const enabled = enabledFromConfig && sources.includes("addyosmani");
40
- if (!enabled) return;
41
-
42
- const refreshDays = Number.isFinite(addyConfig.refreshDays) ? addyConfig.refreshDays : 7;
43
- const refreshMs = Math.max(0, refreshDays) * 24 * 60 * 60 * 1000;
44
-
45
- try {
46
- const refreshResult = await refreshAddyosmaniCatalog({
47
- refreshMs,
48
- repoUrl: addyConfig.repoUrl,
49
- logger,
50
- });
51
-
52
- if (!refreshResult.ok) {
53
- setAddyosmaniSkills(session, { available: false, reason: refreshResult.error || "refresh failed" });
54
- emitProgress(emitter, makeEvent("skills:addyosmani-unavailable", { ...eventBase, stage: "skills" }, {
55
- message: `addyosmani/agent-skills catalog unavailable: ${refreshResult.error || refreshResult.action}`,
56
- detail: { action: refreshResult.action, hint: "Install git to enable process skills from addyosmani/agent-skills" },
57
- }));
58
- return;
59
- }
60
-
61
- const available = new Set(await listAddyosmaniSlugs());
62
- const resolved = resolveAddyosmaniSlugs({ role: null, task }); // role resolution happens per-stage later
63
- const valid = resolved.filter((slug) => available.has(slug));
64
-
65
- setAddyosmaniSkills(session, {
66
- available: true,
67
- action: refreshResult.action,
68
- resolvedSlugs: valid,
69
- allAvailable: Array.from(available),
70
- });
71
-
72
- emitProgress(emitter, makeEvent("skills:addyosmani-ready", { ...eventBase, stage: "skills" }, {
73
- message: `addyosmani/agent-skills ${refreshResult.action} — ${available.size} slug(s) available, ${valid.length} relevant to task`,
74
- detail: {
75
- action: refreshResult.action,
76
- relevantSlugs: valid,
77
- availableCount: available.size,
78
- },
79
- }));
80
- } catch (err) {
81
- logger.warn(`addyosmani catalog step failed (non-blocking): ${err.message}`);
82
- setAddyosmaniSkills(session, { available: false, reason: err.message });
83
- }
84
- }
@@ -1,393 +0,0 @@
1
- /**
2
- * Pre-loop phase: load a persisted plan and inject it into stageResults.
3
- *
4
- * Extracted from `src/orchestrator/drivers/pre-loop.js` in PR-K (audit
5
- * follow-up) to drag runPreLoopStages out of god-function territory
6
- * (was 496 LOC; extracting this 189-LOC block brings it to ~310).
7
- *
8
- * Behaviour is byte-for-byte identical to the inlined version. The
9
- * extraction keeps the dynamic-import pattern (deferred load of
10
- * plan-store / plan-schema / plan-executor / plan-hu-ops) so the
11
- * dynamic-imports budget test stays unchanged.
12
- *
13
- * Returns { handled, plannedTask }:
14
- * - handled=true → caller short-circuits (plan was loaded; skip
15
- * researcher/architect/planner phases below).
16
- * - handled=false → caller continues with the regular planning flow.
17
- * Either flags.plan was unset OR loadPlan returned null OR the
18
- * try/catch swallowed an error (logged as a warning).
19
- *
20
- * The function still mutates session via the existing mutators
21
- * (setPlanRef, setPlanSyncCallback, setLiveStatusUpdater,
22
- * setLiveOutcomeUpdater, setPreLoopContext) and stageResults
23
- * (researcher / architect / planner / huReviewer slots). That
24
- * mutation contract is unchanged from the inlined version — see
25
- * tests/architecture/session-write-boundary.test.js for the
26
- * mutator-routing invariant.
27
- */
28
-
29
- import { saveSession } from "#session/store.js";
30
- import {
31
- setPlanRef, setPlanSyncCallback, setLiveStatusUpdater,
32
- setLiveOutcomeUpdater, setPreLoopContext,
33
- } from "#session/mutators.js";
34
- import { emitProgress, makeEvent } from "#utils/events.js";
35
- import { listActiveRuns } from "../../../utils/run-registry.js";
36
- import { keepRoutingMetrics } from "../../../hu/routing-metrics.js";
37
-
38
- /**
39
- * @param {object} args
40
- * @param {object} args.flags — runFlow flags (reads flags.plan, flags.hu)
41
- * @param {object} args.updatedConfig — the running config (reads projectDir)
42
- * @param {object} args.session
43
- * @param {object} args.stageResults — mutated: researcher / architect / planner / huReviewer slots
44
- * @param {object} args.eventBase
45
- * @param {object} args.emitter
46
- * @param {object} args.logger
47
- * @param {string} args.task — original task description, used to build plannedTask
48
- * @returns {Promise<{handled: boolean, plannedTask: string|null}>}
49
- */
50
- export async function injectLoadedPlan({ flags, updatedConfig, session, stageResults, eventBase, emitter, logger, task }) {
51
- if (!flags.plan) return { handled: false, plannedTask: null };
52
-
53
- try {
54
- const { loadPlan, savePlan: savePlanToDisk } = await import("../../../plan/plan-store.js");
55
- const { isPlanV2 } = await import("../../../plan/plan-schema.js");
56
- const { planToHuBatch, syncResultsToPlan } = await import("../../../plan/plan-executor.js");
57
- const projectDir = updatedConfig.projectDir || process.cwd();
58
- const loadedPlan = await loadPlan(projectDir, flags.plan);
59
- if (!loadedPlan) {
60
- logger.warn(`Plan ${flags.plan} not found — falling back to normal pipeline`);
61
- return { handled: false, plannedTask: null };
62
- }
63
-
64
- logger.info(`Loaded persisted plan: ${flags.plan} (v${loadedPlan.version || 1})`);
65
- emitProgress(emitter, makeEvent("plan:loaded", { ...eventBase, stage: "plan" }, {
66
- message: `Plan loaded: ${flags.plan}`,
67
- detail: { planId: flags.plan, task: loadedPlan.task, version: loadedPlan.version, huCount: loadedPlan.hus?.length || 0 }
68
- }));
69
-
70
- // Zombie reconciliation: a previous run may have died mid-flight,
71
- // leaving HUs stuck in coding/reviewing/running. The board-side
72
- // reaper only runs inside the board; without this, a headless
73
- // `kj run --plan` would refuse to pick those HUs up.
74
- const reaped = await reconcilePlanHuZombies({ loadedPlan, planId: flags.plan, logger });
75
- if (reaped > 0) {
76
- await savePlanToDisk(projectDir, loadedPlan);
77
- emitProgress(emitter, makeEvent("plan:zombies-reaped", { ...eventBase, stage: "plan" }, {
78
- message: `Reset ${reaped} stale HU(s) from a previous run that did not finish`,
79
- detail: { planId: flags.plan, count: reaped }
80
- }));
81
- }
82
- stageResults.researcher = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
83
- stageResults.architect = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
84
- stageResults.planner = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
85
-
86
- // Tests-first gate (v2.7.5): refuse to run a plan when any HU
87
- // has no acceptance_tests. Mirrors the gate in `kj plan ready`
88
- // so the CLI path and the board's "▶ Run plan" button enforce
89
- // the same contract.
90
- if (isPlanV2(loadedPlan) && Array.isArray(loadedPlan.hus)) {
91
- assertEveryHuHasTests({ loadedPlan, planId: flags.plan, eventBase, emitter, logger });
92
-
93
- // PR-E (auto-certify + anomaly guards): the user expects
94
- // pending HUs to be runnable; "certified" was an invisible
95
- // wall. Helpers in plan-hu-ops.js so they can be unit-tested
96
- // without spinning up the orchestrator. Pulled from the same
97
- // dynamic import we use a few lines below for updateHuStatus +
98
- // setHuOutcome — no extra import budget.
99
- const planHuOps = await import("../../../plan/plan-hu-ops.js");
100
- await autoCertifyAndAssertRunnable({ planHuOps, loadedPlan, planId: flags.plan, projectDir, savePlanToDisk, eventBase, emitter, logger });
101
-
102
- // v2 plan with HUs → inject as huReviewer so sub-pipeline
103
- // picks them up. Sets every callback (sync, live status, live
104
- // outcome) and stamps plan.status = "running".
105
- if (loadedPlan.hus?.length > 0) {
106
- const huBatch = planToHuBatch(loadedPlan);
107
- applyHuFilterIfRequested({ huBatch, flags, eventBase, emitter, logger });
108
- if (huBatch.ok) {
109
- stageResults.huReviewer = huBatch;
110
- wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan, savePlanToDisk, planHuOps, logger });
111
- logger.info(`Plan ${flags.plan}: ${huBatch.certified} HUs ready for execution`);
112
- emitProgress(emitter, makeEvent("plan:hus-loaded", { ...eventBase, stage: "plan" }, {
113
- message: `${huBatch.certified} HUs loaded from plan`,
114
- detail: { planId: flags.plan, total: huBatch.total, certified: huBatch.certified }
115
- }));
116
- loadedPlan.status = "running";
117
- await savePlanToDisk(projectDir, loadedPlan);
118
- }
119
- }
120
- }
121
-
122
- // Inject contexts that the legacy planning phases would have
123
- // produced. Then build a plannedTask string for the non-HU /
124
- // fallback iteration path.
125
- const ctx = loadedPlan.context || {};
126
- setPreLoopContext(session, {
127
- research: ctx.researchContext || loadedPlan.researchContext || null,
128
- architect: ctx.architectContext || loadedPlan.architectContext || null,
129
- plan: loadedPlan.approach || loadedPlan.plan || null,
130
- });
131
- await saveSession(session);
132
-
133
- return { handled: true, plannedTask: buildPlannedTaskFromLoadedPlan({ task, loadedPlan }) };
134
- } catch (err) {
135
- logger.warn(`Plan loading failed: ${err.message} — falling back to normal pipeline`);
136
- return { handled: false, plannedTask: null };
137
- }
138
- }
139
-
140
- /**
141
- * Reset HUs that look mid-execution but have no live run behind them.
142
- *
143
- * Resilience audit, Phase 3 (Hallazgo A1 / status-zombi). A previous
144
- * `kj run --plan` flipped HUs to `coding`/`reviewing`/`running` in the
145
- * plan JSON, then died (SIGKILL, terminal closed, OOM, crash). The
146
- * board-side `hu-zombie-reaper` only queries SQLite and only runs
147
- * inside the board process, so the plan JSON on disk kept stale
148
- * in-flight statuses forever. The next `kj run --plan` then refused to
149
- * pick those HUs up.
150
- *
151
- * Cross-check via `run-registry`: if there is a live run with our
152
- * planId, do NOT touch — that run owns the in-flight HUs. Otherwise
153
- * any in-flight status with no live owner is a zombie → reset to
154
- * `pending` so it can run again.
155
- *
156
- * Mutates `loadedPlan.hus` in place. Returns the number of HUs reset
157
- * so the caller can persist + emit a progress event.
158
- *
159
- * @param {object} args
160
- * @param {object} args.loadedPlan
161
- * @param {string} args.planId
162
- * @param {object} [args.logger]
163
- * @returns {number}
164
- */
165
- export function reconcilePlanHuZombies({ loadedPlan, planId, logger }) {
166
- if (!Array.isArray(loadedPlan?.hus) || loadedPlan.hus.length === 0) return 0;
167
- const activeRuns = listActiveRuns({ planId });
168
- if (activeRuns.length > 0) return 0;
169
- const ZOMBIE_STATES = new Set(["coding", "reviewing", "running"]);
170
- let reaped = 0;
171
- for (const hu of loadedPlan.hus) {
172
- if (ZOMBIE_STATES.has(hu.status)) {
173
- const previous = hu.status;
174
- hu.status = "pending";
175
- reaped += 1;
176
- logger?.warn?.(`[plan] HU ${hu.id || hu.hu_id || "?"} was '${previous}' but no live run owns the plan — reset to 'pending'.`);
177
- }
178
- }
179
- return reaped;
180
- }
181
-
182
- /**
183
- * Throws when any HU lacks an acceptance_tests array with at least
184
- * one entry. Logs + emits the same `plan:tests-missing` progress
185
- * event as the inlined version did.
186
- */
187
- function assertEveryHuHasTests({ loadedPlan, planId, eventBase, emitter, logger }) {
188
- const missing = loadedPlan.hus.filter(
189
- (h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0
190
- );
191
- if (missing.length === 0) return;
192
- const offenders = missing.map((h) => ` - ${h.id} ${(h.title || "").slice(0, 60)}`).join("\n");
193
- const msg =
194
- `Refusing to run plan ${planId}: ${missing.length}/${loadedPlan.hus.length} HU(s) have no acceptance_tests.\n`
195
- + `${offenders}\n\n`
196
- + "Tests-first flow requires a test contract per HU. Edit each HU on the board\n"
197
- + "(http://localhost:4000) via the ✎ Edit button, or re-run `kj plan` to regenerate.";
198
- logger.error(msg);
199
- emitProgress(emitter, makeEvent("plan:tests-missing", { ...eventBase, stage: "plan" }, {
200
- status: "fail",
201
- message: `Plan ${planId}: ${missing.length} HU(s) missing acceptance_tests`,
202
- detail: { planId, missingHuIds: missing.map((h) => h.id) }
203
- }));
204
- throw new Error(`Plan ${planId} has ${missing.length} HU(s) with no acceptance_tests — see the run log for details.`);
205
- }
206
-
207
- /**
208
- * Promotes pending HUs with tests to certified, then asserts at
209
- * least one HU is runnable. Throws (with `plan:no-runnable` event)
210
- * when nothing is left to do, so the orchestrator never silently
211
- * falls back to single-task mode and burns tokens.
212
- */
213
- async function autoCertifyAndAssertRunnable({ planHuOps, loadedPlan, planId, projectDir, savePlanToDisk, eventBase, emitter, logger }) {
214
- const { autoCertifyPendingHus, assertPlanRunnable } = planHuOps;
215
- const promoted = autoCertifyPendingHus(loadedPlan);
216
- if (promoted > 0) {
217
- await savePlanToDisk(projectDir, loadedPlan);
218
- logger.info(`Plan ${planId}: auto-certified ${promoted} pending HU(s) — ready to run`);
219
- emitProgress(emitter, makeEvent("plan:auto-certified", { ...eventBase, stage: "plan" }, {
220
- message: `Auto-certificadas ${promoted} HU(s) pendientes`,
221
- detail: { planId, count: promoted }
222
- }));
223
- }
224
- try {
225
- assertPlanRunnable(loadedPlan, planId);
226
- } catch (err) {
227
- logger.error(err.message);
228
- emitProgress(emitter, makeEvent("plan:no-runnable", { ...eventBase, stage: "plan" }, {
229
- status: "fail", message: err.message,
230
- detail: { planId }
231
- }));
232
- throw err;
233
- }
234
- }
235
-
236
- /**
237
- * PR4: when --hu <ids> is set, narrow the batch to those HU ids by
238
- * marking the rest as `done` in memory. The plan JSON on disk is
239
- * NOT touched — this is a run-time filter only. Throws when none of
240
- * the requested ids exist.
241
- */
242
- function applyHuFilterIfRequested({ huBatch, flags, eventBase, emitter, logger }) {
243
- if (!huBatch.ok || !flags.hu) return;
244
- const requested = String(flags.hu).split(",").map((s) => s.trim()).filter(Boolean);
245
- if (requested.length === 0) {
246
- logger.warn(`--hu was empty after parsing — falling back to running every certified HU`);
247
- return;
248
- }
249
- // Humanización IDs: resolver cada ref a un hu.id largo aceptando 3 formas:
250
- // 1. `hu_<planId>_<NNN>` (id largo, exacto)
251
- // 2. `INFRA-001` (short_id que el planner asignó)
252
- // 3. `001` (sufijo numérico — busca un hu.id que termine en `_001`)
253
- // El resolver es case-insensitive para short_id y sufijo.
254
- const resolvedIds = new Set();
255
- const unresolved = [];
256
- for (const ref of requested) {
257
- const id = resolveHuRef(ref, huBatch.stories);
258
- if (id) resolvedIds.add(id);
259
- else unresolved.push(ref);
260
- }
261
- const found = huBatch.stories.filter((s) => resolvedIds.has(s.id));
262
- if (unresolved.length > 0) {
263
- logger.warn(`--hu: ignored ${unresolved.length} unknown HU ref(s): ${unresolved.join(", ")}`);
264
- }
265
- if (found.length === 0) {
266
- throw new Error(`--hu: none of the requested HU ref(s) exist in plan ${flags.plan}: ${requested.join(", ")}`);
267
- }
268
- for (const story of huBatch.stories) {
269
- if (!resolvedIds.has(story.id)) story.status = "done";
270
- }
271
- huBatch.certified = found.length;
272
- logger.info(`Plan ${flags.plan}: --hu filter active — ${found.length}/${huBatch.total} HU(s) will run (${found.map((s) => s.id).join(", ")})`);
273
- emitProgress(emitter, makeEvent("plan:hu-filter", { ...eventBase, stage: "plan" }, {
274
- message: `--hu filter: ${found.length} HU(s) selected, ${huBatch.total - found.length} skipped`,
275
- detail: { planId: flags.plan, requested, ignored: unresolved, willRun: found.map((s) => s.id) }
276
- }));
277
- }
278
-
279
- /**
280
- * Wire the four session-scoped callbacks the run will invoke as it
281
- * progresses: PR1 live status (per HU), PR3 live outcome (per HU
282
- * exit), and the syncResultsToPlan rollup (end-of-run).
283
- */
284
- function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan, savePlanToDisk, planHuOps, logger }) {
285
- setPlanRef(session, { planId: loadedPlan.planId, projectDir });
286
- setPlanSyncCallback(session, async (subResult) => {
287
- syncResultsToPlan(loadedPlan, subResult);
288
- await savePlanToDisk(projectDir, loadedPlan);
289
- });
290
- // PR1: live HU status. Without this the plan only updates at
291
- // end-of-run and the board's Kanban looks frozen during execution.
292
- // (`planHuOps` was loaded above for PR-E auto-certify — reuse the
293
- // reference to stay under the dynamic-import budget.)
294
- const { updateHuStatus: updateHuStatusFn, setHuOutcome: setHuOutcomeFn } = planHuOps;
295
- setLiveStatusUpdater(session, async (huId, status) => {
296
- try {
297
- if (!updateHuStatusFn(loadedPlan, huId, status)) return;
298
- await savePlanToDisk(projectDir, loadedPlan);
299
- } catch (err) {
300
- logger?.warn?.(`Live status update failed for ${huId}: ${err.message}`);
301
- }
302
- });
303
- // PR3: per-HU outcome. Stamped once per HU at runSingleHu's exit
304
- // so the board can show the 📄 indicator as soon as each HU
305
- // finishes (iterations / duration / commits / blockers / summary).
306
- //
307
- // Cost D (KJC-TSK-0515): the outcome may carry `cost_usd` (USD
308
- // spent on this HU). When present and finite, also persist it to
309
- // board.db so the per-card badge (Cost F) and the project total
310
- // chip (Cost G) have data to show — without this write, the
311
- // `cost_usd` column would always stay NULL and the UI would render
312
- // nothing. Lazy-import to keep the pre-loop phase free of board
313
- // deps until we actually need them.
314
- setLiveOutcomeUpdater(session, async (huId, outcome) => {
315
- try {
316
- if (!setHuOutcomeFn(loadedPlan, huId, outcome)) return;
317
- keepRoutingMetrics(loadedPlan, huId, outcome); // KJC-TSK-0702: the measured fields core does not keep
318
- await savePlanToDisk(projectDir, loadedPlan);
319
- if (outcome && Number.isFinite(outcome.cost_usd)) {
320
- try {
321
- const { ensureDb, setStoryCost, setStoryCachedTokens } = await import("../../../../packages/hu-board/src/db.js");
322
- ensureDb(); // KJC-BUG-0090: autorun/run may not have started the board
323
- setStoryCost(huId, outcome.cost_usd);
324
- // Φ0-G (KJC-TSK-0525): cached/in tokens travel in the same
325
- // outcome blob as cost. Write them together so the board
326
- // can render the "🎯 N% cached" badge on the same render.
327
- // Either may be null (older runs, no cache) — setter handles it.
328
- if (Number.isFinite(outcome.cached_tokens) || Number.isFinite(outcome.tokens_in)) {
329
- setStoryCachedTokens(
330
- huId,
331
- Number.isFinite(outcome.cached_tokens) ? outcome.cached_tokens : null,
332
- Number.isFinite(outcome.tokens_in) ? outcome.tokens_in : null,
333
- );
334
- }
335
- } catch (err) {
336
- logger?.warn?.(`Cost write failed for ${huId}: ${err.message}`);
337
- }
338
- }
339
- } catch (err) {
340
- logger?.warn?.(`Live outcome update failed for ${huId}: ${err.message}`);
341
- }
342
- });
343
- }
344
-
345
- /**
346
- * Resuelve una referencia humana a HU al `id` canónico largo. Acepta:
347
- * - id largo exacto: `hu_<planId>_<NNN>` → match directo.
348
- * - short_id: `INFRA-001`, `AUTH-SIGNUP` → match case-insensitive contra hu.short_id.
349
- * - sufijo numérico: `001` → match contra hu.id terminado en `_001`.
350
- * Devuelve el hu.id largo o null si no resuelve.
351
- *
352
- * @param {string} ref
353
- * @param {Array<{id: string, short_id?: string|null}>} hus
354
- * @returns {string|null}
355
- */
356
- function resolveHuRef(ref, hus) {
357
- if (!ref || !Array.isArray(hus)) return null;
358
- // Match exacto contra id largo (camino caliente).
359
- const exact = hus.find((s) => s.id === ref);
360
- if (exact) return exact.id;
361
- // Match case-insensitive contra short_id.
362
- const lower = ref.toLowerCase();
363
- const byShort = hus.find((s) => s.short_id && String(s.short_id).toLowerCase() === lower);
364
- if (byShort) return byShort.id;
365
- // Match por sufijo numérico (`001` → cualquier hu cuyo id termine en `_001`).
366
- if (/^\d+$/.test(ref)) {
367
- const padded = ref.padStart(3, "0");
368
- const bySuffix = hus.find((s) => s.id.endsWith(`_${padded}`));
369
- if (bySuffix) return bySuffix.id;
370
- }
371
- return null;
372
- }
373
-
374
- /**
375
- * Build a single plannedTask string for the non-HU / fallback
376
- * iteration path. The HU sub-pipeline doesn't read this; only the
377
- * legacy single-iteration loop does. Identical to the inlined
378
- * version's behaviour.
379
- */
380
- function buildPlannedTaskFromLoadedPlan({ task, loadedPlan }) {
381
- const plan = loadedPlan.plan || (loadedPlan.approach ? { approach: loadedPlan.approach } : null);
382
- if (plan && typeof plan === "object" && plan.steps) {
383
- const stepList = plan.steps.map((s, idx) => `${idx + 1}. ${s.description || s}`).join("\n");
384
- return `${task}\n\n## Implementation Plan\n${plan.approach || ""}\n\n## Steps\n${stepList}`;
385
- }
386
- if (typeof plan === "string") {
387
- return `${task}\n\n## Implementation Plan\n${plan}`;
388
- }
389
- if (loadedPlan.approach) {
390
- return `${task}\n\n## Approach\n${loadedPlan.approach}`;
391
- }
392
- return task;
393
- }