@karajan-family/code 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (746) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +98 -0
  3. package/bin/karajan-mcp.js +2 -0
  4. package/bin/kj-rag-mcp.js +75 -0
  5. package/bin/kj-tail +361 -0
  6. package/bin/kj.js +2 -0
  7. package/docs/README.es.md +80 -0
  8. package/docs/karajan-code-logo-small.png +0 -0
  9. package/library/logical-clocks-lamport.md +40 -0
  10. package/package.json +140 -0
  11. package/packages/ai-trash/README.md +37 -0
  12. package/packages/ai-trash/bin/kj-trash.js +10 -0
  13. package/packages/ai-trash/package.json +43 -0
  14. package/packages/ai-trash/src/cli.js +200 -0
  15. package/packages/ai-trash/src/destructive-parser.js +74 -0
  16. package/packages/ai-trash/src/git-snapshot.js +128 -0
  17. package/packages/ai-trash/src/hook.js +137 -0
  18. package/packages/ai-trash/src/index.js +14 -0
  19. package/packages/ai-trash/src/install.js +73 -0
  20. package/packages/ai-trash/src/logger.js +60 -0
  21. package/packages/ai-trash/src/manifest.js +91 -0
  22. package/packages/ai-trash/src/permissions.js +26 -0
  23. package/packages/ai-trash/src/snapshot.js +66 -0
  24. package/packages/ai-trash/src/ulid.js +66 -0
  25. package/packages/hu-board/README.md +69 -0
  26. package/packages/hu-board/package.json +28 -0
  27. package/packages/hu-board/public/app.js +259 -0
  28. package/packages/hu-board/public/index.html +77 -0
  29. package/packages/hu-board/public/pipeline.css +132 -0
  30. package/packages/hu-board/public/pipeline.html +48 -0
  31. package/packages/hu-board/public/pipeline.js +200 -0
  32. package/packages/hu-board/public/rag.html +21 -0
  33. package/packages/hu-board/public/rag.js +39 -0
  34. package/packages/hu-board/public/styles.css +973 -0
  35. package/packages/hu-board/public/utils/api.js +139 -0
  36. package/packages/hu-board/public/utils/board-view.js +473 -0
  37. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  38. package/packages/hu-board/public/utils/config-editor.js +181 -0
  39. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  40. package/packages/hu-board/public/utils/formatters.js +300 -0
  41. package/packages/hu-board/public/utils/graph-view.js +216 -0
  42. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  43. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  44. package/packages/hu-board/public/utils/log-panel.js +222 -0
  45. package/packages/hu-board/public/utils/modals.js +309 -0
  46. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  47. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  48. package/packages/hu-board/public/utils/project-actions.js +143 -0
  49. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  50. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  51. package/packages/hu-board/public/utils/server-push.js +252 -0
  52. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  53. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  54. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  55. package/packages/hu-board/public/wiki.html +32 -0
  56. package/packages/hu-board/public/wiki.js +53 -0
  57. package/packages/hu-board/src/auth.js +92 -0
  58. package/packages/hu-board/src/boot-info.js +16 -0
  59. package/packages/hu-board/src/cleanup-zombies.js +145 -0
  60. package/packages/hu-board/src/command-runner.js +133 -0
  61. package/packages/hu-board/src/config-yaml.js +531 -0
  62. package/packages/hu-board/src/db.js +963 -0
  63. package/packages/hu-board/src/ephemeral-cleaner.js +196 -0
  64. package/packages/hu-board/src/event-bus.js +58 -0
  65. package/packages/hu-board/src/format.js +207 -0
  66. package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
  67. package/packages/hu-board/src/journal-parser.js +180 -0
  68. package/packages/hu-board/src/plan-mutations.js +424 -0
  69. package/packages/hu-board/src/preflight.js +286 -0
  70. package/packages/hu-board/src/routes/api.js +1374 -0
  71. package/packages/hu-board/src/routes/pipeline.js +45 -0
  72. package/packages/hu-board/src/routes/rag.js +42 -0
  73. package/packages/hu-board/src/routes/wiki.js +43 -0
  74. package/packages/hu-board/src/run-tracker.js +121 -0
  75. package/packages/hu-board/src/server.js +438 -0
  76. package/packages/hu-board/src/sync.js +732 -0
  77. package/packages/hu-board/src/token-store.js +56 -0
  78. package/packages/hu-board/src/zombie-reaper.js +206 -0
  79. package/scripts/ai-attribution-guard.yml +84 -0
  80. package/scripts/audit-test-diet.mjs +151 -0
  81. package/scripts/build-sea.mjs +150 -0
  82. package/scripts/diet-baseline.mjs +170 -0
  83. package/scripts/dual-publish.mjs +67 -0
  84. package/scripts/esbuild-sea.config.mjs +273 -0
  85. package/scripts/git-hooks/post-merge +18 -0
  86. package/scripts/install-binary.ps1 +174 -0
  87. package/scripts/install-binary.sh +159 -0
  88. package/scripts/install-guard-all-repos.sh +85 -0
  89. package/scripts/install-kj.sh +193 -0
  90. package/scripts/install.js +901 -0
  91. package/scripts/install.sh +7 -0
  92. package/scripts/postinstall.js +117 -0
  93. package/scripts/protect-all-repos.sh +88 -0
  94. package/scripts/quickstart-gate.mjs +97 -0
  95. package/scripts/regen-arch-stats.sh +71 -0
  96. package/scripts/setup-multi-instance.sh +150 -0
  97. package/scripts/verify-pack.mjs +278 -0
  98. package/src/activity-log.js +59 -0
  99. package/src/agents/agy-agent.js +62 -0
  100. package/src/agents/aider-agent.js +78 -0
  101. package/src/agents/availability.js +52 -0
  102. package/src/agents/base-agent.js +134 -0
  103. package/src/agents/claude-agent.js +413 -0
  104. package/src/agents/codex-agent.js +112 -0
  105. package/src/agents/copilot-agent.js +65 -0
  106. package/src/agents/gemini-agent.js +85 -0
  107. package/src/agents/host-agent.js +38 -0
  108. package/src/agents/index.js +59 -0
  109. package/src/agents/kimi-agent.js +45 -0
  110. package/src/agents/model-registry.js +137 -0
  111. package/src/agents/opencode-agent.js +72 -0
  112. package/src/agents/qwen-agent.js +20 -0
  113. package/src/agents/resolve-bin.js +73 -0
  114. package/src/audit/agent-readiness.js +259 -0
  115. package/src/audit/ai-slop-findings.js +102 -0
  116. package/src/audit/audit-fallback.js +66 -0
  117. package/src/audit/audit-history-display.js +61 -0
  118. package/src/audit/audit-history.js +102 -0
  119. package/src/audit/basal-cost.js +281 -0
  120. package/src/audit/circular-deps.js +144 -0
  121. package/src/audit/dead-exports.js +186 -0
  122. package/src/audit/deterministic-summary.js +327 -0
  123. package/src/audit/harness-scorecard.js +100 -0
  124. package/src/audit/harness-section.js +81 -0
  125. package/src/audit/infra-findings.js +100 -0
  126. package/src/audit/injection-findings.js +114 -0
  127. package/src/audit/issue-filter.js +133 -0
  128. package/src/audit/osv-findings.js +167 -0
  129. package/src/audit/plan-adherence.js +224 -0
  130. package/src/audit/semgrep-findings.js +129 -0
  131. package/src/audit/sonar-findings.js +93 -0
  132. package/src/audit/webperf-input.js +133 -0
  133. package/src/autonomy/arbiter-role.js +71 -0
  134. package/src/autonomy/arbiter.js +22 -0
  135. package/src/autonomy/decider.js +18 -0
  136. package/src/autonomy/decision-resolver.js +58 -0
  137. package/src/autonomy/outcome-report.js +55 -0
  138. package/src/autonomy/policy.js +41 -0
  139. package/src/autonomy/stage-ask.js +30 -0
  140. package/src/bootstrap.js +276 -0
  141. package/src/brain/agent-error-classifier.js +116 -0
  142. package/src/brain/decision-tracker.js +125 -0
  143. package/src/brain/decisor.js +243 -0
  144. package/src/brain/solomon-consult.js +171 -0
  145. package/src/brain/standby-scheduler.js +8 -0
  146. package/src/brain/standby-store.js +12 -0
  147. package/src/brain/with-brain-recovery.js +235 -0
  148. package/src/budget/comparison.js +96 -0
  149. package/src/canvas/parse-md.js +238 -0
  150. package/src/canvas/schema.js +101 -0
  151. package/src/canvas/to-flat.js +85 -0
  152. package/src/canvas/validate.js +163 -0
  153. package/src/checks/ai-surface.js +79 -0
  154. package/src/checks/ai-trash.js +34 -0
  155. package/src/checks/binaries.js +207 -0
  156. package/src/checks/ci.js +105 -0
  157. package/src/checks/config-files.js +217 -0
  158. package/src/checks/dir-setup.js +103 -0
  159. package/src/checks/guarantee-level.js +38 -0
  160. package/src/checks/hardware.js +150 -0
  161. package/src/checks/harness-scorecard.js +41 -0
  162. package/src/checks/mcp-health.js +169 -0
  163. package/src/checks/method.js +73 -0
  164. package/src/checks/native-build.js +67 -0
  165. package/src/checks/node.js +70 -0
  166. package/src/checks/ollama.js +41 -0
  167. package/src/checks/orphans.js +77 -0
  168. package/src/checks/ports.js +188 -0
  169. package/src/checks/project-checks.js +245 -0
  170. package/src/checks/qmd.js +41 -0
  171. package/src/checks/rag-hooks.js +54 -0
  172. package/src/checks/release-check.js +102 -0
  173. package/src/checks/rtk.js +36 -0
  174. package/src/checks/runner.js +326 -0
  175. package/src/checks/skills.js +53 -0
  176. package/src/checks/sonar.js +67 -0
  177. package/src/checks/squeezr.js +36 -0
  178. package/src/checks/system.js +53 -0
  179. package/src/checks/tokens.js +172 -0
  180. package/src/checks/types.js +103 -0
  181. package/src/ci/dispatch.js +99 -0
  182. package/src/ci/index.js +3 -0
  183. package/src/ci/pr-diff.js +26 -0
  184. package/src/ci/repo.js +45 -0
  185. package/src/cli/_shared.js +30 -0
  186. package/src/cli/advanced-commands.js +61 -0
  187. package/src/cli/register-meta.js +772 -0
  188. package/src/cli/register-pipeline.js +335 -0
  189. package/src/cli/register-plan.js +224 -0
  190. package/src/cli/register-roles-skills.js +54 -0
  191. package/src/cli/register-sonar.js +28 -0
  192. package/src/cli/register-standby.js +26 -0
  193. package/src/cli.js +155 -0
  194. package/src/commands/advanced.js +31 -0
  195. package/src/commands/agent-run.js +63 -0
  196. package/src/commands/agents.js +105 -0
  197. package/src/commands/architect.js +123 -0
  198. package/src/commands/audit.js +430 -0
  199. package/src/commands/autorun.js +49 -0
  200. package/src/commands/board.js +458 -0
  201. package/src/commands/check.js +40 -0
  202. package/src/commands/clean.js +145 -0
  203. package/src/commands/code.js +45 -0
  204. package/src/commands/config.js +74 -0
  205. package/src/commands/discover.js +105 -0
  206. package/src/commands/doctor.js +251 -0
  207. package/src/commands/env.js +207 -0
  208. package/src/commands/harden.js +203 -0
  209. package/src/commands/hu.js +210 -0
  210. package/src/commands/identity.js +60 -0
  211. package/src/commands/init.js +950 -0
  212. package/src/commands/install-tools.js +432 -0
  213. package/src/commands/mutate.js +94 -0
  214. package/src/commands/ollama.js +44 -0
  215. package/src/commands/onboard.js +45 -0
  216. package/src/commands/plan/_shared.js +55 -0
  217. package/src/commands/plan/add-hu.js +23 -0
  218. package/src/commands/plan/delete.js +9 -0
  219. package/src/commands/plan/fix.js +99 -0
  220. package/src/commands/plan/generate.js +532 -0
  221. package/src/commands/plan/index.js +13 -0
  222. package/src/commands/plan/list.js +21 -0
  223. package/src/commands/plan/migrate-result.js +41 -0
  224. package/src/commands/plan/ready.js +46 -0
  225. package/src/commands/plan/remove-hu.js +17 -0
  226. package/src/commands/plan/share.js +85 -0
  227. package/src/commands/plan/show.js +81 -0
  228. package/src/commands/plan/unshare.js +41 -0
  229. package/src/commands/plan/validate.js +20 -0
  230. package/src/commands/plan.js +17 -0
  231. package/src/commands/policy.js +164 -0
  232. package/src/commands/privacy.js +45 -0
  233. package/src/commands/qmd.js +86 -0
  234. package/src/commands/rag-mcp.js +18 -0
  235. package/src/commands/rag.js +173 -0
  236. package/src/commands/report-issue.js +77 -0
  237. package/src/commands/report.js +469 -0
  238. package/src/commands/researcher.js +54 -0
  239. package/src/commands/resume.js +68 -0
  240. package/src/commands/review-gate.js +355 -0
  241. package/src/commands/review.js +89 -0
  242. package/src/commands/roles.js +116 -0
  243. package/src/commands/run.js +185 -0
  244. package/src/commands/scan.js +18 -0
  245. package/src/commands/skills.js +86 -0
  246. package/src/commands/sonar.js +53 -0
  247. package/src/commands/standby.js +117 -0
  248. package/src/commands/start.js +121 -0
  249. package/src/commands/status.js +36 -0
  250. package/src/commands/sync.js +103 -0
  251. package/src/commands/telemetry.js +70 -0
  252. package/src/commands/tournament.js +85 -0
  253. package/src/commands/triage.js +78 -0
  254. package/src/commands/undo.js +62 -0
  255. package/src/commands/watch.js +46 -0
  256. package/src/commands/webperf.js +162 -0
  257. package/src/commands/worktree.js +76 -0
  258. package/src/config/defaults.js +270 -0
  259. package/src/config/loader.js +204 -0
  260. package/src/config/overrides.js +220 -0
  261. package/src/config/role-resolver.js +171 -0
  262. package/src/config/schema.js +299 -0
  263. package/src/config/test-harness.js +78 -0
  264. package/src/config.js +34 -0
  265. package/src/domains/domain-loader.js +245 -0
  266. package/src/domains/domain-registry.js +140 -0
  267. package/src/domains/domain-synthesizer.js +129 -0
  268. package/src/environment/adr.js +45 -0
  269. package/src/environment/board-access.js +97 -0
  270. package/src/environment/board-select.js +59 -0
  271. package/src/environment/briefs.js +129 -0
  272. package/src/environment/issue-report.js +72 -0
  273. package/src/environment/mcp-wiring.js +44 -0
  274. package/src/environment/playbook.js +127 -0
  275. package/src/git/automation.js +334 -0
  276. package/src/git/hu-automation.js +171 -0
  277. package/src/git/hu-snapshot.js +10 -0
  278. package/src/golden/asserter.js +92 -0
  279. package/src/golden/loader.js +52 -0
  280. package/src/golden/runner.js +92 -0
  281. package/src/golden/schema.js +41 -0
  282. package/src/guards/intent-guard.js +161 -0
  283. package/src/guards/output-guard.js +149 -0
  284. package/src/guards/perf-guard.js +126 -0
  285. package/src/guards/policy-guard.js +90 -0
  286. package/src/guards/policy-resolver.js +85 -0
  287. package/src/guards/secret-redactor.js +71 -0
  288. package/src/harden/advisory.js +203 -0
  289. package/src/harden/alternatives.js +50 -0
  290. package/src/harden/check.js +81 -0
  291. package/src/harden/config-engine.js +128 -0
  292. package/src/harden/config-templates.js +122 -0
  293. package/src/harden/guidelines-engine.js +48 -0
  294. package/src/harden/guidelines-templates.js +36 -0
  295. package/src/harden/harden-engine.js +80 -0
  296. package/src/harden/harness-hooks.js +107 -0
  297. package/src/harden/hook-commands.js +25 -0
  298. package/src/harden/hook-templates.js +183 -0
  299. package/src/harden/interactive.js +76 -0
  300. package/src/harden/sentinel-hooks.js +721 -0
  301. package/src/harden/stack-roots.js +105 -0
  302. package/src/harden/test-on-push.js +38 -0
  303. package/src/harden/workflow-engine.js +116 -0
  304. package/src/harden/workflow-templates.js +285 -0
  305. package/src/hu/acceptance-runner.js +138 -0
  306. package/src/hu/auto-generator.js +358 -0
  307. package/src/hu/graph.js +71 -0
  308. package/src/hu/lazy-planner.js +109 -0
  309. package/src/hu/model-router.js +108 -0
  310. package/src/hu/parallel-executor.js +94 -0
  311. package/src/hu/split-validator.js +143 -0
  312. package/src/hu/splitting-detector.js +158 -0
  313. package/src/hu/splitting-generator.js +175 -0
  314. package/src/hu/store.js +392 -0
  315. package/src/hu/worktree-bootstrap.js +59 -0
  316. package/src/identity/bootstrap.js +48 -0
  317. package/src/identity/compare.js +38 -0
  318. package/src/identity/detect.js +51 -0
  319. package/src/identity/store.js +66 -0
  320. package/src/infrastructure/command-runner.js +44 -0
  321. package/src/infrastructure/environment.js +38 -0
  322. package/src/infrastructure/file-system-service.js +63 -0
  323. package/src/infrastructure/mocks.js +182 -0
  324. package/src/lang/chunk-go.js +94 -0
  325. package/src/lang/chunk-java.js +90 -0
  326. package/src/lang/chunk-python.js +88 -0
  327. package/src/lang/chunk-rust.js +102 -0
  328. package/src/lang/go.js +14 -0
  329. package/src/lang/java.js +16 -0
  330. package/src/lang/js.js +8 -0
  331. package/src/lang/python.js +14 -0
  332. package/src/lang/registry.js +79 -0
  333. package/src/lang/rust.js +14 -0
  334. package/src/lang/tree-sitter-loader.js +45 -0
  335. package/src/mcp/direct-role-runner.js +112 -0
  336. package/src/mcp/error-classifiers.js +66 -0
  337. package/src/mcp/handlers/direct-handlers.js +486 -0
  338. package/src/mcp/handlers/hu-handlers.js +93 -0
  339. package/src/mcp/handlers/management-handlers.js +265 -0
  340. package/src/mcp/handlers/rag-handler.js +52 -0
  341. package/src/mcp/handlers/run-handler.js +313 -0
  342. package/src/mcp/orphan-guard.js +131 -0
  343. package/src/mcp/preflight.js +47 -0
  344. package/src/mcp/progress.js +149 -0
  345. package/src/mcp/response-compressor.js +81 -0
  346. package/src/mcp/run-kj.js +135 -0
  347. package/src/mcp/server-handlers.js +79 -0
  348. package/src/mcp/server.js +66 -0
  349. package/src/mcp/shared-helpers.js +297 -0
  350. package/src/mcp/sovereignty-guard.js +112 -0
  351. package/src/mcp/suggest-handler.js +92 -0
  352. package/src/mcp/tool-arg-normalizers.js +16 -0
  353. package/src/mcp/tools.js +432 -0
  354. package/src/mutate/diff-scope.js +105 -0
  355. package/src/mutate/reviewer-signal.js +76 -0
  356. package/src/mutate/runner.js +72 -0
  357. package/src/mutate/tool-registry.js +90 -0
  358. package/src/onboarder/cache.js +24 -0
  359. package/src/onboarder/collectors/framework-multilang.js +64 -0
  360. package/src/onboarder/collectors/index.js +136 -0
  361. package/src/orchestrator/agent-fallback.js +81 -0
  362. package/src/orchestrator/brain-coordinator.js +186 -0
  363. package/src/orchestrator/ci-integration.js +139 -0
  364. package/src/orchestrator/config-init.js +510 -0
  365. package/src/orchestrator/direct-actions.js +217 -0
  366. package/src/orchestrator/drivers/error-recovery.js +287 -0
  367. package/src/orchestrator/drivers/init-context.js +227 -0
  368. package/src/orchestrator/drivers/iteration-loop.js +380 -0
  369. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +69 -0
  370. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  371. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  372. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +114 -0
  373. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  374. package/src/orchestrator/drivers/post-loop.js +383 -0
  375. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +136 -0
  376. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  377. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  378. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +391 -0
  379. package/src/orchestrator/drivers/pre-loop.js +507 -0
  380. package/src/orchestrator/drivers/run-hu-batch.js +427 -0
  381. package/src/orchestrator/feedback-enrichment.js +140 -0
  382. package/src/orchestrator/feedback-queue.js +151 -0
  383. package/src/orchestrator/flow-control.js +170 -0
  384. package/src/orchestrator/flow-runner.js +364 -0
  385. package/src/orchestrator/fs-leak-detector.js +210 -0
  386. package/src/orchestrator/hu-scheduler.js +114 -0
  387. package/src/orchestrator/hu-sub-pipeline.js +616 -0
  388. package/src/orchestrator/integrations.js +81 -0
  389. package/src/orchestrator/iteration-gate.js +73 -0
  390. package/src/orchestrator/iteration-stages.js +9 -0
  391. package/src/orchestrator/parallel-limiter.js +74 -0
  392. package/src/orchestrator/pipeline-context.js +72 -0
  393. package/src/orchestrator/post-loop-stages.js +468 -0
  394. package/src/orchestrator/pre-loop-stages.js +11 -0
  395. package/src/orchestrator/preflight-checks.js +462 -0
  396. package/src/orchestrator/repair/repair-detector.js +117 -0
  397. package/src/orchestrator/repair/repair-runner.js +142 -0
  398. package/src/orchestrator/reviewer-fallback.js +45 -0
  399. package/src/orchestrator/role-output-compressor.js +189 -0
  400. package/src/orchestrator/session-journal.js +308 -0
  401. package/src/orchestrator/solomon-escalation.js +305 -0
  402. package/src/orchestrator/solomon-rules.js +212 -0
  403. package/src/orchestrator/stages/architect-stage.js +172 -0
  404. package/src/orchestrator/stages/coder-stage.js +480 -0
  405. package/src/orchestrator/stages/domain-curator-stage.js +82 -0
  406. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  407. package/src/orchestrator/stages/hu-reviewer-stage.js +394 -0
  408. package/src/orchestrator/stages/perf-stage.js +140 -0
  409. package/src/orchestrator/stages/planner-stage.js +85 -0
  410. package/src/orchestrator/stages/rag-context-stage.js +77 -0
  411. package/src/orchestrator/stages/rag-preload-decisor.js +58 -0
  412. package/src/orchestrator/stages/research-stage.js +69 -0
  413. package/src/orchestrator/stages/reviewer-stage.js +422 -0
  414. package/src/orchestrator/stages/sonar-stage.js +421 -0
  415. package/src/orchestrator/stages/stage-classes.js +84 -0
  416. package/src/orchestrator/stages/stage-executor.js +155 -0
  417. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  418. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  419. package/src/orchestrator/stages/triage-stage.js +172 -0
  420. package/src/orchestrator/standby.js +71 -0
  421. package/src/orchestrator/verification-gate.js +183 -0
  422. package/src/orchestrator.js +22 -0
  423. package/src/plan/adr-generator.js +170 -0
  424. package/src/plan/adr-loader.js +60 -0
  425. package/src/plan/plan-executor.js +100 -0
  426. package/src/plan/plan-fixer.js +192 -0
  427. package/src/plan/plan-hu-ops.js +16 -0
  428. package/src/plan/plan-id.js +4 -0
  429. package/src/plan/plan-migrate-result.js +24 -0
  430. package/src/plan/plan-reviewer.js +236 -0
  431. package/src/plan/plan-schema.js +334 -0
  432. package/src/plan/plan-store.js +260 -0
  433. package/src/plan/plan-structural-pass.js +122 -0
  434. package/src/plan/plan-validation.js +4 -0
  435. package/src/plan/preflight-hu.js +105 -0
  436. package/src/plan/spec-detector.js +61 -0
  437. package/src/plan/tests-synthesizer.js +131 -0
  438. package/src/planning-game/adapter.js +108 -0
  439. package/src/planning-game/architect-adrs.js +45 -0
  440. package/src/planning-game/client.js +108 -0
  441. package/src/planning-game/decomposition.js +87 -0
  442. package/src/planning-game/pipeline-adapter.js +251 -0
  443. package/src/plugins/loader.js +67 -0
  444. package/src/policy/add.js +111 -0
  445. package/src/policy/decisions.js +42 -0
  446. package/src/policy/engine.js +82 -0
  447. package/src/policy/exceptions.js +60 -0
  448. package/src/privacy/onboarding.js +35 -0
  449. package/src/privacy/scan.js +120 -0
  450. package/src/prompts/arbiter.js +38 -0
  451. package/src/prompts/architect.js +103 -0
  452. package/src/prompts/audit.js +474 -0
  453. package/src/prompts/coder.js +287 -0
  454. package/src/prompts/diff-clip.js +27 -0
  455. package/src/prompts/discover.js +222 -0
  456. package/src/prompts/hu-reviewer.js +219 -0
  457. package/src/prompts/planner.js +312 -0
  458. package/src/prompts/prompt-layout.js +80 -0
  459. package/src/prompts/prompt-resolver.js +120 -0
  460. package/src/prompts/reviewer.js +75 -0
  461. package/src/prompts/rtk-snippet.js +25 -0
  462. package/src/prompts/spec-reviewer.js +66 -0
  463. package/src/prompts/split-signal.js +82 -0
  464. package/src/prompts/start-decision.js +52 -0
  465. package/src/prompts/tool-correctness-judge.js +31 -0
  466. package/src/prompts/triage.js +57 -0
  467. package/src/rag/auto-update.js +72 -0
  468. package/src/rag/chunker-ast.js +56 -0
  469. package/src/rag/chunker.js +132 -0
  470. package/src/rag/embedder.js +2 -0
  471. package/src/rag/embedders/_cloud-base.js +2 -0
  472. package/src/rag/embedders/cohere.js +2 -0
  473. package/src/rag/embedders/factory.js +2 -0
  474. package/src/rag/embedders/mistral.js +2 -0
  475. package/src/rag/embedders/onnx.js +2 -0
  476. package/src/rag/embedders/openai.js +2 -0
  477. package/src/rag/embedders/voyage.js +2 -0
  478. package/src/rag/eval.js +61 -0
  479. package/src/rag/governed-embedder.js +39 -0
  480. package/src/rag/indexer.js +199 -0
  481. package/src/rag/library.js +62 -0
  482. package/src/rag/ollama-capability.js +59 -0
  483. package/src/rag/ollama-manager.js +98 -0
  484. package/src/rag/onnx-fallback.js +69 -0
  485. package/src/rag/rerank.js +4 -0
  486. package/src/rag/retriever.js +2 -0
  487. package/src/rag/vec-store.js +17 -0
  488. package/src/rag/watcher.js +62 -0
  489. package/src/rag/where-parser.js +4 -0
  490. package/src/repeat-detector.js +88 -0
  491. package/src/review/card-first.js +99 -0
  492. package/src/review/card-verify.js +83 -0
  493. package/src/review/diff-generator.js +112 -0
  494. package/src/review/gate-gitignore.js +77 -0
  495. package/src/review/git-stash-helper.js +104 -0
  496. package/src/review/mutation-pregate.js +48 -0
  497. package/src/review/one-shot-review.js +122 -0
  498. package/src/review/parser.js +80 -0
  499. package/src/review/policy-gate.js +26 -0
  500. package/src/review/profiles.js +66 -0
  501. package/src/review/reviewer-fallback.js +125 -0
  502. package/src/review/schema.js +31 -0
  503. package/src/review/scope-filter.js +152 -0
  504. package/src/review/snapshot-diff.js +122 -0
  505. package/src/review/solomon-arbitration.js +113 -0
  506. package/src/review/sonar-pregate.js +63 -0
  507. package/src/review/tdd-discipline.js +90 -0
  508. package/src/review/tdd-policy.js +69 -0
  509. package/src/review/tests-with-code.js +33 -0
  510. package/src/review/verdict-store.js +87 -0
  511. package/src/review/workspace.js +21 -0
  512. package/src/roles/agent-role.js +194 -0
  513. package/src/roles/architect-role.js +77 -0
  514. package/src/roles/audit-role.js +258 -0
  515. package/src/roles/base-role.js +165 -0
  516. package/src/roles/coder-role.js +107 -0
  517. package/src/roles/commiter-role.js +110 -0
  518. package/src/roles/discover-role.js +81 -0
  519. package/src/roles/domain-curator-role.js +141 -0
  520. package/src/roles/hu-reviewer-role.js +64 -0
  521. package/src/roles/impeccable-role.js +85 -0
  522. package/src/roles/index.js +18 -0
  523. package/src/roles/karajan-brain-role.js +134 -0
  524. package/src/roles/onboarder-role.js +61 -0
  525. package/src/roles/perf-role.js +122 -0
  526. package/src/roles/planner-role.js +151 -0
  527. package/src/roles/refactorer-role.js +30 -0
  528. package/src/roles/repairer-role.js +181 -0
  529. package/src/roles/researcher-role.js +70 -0
  530. package/src/roles/reviewer-role.js +104 -0
  531. package/src/roles/security-role.js +73 -0
  532. package/src/roles/solomon-role.js +265 -0
  533. package/src/roles/sonar-role.js +70 -0
  534. package/src/roles/spec-reviewer-role.js +106 -0
  535. package/src/roles/tester-role.js +235 -0
  536. package/src/roles/tool-correctness-judge-role.js +78 -0
  537. package/src/roles/triage-role.js +99 -0
  538. package/src/session/journal/decisions-writer.js +200 -0
  539. package/src/session/journal/iteration-logger.js +194 -0
  540. package/src/session/journal/summary-writer.js +320 -0
  541. package/src/session/journal/tree-writer.js +164 -0
  542. package/src/session/journal/write-all.js +89 -0
  543. package/src/session/mutators.js +339 -0
  544. package/src/session/runtime-overrides.js +59 -0
  545. package/src/session/store.js +106 -0
  546. package/src/session-cleanup.js +116 -0
  547. package/src/skills/addyosmani-catalog.js +315 -0
  548. package/src/skills/addyosmani-role-map.js +139 -0
  549. package/src/skills/openskills-client.js +157 -0
  550. package/src/skills/semantic-detector.js +167 -0
  551. package/src/skills/skill-detector.js +385 -0
  552. package/src/skills/skill-loader.js +219 -0
  553. package/src/skills/skills-cache.js +128 -0
  554. package/src/sonar/api.js +128 -0
  555. package/src/sonar/cloud-scanner.js +78 -0
  556. package/src/sonar/config-resolver.js +88 -0
  557. package/src/sonar/credentials.js +60 -0
  558. package/src/sonar/discovery.js +124 -0
  559. package/src/sonar/enforcer.js +19 -0
  560. package/src/sonar/issue-filter.js +12 -0
  561. package/src/sonar/manager.js +181 -0
  562. package/src/sonar/project-key.js +120 -0
  563. package/src/sonar/scanner.js +307 -0
  564. package/src/sonar/token-bootstrap.js +205 -0
  565. package/src/sonar/token-recovery.js +67 -0
  566. package/src/spec-review/refine-loop.js +58 -0
  567. package/src/spec-review/run-spec-review.js +119 -0
  568. package/src/start/assessment.js +74 -0
  569. package/src/start/maturity.js +73 -0
  570. package/src/start/start-decider-role.js +79 -0
  571. package/src/start/sweep.js +157 -0
  572. package/src/sync/apply-canvas-patch.js +55 -0
  573. package/src/sync/detect-drift.js +113 -0
  574. package/src/tournament/crown.js +88 -0
  575. package/src/tournament/judge.js +150 -0
  576. package/src/tournament/run.js +142 -0
  577. package/src/tournament/scoreboard.js +124 -0
  578. package/src/tracker-bootstrap.js +44 -0
  579. package/src/types/agent.js +55 -0
  580. package/src/types/config.js +114 -0
  581. package/src/types/hu.js +47 -0
  582. package/src/types/index.js +38 -0
  583. package/src/types/policy.js +43 -0
  584. package/src/types/session.js +75 -0
  585. package/src/types/solomon.js +87 -0
  586. package/src/types/stage.js +68 -0
  587. package/src/utils/agent-detect.js +93 -0
  588. package/src/utils/ai-trash.js +48 -0
  589. package/src/utils/async-lock.js +31 -0
  590. package/src/utils/atomic-write.js +6 -0
  591. package/src/utils/banner.js +66 -0
  592. package/src/utils/binary-sources.js +70 -0
  593. package/src/utils/board-prompt-bridge.js +108 -0
  594. package/src/utils/budget.js +229 -0
  595. package/src/utils/cli-ask-question.js +93 -0
  596. package/src/utils/cli-progress.js +220 -0
  597. package/src/utils/cli-run-log.js +83 -0
  598. package/src/utils/cwd-confirm.js +39 -0
  599. package/src/utils/derive-project-name-from-cwd.js +119 -0
  600. package/src/utils/display/event-handlers.js +403 -0
  601. package/src/utils/display/formatters.js +132 -0
  602. package/src/utils/display/header.js +47 -0
  603. package/src/utils/display/resume-hint.js +41 -0
  604. package/src/utils/display/session-summary.js +118 -0
  605. package/src/utils/display/solomon.js +42 -0
  606. package/src/utils/display/spec-findings.js +44 -0
  607. package/src/utils/doc-links.js +29 -0
  608. package/src/utils/docker-install.js +34 -0
  609. package/src/utils/escape-regexp.js +15 -0
  610. package/src/utils/events.js +35 -0
  611. package/src/utils/fs.js +19 -0
  612. package/src/utils/garbage-collector.js +481 -0
  613. package/src/utils/git.js +279 -0
  614. package/src/utils/home-migration.js +87 -0
  615. package/src/utils/injection-guard.js +186 -0
  616. package/src/utils/install-hints.js +180 -0
  617. package/src/utils/json-extract.js +64 -0
  618. package/src/utils/locale.js +59 -0
  619. package/src/utils/logger.js +96 -0
  620. package/src/utils/managed-markers.js +93 -0
  621. package/src/utils/manifest-sniff.js +82 -0
  622. package/src/utils/messages.js +61 -0
  623. package/src/utils/model-selector.js +107 -0
  624. package/src/utils/os-detect.js +79 -0
  625. package/src/utils/paths.js +13 -0
  626. package/src/utils/pending-user-action.js +95 -0
  627. package/src/utils/port-check.js +6 -0
  628. package/src/utils/port-occupant.js +96 -0
  629. package/src/utils/pricing.js +29 -0
  630. package/src/utils/process.js +4 -0
  631. package/src/utils/project-detect.js +160 -0
  632. package/src/utils/prompt-project-name.js +35 -0
  633. package/src/utils/provider-env.js +116 -0
  634. package/src/utils/qmd-collection.js +72 -0
  635. package/src/utils/qmd-detect.js +18 -0
  636. package/src/utils/qmd-install.js +37 -0
  637. package/src/utils/quota-simulator.js +56 -0
  638. package/src/utils/rate-limit-detector.js +181 -0
  639. package/src/utils/repo-cleaner.js +79 -0
  640. package/src/utils/retry.js +88 -0
  641. package/src/utils/role-assigner.js +137 -0
  642. package/src/utils/role-env.js +58 -0
  643. package/src/utils/rtk-detect.js +18 -0
  644. package/src/utils/rtk-install.js +38 -0
  645. package/src/utils/rtk-wrapper.js +87 -0
  646. package/src/utils/run-context.js +75 -0
  647. package/src/utils/run-log.js +217 -0
  648. package/src/utils/run-registry.js +9 -0
  649. package/src/utils/shared-paths.js +12 -0
  650. package/src/utils/squeezr-detect.js +21 -0
  651. package/src/utils/squeezr-install.js +37 -0
  652. package/src/utils/stack-detect.js +135 -0
  653. package/src/utils/stall-detector.js +159 -0
  654. package/src/utils/status-dashboard.js +254 -0
  655. package/src/utils/task-file.js +109 -0
  656. package/src/utils/telemetry.js +124 -0
  657. package/src/utils/templates-root.js +61 -0
  658. package/src/utils/tool-call-extractor.js +56 -0
  659. package/src/utils/tool-governor.js +116 -0
  660. package/src/utils/tool-installer.js +93 -0
  661. package/src/utils/update-check.js +280 -0
  662. package/src/utils/vector-store-orphans.js +89 -0
  663. package/src/utils/welcome.js +47 -0
  664. package/src/utils/with-timeout.js +42 -0
  665. package/src/utils/wizard.js +47 -0
  666. package/src/webperf/cwv-gate.js +94 -0
  667. package/src/webperf/devtools-detect.js +112 -0
  668. package/src/webperf/scanner.js +209 -0
  669. package/src/webperf/webperf-role-integration.js +66 -0
  670. package/templates/coder-rules.md +45 -0
  671. package/templates/docker-compose.sonar.yml +60 -0
  672. package/templates/domains/example-dental.md +57 -0
  673. package/templates/kj.config.yml +128 -0
  674. package/templates/review-rules.md +17 -0
  675. package/templates/roles/architect/anthropic.md +74 -0
  676. package/templates/roles/architect/default.md +72 -0
  677. package/templates/roles/architect/google.md +61 -0
  678. package/templates/roles/architect/openai.md +60 -0
  679. package/templates/roles/architect/opencode.md +59 -0
  680. package/templates/roles/architect.md +76 -0
  681. package/templates/roles/audit.md +68 -0
  682. package/templates/roles/coder/anthropic.md +81 -0
  683. package/templates/roles/coder/default.md +78 -0
  684. package/templates/roles/coder/google.md +87 -0
  685. package/templates/roles/coder/openai.md +68 -0
  686. package/templates/roles/coder/opencode.md +58 -0
  687. package/templates/roles/coder.md +152 -0
  688. package/templates/roles/commiter.md +44 -0
  689. package/templates/roles/discover.md +167 -0
  690. package/templates/roles/domain-curator.md +52 -0
  691. package/templates/roles/hu-reviewer.md +236 -0
  692. package/templates/roles/impeccable-design.md +87 -0
  693. package/templates/roles/impeccable.md +125 -0
  694. package/templates/roles/karajan-brain.md +95 -0
  695. package/templates/roles/onboarder.md +45 -0
  696. package/templates/roles/planner/anthropic.md +59 -0
  697. package/templates/roles/planner/default.md +57 -0
  698. package/templates/roles/planner/google.md +75 -0
  699. package/templates/roles/planner/openai.md +55 -0
  700. package/templates/roles/planner/opencode.md +62 -0
  701. package/templates/roles/planner.md +61 -0
  702. package/templates/roles/refactorer.md +48 -0
  703. package/templates/roles/repairer.md +103 -0
  704. package/templates/roles/researcher.md +41 -0
  705. package/templates/roles/reviewer/anthropic.md +61 -0
  706. package/templates/roles/reviewer/default.md +62 -0
  707. package/templates/roles/reviewer/google.md +63 -0
  708. package/templates/roles/reviewer/openai.md +57 -0
  709. package/templates/roles/reviewer/opencode.md +42 -0
  710. package/templates/roles/reviewer-paranoid.md +38 -0
  711. package/templates/roles/reviewer-relaxed.md +34 -0
  712. package/templates/roles/reviewer-strict.md +37 -0
  713. package/templates/roles/reviewer.md +62 -0
  714. package/templates/roles/security.md +54 -0
  715. package/templates/roles/solomon/anthropic.md +71 -0
  716. package/templates/roles/solomon/default.md +83 -0
  717. package/templates/roles/solomon/google.md +61 -0
  718. package/templates/roles/solomon/openai.md +53 -0
  719. package/templates/roles/solomon/opencode.md +55 -0
  720. package/templates/roles/solomon.md +83 -0
  721. package/templates/roles/sonar.md +49 -0
  722. package/templates/roles/spec-reviewer.md +47 -0
  723. package/templates/roles/tester.md +50 -0
  724. package/templates/roles/triage.md +69 -0
  725. package/templates/skills/kj-architect.md +45 -0
  726. package/templates/skills/kj-audit.md +63 -0
  727. package/templates/skills/kj-board.md +16 -0
  728. package/templates/skills/kj-code.md +51 -0
  729. package/templates/skills/kj-csv-transform.md +40 -0
  730. package/templates/skills/kj-data-report.md +44 -0
  731. package/templates/skills/kj-discover.md +24 -0
  732. package/templates/skills/kj-rag-query.md +50 -0
  733. package/templates/skills/kj-review.md +47 -0
  734. package/templates/skills/kj-run.md +73 -0
  735. package/templates/skills/kj-security.md +49 -0
  736. package/templates/skills/kj-sonar.md +41 -0
  737. package/templates/skills/kj-sql-analysis.md +40 -0
  738. package/templates/skills/kj-test.md +40 -0
  739. package/templates/workflows/automerge.yml +30 -0
  740. package/templates/workflows/houston-override.yml +46 -0
  741. package/templates/workflows/kj-ci-gateway.yml +58 -0
  742. package/vendor/tree-sitter-grammars/README.md +21 -0
  743. package/vendor/tree-sitter-grammars/go.wasm +0 -0
  744. package/vendor/tree-sitter-grammars/java.wasm +0 -0
  745. package/vendor/tree-sitter-grammars/python.wasm +0 -0
  746. package/vendor/tree-sitter-grammars/rust.wasm +0 -0
