karajan-code 4.70.4 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/dir-setup.js +34 -1
  61. package/src/checks/native-build.js +21 -39
  62. package/src/checks/runner.js +128 -142
  63. package/src/cli/advanced-commands.js +6 -6
  64. package/src/cli/register-meta.js +42 -82
  65. package/src/cli/register-pipeline.js +8 -143
  66. package/src/cli/register-plan.js +9 -100
  67. package/src/cli/register-roles-skills.js +1 -1
  68. package/src/cli/retired-commands.js +49 -0
  69. package/src/cli.js +18 -5
  70. package/src/commands/agent-run.js +40 -26
  71. package/src/commands/audit.js +127 -124
  72. package/src/commands/board.js +5 -9
  73. package/src/commands/check.js +29 -16
  74. package/src/commands/doctor.js +2 -0
  75. package/src/commands/env.js +176 -150
  76. package/src/commands/go.js +71 -53
  77. package/src/commands/harden.js +90 -81
  78. package/src/commands/hu-start.js +52 -0
  79. package/src/commands/hu.js +288 -221
  80. package/src/commands/identity.js +111 -78
  81. package/src/commands/init-atril.js +47 -0
  82. package/src/commands/init.js +11 -40
  83. package/src/commands/install-tools.js +70 -85
  84. package/src/commands/main-guard.js +78 -0
  85. package/src/commands/policy.js +226 -184
  86. package/src/commands/qmd.js +27 -21
  87. package/src/commands/rag.js +121 -55
  88. package/src/commands/review-gate.js +421 -341
  89. package/src/commands/skills.js +4 -1
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +107 -26
  94. package/src/config/defaults.js +1 -0
  95. package/src/config/loader.js +34 -17
  96. package/src/config/role-resolver.js +1 -0
  97. package/src/config/schema.js +1 -0
  98. package/src/config/secrets.js +36 -0
  99. package/src/config.js +1 -0
  100. package/src/environment/adr.js +16 -2
  101. package/src/git/karajan-label.js +20 -8
  102. package/src/guards/main-guard.js +54 -0
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  106. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  107. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-templates.js +1 -1
  111. package/src/hu/board-lookup.js +29 -0
  112. package/src/hu/gh-identity.js +23 -0
  113. package/src/hu/github-board.js +4 -2
  114. package/src/hu/lane-prune.js +83 -13
  115. package/src/hu/lane-setup.js +65 -0
  116. package/src/hu/lanes.js +66 -0
  117. package/src/hu/team-board-mirror.js +3 -2
  118. package/src/infrastructure/command-runner.js +1 -1
  119. package/src/infrastructure/file-system-service.js +1 -1
  120. package/src/mcp/handlers/direct-handlers.js +2 -292
  121. package/src/mcp/handlers/hu-handlers.js +1 -35
  122. package/src/mcp/handlers/management-handlers.js +2 -42
  123. package/src/mcp/handlers/rag-handler.js +9 -1
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +1 -196
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +71 -44
  134. package/src/rag/bootstrap-index.js +46 -54
  135. package/src/rag/indexer.js +9 -4
  136. package/src/rag/migrate.js +1 -1
  137. package/src/rag/onnx-fallback.js +2 -2
  138. package/src/rag/pid-lease.js +181 -0
  139. package/src/rag/project-store.js +2 -12
  140. package/src/review/card-first.js +57 -46
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +12 -4
  143. package/src/review/one-shot-review.js +45 -35
  144. package/src/review/publish-pr.js +14 -2
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +19 -4
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +1 -24
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-cli.js +19 -1
  156. package/src/skills/atril-skills.js +5 -4
  157. package/src/skills/skills-search.js +72 -0
  158. package/src/skills/translate.js +62 -0
  159. package/src/sonar/scanner.js +116 -101
  160. package/src/steward/invariants.js +5 -4
  161. package/src/steward/phantom-sweep.js +66 -0
  162. package/src/tournament/judge.js +60 -53
  163. package/src/utils/gh-account.js +37 -0
  164. package/src/utils/git.js +0 -13
  165. package/src/utils/home-migration.js +7 -1
  166. package/src/utils/main-checkout.js +53 -0
  167. package/src/utils/qmd-collection.js +3 -1
  168. package/src/utils/welcome.js +3 -3
  169. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  180. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  181. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  182. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  183. package/packages/hu-board/src/run-tracker.js +0 -121
  184. package/src/activity-log.js +0 -59
  185. package/src/agents/host-agent.js +0 -38
  186. package/src/agents/model-registry.js +0 -142
  187. package/src/audit/plan-adherence.js +0 -224
  188. package/src/autonomy/arbiter-role.js +0 -71
  189. package/src/autonomy/arbiter.js +0 -22
  190. package/src/autonomy/decider.js +0 -18
  191. package/src/autonomy/decision-resolver.js +0 -58
  192. package/src/autonomy/outcome-report.js +0 -55
  193. package/src/autonomy/stage-ask.js +0 -30
  194. package/src/brain/decision-tracker.js +0 -125
  195. package/src/brain/decisor.js +0 -243
  196. package/src/brain/solomon-consult.js +0 -171
  197. package/src/brain/standby-scheduler.js +0 -8
  198. package/src/budget/comparison.js +0 -96
  199. package/src/canvas/parse-md.js +0 -238
  200. package/src/canvas/schema.js +0 -101
  201. package/src/canvas/to-flat.js +0 -85
  202. package/src/canvas/validate.js +0 -163
  203. package/src/ci/dispatch.js +0 -99
  204. package/src/ci/index.js +0 -3
  205. package/src/ci/pr-diff.js +0 -26
  206. package/src/cli/register-standby.js +0 -26
  207. package/src/commands/architect.js +0 -111
  208. package/src/commands/autorun.js +0 -49
  209. package/src/commands/researcher.js +0 -54
  210. package/src/commands/resume.js +0 -72
  211. package/src/commands/review.js +0 -89
  212. package/src/commands/run.js +0 -185
  213. package/src/commands/standby.js +0 -117
  214. package/src/commands/status.js +0 -36
  215. package/src/commands/triage.js +0 -78
  216. package/src/commands/undo.js +0 -62
  217. package/src/domains/domain-loader.js +0 -245
  218. package/src/domains/domain-registry.js +0 -140
  219. package/src/domains/domain-synthesizer.js +0 -129
  220. package/src/git/automation.js +0 -433
  221. package/src/git/coder-commit-guard.js +0 -40
  222. package/src/git/hu-automation.js +0 -243
  223. package/src/git/hu-snapshot.js +0 -10
  224. package/src/git/symlink-targets.js +0 -57
  225. package/src/golden/asserter.js +0 -92
  226. package/src/golden/loader.js +0 -52
  227. package/src/golden/runner.js +0 -92
  228. package/src/golden/schema.js +0 -41
  229. package/src/guards/duplicate-members.js +0 -86
  230. package/src/guards/intent-guard.js +0 -161
  231. package/src/guards/perf-guard.js +0 -126
  232. package/src/guards/policy-guard.js +0 -90
  233. package/src/guards/policy-resolver.js +0 -85
  234. package/src/guards/secret-redactor.js +0 -71
  235. package/src/hu/acceptance-runner.js +0 -138
  236. package/src/hu/lazy-planner.js +0 -109
  237. package/src/hu/parallel-executor.js +0 -94
  238. package/src/hu/routing-metrics.js +0 -17
  239. package/src/hu/split-validator.js +0 -143
  240. package/src/hu/splitting-detector.js +0 -158
  241. package/src/hu/splitting-generator.js +0 -175
  242. package/src/hu/store.js +0 -394
  243. package/src/infrastructure/mocks.js +0 -182
  244. package/src/mcp/handlers/run-handler.js +0 -318
  245. package/src/mcp/sovereignty-guard.js +0 -113
  246. package/src/mcp/tool-arg-normalizers.js +0 -16
  247. package/src/mutate/reviewer-signal.js +0 -76
  248. package/src/orchestrator/agent-fallback.js +0 -81
  249. package/src/orchestrator/brain-coordinator.js +0 -186
  250. package/src/orchestrator/ci-integration.js +0 -139
  251. package/src/orchestrator/config-init.js +0 -426
  252. package/src/orchestrator/direct-actions.js +0 -217
  253. package/src/orchestrator/drivers/error-recovery.js +0 -287
  254. package/src/orchestrator/drivers/init-context.js +0 -238
  255. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  256. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  257. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  258. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  259. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  260. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  261. package/src/orchestrator/drivers/post-loop.js +0 -404
  262. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  263. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  264. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  265. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  266. package/src/orchestrator/drivers/pre-loop.js +0 -524
  267. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  268. package/src/orchestrator/feedback-enrichment.js +0 -140
  269. package/src/orchestrator/feedback-queue.js +0 -151
  270. package/src/orchestrator/flow-control.js +0 -170
  271. package/src/orchestrator/flow-runner.js +0 -365
  272. package/src/orchestrator/fs-leak-detector.js +0 -228
  273. package/src/orchestrator/hu-scheduler.js +0 -114
  274. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  275. package/src/orchestrator/iteration-gate.js +0 -73
  276. package/src/orchestrator/iteration-stages.js +0 -9
  277. package/src/orchestrator/parallel-limiter.js +0 -74
  278. package/src/orchestrator/pipeline-context.js +0 -72
  279. package/src/orchestrator/post-loop-stages.js +0 -468
  280. package/src/orchestrator/pre-loop-stages.js +0 -11
  281. package/src/orchestrator/preflight-checks.js +0 -487
  282. package/src/orchestrator/repair/repair-detector.js +0 -117
  283. package/src/orchestrator/repair/repair-runner.js +0 -142
  284. package/src/orchestrator/reviewer-fallback.js +0 -45
  285. package/src/orchestrator/role-output-compressor.js +0 -189
  286. package/src/orchestrator/session-journal.js +0 -308
  287. package/src/orchestrator/solomon-escalation.js +0 -331
  288. package/src/orchestrator/solomon-rules.js +0 -212
  289. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  290. package/src/orchestrator/stages/architect-stage.js +0 -172
  291. package/src/orchestrator/stages/coder-stage.js +0 -506
  292. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  293. package/src/orchestrator/stages/format-stage.js +0 -61
  294. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  295. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  296. package/src/orchestrator/stages/perf-stage.js +0 -140
  297. package/src/orchestrator/stages/planner-stage.js +0 -85
  298. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  299. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  300. package/src/orchestrator/stages/research-stage.js +0 -69
  301. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  302. package/src/orchestrator/stages/sonar-stage.js +0 -439
  303. package/src/orchestrator/stages/stage-classes.js +0 -84
  304. package/src/orchestrator/stages/stage-executor.js +0 -155
  305. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  306. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  307. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  308. package/src/orchestrator/stages/triage-stage.js +0 -172
  309. package/src/orchestrator/standby.js +0 -71
  310. package/src/orchestrator/verification-gate.js +0 -183
  311. package/src/orchestrator.js +0 -22
  312. package/src/plan/adr-generator.js +0 -170
  313. package/src/plan/adr-loader.js +0 -60
  314. package/src/plan/plan-executor.js +0 -100
  315. package/src/plan/plan-validation.js +0 -4
  316. package/src/prompts/acceptance-coverage.js +0 -32
  317. package/src/prompts/arbiter.js +0 -38
  318. package/src/prompts/architect.js +0 -103
  319. package/src/prompts/hu-reviewer.js +0 -244
  320. package/src/prompts/tool-correctness-judge.js +0 -31
  321. package/src/prompts/triage.js +0 -57
  322. package/src/rag/embedder.js +0 -2
  323. package/src/rag/embedders/_cloud-base.js +0 -2
  324. package/src/rag/embedders/cohere.js +0 -2
  325. package/src/rag/embedders/mistral.js +0 -2
  326. package/src/rag/embedders/onnx.js +0 -2
  327. package/src/rag/embedders/openai.js +0 -2
  328. package/src/rag/embedders/voyage.js +0 -2
  329. package/src/rag/rerank.js +0 -4
  330. package/src/rag/where-parser.js +0 -4
  331. package/src/repeat-detector.js +0 -88
  332. package/src/review/diff-generator.js +0 -112
  333. package/src/review/git-stash-helper.js +0 -104
  334. package/src/review/schema.js +0 -31
  335. package/src/review/scope-filter.js +0 -152
  336. package/src/review/snapshot-diff.js +0 -122
  337. package/src/review/tdd-discipline.js +0 -90
  338. package/src/review/tdd-policy.js +0 -69
  339. package/src/roles/acceptance-coverage-role.js +0 -78
  340. package/src/roles/architect-role.js +0 -77
  341. package/src/roles/coder-role.js +0 -113
  342. package/src/roles/commiter-role.js +0 -113
  343. package/src/roles/domain-curator-role.js +0 -134
  344. package/src/roles/hu-reviewer-role.js +0 -64
  345. package/src/roles/impeccable-role.js +0 -85
  346. package/src/roles/index.js +0 -18
  347. package/src/roles/karajan-brain-role.js +0 -134
  348. package/src/roles/perf-role.js +0 -122
  349. package/src/roles/planner-role.js +0 -151
  350. package/src/roles/refactorer-role.js +0 -30
  351. package/src/roles/repairer-role.js +0 -181
  352. package/src/roles/researcher-role.js +0 -70
  353. package/src/roles/reviewer-role.js +0 -115
  354. package/src/roles/security-role.js +0 -73
  355. package/src/roles/solomon-role.js +0 -271
  356. package/src/roles/sonar-role.js +0 -72
  357. package/src/roles/tester-role.js +0 -235
  358. package/src/roles/tool-correctness-judge-role.js +0 -78
  359. package/src/roles/triage-role.js +0 -99
  360. package/src/session/journal/decisions-writer.js +0 -200
  361. package/src/session/journal/iteration-logger.js +0 -194
  362. package/src/session/journal/summary-writer.js +0 -320
  363. package/src/session/journal/tree-writer.js +0 -164
  364. package/src/session/journal/write-all.js +0 -89
  365. package/src/session-cleanup.js +0 -116
  366. package/src/skills/semantic-detector.js +0 -167
  367. package/src/skills/skill-detector.js +0 -385
  368. package/src/sonar/cloud-scanner.js +0 -78
  369. package/src/tracker-bootstrap.js +0 -44
  370. package/src/types/index.js +0 -38
  371. package/src/types/policy.js +0 -43
  372. package/src/types/solomon.js +0 -87
  373. package/src/utils/async-lock.js +0 -31
  374. package/src/utils/budget.js +0 -229
  375. package/src/utils/cwd-confirm.js +0 -39
  376. package/src/utils/display/event-handlers.js +0 -403
  377. package/src/utils/display/formatters.js +0 -132
  378. package/src/utils/display/header.js +0 -47
  379. package/src/utils/display/session-summary.js +0 -118
  380. package/src/utils/display/solomon.js +0 -42
  381. package/src/utils/pricing.js +0 -29
  382. package/src/utils/quota-simulator.js +0 -56
  383. package/src/utils/role-assigner.js +0 -137
  384. package/src/utils/rtk-wrapper.js +0 -87
  385. package/src/utils/run-registry.js +0 -9
  386. package/src/utils/status-dashboard.js +0 -254
  387. package/src/utils/tool-call-extractor.js +0 -56
  388. package/src/webperf/devtools-detect.js +0 -112
  389. package/src/webperf/webperf-role-integration.js +0 -66
  390. package/templates/skills/kj-run.md +0 -73
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * Generic Karajan-CLI launcher used by the board's "Commands" panel.
3
- * Wraps the same spawn-detached-with-log pattern that `runPlan`
4
- * already uses (see plan-mutations.js), but covers the broader set
5
- * of commands the user wants to trigger from the UI: plan,
6
- * architect, clean, etc.
3
+ * Spawns a whitelisted `kj` command detached, with its output in a
4
+ * log the board can tail. Today the whitelist holds `clean`.
7
5
  *
