karajan-code 4.70.3 → 5.0.0

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