@@ -0,0 +1,963 @@
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
+ }
374
+
375
+ /**
376
+ * Inserts or updates a project record.
377
+ * @param {{ id: string, name?: string, first_seen?: string, last_activity?: string, total_stories?: number }} project
378
+ */
379
+ export function upsertProject(project) {
380
+ // is_shared (KJC-PRP-0002 PR3) is set by sync when a plan came from
381
+ // `.karajan-shared/`. COALESCE with the existing column keeps the badge
382
+ // once it's been promoted — a second non-shared plan won't unset it.
383
+ const stmt = getDb().prepare(`
384
+ INSERT INTO projects (id, name, first_seen, last_activity, total_stories, is_shared)
385
+ VALUES (@id, @name, @first_seen, @last_activity, @total_stories, @is_shared)
386
+ ON CONFLICT(id) DO UPDATE SET
387
+ name = COALESCE(@name, name),
388
+ last_activity = COALESCE(@last_activity, last_activity),
389
+ total_stories = COALESCE(@total_stories, total_stories),
390
+ is_shared = COALESCE(@is_shared, is_shared)
391
+ `);
392
+ stmt.run({
393
+ id: project.id,
394
+ name: project.name || project.id,
395
+ first_seen: project.first_seen || new Date().toISOString(),
396
+ last_activity: project.last_activity || new Date().toISOString(),
397
+ total_stories: project.total_stories || 0,
398
+ is_shared: project.is_shared ?? null,
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Inserts or updates a story record.
404
+ * @param {object} story
405
+ */
406
+ export function upsertStory(story) {
407
+ const stmt = getDb().prepare(`
408
+ INSERT INTO stories (
409
+ id, project_id, session_id, status, title, original_text,
410
+ certified_as, certified_want, certified_so_that,
411
+ quality_total, quality_d1, quality_d2, quality_d3, quality_d4, quality_d5, quality_d6,
412
+ antipatterns, ac_format, acceptance_criteria,
413
+ created_at, updated_at, certified_at, plan_id,
414
+ ac_count, test_count, blocked_by, acceptance_tests, plan_order, spec_section, outcome, result,
415
+ coder_model, reviewer_model, coder_provider, reviewer_provider, assignee
416
+ ) VALUES (
417
+ @id, @project_id, @session_id, @status, @title, @original_text,
418
+ @certified_as, @certified_want, @certified_so_that,
419
+ @quality_total, @quality_d1, @quality_d2, @quality_d3, @quality_d4, @quality_d5, @quality_d6,
420
+ @antipatterns, @ac_format, @acceptance_criteria,
421
+ @created_at, @updated_at, @certified_at, @plan_id,
422
+ @ac_count, @test_count, @blocked_by, @acceptance_tests, @plan_order, @spec_section, @outcome, @result,
423
+ @coder_model, @reviewer_model, @coder_provider, @reviewer_provider, @assignee
424
+ )
425
+ ON CONFLICT(id) DO UPDATE SET
426
+ status = @status,
427
+ title = COALESCE(@title, title),
428
+ original_text = COALESCE(@original_text, original_text),
429
+ certified_as = COALESCE(@certified_as, certified_as),
430
+ certified_want = COALESCE(@certified_want, certified_want),
431
+ certified_so_that = COALESCE(@certified_so_that, certified_so_that),
432
+ quality_total = COALESCE(@quality_total, quality_total),
433
+ quality_d1 = COALESCE(@quality_d1, quality_d1),
434
+ quality_d2 = COALESCE(@quality_d2, quality_d2),
435
+ quality_d3 = COALESCE(@quality_d3, quality_d3),
436
+ quality_d4 = COALESCE(@quality_d4, quality_d4),
437
+ quality_d5 = COALESCE(@quality_d5, quality_d5),
438
+ quality_d6 = COALESCE(@quality_d6, quality_d6),
439
+ antipatterns = COALESCE(@antipatterns, antipatterns),
440
+ ac_format = COALESCE(@ac_format, ac_format),
441
+ acceptance_criteria = COALESCE(@acceptance_criteria, acceptance_criteria),
442
+ updated_at = @updated_at,
443
+ certified_at = COALESCE(@certified_at, certified_at),
444
+ plan_id = COALESCE(@plan_id, plan_id),
445
+ ac_count = COALESCE(@ac_count, ac_count),
446
+ test_count = COALESCE(@test_count, test_count),
447
+ blocked_by = COALESCE(@blocked_by, blocked_by),
448
+ acceptance_tests = COALESCE(@acceptance_tests, acceptance_tests),
449
+ plan_order = COALESCE(@plan_order, plan_order),
450
+ spec_section = COALESCE(@spec_section, spec_section),
451
+ outcome = COALESCE(@outcome, outcome),
452
+ result = COALESCE(@result, result),
453
+ coder_model = COALESCE(@coder_model, coder_model),
454
+ reviewer_model = COALESCE(@reviewer_model, reviewer_model),
455
+ coder_provider = COALESCE(@coder_provider, coder_provider),
456
+ reviewer_provider = COALESCE(@reviewer_provider, reviewer_provider),
457
+ assignee = COALESCE(@assignee, assignee)
458
+ `);
459
+ stmt.run({
460
+ id: story.id,
461
+ project_id: story.project_id || 'default',
462
+ session_id: story.session_id || null,
463
+ status: story.status || 'pending',
464
+ title: story.title || null,
465
+ original_text: story.original_text || null,
466
+ certified_as: story.certified_as || null,
467
+ certified_want: story.certified_want || null,
468
+ certified_so_that: story.certified_so_that || null,
469
+ quality_total: story.quality_total ?? null,
470
+ quality_d1: story.quality_d1 ?? null,
471
+ quality_d2: story.quality_d2 ?? null,
472
+ quality_d3: story.quality_d3 ?? null,
473
+ quality_d4: story.quality_d4 ?? null,
474
+ quality_d5: story.quality_d5 ?? null,
475
+ quality_d6: story.quality_d6 ?? null,
476
+ antipatterns: story.antipatterns || null,
477
+ ac_format: story.ac_format || null,
478
+ acceptance_criteria: story.acceptance_criteria || null,
479
+ created_at: story.created_at || new Date().toISOString(),
480
+ updated_at: story.updated_at || new Date().toISOString(),
481
+ certified_at: story.certified_at || null,
482
+ plan_id: story.plan_id || null,
483
+ ac_count: story.ac_count ?? null,
484
+ test_count: story.test_count ?? null,
485
+ blocked_by: story.blocked_by || null,
486
+ acceptance_tests: story.acceptance_tests || null,
487
+ plan_order: story.plan_order ?? null,
488
+ spec_section: story.spec_section || null,
489
+ outcome: story.outcome || null,
490
+ result: story.result || null,
491
+ coder_model: story.coder_model || null,
492
+ reviewer_model: story.reviewer_model || null,
493
+ coder_provider: story.coder_provider || null,
494
+ reviewer_provider: story.reviewer_provider || null,
495
+ assignee: story.assignee || null,
496
+ });
497
+ }
498
+
499
+ /**
500
+ * Look up a story row (minimal fields used by the mutation endpoints).
501
+ * @param {string} storyId
502
+ * @returns {{ id: string, project_id: string, plan_id: string|null, status: string }|null}
503
+ */
504
+ export function getStoryRow(storyId) {
505
+ const row = getDb()
506
+ .prepare('SELECT id, project_id, plan_id, status FROM stories WHERE id = ?')
507
+ .get(storyId);
508
+ return row || null;
509
+ }
510
+
511
+ /**
512
+ * Update a single story's status (in-memory — the source-of-truth plan
513
+ * JSON is mutated separately by the caller before or after this write).
514
+ * @param {string} storyId
515
+ * @param {string} status
516
+ * @returns {boolean} true if a row was updated
517
+ */
518
+ export function updateStoryStatus(storyId, status) {
519
+ const res = getDb()
520
+ .prepare('UPDATE stories SET status = ?, updated_at = ? WHERE id = ?')
521
+ .run(status, new Date().toISOString(), storyId);
522
+ return res.changes > 0;
523
+ }
524
+
525
+ /**
526
+ * Persist the USD cost of a HU. Called by the orchestrator (Cost D) once
527
+ * the HU finishes — the value comes from `aggregateRunCost(events).totalUsd`
528
+ * (Cost B). `null` clears the cell so the UI renders "—" again. Other
529
+ * numeric inputs are coerced via `Number`; non-finite values reject with
530
+ * an explicit throw so callers don't silently NULL-out real cost rows.
531
+ *
532
+ * @param {string} storyId
533
+ * @param {number|null} costUsd
534
+ * @returns {boolean} true when the row existed and was updated
535
+ */
536
+ export function setStoryCost(storyId, costUsd) {
537
+ let value = null;
538
+ if (costUsd !== null && costUsd !== undefined) {
539
+ const n = Number(costUsd);
540
+ if (!Number.isFinite(n)) {
541
+ throw new Error(`setStoryCost: costUsd must be a finite number or null (got ${costUsd})`);
542
+ }
543
+ value = n;
544
+ }
545
+ const res = getDb()
546
+ .prepare('UPDATE stories SET cost_usd = ?, updated_at = ? WHERE id = ?')
547
+ .run(value, new Date().toISOString(), storyId);
548
+ return res.changes > 0;
549
+ }
550
+
551
+ /**
552
+ * Persist cached/input tokens for a HU (KJC-TSK-0525, Φ0-G). Called by
553
+ * the orchestrator alongside `setStoryCost` once the HU finishes. Both
554
+ * values come from `BudgetTracker.since(huCursor)` (Φ0-E). Pass `null`
555
+ * to clear either cell. Non-finite numbers throw so callers don't
556
+ * silently null-out real telemetry.
557
+ *
558
+ * @param {string} storyId
559
+ * @param {number|null} cachedTokens
560
+ * @param {number|null} tokensIn
561
+ * @returns {boolean} true when the row existed and was updated
562
+ */
563
+ export function setStoryCachedTokens(storyId, cachedTokens, tokensIn) {
564
+ const coerce = (v, name) => {
565
+ if (v === null || v === undefined) return null;
566
+ const n = Number(v);
567
+ if (!Number.isFinite(n) || n < 0) {
568
+ throw new Error(`setStoryCachedTokens: ${name} must be a non-negative finite number or null (got ${v})`);
569
+ }
570
+ return Math.round(n);
571
+ };
572
+ const cached = coerce(cachedTokens, 'cachedTokens');
573
+ const tin = coerce(tokensIn, 'tokensIn');
574
+ const res = getDb()
575
+ .prepare('UPDATE stories SET cached_tokens = ?, tokens_in = ?, updated_at = ? WHERE id = ?')
576
+ .run(cached, tin, new Date().toISOString(), storyId);
577
+ return res.changes > 0;
578
+ }
579
+
580
+ /**
581
+ * List every distinct `plan_id` stamped on the stories of a given project.
582
+ * Used by "mark plan ready" so the board can act on each plan of a project
583
+ * without making the user pick one.
584
+ * @param {string} projectId
585
+ * @returns {string[]}
586
+ */
587
+ export function listPlanIdsForProject(projectId) {
588
+ return getDb()
589
+ .prepare(`
590
+ SELECT DISTINCT plan_id FROM stories
591
+ WHERE project_id = ? AND plan_id IS NOT NULL
592
+ `)
593
+ .all(projectId)
594
+ .map((r) => r.plan_id);
595
+ }
596
+
597
+ /**
598
+ * Inserts or updates a session record.
599
+ * @param {object} session
600
+ */
601
+ export function upsertSession(session) {
602
+ const stmt = getDb().prepare(`
603
+ INSERT INTO sessions (
604
+ id, project_id, task, status, created_at, updated_at,
605
+ iterations, duration_ms, approved, commits, stages_completed,
606
+ checkpoints, llm_calls, config_snapshot, budget
607
+ ) VALUES (
608
+ @id, @project_id, @task, @status, @created_at, @updated_at,
609
+ @iterations, @duration_ms, @approved, @commits, @stages_completed,
610
+ @checkpoints, @llm_calls, @config_snapshot, @budget
611
+ )
612
+ ON CONFLICT(id) DO UPDATE SET
613
+ status = @status,
614
+ task = COALESCE(@task, task),
615
+ updated_at = @updated_at,
616
+ iterations = COALESCE(@iterations, iterations),
617
+ duration_ms = COALESCE(@duration_ms, duration_ms),
618
+ approved = @approved,
619
+ commits = COALESCE(@commits, commits),
620
+ stages_completed = COALESCE(@stages_completed, stages_completed),
621
+ checkpoints = COALESCE(@checkpoints, checkpoints),
622
+ llm_calls = COALESCE(@llm_calls, llm_calls),
623
+ config_snapshot = COALESCE(@config_snapshot, config_snapshot),
624
+ budget = COALESCE(@budget, budget)
625
+ `);
626
+ stmt.run({
627
+ id: session.id,
628
+ project_id: session.project_id || 'default',
629
+ task: session.task || null,
630
+ status: session.status || 'unknown',
631
+ created_at: session.created_at || new Date().toISOString(),
632
+ updated_at: session.updated_at || new Date().toISOString(),
633
+ iterations: session.iterations ?? 0,
634
+ duration_ms: session.duration_ms ?? null,
635
+ approved: session.approved ? 1 : 0,
636
+ commits: session.commits || null,
637
+ stages_completed: session.stages_completed || null,
638
+ checkpoints: session.checkpoints || null,
639
+ llm_calls: session.llm_calls || null,
640
+ config_snapshot: session.config_snapshot || null,
641
+ budget: session.budget || null,
642
+ });
643
+ }
644
+
645
+ /**
646
+ * Returns all projects with aggregated stats.
647
+ * @returns {Array<object>}
648
+ */
649
+ export function getProjects() {
650
+ return getDb().prepare(`
651
+ SELECT p.*,
652
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id) AS story_count,
653
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'certified') AS certified_count,
654
+ (SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'pending') AS pending_count,
655
+ (SELECT COUNT(*) FROM sessions ss WHERE ss.project_id = p.id) AS session_count
656
+ FROM projects p
657
+ ORDER BY p.last_activity DESC
658
+ `).all();
659
+ }
660
+
661
+ /**
662
+ * Returns stories for a specific project.
663
+ * @param {string} projectId
664
+ * @returns {Array<object>}
665
+ */
666
+ /**
667
+ * Fetch all stories of a project ordered by plan_order ASC (roots first,
668
+ * dependents last — matches what `kj run --plan` will actually execute)
669
+ * and falling back to updated_at DESC for legacy rows that never had
670
+ * plan_order stamped.
671
+ */
672
+ export function getStoriesByProject(projectId) {
673
+ return getDb().prepare(`
674
+ SELECT * FROM stories WHERE project_id = ?
675
+ ORDER BY plan_order IS NULL, plan_order ASC, updated_at DESC
676
+ `).all(projectId);
677
+ }
678
+
679
+ /**
680
+ * Aggregate $ cost for a project (KJC-TSK-0516, Cost E).
681
+ * Sums stories.cost_usd, groups by plan_id ('unassigned' for NULL).
682
+ * Returns null if project does not exist; otherwise an object with
683
+ * totalUsd=0 and byPlan=[] when no stories carry cost_usd yet.
684
+ * @param {string} projectId
685
+ * @returns {{ totalUsd:number, byPlan:Array<{planId:string,totalUsd:number,huCount:number}>, unknownModelTokens:object, currency:string } | null}
686
+ */
687
+ export function getProjectCost(projectId) {
688
+ const project = getDb()
689
+ .prepare('SELECT id FROM projects WHERE id = ?')
690
+ .get(projectId);
691
+ if (!project) return null;
692
+
693
+ // Φ0-G: cached_tokens / tokens_in are nullable independently of
694
+ // cost_usd, so we read all three columns but keep cost as the
695
+ // primary filter (a row with only cache telemetry but no $ should
696
+ // not appear in the per-plan totals — cost rules the byPlan list).
697
+ const rows = getDb()
698
+ .prepare(
699
+ `SELECT plan_id, cost_usd, cached_tokens, tokens_in FROM stories
700
+ WHERE project_id = ? AND cost_usd IS NOT NULL`
701
+ )
702
+ .all(projectId);
703
+
704
+ const round4 = (n) => Math.round(n * 10000) / 10000;
705
+ const byPlanMap = new Map();
706
+ let totalUsd = 0;
707
+ let totalCached = 0;
708
+ let totalTokensIn = 0;
709
+ for (const r of rows) {
710
+ const planId = r.plan_id || 'unassigned';
711
+ const cost = Number(r.cost_usd);
712
+ if (!Number.isFinite(cost)) continue;
713
+ totalUsd += cost;
714
+ const cached = Number(r.cached_tokens);
715
+ const tin = Number(r.tokens_in);
716
+ if (Number.isFinite(cached) && cached >= 0) totalCached += cached;
717
+ if (Number.isFinite(tin) && tin >= 0) totalTokensIn += tin;
718
+ const cur = byPlanMap.get(planId) || { planId, totalUsd: 0, huCount: 0, cachedTokens: 0, tokensIn: 0 };
719
+ cur.totalUsd += cost;
720
+ cur.huCount += 1;
721
+ if (Number.isFinite(cached) && cached >= 0) cur.cachedTokens += cached;
722
+ if (Number.isFinite(tin) && tin >= 0) cur.tokensIn += tin;
723
+ byPlanMap.set(planId, cur);
724
+ }
725
+
726
+ const ratio = (cached, tin) => tin > 0 ? Math.round((cached / tin) * 1000) / 10 : null;
727
+ const byPlan = Array.from(byPlanMap.values())
728
+ .map((p) => ({
729
+ planId: p.planId,
730
+ totalUsd: round4(p.totalUsd),
731
+ huCount: p.huCount,
732
+ cachedTokens: p.cachedTokens,
733
+ tokensIn: p.tokensIn,
734
+ cachedRatioPct: ratio(p.cachedTokens, p.tokensIn),
735
+ }))
736
+ .sort((a, b) => b.totalUsd - a.totalUsd);
737
+
738
+ return {
739
+ totalUsd: round4(totalUsd),
740
+ byPlan,
741
+ cachedTokens: totalCached,
742
+ tokensIn: totalTokensIn,
743
+ cachedRatioPct: ratio(totalCached, totalTokensIn),
744
+ unknownModelTokens: { tokensIn: 0, tokensOut: 0, models: [] },
745
+ currency: 'USD',
746
+ };
747
+ }
748
+
749
+ /**
750
+ * Returns full story detail including context requests.
751
+ * @param {string} storyId
752
+ * @returns {object | null}
753
+ */
754
+ export function getStoryDetail(storyId) {
755
+ const story = getDb().prepare('SELECT * FROM stories WHERE id = ?').get(storyId);
756
+ if (!story) return null;
757
+
758
+ const contextRequests = getDb().prepare(
759
+ 'SELECT * FROM context_requests WHERE story_id = ? ORDER BY requested_at DESC'
760
+ ).all(storyId);
761
+
762
+ return { ...story, context_requests: contextRequests };
763
+ }
764
+
765
+ /**
766
+ * Returns sessions for a specific project.
767
+ *
768
+ * The `project_name` column is joined in so the UI can show a human
769
+ * label like `tmp_kj-test-4 · trivial(doc) · 08:35` instead of just
770
+ * the cryptic `s_2026-05-07T...` id.
771
+ *
772
+ * @param {string} projectId
773
+ * @returns {Array<object>}
774
+ */
775
+ export function getSessionsByProject(projectId) {
776
+ return getDb().prepare(`
777
+ SELECT s.id, s.project_id, s.task, s.status, s.created_at, s.updated_at,
778
+ s.iterations, s.duration_ms, s.approved, s.stages_completed,
779
+ p.name AS project_name
780
+ FROM sessions s
781
+ LEFT JOIN projects p ON p.id = s.project_id
782
+ WHERE s.project_id = ?
783
+ ORDER BY s.created_at DESC
784
+ `).all(projectId);
785
+ }
786
+
787
+ /**
788
+ * Returns full session detail (with the joined project_name).
789
+ * @param {string} sessionId
790
+ * @returns {object | null}
791
+ */
792
+ export function getSessionDetail(sessionId) {
793
+ return getDb().prepare(`
794
+ SELECT s.*, p.name AS project_name
795
+ FROM sessions s
796
+ LEFT JOIN projects p ON p.id = s.project_id
797
+ WHERE s.id = ?
798
+ `).get(sessionId);
799
+ }
800
+
801
+ /**
802
+ * Returns global dashboard statistics.
803
+ * @returns {object}
804
+ */
805
+ export function getDashboardStats() {
806
+ const db = getDb();
807
+ const totalStories = db.prepare('SELECT COUNT(*) AS count FROM stories').get().count;
808
+ const certifiedStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'certified'").get().count;
809
+ const pendingStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'pending'").get().count;
810
+ const doneStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'done'").get().count;
811
+ const needsContextStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'needs_context'").get().count;
812
+ const avgQuality = db.prepare('SELECT AVG(quality_total) AS avg FROM stories WHERE quality_total IS NOT NULL').get().avg;
813
+ const totalSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions').get().count;
814
+ const approvedSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE approved = 1').get().count;
815
+ const totalProjects = db.prepare('SELECT COUNT(*) AS count FROM projects').get().count;
816
+
817
+ return {
818
+ total_stories: totalStories,
819
+ certified_stories: certifiedStories,
820
+ pending_stories: pendingStories,
821
+ done_stories: doneStories,
822
+ needs_context_stories: needsContextStories,
823
+ avg_quality: avgQuality ? Math.round(avgQuality * 10) / 10 : null,
824
+ total_sessions: totalSessions,
825
+ approved_sessions: approvedSessions,
826
+ total_projects: totalProjects,
827
+ };
828
+ }
829
+
830
+ /**
831
+ * Inserts a context request for a story.
832
+ * @param {object} req
833
+ */
834
+ export function insertContextRequest(req) {
835
+ getDb().prepare(`
836
+ INSERT INTO context_requests (story_id, fields_needed, question, answer, requested_at, answered_at)
837
+ VALUES (@story_id, @fields_needed, @question, @answer, @requested_at, @answered_at)
838
+ `).run({
839
+ story_id: req.story_id,
840
+ fields_needed: req.fields_needed || null,
841
+ question: req.question || null,
842
+ answer: req.answer || null,
843
+ requested_at: req.requested_at || new Date().toISOString(),
844
+ answered_at: req.answered_at || null,
845
+ });
846
+ }
847
+
848
+ /**
849
+ * Delete a project and all its stories + sessions (cascade).
850
+ * Returns true if the project existed.
851
+ */
852
+ export function deleteProject(projectId) {
853
+ const db = getDb();
854
+ const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(projectId);
855
+ if (!existing) return false;
856
+ db.transaction(() => {
857
+ db.prepare('DELETE FROM stories WHERE project_id = ?').run(projectId);
858
+ db.prepare('DELETE FROM sessions WHERE project_id = ?').run(projectId);
859
+ db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
860
+ })();
861
+ return true;
862
+ }
863
+
864
+ /**
865
+ * Set or clear the `is_test` flag on a project. Drives the
866
+ * ephemeral-cleanup heuristic in ephemeral-cleaner.js:
867
+ * - 1 → always treat as ephemeral (cleanup once last_activity > TTL)
868
+ * - 0 → NEVER auto-clean, regardless of id pattern (user override)
869
+ * - null → follow the default id-pattern heuristic
870
+ *
871
+ * @param {string} projectId
872
+ * @param {number|null} value one of 0, 1, null
873
+ * @returns {boolean} true when the project existed and was updated
874
+ */
875
+ export function setProjectIsTest(projectId, value) {
876
+ if (value !== 0 && value !== 1 && value !== null) {
877
+ throw new Error(`setProjectIsTest: value must be 0, 1, or null (got ${value})`);
878
+ }
879
+ const db = getDb();
880
+ const result = db
881
+ .prepare('UPDATE projects SET is_test = ? WHERE id = ?')
882
+ .run(value, projectId);
883
+ return result.changes > 0;
884
+ }
885
+
886
+ /**
887
+ * Delete a single story.
888
+ */
889
+ export function deleteStory(storyId) {
890
+ const db = getDb();
891
+ const res = db.prepare('DELETE FROM stories WHERE id = ?').run(storyId);
892
+ return res.changes > 0;
893
+ }
894
+
895
+ /**
896
+ * Delete a single session.
897
+ */
898
+ export function deleteSession(sessionId) {
899
+ const db = getDb();
900
+ const res = db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
901
+ return res.changes > 0;
902
+ }
903
+
904
+ // --- Tombstones (KJC-TSK-0380) ---------------------------------------
905
+ // "This (resource_type, resource_id) is dead — don't bring it back." The
906
+ // sync layer consults this table BEFORE every upsert and skips records
907
+ // whose id is buried. Restoration is explicit via removeTombstone().
908
+
909
+ export function addTombstone(resourceType, resourceId, opts = {}) {
910
+ getDb().prepare(`
911
+ INSERT OR REPLACE INTO tombstones (resource_type, resource_id, deleted_at, source, fs_paths)
912
+ VALUES (?, ?, ?, ?, ?)
913
+ `).run(
914
+ resourceType, resourceId,
915
+ new Date().toISOString(),
916
+ opts.source || 'api',
917
+ opts.fsPaths ? JSON.stringify(opts.fsPaths) : null,
918
+ );
919
+ }
920
+
921
+ export function isTombstoned(resourceType, resourceId) {
922
+ return !!getDb().prepare(
923
+ 'SELECT 1 FROM tombstones WHERE resource_type = ? AND resource_id = ? LIMIT 1'
924
+ ).get(resourceType, resourceId);
925
+ }
926
+
927
+ /**
928
+ * Read a single tombstone row (or null). Needed so callers can compare
929
+ * `deleted_at` against a resource's own timestamp before deciding whether
930
+ * to revive it (KJC-BUG-0055).
931
+ */
932
+ export function getTombstone(resourceType, resourceId) {
933
+ const row = getDb().prepare(
934
+ 'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones WHERE resource_type = ? AND resource_id = ?'
935
+ ).get(resourceType, resourceId);
936
+ if (!row) return null;
937
+ return { ...row, fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [] };
938
+ }
939
+
940
+ export function removeTombstone(resourceType, resourceId) {
941
+ return getDb().prepare(
942
+ 'DELETE FROM tombstones WHERE resource_type = ? AND resource_id = ?'
943
+ ).run(resourceType, resourceId).changes > 0;
944
+ }
945
+
946
+ export function listTombstones() {
947
+ return getDb().prepare(
948
+ 'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones ORDER BY deleted_at DESC'
949
+ ).all().map((row) => ({
950
+ ...row,
951
+ fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [],
952
+ }));
953
+ }
954
+
955
+ /**
956
+ * Closes the database connection.
957
+ */
958
+ export function closeDb() {
959
+ if (db) {
960
+ db.close();
961
+ db = null;
962
+ }
963
+ }