8
6
  * Security: the command name is matched against an explicit
9
7
  * whitelist. Free-form arg arrays are NOT allowed — each command
@@ -16,8 +14,8 @@
16
14
  * planId, in routes/api.js).
17
15
  */
18
16
 
19
- import { mkdirSync, openSync } from "node:fs";
20
- import { join, dirname } from "node:path";
17
+ import { existsSync, mkdirSync, openSync, readdirSync } from "node:fs";
18
+ import { join, dirname, isAbsolute } from "node:path";
21
19
  import { spawn } from "node:child_process";
22
20
  import { fileURLToPath } from "node:url";
23
21
  import { randomUUID } from "node:crypto";
@@ -36,7 +34,7 @@ import { getHuBoardRunsDir } from "./db.js";
36
34
 
37
35
  /**
38
36
  * Whitelist + argv builders. Each entry maps a logical command name
39
- * (`plan`, `architect`, `clean`) to a function that turns the
37
+ * (e.g. `clean`) to a function that turns the
40
38
  * board-side input into a CLI argv. Adding a new command means: add
41
39
  * an entry here and an entry in the board UI's `COMMANDS` table.
42
40
  *
@@ -47,25 +45,9 @@ import { getHuBoardRunsDir } from "./db.js";
47
45
  * Each builder returns `{ args, projectDir? }`. `projectDir` is the
48
46
  * cwd the spawn should use (defaults to the board's cwd).
49
47
  */
