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,376 +1,31 @@
1
- import Database from 'better-sqlite3';
2
- import { join, dirname } from 'node:path';
3
- import { mkdirSync, renameSync, rmSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
-
6
- /** @type {import('better-sqlite3').Database | null} */
7
- let db = null;
8
-
9
- // Per-process tmp dir for VITEST runs that forget to set KJ_HOME.
10
- // Without this, a test that touches initDb() / upsertProject() drops
11
- // rows into the developer's real `~/.karajan/hu-board.db` and the
12
- // running board UI shows phantom projects (observed: "sess-1",
13
- // "default", "Karajan Code" with 49 test fixture plans). Memoise so
14
- // every consumer in the same process agrees on the path. The
15
- // trailing `.karajan` segment keeps semantic parity with non-VITEST
16
- // defaults so any caller assuming the path ends in `.karajan` still
17
- // works.
18
- let _vitestKjHome = null;
19
- function vitestTmpKjHome() {
20
- if (_vitestKjHome) return _vitestKjHome;
21
- _vitestKjHome = join(
22
- tmpdir(),
23
- `karajan-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`,
24
- '.karajan'
25
- );
26
- // KJC-BUG-0075: clean up on clean exit so /tmp doesn't accumulate one
27
- // dir per fork × run. We rm the *parent* (the `karajan-vitest-<pid>-<rand>`
28
- // wrapper), not the `.karajan` segment, so the whole tmp tree goes.
29
- // Only fires on graceful exit; SIGKILL leaks are caught by the
30
- // global-setup mtime purge at the start of the next run.
31
- const parent = dirname(_vitestKjHome);
32
- process.on('exit', () => {
33
- try {
34
- rmSync(parent, { recursive: true, force: true });
35
- } catch {
36
- // best-effort: never crash a test process on cleanup failure
37
- }
38
- });
39
- return _vitestKjHome;
40
- }
41
-
42
- /**
43
- * Returns the KJ home directory, respecting KJ_HOME env var.
44
- * Under VITEST without KJ_HOME, returns a per-process tmp dir to
45
- * prevent accidental writes into the developer's real `~/.karajan/`.
46
- * @returns {string}
47
- */
48
- export function getKjHome() {
49
- // KJC-TSK-0420: same precedence chain as src/utils/paths.js::resolveHome():
50
- // KARAJAN_HOME (canonical) > KJ_HOME (legacy) > VITEST tmp > ~/.karajan.
51
- // Kept inline rather than cross-workspace imported so the hu-board package
52
- // does not gain a new src/ dep; the precedence behaviour is the same.
53
- if (process.env.KARAJAN_HOME) return process.env.KARAJAN_HOME;
54
- if (process.env.KJ_HOME) return process.env.KJ_HOME;
55
- if (process.env.VITEST) return vitestTmpKjHome();
56
- return join(process.env.HOME || '/root', '.karajan');
57
- }
58
-
59
- /**
60
- * Canonical path to the HU Board's run-logs directory
61
- * (`<karajan-home>/hu-board-runs/`). Centralised here so the four
62
- * board callers (plan-mutations, command-runner, api.js GET routes
63
- * for plan + commandId logs) and the garbage-collector all agree on
64
- * the same string — KJC-TSK-0421.
65
- *
66
- * @returns {string}
67
- */
68
- export function getHuBoardRunsDir() {
69
- return join(getKjHome(), 'hu-board-runs');
70
- }
71
-
72
- /**
73
- * Canonical path to the plans directory the board scans
74
- * (`<karajan-home>/plans/`). Respects `KJ_PLANS_DIR` as an absolute
75
- * override, mirroring sync.js. KJC-BUG-0059 (v2.19.3): preflight
76
- * endpoint used a hard-coded `~/.kj/plans/` legacy fallback that
77
- * survived the v2.19.0 consolidation, causing the board to report
78
- * "directorio del proyecto: no detectado" even when the plan on disk
79
- * had a valid `projectDir`.
80
- *
81
- * @returns {string}
82
- */
83
- export function getHuBoardPlansDir() {
84
- return process.env.KJ_PLANS_DIR || join(getKjHome(), 'plans');
85
- }
86
-
87
- /**
88
- * Legacy `~/.kj/plans/` directory — only returned when KJ_PLANS_DIR
89
- * is unset (it would be the same path anyway when overridden).
90
- * Callers iterate canonical + legacy so users who have not yet run
91
- * the v2.19.0 auto-migrator still see their plans. KJC-BUG-0059.
92
- *
93
- * @returns {string|null}
94
- */
95
- export function getHuBoardLegacyPlansDir() {
96
- if (process.env.KJ_PLANS_DIR) return null;
97
- return join(process.env.HOME || '/root', '.kj', 'plans');
98
- }
99
-
100
- /**
101
- * Ordered list of every plans root the board should scan: canonical
102
- * first (`~/.karajan/plans/`), legacy second when applicable
103
- * (`~/.kj/plans/`). Honours `KJ_PLANS_DIR` as a single override,
104
- * matching `sync.js`'s fullScan / startWatcher precedence so the
105
- * preflight, plans-outcome, delete-project and delete-plan endpoints
106
- * see the same set of files the watcher sees. KJC-BUG-0059.
107
- *
108
- * @returns {string[]}
109
- */
110
- export function getHuBoardPlansDirs() {
111
- const dirs = [getHuBoardPlansDir()];
112
- const legacy = getHuBoardLegacyPlansDir();
113
- if (legacy) dirs.push(legacy);
114
- return dirs;
115
- }
116
-
117
- // Bump when the schema gains a column / index / table that older
118
- // Karajan versions cannot understand. A DB written by a newer Karajan
119
- // refuses to open here so it does not get silently downgraded.
120
- const SCHEMA_VERSION = 1;
121
-
122
- /**
123
- * Tries to open the SQLite database; if it is corrupt, moves it aside
124
- * and opens a fresh one. `fullScan` afterwards rebuilds the cache from
125
- * the JSON files on disk — the DB is a cache, never the source of
126
- * truth. Without this the board crashed at startup with a raw
127
- * `SqliteError: database disk image is malformed` whenever the WAL
128
- * checkpoint died in the wrong place.
129
- *
130
- * @param {string} dbPath
131
- * @returns {import('better-sqlite3').Database}
132
- */
133
- function openDbResilient(dbPath) {
134
- try {
135
- const handle = new Database(dbPath);
136
- // better-sqlite3 opens lazily — the first pragma is what actually
137
- // touches the file and surfaces "file is not a database". Probe
138
- // `schema_version` (a built-in SQLite pragma) to force it.
139
- handle.pragma("schema_version", { simple: true });
140
- return handle;
141
- } catch (err) {
142
- const code = err?.code || "";
143
- const msg = err?.message || "";
144
- if (code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB" || /malformed|not a database/i.test(msg)) {
145
- const aside = `${dbPath}.corrupt-${Date.now()}`;
146
- try { renameSync(dbPath, aside); } catch { /* may already be gone */ }
147
- // eslint-disable-next-line no-console
148
- console.warn(`[hu-board] DB at ${dbPath} was corrupt; moved aside to ${aside}. Rebuilding from disk via fullScan.`);
149
- return new Database(dbPath);
150
- }
151
- throw err;
152
- }
153
- }
154
-
155
- /**
156
- * Initializes the SQLite database and creates tables if they don't exist.
157
- * @returns {import('better-sqlite3').Database}
158
- */
159
- export function initDb() {
160
- const kjHome = getKjHome();
161
- mkdirSync(kjHome, { recursive: true });
162
-
163
- const dbPath = join(kjHome, 'hu-board.db');
164
- db = openDbResilient(dbPath);
165
-
166
- db.pragma('journal_mode = WAL');
167
- db.pragma('foreign_keys = ON');
168
- // Wait up to 5s for a competing writer (e.g. concurrent fullScan +
169
- // mutation) before giving up. Without this any contention surfaced
170
- // as SQLITE_BUSY and crashed the request mid-flight.
171
- db.pragma('busy_timeout = 5000');
172
-
173
- // Schema-version guard. If the DB was written by a newer Karajan with
174
- // a different schema, refuse to open instead of silently mis-binding
175
- // named parameters against a renamed column.
176
- const onDisk = db.pragma('user_version', { simple: true });
177
- if (onDisk > SCHEMA_VERSION) {
178
- throw new Error(
179
- `[hu-board] DB schema is from a newer Karajan (user_version=${onDisk}, this build expects ${SCHEMA_VERSION}). `
180
- + `Upgrade karajan-code or remove ${dbPath} (it will be rebuilt from disk).`
181
- );
182
- }
183
- if (onDisk < SCHEMA_VERSION) {
184
- db.pragma(`user_version = ${SCHEMA_VERSION}`);
185
- }
186
-
187
- db.exec(`
188
- CREATE TABLE IF NOT EXISTS projects (
189
- id TEXT PRIMARY KEY,
190
- name TEXT,
191
- first_seen TEXT,
192
- last_activity TEXT,
193
- total_stories INTEGER DEFAULT 0
194
- );
195
-
196
- CREATE TABLE IF NOT EXISTS stories (
197
- id TEXT PRIMARY KEY,
198
- project_id TEXT,
199
- session_id TEXT,
200
- status TEXT DEFAULT 'pending',
201
- title TEXT,
202
- original_text TEXT,
203
- certified_as TEXT,
204
- certified_want TEXT,
205
- certified_so_that TEXT,
206
- quality_total INTEGER,
207
- quality_d1 INTEGER,
208
- quality_d2 INTEGER,
209
- quality_d3 INTEGER,
210
- quality_d4 INTEGER,
211
- quality_d5 INTEGER,
212
- quality_d6 INTEGER,
213
- antipatterns TEXT,
214
- ac_format TEXT,
215
- acceptance_criteria TEXT,
216
- created_at TEXT,
217
- updated_at TEXT,
218
- certified_at TEXT
219
- );
220
-
221
- CREATE TABLE IF NOT EXISTS sessions (
222
- id TEXT PRIMARY KEY,
223
- project_id TEXT,
224
- task TEXT,
225
- status TEXT,
226
- created_at TEXT,
227
- updated_at TEXT,
228
- iterations INTEGER DEFAULT 0,
229
- duration_ms INTEGER,
230
- approved INTEGER DEFAULT 0,
231
- commits TEXT,
232
- stages_completed TEXT,
233
- checkpoints TEXT,
234
- llm_calls TEXT,
235
- config_snapshot TEXT,
236
- budget TEXT
237
- );
238
-
239
- CREATE TABLE IF NOT EXISTS context_requests (
240
- id INTEGER PRIMARY KEY AUTOINCREMENT,
241
- story_id TEXT,
242
- fields_needed TEXT,
243
- question TEXT,
244
- answer TEXT,
245
- requested_at TEXT,
246
- answered_at TEXT
247
- );
248
-
249
- -- KJC-TSK-0380 — Tombstones: "deleted, do not resurrect".
250
- -- The board's data is reconstructed from the filesystem on every
251
- -- fullScan(), so a plain DELETE from the table is silently undone
252
- -- the next time chokidar fires. The tombstone table holds the ids
253
- -- the user explicitly killed; sync* functions check it before any
254
- -- upsert and skip + clean fs paths.
255
- CREATE TABLE IF NOT EXISTS tombstones (
256
- resource_type TEXT NOT NULL,
257
- resource_id TEXT NOT NULL,
258
- deleted_at TEXT NOT NULL,
259
- source TEXT NOT NULL,
260
- fs_paths TEXT,
261
- PRIMARY KEY (resource_type, resource_id)
262
- );
263
-
264
- CREATE INDEX IF NOT EXISTS idx_stories_project ON stories(project_id);
265
- CREATE INDEX IF NOT EXISTS idx_stories_status ON stories(status);
266
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
267
- CREATE INDEX IF NOT EXISTS idx_context_story ON context_requests(story_id);
268
- CREATE INDEX IF NOT EXISTS idx_tombstones_deleted_at ON tombstones(deleted_at);
269
- `);
270
-
271
- // --- Migrations ---
272
- // Each ALTER is idempotent on restart — we swallow the duplicate-column
273
- // error so adding one over time doesn't force a board DB nuke.
274
- //
275
- // plan_id: lets the mutation endpoints locate the source-of-truth plan
276
- // JSON at ~/.karajan/plans/<slug>/<planId>.json without a scan.
277
- // ac_count: number of acceptance criteria, surfaced on the card.
278
- // test_count: number of acceptance_tests, surfaced on the card.
279
- // blocked_by: denormalised JSON array of HU ids this story waits on,
280
- // so the card can render `⏳ waits for: lao-001` inline.
281
- try { db.exec('ALTER TABLE stories ADD COLUMN plan_id TEXT'); } catch { /* already migrated */ }
282
- try { db.exec('ALTER TABLE stories ADD COLUMN ac_count INTEGER'); } catch { /* already migrated */ }
283
- try { db.exec('ALTER TABLE stories ADD COLUMN test_count INTEGER'); } catch { /* already migrated */ }
284
- try { db.exec('ALTER TABLE stories ADD COLUMN blocked_by TEXT'); } catch { /* already migrated */ }
285
- // acceptance_tests is the raw JSON of `hu.acceptance_tests` — the
286
- // plan stores each test as either a plain string or an object with
287
- // { name, description, given/when/then, ... }. The modal renders
288
- // the list; the card just shows the count (test_count above).
289
- try { db.exec('ALTER TABLE stories ADD COLUMN acceptance_tests TEXT'); } catch { /* already migrated */ }
290
- // plan_order is the index of the HU in `plan.hus[]` as the planner
291
- // emitted it. The planner's output is already topological (it calls
292
- // addHu with `blocked_by: [prevId]`), so sorting the board by this
293
- // column puts roots first and dependents last — matching what
294
- // `kj run --plan` will actually execute.
295
- try { db.exec('ALTER TABLE stories ADD COLUMN plan_order INTEGER'); } catch { /* already migrated */ }
296
- // spec_section (v2.7.5 PR C): the SPEC.md heading this HU
297
- // implements (e.g. "5.3" or "§5 Initial Scope"). Surfaces in the
298
- // card + modal so the user can trace HU → spec coverage.
299
- try { db.exec('ALTER TABLE stories ADD COLUMN spec_section TEXT'); } catch { /* already migrated */ }
300
- // outcome (PR3): JSON blob with what actually happened during
301
- // execution — iterations, duration, commits, branch, blockers,
302
- // human summary. Stamped by hu-sub-pipeline at the end of each
303
- // HU and ingested by syncPlanFile so the board can show the 📄
304
- // indicator and detail modal without parsing the plan JSON
305
- // again. Stored as TEXT (JSON.stringify'd) for simplicity —
306
- // parsed on read by the UI.
307
- try { db.exec('ALTER TABLE stories ADD COLUMN outcome TEXT'); } catch { /* already migrated */ }
308
- // KJC-TSK-0394: result (pass | fail | partial | null) ortogonal al status.
309
- // Diferente del campo `outcome` arriba (que es blob JSON con metadata del
310
- // run). result es el enum de "último resultado" para mostrar como badge.
311
- try { db.exec('ALTER TABLE stories ADD COLUMN result TEXT'); } catch { /* already migrated */ }
312
- // KJC-TSK-0406: coder_model y reviewer_model independientes por HU.
313
- // Asignados por el triage según complexity (KJC-TSK-0405) y override-able
314
- // desde el modal del board. coder_provider y reviewer_provider permiten
315
- // cross-provider review (claude↔codex) sin tocar el config global.
316
- try { db.exec('ALTER TABLE stories ADD COLUMN coder_model TEXT'); } catch { /* already migrated */ }
317
- try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_model TEXT'); } catch { /* already migrated */ }
318
- try { db.exec('ALTER TABLE stories ADD COLUMN coder_provider TEXT'); } catch { /* already migrated */ }
319
- try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_provider TEXT'); } catch { /* already migrated */ }
320
- // KJC-PRP-0002 PR6: free-form handle of the human (or dev_XXX AI) who
321
- // owns this HU on a team-shared board. NULL = unassigned. Only surfaced
322
- // in the UI when the parent project is `is_shared = 1`, but stored
323
- // unconditionally so solo devs who later promote a plan don't lose
324
- // values they typed pre-share.
325
- try { db.exec('ALTER TABLE stories ADD COLUMN assignee TEXT'); } catch { /* already migrated */ }
326
- // KJC-TSK-0514 (Cost C): persisted USD cost of running this HU. NULL
327
- // until the orchestrator (Cost D) calls setStoryCost on HU close. UI
328
- // renders "—" while NULL. Stored as REAL so the SQL aggregations in
329
- // Cost E (/api/projects/:id/cost) can SUM directly without parsing.
330
- try { db.exec('ALTER TABLE stories ADD COLUMN cost_usd REAL'); } catch { /* already migrated */ }
331
- // KJC-TSK-0525 (Φ0-G): persisted cached/input tokens per HU so the
332
- // board can render the "🎯 N% cached" badge without re-parsing the
333
- // session summary. NULL = unmeasured (older runs, cold runs without
334
- // cache support). INTEGER because providers report token counts as
335
- // ints. Ratio is computed at read time as cached_tokens / tokens_in.
336
- try { db.exec('ALTER TABLE stories ADD COLUMN cached_tokens INTEGER'); } catch { /* already migrated */ }
337
- try { db.exec('ALTER TABLE stories ADD COLUMN tokens_in INTEGER'); } catch { /* already migrated */ }
338
- try { db.exec('CREATE INDEX IF NOT EXISTS idx_stories_plan ON stories(plan_id)'); } catch { /* ignore */ }
339
-
340
- // is_test (KJC-TSK-0371 — board polish #3): per-project flag that
341
- // controls the ephemeral-cleanup heuristic. NULL = follow heuristic
342
- // (id under tmp_/test_/demo_/kj-test- → ephemeral after 24h);
343
- // 1 = always treat as ephemeral; 0 = NEVER auto-clean (user override).
344
- // The UI surfaces this as a toggle on each project card.
345
- try { db.exec('ALTER TABLE projects ADD COLUMN is_test INTEGER'); } catch { /* already migrated */ }
346
-
347
- // is_shared (KJC-PRP-0002 PR3): 1 when at least one plan of the project came
348
- // from `<projectDir>/.karajan-shared/plans/`. Surfaced as a 🔗 badge on the
349
- // board so the user can tell at a glance which projects are team-tracked.
350
- try { db.exec('ALTER TABLE projects ADD COLUMN is_shared INTEGER'); } catch { /* already migrated */ }
351
-
352
- return db;
353
- }
354
-
355
- /**
356
- * Returns the current database instance.
357
- * @returns {import('better-sqlite3').Database}
358
- */
359
- export function getDb() {
360
- if (!db) throw new Error('Database not initialized. Call initDb() first.');
361
- return db;
362
- }
363
-
364
- /**
365
- * Lazily initialise the DB if it isn't open yet (KJC-BUG-0090). The cost
366
- * writers run from `kj run`/`kj autorun` paths that may never start the board
367
- * (which is what normally calls initDb), so cost writes failed with
368
- * "Database not initialized". This makes those writers self-sufficient.
369
- */
370
- export function ensureDb() {
371
- if (!db) initDb();
372
- return db;
373
- }
1
+ import { getDb } from './db-connection.js';
2
+
3
+ // KJC-TSK-0542: db.js is the facade every caller imports. The paths and the connection (with
4
+ // the schema) live in their own modules and are re-exported from here.
5
+ export {
6
+ getKjHome,
7
+ getHuBoardRunsDir,
8
+ getHuBoardPlansDir,
9
+ getHuBoardLegacyPlansDir,
10
+ getHuBoardPlansDirs,
11
+ } from './db-paths.js';
12
+ export { initDb, getDb, ensureDb, closeDb } from './db-connection.js';
13
+ export {
14
+ addTombstone,
15
+ isTombstoned,
16
+ getTombstone,
17
+ removeTombstone,
18
+ listTombstones,
19
+ } from './db-tombstones.js';
20
+ export {
21
+ getProjects,
22
+ getStoriesByProject,
23
+ getProjectCost,
24
+ getStoryDetail,
25
+ getSessionsByProject,
26
+ getSessionDetail,
27
+ getDashboardStats,
28
+ } from './db-queries.js';
374
29
 
375
30
  /**
376
31
  * Inserts or updates a project record.
@@ -655,191 +310,6 @@ export function upsertSession(session) {
655
310
  });
656
311
  }
657
312
 
658
- /**
659
- * Returns all projects with aggregated stats.
660
- * @returns {Array<object>}
661
- */
662
- export function getProjects() {
663
- return getDb().prepare(`
664
- SELECT p.*,
665
- (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id) AS story_count,
666
- (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'certified') AS certified_count,
667
- (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'pending') AS pending_count,
668
- (SELECT COUNT(*) FROM sessions ss WHERE ss.project_id = p.id) AS session_count
669
- FROM projects p
670
- ORDER BY p.last_activity DESC
671
- `).all();
672
- }
673
-
674
- /**
675
- * Returns stories for a specific project.
676
- * @param {string} projectId
677
- * @returns {Array<object>}
678
- */
679
- /**
680
- * Fetch all stories of a project ordered by plan_order ASC (roots first,
681
- * dependents last — matches what `kj run --plan` will actually execute)
682
- * and falling back to updated_at DESC for legacy rows that never had
683
- * plan_order stamped.
684
- */
685
- export function getStoriesByProject(projectId) {
686
- return getDb().prepare(`
687
- SELECT * FROM stories WHERE project_id = ?
688
- ORDER BY plan_order IS NULL, plan_order ASC, updated_at DESC
689
- `).all(projectId);
690
- }
691
-
692
- /**
693
- * Aggregate $ cost for a project (KJC-TSK-0516, Cost E).
694
- * Sums stories.cost_usd, groups by plan_id ('unassigned' for NULL).
695
- * Returns null if project does not exist; otherwise an object with
696
- * totalUsd=0 and byPlan=[] when no stories carry cost_usd yet.
697
- * @param {string} projectId
698
- * @returns {{ totalUsd:number, byPlan:Array<{planId:string,totalUsd:number,huCount:number}>, unknownModelTokens:object, currency:string } | null}
699
- */
700
- export function getProjectCost(projectId) {
701
- const project = getDb()
702
- .prepare('SELECT id FROM projects WHERE id = ?')
703
- .get(projectId);
704
- if (!project) return null;
705
-
706
- // Φ0-G: cached_tokens / tokens_in are nullable independently of
707
- // cost_usd, so we read all three columns but keep cost as the
708
- // primary filter (a row with only cache telemetry but no $ should
709
- // not appear in the per-plan totals — cost rules the byPlan list).
710
- const rows = getDb()
711
- .prepare(
712
- `SELECT plan_id, cost_usd, cached_tokens, tokens_in FROM stories
713
- WHERE project_id = ? AND cost_usd IS NOT NULL`
714
- )
715
- .all(projectId);
716
-
717
- const round4 = (n) => Math.round(n * 10000) / 10000;
718
- const byPlanMap = new Map();
719
- let totalUsd = 0;
720
- let totalCached = 0;
721
- let totalTokensIn = 0;
722
- for (const r of rows) {
723
- const planId = r.plan_id || 'unassigned';
724
- const cost = Number(r.cost_usd);
725
- if (!Number.isFinite(cost)) continue;
726
- totalUsd += cost;
727
- const cached = Number(r.cached_tokens);
728
- const tin = Number(r.tokens_in);
729
- if (Number.isFinite(cached) && cached >= 0) totalCached += cached;
730
- if (Number.isFinite(tin) && tin >= 0) totalTokensIn += tin;
731
- const cur = byPlanMap.get(planId) || { planId, totalUsd: 0, huCount: 0, cachedTokens: 0, tokensIn: 0 };
732
- cur.totalUsd += cost;
733
- cur.huCount += 1;
734
- if (Number.isFinite(cached) && cached >= 0) cur.cachedTokens += cached;
735
- if (Number.isFinite(tin) && tin >= 0) cur.tokensIn += tin;
736
- byPlanMap.set(planId, cur);
737
- }
738
-
739
- const ratio = (cached, tin) => tin > 0 ? Math.round((cached / tin) * 1000) / 10 : null;
740
- const byPlan = Array.from(byPlanMap.values())
741
- .map((p) => ({
742
- planId: p.planId,
743
- totalUsd: round4(p.totalUsd),
744
- huCount: p.huCount,
745
- cachedTokens: p.cachedTokens,
746
- tokensIn: p.tokensIn,
747
- cachedRatioPct: ratio(p.cachedTokens, p.tokensIn),
748
- }))
749
- .sort((a, b) => b.totalUsd - a.totalUsd);
750
-
751
- return {
752
- totalUsd: round4(totalUsd),
753
- byPlan,
754
- cachedTokens: totalCached,
755
- tokensIn: totalTokensIn,
756
- cachedRatioPct: ratio(totalCached, totalTokensIn),
757
- unknownModelTokens: { tokensIn: 0, tokensOut: 0, models: [] },
758
- currency: 'USD',
759
- };
760
- }
761
-
762
- /**
763
- * Returns full story detail including context requests.
764
- * @param {string} storyId
765
- * @returns {object | null}
766
- */
767
- export function getStoryDetail(storyId) {
768
- const story = getDb().prepare('SELECT * FROM stories WHERE id = ?').get(storyId);
769
- if (!story) return null;
770
-
771
- const contextRequests = getDb().prepare(
772
- 'SELECT * FROM context_requests WHERE story_id = ? ORDER BY requested_at DESC'
773
- ).all(storyId);
774
-
775
- return { ...story, context_requests: contextRequests };
776
- }
777
-
778
- /**
779
- * Returns sessions for a specific project.
780
- *
781
- * The `project_name` column is joined in so the UI can show a human
782
- * label like `tmp_kj-test-4 · trivial(doc) · 08:35` instead of just
783
- * the cryptic `s_2026-05-07T...` id.
784
- *
785
- * @param {string} projectId
786
- * @returns {Array<object>}
787
- */
788
- export function getSessionsByProject(projectId) {
789
- return getDb().prepare(`
790
- SELECT s.id, s.project_id, s.task, s.status, s.created_at, s.updated_at,
791
- s.iterations, s.duration_ms, s.approved, s.stages_completed,
792
- p.name AS project_name
793
- FROM sessions s
794
- LEFT JOIN projects p ON p.id = s.project_id
795
- WHERE s.project_id = ?
796
- ORDER BY s.created_at DESC
797
- `).all(projectId);
798
- }
799
-
800
- /**
801
- * Returns full session detail (with the joined project_name).
802
- * @param {string} sessionId
803
- * @returns {object | null}
804
- */
805
- export function getSessionDetail(sessionId) {
806
- return getDb().prepare(`
807
- SELECT s.*, p.name AS project_name
808
- FROM sessions s
809
- LEFT JOIN projects p ON p.id = s.project_id
810
- WHERE s.id = ?
811
- `).get(sessionId);
812
- }
813
-
814
- /**
815
- * Returns global dashboard statistics.
816
- * @returns {object}
817
- */
818
- export function getDashboardStats() {
819
- const db = getDb();
820
- const totalStories = db.prepare('SELECT COUNT(*) AS count FROM stories').get().count;
821
- const certifiedStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'certified'").get().count;
822
- const pendingStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'pending'").get().count;
823
- const doneStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'done'").get().count;
824
- const needsContextStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'needs_context'").get().count;
825
- const avgQuality = db.prepare('SELECT AVG(quality_total) AS avg FROM stories WHERE quality_total IS NOT NULL').get().avg;
826
- const totalSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions').get().count;
827
- const approvedSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE approved = 1').get().count;
828
- const totalProjects = db.prepare('SELECT COUNT(*) AS count FROM projects').get().count;
829
-
830
- return {
831
- total_stories: totalStories,
832
- certified_stories: certifiedStories,
833
- pending_stories: pendingStories,
834
- done_stories: doneStories,
835
- needs_context_stories: needsContextStories,
836
- avg_quality: avgQuality ? Math.round(avgQuality * 10) / 10 : null,
837
- total_sessions: totalSessions,
838
- approved_sessions: approvedSessions,
839
- total_projects: totalProjects,
840
- };
841
- }
842
-
843
313
  /**
844
314
  * Inserts a context request for a story.
845
315
  * @param {object} req
@@ -913,64 +383,3 @@ export function deleteSession(sessionId) {
913
383
  const res = db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
914
384
  return res.changes > 0;
915
385
  }
916
-
917
- // --- Tombstones (KJC-TSK-0380) ---------------------------------------
918
- // "This (resource_type, resource_id) is dead — don't bring it back." The
919
- // sync layer consults this table BEFORE every upsert and skips records
920
- // whose id is buried. Restoration is explicit via removeTombstone().
921
-
922
- export function addTombstone(resourceType, resourceId, opts = {}) {
923
- getDb().prepare(`
924
- INSERT OR REPLACE INTO tombstones (resource_type, resource_id, deleted_at, source, fs_paths)
925
- VALUES (?, ?, ?, ?, ?)
926
- `).run(
927
- resourceType, resourceId,
928
- new Date().toISOString(),
929
- opts.source || 'api',
930
- opts.fsPaths ? JSON.stringify(opts.fsPaths) : null,
931
- );
932
- }
933
-
934
- export function isTombstoned(resourceType, resourceId) {
935
- return !!getDb().prepare(
936
- 'SELECT 1 FROM tombstones WHERE resource_type = ? AND resource_id = ? LIMIT 1'
937
- ).get(resourceType, resourceId);
938
- }
939
-
940
- /**
941
- * Read a single tombstone row (or null). Needed so callers can compare
942
- * `deleted_at` against a resource's own timestamp before deciding whether
943
- * to revive it (KJC-BUG-0055).
944
- */
945
- export function getTombstone(resourceType, resourceId) {
946
- const row = getDb().prepare(
947
- 'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones WHERE resource_type = ? AND resource_id = ?'
948
- ).get(resourceType, resourceId);
949
- if (!row) return null;
950
- return { ...row, fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [] };
951
- }
952
-
953
- export function removeTombstone(resourceType, resourceId) {
954
- return getDb().prepare(
955
- 'DELETE FROM tombstones WHERE resource_type = ? AND resource_id = ?'
956
- ).run(resourceType, resourceId).changes > 0;
957
- }
958
-
959
- export function listTombstones() {
960
- return getDb().prepare(
961
- 'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones ORDER BY deleted_at DESC'
962
- ).all().map((row) => ({
963
- ...row,
964
- fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [],
965
- }));
966
- }
967
-
968
- /**
969
- * Closes the database connection.
970
- */
971
- export function closeDb() {
972
- if (db) {
973
- db.close();
974
- db = null;
975
- }
976
- }
@@ -27,7 +27,7 @@
27
27
  *
28
28
  * What "cleanup" means: DELETE the project row + cascade-delete its
29
29
  * stories and sessions, ADD a project tombstone, and rm-rf the
30
- * on-disk dirs (`hu-stories/<sid>/`, `sessions/<sid>/`,
30
+ * on-disk dirs (`sessions/<sid>/`,
31
31
  * `~/.kj/plans/<projectId>/`). Without the tombstone + fs cleanup,
32
32
  * the very next chokidar `add` event or `fullScan` would re-import
33
33
  * those files as a brand-new project (KJC-BUG-0055).
@@ -123,7 +123,7 @@ export function findEphemeralProjects(projects, opts = {}) {
123
123
  * has no effect on the second pass.
124
124
  *
125
125
  * @param {object} deps
126
- * @param {object} deps.db better-sqlite3 Database handle
126
+ * @param {object} deps.db SQLite handle (karajan-core/db adapter)
127
127
  * @param {object} [deps.opts] threshold overrides
128
128
  * @param {Function} [deps.now] () => number, for tests
129
129
  * @returns {Array<{ id: string, reason: string, stories_deleted: number, sessions_deleted: number }>}
@@ -165,7 +165,6 @@ export function cleanupEphemeralProjects({
165
165
  const sessionIds = selSessions.all(project.id).map((r) => r.id);
166
166
  const fsPaths = home
167
167
  ? [
168
- ...storyIds.map((sid) => path.join(home, "hu-stories", sid)),
169
168
  ...sessionIds.map((sid) => path.join(home, "sessions", sid)),
170
169
  path.join(plansRoot, project.id),
171
170
  ]