48
+ // KJC-TSK-1323: `plan` (generate) and `architect` left with 5.0, which retired them;
49
+ // a request for either now gets the 400 of any command outside the whitelist.
50
50
  const COMMAND_BUILDERS = {
51
- plan: (input) => {
52
- const args = ["plan"];
53
- if (input.taskFile) args.push("--task-file", String(input.taskFile));
54
- if (input.context) args.push("--context", String(input.context));
55
- if (input.quick) args.push("--quick");
56
- if (input.noTestsSynth) args.push("--no-tests-synth");
57
- if (input.noPlanReview) args.push("--no-plan-review");
58
- // Positional task goes last when no --task-file was given.
59
- if (!input.taskFile && input.task) args.push(String(input.task));
60
- return { args, projectDir: input.projectDir };
61
- },
62
- architect: (input) => {
63
- const args = ["architect"];
64
- if (input.taskFile) args.push("--task-file", String(input.taskFile));
65
- if (input.context) args.push("--context", String(input.context));
66
- if (!input.taskFile && input.task) args.push(String(input.task));
67
- return { args, projectDir: input.projectDir };
68
- },
69
51
  clean: (input) => {
70
52
  const args = ["clean", "--yes"]; // never prompt; UI confirms ahead
71
53
  if (input.nuke) args.push("--nuke");
@@ -101,6 +83,11 @@ export function runKjCommand({ command, input = {} } = {}) {
101
83
  return { ok: false, error: `Bad input for "${command}": ${err.message}` };
102
84
  }
103
85
 
86
+ return spawnKj(command, built);
87
+ }
88
+
89
+ /** Spawn `kj <args>` detached, its output in a run log the board can tail. */
90
+ function spawnKj(command, built) {
104
91
  const dir = getHuBoardRunsDir();
105
92
  mkdirSync(dir, { recursive: true });
106
93
  const commandId = `cmd-${command}-${Date.now()}-${randomUUID().slice(0, 8)}`;
@@ -127,6 +114,30 @@ export function runKjCommand({ command, input = {} } = {}) {
127
114
  return { ok: true, commandId, pid: child.pid ?? 0, logPath };
128
115
  }
129
116
 
117
+ const CARD_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
118
+
119
+ /**
120
+ * KJC-TSK-1324: the ▶ of a card. `kj hu start <id> --code` checks the card, opens its lane
121
+ * and has the panel coder write it there; review and commit stay with the person. Only a card
122
+ * id and an absolute git checkout get through, never a free-form argument.
123
+ */
124
+ export function startCardInLane({ id, projectDir } = {}) {
125
+ if (!CARD_ID.test(String(id ?? ""))) return { ok: false, error: `not a card id: ${id}` };
126
+ if (!isAbsolute(String(projectDir ?? "")) || !existsSync(join(projectDir, ".git"))) {
127
+ return { ok: false, error: `not a git checkout: ${projectDir}` };
128
+ }
129
+ const lane = laneOfCard({ id, projectDir });
130
+ if (lane) return { ok: false, lane, error: `${id} already has a lane: ${lane}` };
131
+ return spawnKj("hu-start", { args: ["hu", "start", id, "--code"], projectDir });
132
+ }
133
+
134
+ /** The lane a card already has (kj names it <id>-<slug>), or null. */
135
+ export function laneOfCard({ id, projectDir }) {
136
+ const lanes = join(projectDir, ".kj", "worktrees");
137
+ const found = existsSync(lanes) ? readdirSync(lanes).find((name) => name.startsWith(`${id}-`)) : null;
138
+ return found ? join(lanes, found) : null;
139
+ }
140
+
130
141
  /** Test helper: list of command names exposed. */
131
142
  export function listSupportedCommands() {
132
143
  return Object.keys(COMMAND_BUILDERS);
@@ -0,0 +1,282 @@
1
+ // KJC-TSK-0542: the board's SQLite connection, its schema and migrations. Other modules reach the
2
+ // handle through getDb(); db.js re-exports these functions, so callers keep importing from there.
3
+ // KJC-TSK-0617 (SQL-C): the handle is karajan-core's better-sqlite3-compatible adapter over
4
+ // node:sqlite, built into Node: no native addon to compile.
5
+ import { openDatabase } from 'karajan-core/db';
6
+ import { join } from 'node:path';
7
+ import { mkdirSync, renameSync } from 'node:fs';
8
+
9
+ import { getKjHome } from './db-paths.js';
10
+
11
+ /** @type {ReturnType<typeof openDatabase> | null} */
12
+ let db = null;
13
+
14
+ // Bump when the schema gains a column / index / table that older
15
+ // Karajan versions cannot understand. A DB written by a newer Karajan
16
+ // refuses to open here so it does not get silently downgraded.
17
+ const SCHEMA_VERSION = 1;
18
+
19
+ /**
20
+ * Tries to open the SQLite database; if it is corrupt, moves it aside
21
+ * and opens a fresh one. `fullScan` afterwards rebuilds the cache from
22
+ * the JSON files on disk — the DB is a cache, never the source of
23
+ * truth. Without this the board crashed at startup with a raw
24
+ * `SqliteError: database disk image is malformed` whenever the WAL
25
+ * checkpoint died in the wrong place.
26
+ *
27
+ * @param {string} dbPath
28
+ * @returns {ReturnType<typeof openDatabase>}
29
+ */
30
+ function openDbResilient(dbPath) {
31
+ let handle = null;
32
+ try {
33
+ handle = openDatabase(dbPath);
34
+ // SQLite reads the file header on the first statement, which is what
35
+ // surfaces "file is not a database". Probe `schema_version` (a built-in
36
+ // SQLite pragma) to force it.
37
+ handle.pragma("schema_version", { simple: true });
38
+ return handle;
39
+ } catch (err) {
40
+ const code = err?.code || "";
41
+ const msg = err?.message || "";
42
+ if (code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB" || /malformed|not a database/i.test(msg)) {
43
+ try { handle?.close(); } catch { /* already unusable */ }
44
+ const aside = `${dbPath}.corrupt-${Date.now()}`;
45
+ try { renameSync(dbPath, aside); } catch { /* may already be gone */ }
46
+ // eslint-disable-next-line no-console
47
+ console.warn(`[hu-board] DB at ${dbPath} was corrupt; moved aside to ${aside}. Rebuilding from disk via fullScan.`);
48
+ return openDatabase(dbPath);
49
+ }
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Initializes the SQLite database and creates tables if they don't exist.
56
+ * @returns {ReturnType<typeof openDatabase>}
57
+ */
58
+ export function initDb() {
59
+ const kjHome = getKjHome();
60
+ mkdirSync(kjHome, { recursive: true });
61
+
62
+ const dbPath = join(kjHome, 'hu-board.db');
63
+ db = openDbResilient(dbPath);
64
+
65
+ db.pragma('journal_mode = WAL');
66
+ db.pragma('foreign_keys = ON');
67
+ // Wait up to 5s for a competing writer (e.g. concurrent fullScan +
68
+ // mutation) before giving up. Without this any contention surfaced
69
+ // as SQLITE_BUSY and crashed the request mid-flight.
70
+ db.pragma('busy_timeout = 5000');
71
+
72
+ // Schema-version guard. If the DB was written by a newer Karajan with
73
+ // a different schema, refuse to open instead of silently mis-binding
74
+ // named parameters against a renamed column.
75
+ const onDisk = db.pragma('user_version', { simple: true });
76
+ if (onDisk > SCHEMA_VERSION) {
77
+ throw new Error(
78
+ `[hu-board] DB schema is from a newer Karajan (user_version=${onDisk}, this build expects ${SCHEMA_VERSION}). `
79
+ + `Upgrade karajan-code or remove ${dbPath} (it will be rebuilt from disk).`
80
+ );
81
+ }
82
+ if (onDisk < SCHEMA_VERSION) {
83
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
84
+ }
85
+
86
+ db.exec(`
87
+ CREATE TABLE IF NOT EXISTS projects (
88
+ id TEXT PRIMARY KEY,
89
+ name TEXT,
90
+ first_seen TEXT,
91
+ last_activity TEXT,
92
+ total_stories INTEGER DEFAULT 0
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS stories (
96
+ id TEXT PRIMARY KEY,
97
+ project_id TEXT,
98
+ session_id TEXT,
99
+ status TEXT DEFAULT 'pending',
100
+ title TEXT,
101
+ original_text TEXT,
102
+ certified_as TEXT,
103
+ certified_want TEXT,
104
+ certified_so_that TEXT,
105
+ quality_total INTEGER,
106
+ quality_d1 INTEGER,
107
+ quality_d2 INTEGER,
108
+ quality_d3 INTEGER,
109
+ quality_d4 INTEGER,
110
+ quality_d5 INTEGER,
111
+ quality_d6 INTEGER,
112
+ antipatterns TEXT,
113
+ ac_format TEXT,
114
+ acceptance_criteria TEXT,
115
+ created_at TEXT,
116
+ updated_at TEXT,
117
+ certified_at TEXT
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS sessions (
121
+ id TEXT PRIMARY KEY,
122
+ project_id TEXT,
123
+ task TEXT,
124
+ status TEXT,
125
+ created_at TEXT,
126
+ updated_at TEXT,
127
+ iterations INTEGER DEFAULT 0,
128
+ duration_ms INTEGER,
129
+ approved INTEGER DEFAULT 0,
130
+ commits TEXT,
131
+ stages_completed TEXT,
132
+ checkpoints TEXT,
133
+ llm_calls TEXT,
134
+ config_snapshot TEXT,
135
+ budget TEXT
136
+ );
137
+
138
+ CREATE TABLE IF NOT EXISTS context_requests (
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ story_id TEXT,
141
+ fields_needed TEXT,
142
+ question TEXT,
143
+ answer TEXT,
144
+ requested_at TEXT,
145
+ answered_at TEXT
146
+ );
147
+
148
+ -- KJC-TSK-0380 — Tombstones: "deleted, do not resurrect".
149
+ -- The board's data is reconstructed from the filesystem on every
150
+ -- fullScan(), so a plain DELETE from the table is silently undone
151
+ -- the next time chokidar fires. The tombstone table holds the ids
152
+ -- the user explicitly killed; sync* functions check it before any
153
+ -- upsert and skip + clean fs paths.
154
+ CREATE TABLE IF NOT EXISTS tombstones (
155
+ resource_type TEXT NOT NULL,
156
+ resource_id TEXT NOT NULL,
157
+ deleted_at TEXT NOT NULL,
158
+ source TEXT NOT NULL,
159
+ fs_paths TEXT,
160
+ PRIMARY KEY (resource_type, resource_id)
161
+ );
162
+
163
+ CREATE INDEX IF NOT EXISTS idx_stories_project ON stories(project_id);
164
+ CREATE INDEX IF NOT EXISTS idx_stories_status ON stories(status);
165
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
166
+ CREATE INDEX IF NOT EXISTS idx_context_story ON context_requests(story_id);
167
+ CREATE INDEX IF NOT EXISTS idx_tombstones_deleted_at ON tombstones(deleted_at);
168
+ `);
169
+
170
+ // --- Migrations ---
171
+ // Each ALTER is idempotent on restart — we swallow the duplicate-column
172
+ // error so adding one over time doesn't force a board DB nuke.
173
+ //
174
+ // plan_id: lets the mutation endpoints locate the source-of-truth plan
175
+ // JSON at ~/.karajan/plans/<slug>/<planId>.json without a scan.
176
+ // ac_count: number of acceptance criteria, surfaced on the card.
177
+ // test_count: number of acceptance_tests, surfaced on the card.
178
+ // blocked_by: denormalised JSON array of HU ids this story waits on,
179
+ // so the card can render `⏳ waits for: lao-001` inline.
180
+ try { db.exec('ALTER TABLE stories ADD COLUMN plan_id TEXT'); } catch { /* already migrated */ }
181
+ try { db.exec('ALTER TABLE stories ADD COLUMN ac_count INTEGER'); } catch { /* already migrated */ }
182
+ try { db.exec('ALTER TABLE stories ADD COLUMN test_count INTEGER'); } catch { /* already migrated */ }
183
+ try { db.exec('ALTER TABLE stories ADD COLUMN blocked_by TEXT'); } catch { /* already migrated */ }
184
+ // acceptance_tests is the raw JSON of `hu.acceptance_tests` — the
185
+ // plan stores each test as either a plain string or an object with
186
+ // { name, description, given/when/then, ... }. The modal renders
187
+ // the list; the card just shows the count (test_count above).
188
+ try { db.exec('ALTER TABLE stories ADD COLUMN acceptance_tests TEXT'); } catch { /* already migrated */ }
189
+ // plan_order is the index of the HU in `plan.hus[]` as the planner
190
+ // emitted it. The planner's output is already topological (it calls
191
+ // addHu with `blocked_by: [prevId]`), so sorting the board by this
192
+ // column puts roots first and dependents last — matching what
193
+ // `kj run --plan` will actually execute.
194
+ try { db.exec('ALTER TABLE stories ADD COLUMN plan_order INTEGER'); } catch { /* already migrated */ }
195
+ // spec_section (v2.7.5 PR C): the SPEC.md heading this HU
196
+ // implements (e.g. "5.3" or "§5 Initial Scope"). Surfaces in the
197
+ // card + modal so the user can trace HU → spec coverage.
198
+ try { db.exec('ALTER TABLE stories ADD COLUMN spec_section TEXT'); } catch { /* already migrated */ }
199
+ // outcome (PR3): JSON blob with what actually happened during
200
+ // execution — iterations, duration, commits, branch, blockers,
201
+ // human summary. Stamped by hu-sub-pipeline at the end of each
202
+ // HU and ingested by syncPlanFile so the board can show the 📄
203
+ // indicator and detail modal without parsing the plan JSON
204
+ // again. Stored as TEXT (JSON.stringify'd) for simplicity —
205
+ // parsed on read by the UI.
206
+ try { db.exec('ALTER TABLE stories ADD COLUMN outcome TEXT'); } catch { /* already migrated */ }
207
+ // KJC-TSK-0394: result (pass | fail | partial | null) ortogonal al status.
208
+ // Diferente del campo `outcome` arriba (que es blob JSON con metadata del
209
+ // run). result es el enum de "último resultado" para mostrar como badge.
210
+ try { db.exec('ALTER TABLE stories ADD COLUMN result TEXT'); } catch { /* already migrated */ }
211
+ // KJC-TSK-0406: coder_model y reviewer_model independientes por HU.
212
+ // Asignados por el triage según complexity (KJC-TSK-0405) y override-able
213
+ // desde el modal del board. coder_provider y reviewer_provider permiten
214
+ // cross-provider review (claude↔codex) sin tocar el config global.
215
+ try { db.exec('ALTER TABLE stories ADD COLUMN coder_model TEXT'); } catch { /* already migrated */ }
216
+ try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_model TEXT'); } catch { /* already migrated */ }
217
+ try { db.exec('ALTER TABLE stories ADD COLUMN coder_provider TEXT'); } catch { /* already migrated */ }
218
+ try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_provider TEXT'); } catch { /* already migrated */ }
219
+ // KJC-PRP-0002 PR6: free-form handle of the human (or dev_XXX AI) who
220
+ // owns this HU on a team-shared board. NULL = unassigned. Only surfaced
221
+ // in the UI when the parent project is `is_shared = 1`, but stored
222
+ // unconditionally so solo devs who later promote a plan don't lose
223
+ // values they typed pre-share.
224
+ try { db.exec('ALTER TABLE stories ADD COLUMN assignee TEXT'); } catch { /* already migrated */ }
225
+ // KJC-TSK-0514 (Cost C): persisted USD cost of running this HU. NULL
226
+ // until the orchestrator (Cost D) calls setStoryCost on HU close. UI
227
+ // renders "—" while NULL. Stored as REAL so the SQL aggregations in
228
+ // Cost E (/api/projects/:id/cost) can SUM directly without parsing.
229
+ try { db.exec('ALTER TABLE stories ADD COLUMN cost_usd REAL'); } catch { /* already migrated */ }
230
+ // KJC-TSK-0525 (Φ0-G): persisted cached/input tokens per HU so the
231
+ // board can render the "🎯 N% cached" badge without re-parsing the
232
+ // session summary. NULL = unmeasured (older runs, cold runs without
233
+ // cache support). INTEGER because providers report token counts as
234
+ // ints. Ratio is computed at read time as cached_tokens / tokens_in.
235
+ try { db.exec('ALTER TABLE stories ADD COLUMN cached_tokens INTEGER'); } catch { /* already migrated */ }
236
+ try { db.exec('ALTER TABLE stories ADD COLUMN tokens_in INTEGER'); } catch { /* already migrated */ }
237
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_stories_plan ON stories(plan_id)'); } catch { /* ignore */ }
238
+
239
+ // is_test (KJC-TSK-0371 — board polish #3): per-project flag that
240
+ // controls the ephemeral-cleanup heuristic. NULL = follow heuristic
241
+ // (id under tmp_/test_/demo_/kj-test- → ephemeral after 24h);
242
+ // 1 = always treat as ephemeral; 0 = NEVER auto-clean (user override).
243
+ // The UI surfaces this as a toggle on each project card.
244
+ try { db.exec('ALTER TABLE projects ADD COLUMN is_test INTEGER'); } catch { /* already migrated */ }
245
+
246
+ // is_shared (KJC-PRP-0002 PR3): 1 when at least one plan of the project came
247
+ // from `<projectDir>/.karajan-shared/plans/`. Surfaced as a 🔗 badge on the
248
+ // board so the user can tell at a glance which projects are team-tracked.
249
+ try { db.exec('ALTER TABLE projects ADD COLUMN is_shared INTEGER'); } catch { /* already migrated */ }
250
+
251
+ return db;
252
+ }
253
+
254
+ /**
255
+ * Returns the current database instance.
256
+ * @returns {ReturnType<typeof openDatabase>}
257
+ */
258
+ export function getDb() {
259
+ if (!db) throw new Error('Database not initialized. Call initDb() first.');
260
+ return db;
261
+ }
262
+
263
+ /**
264
+ * Lazily initialise the DB if it isn't open yet (KJC-BUG-0090). The cost
265
+ * writers run from `kj run`/`kj autorun` paths that may never start the board
266
+ * (which is what normally calls initDb), so cost writes failed with
267
+ * "Database not initialized". This makes those writers self-sufficient.
268
+ */
269
+ export function ensureDb() {
270
+ if (!db) initDb();
271
+ return db;
272
+ }
273
+
274
+ /**
275
+ * Closes the database connection.
276
+ */
277
+ export function closeDb() {
278
+ if (db) {
279
+ db.close();
280
+ db = null;
281
+ }
282
+ }
@@ -0,0 +1,113 @@
1
+ // KJC-TSK-0542: where the board keeps its files (karajan home, plans, run logs).
2
+ // Re-exported by db.js, so callers keep importing from there.
3
+ import { join, dirname } from 'node:path';
4
+ import { rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ // Per-process tmp dir for VITEST runs that forget to set KJ_HOME.
8
+ // Without this, a test that touches initDb() / upsertProject() drops
9
+ // rows into the developer's real `~/.karajan/hu-board.db` and the
10
+ // running board UI shows phantom projects (observed: "sess-1",
11
+ // "default", "Karajan Code" with 49 test fixture plans). Memoise so
12
+ // every consumer in the same process agrees on the path. The
13
+ // trailing `.karajan` segment keeps semantic parity with non-VITEST
14
+ // defaults so any caller assuming the path ends in `.karajan` still
15
+ // works.
16
+ let _vitestKjHome = null;
17
+ function vitestTmpKjHome() {
18
+ if (_vitestKjHome) return _vitestKjHome;
19
+ _vitestKjHome = join(
20
+ tmpdir(),
21
+ `karajan-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`,
22
+ '.karajan'
23
+ );
24
+ // KJC-BUG-0075: clean up on clean exit so /tmp doesn't accumulate one
25
+ // dir per fork × run. We rm the *parent* (the `karajan-vitest-<pid>-<rand>`
26
+ // wrapper), not the `.karajan` segment, so the whole tmp tree goes.
27
+ // Only fires on graceful exit; SIGKILL leaks are caught by the
28
+ // global-setup mtime purge at the start of the next run.
29
+ const parent = dirname(_vitestKjHome);
30
+ process.on('exit', () => {
31
+ try {
32
+ rmSync(parent, { recursive: true, force: true });
33
+ } catch {
34
+ // best-effort: never crash a test process on cleanup failure
35
+ }
36
+ });
37
+ return _vitestKjHome;
38
+ }
39
+
40
+ /**
41
+ * Returns the KJ home directory, respecting KJ_HOME env var.
42
+ * Under VITEST without KJ_HOME, returns a per-process tmp dir to
43
+ * prevent accidental writes into the developer's real `~/.karajan/`.
44
+ * @returns {string}
45
+ */
46
+ export function getKjHome() {
47
+ // KJC-TSK-0420: same precedence chain as src/utils/paths.js::resolveHome():
48
+ // KARAJAN_HOME (canonical) > KJ_HOME (legacy) > VITEST tmp > ~/.karajan.
49
+ // Kept inline rather than cross-workspace imported so the hu-board package
50
+ // does not gain a new src/ dep; the precedence behaviour is the same.
51
+ if (process.env.KARAJAN_HOME) return process.env.KARAJAN_HOME;
52
+ if (process.env.KJ_HOME) return process.env.KJ_HOME;
53
+ if (process.env.VITEST) return vitestTmpKjHome();
54
+ return join(process.env.HOME || '/root', '.karajan');
55
+ }
56
+
57
+ /**
58
+ * Canonical path to the HU Board's run-logs directory
59
+ * (`<karajan-home>/hu-board-runs/`). Centralised here so the four
60
+ * board callers (plan-mutations, command-runner, api.js GET routes
61
+ * for plan + commandId logs) and the garbage-collector all agree on
62
+ * the same string — KJC-TSK-0421.
63
+ *
64
+ * @returns {string}
65
+ */
66
+ export function getHuBoardRunsDir() {
67
+ return join(getKjHome(), 'hu-board-runs');
68
+ }
69
+
70
+ /**
71
+ * Canonical path to the plans directory the board scans
72
+ * (`<karajan-home>/plans/`). Respects `KJ_PLANS_DIR` as an absolute
73
+ * override, mirroring sync.js. KJC-BUG-0059 (v2.19.3): preflight
74
+ * endpoint used a hard-coded `~/.kj/plans/` legacy fallback that
75
+ * survived the v2.19.0 consolidation, causing the board to report
76
+ * "directorio del proyecto: no detectado" even when the plan on disk
77
+ * had a valid `projectDir`.
78
+ *
79
+ * @returns {string}
80
+ */
81
+ export function getHuBoardPlansDir() {
82
+ return process.env.KJ_PLANS_DIR || join(getKjHome(), 'plans');
83
+ }
84
+
85
+ /**
86
+ * Legacy `~/.kj/plans/` directory — only returned when KJ_PLANS_DIR
87
+ * is unset (it would be the same path anyway when overridden).
88
+ * Callers iterate canonical + legacy so users who have not yet run
89
+ * the v2.19.0 auto-migrator still see their plans. KJC-BUG-0059.
90
+ *
91
+ * @returns {string|null}
92
+ */
93
+ export function getHuBoardLegacyPlansDir() {
94
+ if (process.env.KJ_PLANS_DIR) return null;
95
+ return join(process.env.HOME || '/root', '.kj', 'plans');
96
+ }
97
+
98
+ /**
99
+ * Ordered list of every plans root the board should scan: canonical
100
+ * first (`~/.karajan/plans/`), legacy second when applicable
101
+ * (`~/.kj/plans/`). Honours `KJ_PLANS_DIR` as a single override,
102
+ * matching `sync.js`'s fullScan / startWatcher precedence so the
103
+ * preflight, plans-outcome, delete-project and delete-plan endpoints
104
+ * see the same set of files the watcher sees. KJC-BUG-0059.
105
+ *
106
+ * @returns {string[]}
107
+ */
108
+ export function getHuBoardPlansDirs() {
109
+ const dirs = [getHuBoardPlansDir()];
110
+ const legacy = getHuBoardLegacyPlansDir();
111
+ if (legacy) dirs.push(legacy);
112
+ return dirs;
113
+ }
@@ -0,0 +1,188 @@
1
+ // KJC-TSK-0542: the read side of the board DB (projects, stories, sessions, cost, dashboard),
2
+ // re-exported by db.js.
3
+ import { getDb } from './db-connection.js';
4
+
5
+ /**
6
+ * Returns all projects with aggregated stats.
7
+ * @returns {Array<object>}
8
+ */
9
+ export function getProjects() {
10
+ return getDb().prepare(`
11
+ SELECT p.*,
12
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id) AS story_count,
13
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'certified') AS certified_count,
14
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'pending') AS pending_count,
15
+ (SELECT COUNT(*) FROM sessions ss WHERE ss.project_id = p.id) AS session_count
16
+ FROM projects p
17
+ ORDER BY p.last_activity DESC
18
+ `).all();
19
+ }
20
+
21
+ /**
22
+ * Returns stories for a specific project.
23
+ * @param {string} projectId
24
+ * @returns {Array<object>}
25
+ */
26
+ /**
27
+ * Fetch all stories of a project ordered by plan_order ASC (roots first,
28
+ * dependents last — matches what `kj run --plan` will actually execute)
29
+ * and falling back to updated_at DESC for legacy rows that never had
30
+ * plan_order stamped.
31
+ */
32
+ export function getStoriesByProject(projectId) {
33
+ return getDb().prepare(`
34
+ SELECT * FROM stories WHERE project_id = ?
35
+ ORDER BY plan_order IS NULL, plan_order ASC, updated_at DESC
36
+ `).all(projectId);
37
+ }
38
+
39
+ /**
40
+ * Aggregate $ cost for a project (KJC-TSK-0516, Cost E).
41
+ * Sums stories.cost_usd, groups by plan_id ('unassigned' for NULL).
42
+ * Returns null if project does not exist; otherwise an object with
43
+ * totalUsd=0 and byPlan=[] when no stories carry cost_usd yet.
44
+ * @param {string} projectId
45
+ * @returns {{ totalUsd:number, byPlan:Array<{planId:string,totalUsd:number,huCount:number}>, unknownModelTokens:object, currency:string } | null}
46
+ */
47
+ export function getProjectCost(projectId) {
48
+ const project = getDb()
49
+ .prepare('SELECT id FROM projects WHERE id = ?')
50
+ .get(projectId);
51
+ if (!project) return null;
52
+
53
+ // Φ0-G: cached_tokens / tokens_in are nullable independently of
54
+ // cost_usd, so we read all three columns but keep cost as the
55
+ // primary filter (a row with only cache telemetry but no $ should
56
+ // not appear in the per-plan totals — cost rules the byPlan list).
57
+ const rows = getDb()
58
+ .prepare(
59
+ `SELECT plan_id, cost_usd, cached_tokens, tokens_in FROM stories
60
+ WHERE project_id = ? AND cost_usd IS NOT NULL`
61
+ )
62
+ .all(projectId);
63
+
64
+ const round4 = (n) => Math.round(n * 10000) / 10000;
65
+ const byPlanMap = new Map();
66
+ let totalUsd = 0;
67
+ let totalCached = 0;
68
+ let totalTokensIn = 0;
69
+ for (const r of rows) {
70
+ const planId = r.plan_id || 'unassigned';
71
+ const cost = Number(r.cost_usd);
72
+ if (!Number.isFinite(cost)) continue;
73
+ totalUsd += cost;
74
+ const cached = Number(r.cached_tokens);
75
+ const tin = Number(r.tokens_in);
76
+ if (Number.isFinite(cached) && cached >= 0) totalCached += cached;
77
+ if (Number.isFinite(tin) && tin >= 0) totalTokensIn += tin;
78
+ const cur = byPlanMap.get(planId) || { planId, totalUsd: 0, huCount: 0, cachedTokens: 0, tokensIn: 0 };
79
+ cur.totalUsd += cost;
80
+ cur.huCount += 1;
81
+ if (Number.isFinite(cached) && cached >= 0) cur.cachedTokens += cached;
82
+ if (Number.isFinite(tin) && tin >= 0) cur.tokensIn += tin;
83
+ byPlanMap.set(planId, cur);
84
+ }
85
+
86
+ const ratio = (cached, tin) => tin > 0 ? Math.round((cached / tin) * 1000) / 10 : null;
87
+ const byPlan = Array.from(byPlanMap.values())
88
+ .map((p) => ({
89
+ planId: p.planId,
90
+ totalUsd: round4(p.totalUsd),
91
+ huCount: p.huCount,
92
+ cachedTokens: p.cachedTokens,
93
+ tokensIn: p.tokensIn,
94
+ cachedRatioPct: ratio(p.cachedTokens, p.tokensIn),
95
+ }))
96
+ .sort((a, b) => b.totalUsd - a.totalUsd);
97
+
98
+ return {
99
+ totalUsd: round4(totalUsd),
100
+ byPlan,
101
+ cachedTokens: totalCached,
102
+ tokensIn: totalTokensIn,
103
+ cachedRatioPct: ratio(totalCached, totalTokensIn),
104
+ unknownModelTokens: { tokensIn: 0, tokensOut: 0, models: [] },
105
+ currency: 'USD',
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Returns full story detail including context requests.
111
+ * @param {string} storyId
112
+ * @returns {object | null}
113
+ */
114
+ export function getStoryDetail(storyId) {
115
+ const story = getDb().prepare('SELECT * FROM stories WHERE id = ?').get(storyId);
116
+ if (!story) return null;
117
+
118
+ const contextRequests = getDb().prepare(
119
+ 'SELECT * FROM context_requests WHERE story_id = ? ORDER BY requested_at DESC'
120
+ ).all(storyId);
121
+
122
+ return { ...story, context_requests: contextRequests };
123
+ }
124
+
125
+ /**
126
+ * Returns sessions for a specific project.
127
+ *
128
+ * The `project_name` column is joined in so the UI can show a human
129
+ * label like `tmp_kj-test-4 · trivial(doc) · 08:35` instead of just
130
+ * the cryptic `s_2026-05-07T...` id.
131
+ *
132
+ * @param {string} projectId
133
+ * @returns {Array<object>}
134
+ */
135
+ export function getSessionsByProject(projectId) {
136
+ return getDb().prepare(`
137
+ SELECT s.id, s.project_id, s.task, s.status, s.created_at, s.updated_at,
138
+ s.iterations, s.duration_ms, s.approved, s.stages_completed,
139
+ p.name AS project_name
140
+ FROM sessions s
141
+ LEFT JOIN projects p ON p.id = s.project_id
142
+ WHERE s.project_id = ?
143
+ ORDER BY s.created_at DESC
144
+ `).all(projectId);
145
+ }
146
+
147
+ /**
148
+ * Returns full session detail (with the joined project_name).
149
+ * @param {string} sessionId
150
+ * @returns {object | null}
151
+ */
152
+ export function getSessionDetail(sessionId) {
153
+ return getDb().prepare(`
154
+ SELECT s.*, p.name AS project_name
155
+ FROM sessions s
156
+ LEFT JOIN projects p ON p.id = s.project_id
157
+ WHERE s.id = ?
158
+ `).get(sessionId);
159
+ }
160
+
161
+ /**
162
+ * Returns global dashboard statistics.
163
+ * @returns {object}
164
+ */
165
+ export function getDashboardStats() {
166
+ const db = getDb();
167
+ const totalStories = db.prepare('SELECT COUNT(*) AS count FROM stories').get().count;
168
+ const certifiedStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'certified'").get().count;
169
+ const pendingStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'pending'").get().count;
170
+ const doneStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'done'").get().count;
171
+ const needsContextStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'needs_context'").get().count;
172
+ const avgQuality = db.prepare('SELECT AVG(quality_total) AS avg FROM stories WHERE quality_total IS NOT NULL').get().avg;
173
+ const totalSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions').get().count;
174
+ const approvedSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE approved = 1').get().count;
175
+ const totalProjects = db.prepare('SELECT COUNT(*) AS count FROM projects').get().count;
176
+
177
+ return {
178
+ total_stories: totalStories,
179
+ certified_stories: certifiedStories,
180
+ pending_stories: pendingStories,
181
+ done_stories: doneStories,
182
+ needs_context_stories: needsContextStories,
183
+ avg_quality: avgQuality ? Math.round(avgQuality * 10) / 10 : null,
184
+ total_sessions: totalSessions,
185
+ approved_sessions: approvedSessions,
186
+ total_projects: totalProjects,
187
+ };
188
+ }