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
@@ -84,9 +84,31 @@ router.get("/", async (req, res) => {
84
84
  if (err?.code === "ENOENT") return res.status(503).json({ ok: false, error: "kj not installed", installable: true });
85
85
  return res.status(500).json({ ok: false, error: String(err.message || err) });
86
86
  }
87
- res.json({ ok: true, dir, policy: policySummary(dir), report, anchor: anchorState(dir, report.chain?.length ?? 0), identity: identityState(dir) });
87
+ res.json({ ok: true, dir, policy: policySummary(dir), report, anchor: anchorState(dir, report.chain?.length ?? 0), identity: identityState(dir), lanes: await laneWarnings(dir) });
88
88
  });
89
89
 
90
+ /**
91
+ * KJC-TSK-1314: lanes outside the method. A branch naming no card, and a lane whose PR is merged
92
+ * with nothing after it (a fresh lane also has 0 commits ahead, so the merged PR is what tells).
93
+ */
94
+ async function laneWarnings(dir) {
95
+ const none = (error) => ({ noCard: [], merged: [], error });
96
+ try {
97
+ const r = await runCommand("kj", ["worktree", "list", "--json", "--prs"], { cwd: dir });
98
+ const line = String(r.stdout || "").trim().split("\n").findLast((l) => l.startsWith("["));
99
+ if (r.exitCode !== 0 || !line) return none(`kj worktree list --json failed (exit ${r.exitCode})`);
100
+ const lanes = JSON.parse(line);
101
+ return {
102
+ noCard: lanes.filter((l) => l.cards.length === 0).map(({ slug, branch }) => ({ slug, branch })),
103
+ merged: lanes.filter((l) => l.pr?.state === "MERGED" && l.ahead === 0)
104
+ .map(({ slug, branch, pr }) => ({ slug, branch, pr: pr.number, done: `kj worktree done ${slug}` })),
105
+ error: null,
106
+ };
107
+ } catch (err) {
108
+ return none(String(err.message || err));
109
+ }
110
+ }
111
+
90
112
  // GUI-C (KJC-TSK-0773): actions go through the SAME CLI commands the
91
113
  // terminal uses — the inexemptable (security, defaults.*) is refused by kj
92
114
  // itself and its message travels back verbatim; nothing is re-implemented.
@@ -0,0 +1,40 @@
1
+ // KJC-TSK-1313: the worktree lanes working each card of a project, for the badge on the card.
2
+ // They come from `kj worktree list --json`: read from git every time, never stored, and the board
3
+ // stays decoupled from the CLI tree (KJC-TSK-0632). A failure is data for the view, never a 500.
4
+ import { Router } from 'express';
5
+ import { runCommand } from 'karajan-core/process';
6
+
7
+ import { requestProjectDir } from '../project-dir.js';
8
+
9
+ const router = Router();
10
+
11
+ // KJC-TSK-1316: each lane's PR comes from GitHub; a good answer is kept a minute per project.
12
+ const CACHE_MS = 60_000;
13
+ const cache = new Map();
14
+
15
+ router.get('/:project', async (req, res) => {
16
+ const { project } = req.params;
17
+ const hit = cache.get(project);
18
+ if (hit && Date.now() - hit.at < CACHE_MS) return res.json(hit.body);
19
+ try {
20
+ const dir = requestProjectDir(project);
21
+ const r = await runCommand('kj', ['worktree', 'list', '--json', '--prs'], { cwd: dir });
22
+ const line = String(r.stdout || '').trim().split('\n').findLast((l) => l.startsWith('['));
23
+ if (r.exitCode !== 0 || !line) return res.json({ byStory: {}, error: `kj worktree list --json failed (exit ${r.exitCode})` });
24
+ const byStory = {};
25
+ for (const lane of JSON.parse(line)) {
26
+ // Story ids on the board are `<project>::<hu id>` (sync.js).
27
+ for (const card of lane.cards) {
28
+ const key = `${project}::${card}`;
29
+ byStory[key] ??= [];
30
+ byStory[key].push(lane);
31
+ }
32
+ }
33
+ cache.set(project, { at: Date.now(), body: { byStory } });
34
+ return res.json({ byStory });
35
+ } catch (err) {
36
+ return res.json({ byStory: {}, error: String(err.message || err) });
37
+ }
38
+ });
39
+
40
+ export default router;
@@ -4,7 +4,7 @@
4
4
  // "uninitialized" payload so the dashboard renders an empty state cleanly.
5
5
  import { Router } from "express";
6
6
  import { existsSync, statSync } from "node:fs";
7
- import Database from "better-sqlite3";
7
+ import { openDatabase } from "karajan-core/db";
8
8
  import { dbPath } from "karajan-core/vec-store";
9
9
  import { readConfig } from "../config-yaml.js";
10
10
 
@@ -23,8 +23,9 @@ router.get("/stats", (_req, res) => {
23
23
  const path = dbPath();
24
24
  if (!existsSync(path)) return res.json({ ok: true, initialized: false, message: "rag.db not initialized — run `kj rag index <dir>` to start indexing.", embedder: embedder() });
25
25
  try {
26
- const db = new Database(path, { readonly: true, fileMustExist: true });
27
- // KJC-BUG-0068 — readonly handler cannot run the project_slug migration
26
+ // KJC-TSK-0617: node:sqlite adapter. The file was checked above and this route only reads.
27
+ const db = openDatabase(path);
28
+ // KJC-BUG-0068 — this route never runs the project_slug migration
28
29
  // (KJC-TSK-0438). Legacy DBs created pre-v2.27 lack the column; detect
29
30
  // and fall back so the dashboard renders instead of crashing with 500.
30
31
  const hasProjectSlug = db.prepare("PRAGMA table_info(chunks)").all().some((c) => c.name === "project_slug");
@@ -11,6 +11,7 @@ import pipelineRoutes from './routes/pipeline.js';
11
11
  import ragRoutes from './routes/rag.js';
12
12
  import wikiRoutes from './routes/wiki.js';
13
13
  import governanceRoutes from './routes/governance.js';
14
+ import lanesRoutes from './routes/lanes.js';
14
15
  import { authMiddleware } from './auth.js';
15
16
  import { getOrCreateToken, getTokenPath } from './token-store.js';
16
17
  import { createRequire } from 'node:module';
@@ -165,21 +166,29 @@ export function buildRateLimiter() {
165
166
  });
166
167
  }
167
168
 
169
+ /**
170
+ * KJC-BUG-0463: mounted on /api after every API router, so an API route that does not
171
+ * exist gets a 404 JSON naming it instead of falling to the SPA's index.html (a 200
172
+ * the caller then fails to parse).
173
+ * @type {import('express').RequestHandler}
174
+ */
175
+ export function apiNotFound(req, res) {
176
+ res.status(404).json({ error: `no such API route: ${req.method} ${req.originalUrl}` });
177
+ }
178
+
168
179
  /**
169
180
  * Main entry point: initializes database, syncs data, and starts the server.
170
181
  */
171
182
  async function main() {
172
- // Initialize SQLite. better-sqlite3 is a native module — when its
173
- // prebuilt binary is missing or ABI-mismatched (Node was upgraded),
174
- // initDb() throws. Surface an actionable message instead of a bare
175
- // stack trace so the user knows exactly what to run.
183
+ // Initialize SQLite (node:sqlite, built into Node 22+: KJC-TSK-0617). When
184
+ // initDb() throws, surface an actionable message instead of a bare stack trace.
176
185
  console.log('[server] Initializing database...');
177
186
  try {
178
187
  initDb();
179
188
  } catch (err) {
180
189
  console.error('[server] FATAL: could not initialize the database.');
181
- console.error('[server] The native module better-sqlite3 failed to load, or the DB file could not be opened.');
182
- console.error('[server] Fix: run `npm rebuild better-sqlite3` (or reinstall karajan-code).');
190
+ console.error('[server] The DB file could not be opened, or this Node has no node:sqlite (it needs Node 22.12 or newer).');
191
+ console.error('[server] Fix: check the path in the error below, or upgrade Node.');
183
192
  console.error('[server] Original error:', err?.stack || String(err));
184
193
  process.exit(1);
185
194
  }
@@ -235,34 +244,6 @@ async function main() {
235
244
  console.warn(`[hu-zombie-reaper] skipped: ${err.message}`);
236
245
  }
237
246
 
238
- // KJC-TSK-0414 PR2 — reconciliación de sesiones hibernadas al arrancar.
239
- // Para cada session en ~/.kj/standby/<id>.json:
240
- // - cooldownUntil <= now → spawn `kj resume <id>` inmediato
241
- // - cooldownUntil > now → re-programa setTimeout que dispara en cooldown
242
- // Idempotente (lockfile per session). Cero polling.
243
- try {
244
- const { reconcileAll } = await import('karajan-core/standby-scheduler');
245
- const { spawn } = await import('node:child_process');
246
- const { join, dirname } = await import('node:path');
247
- const { fileURLToPath } = await import('node:url');
248
- const KJ_CLI = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'src', 'cli.js');
249
- const onResume = (sessionId) => {
250
- console.log(`[standby] reanudando sesión ${sessionId} (spawn kj standby resume)`);
251
- try {
252
- const child = spawn(process.execPath, [KJ_CLI, 'standby', 'resume', sessionId], {
253
- detached: true, stdio: 'ignore', env: process.env,
254
- });
255
- child.unref();
256
- } catch (err) {
257
- console.warn(`[standby] spawn resume falló para ${sessionId}: ${err.message}`);
258
- }
259
- };
260
- const r = reconcileAll({ onResume, logger: { info: console.log.bind(console), warn: console.warn.bind(console) } });
261
- if (r.total > 0) console.log(`[standby] reconcile: ${r.total} session(es) — ${r.immediate} resume inmediato, ${r.scheduled} programadas`);
262
- } catch (err) {
263
- console.warn(`[standby] reconcile skipped: ${err.message}`);
264
- }
265
-
266
247
  // KJC-TSK-0371 (board polish #3) — clean up ephemeral projects
267
248
  // (`/tmp/`-rooted, `tmp_*`/`test_*`/`demo_*`/`kj-test-*` prefixed)
268
249
  // that have been inactive for >24h. Runs AFTER the zombie-reaper
@@ -308,6 +289,7 @@ async function main() {
308
289
  app.use('/api/rag', authMiddleware(), ragRoutes);
309
290
  app.use('/api/wiki', authMiddleware(), wikiRoutes);
310
291
  app.use('/api/governance', authMiddleware(), governanceRoutes);
292
+ app.use('/api/lanes', authMiddleware(), lanesRoutes);
311
293
 
312
294
  // Terminal embebida (KJC-TSK-0816, ADR 0008): la lógica vive en
313
295
  // terminal.js; aquí solo el montaje. El agente arranca en el cwd del
@@ -327,6 +309,10 @@ async function main() {
327
309
  '/vendor/xterm',
328
310
  express.static(dirname(requireCjs.resolve('@xterm/xterm/package.json'))),
329
311
  );
312
+ // KJC-TSK-1198: driver.js (MIT, pinned) for the guided tour, from the dependency's dist/.
313
+ app.use('/vendor/driver', express.static(dirname(requireCjs.resolve('driver.js'))));
314
+
315
+ app.use('/api', apiNotFound);
330
316
 
331
317
  // SPA fallback: serve index.html for non-API, non-static routes
332
318
  app.get('/{*splat}', (_req, res) => {
@@ -0,0 +1,318 @@
1
+ // KJC-TSK-0542: how one file on disk becomes rows in the board DB: a session.json or a v2 plan.
2
+ // sync.js runs them from fullScan and the watcher, and re-exports syncPlanFile.
3
+ import { readFileSync, rmSync } from 'node:fs';
4
+ import { dirname, join, basename } from 'node:path';
5
+ import {
6
+ upsertProject,
7
+ upsertStory,
8
+ upsertSession,
9
+ getDb,
10
+ isTombstoned,
11
+ getTombstone,
12
+ removeTombstone,
13
+ } from './db.js';
14
+ import { publish as publishEvent } from './event-bus.js';
15
+ import { isSharedPath } from 'karajan-core/shared-paths';
16
+ import {
17
+ deriveProjectIdFromDir,
18
+ canonicalProjectDir,
19
+ titleCaseBasename,
20
+ slugToTitle,
21
+ } from './sync-project.js';
22
+
23
+ /**
24
+ * Skip + best-effort fs cleanup when the resource is tombstoned. Returns
25
+ * true if the caller should bail out (resource is dead).
26
+ *
27
+ * KJC-TSK-0380: without this, every fullScan/chokidar event resurrects
28
+ * resources the user already deleted via the API.
29
+ */
30
+ function skipIfTombstoned(resourceType, resourceId, fsPathToRemove) {
31
+ if (!isTombstoned(resourceType, resourceId)) return false;
32
+ if (fsPathToRemove) {
33
+ try { rmSync(fsPathToRemove, { recursive: true, force: true }); } catch { /* best effort */ }
34
+ }
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * Parses a session.json file and syncs to SQLite.
40
+ * @param {string} filePath - Absolute path to session.json
41
+ */
42
+ export function syncSessionFile(filePath) {
43
+ try {
44
+ const raw = readFileSync(filePath, 'utf-8');
45
+ const data = JSON.parse(raw);
46
+ const sessionId = data.id || basename(join(filePath, '..'));
47
+
48
+ if (skipIfTombstoned('session', sessionId, dirname(filePath))) return;
49
+
50
+ // Resolve which project this session belongs to:
51
+ // 1. If a plan / HU batch already created `auto-<sessionId>`, reuse it
52
+ // so plan-based runs group with their HU batch.
53
+ // 2. Otherwise fall back to the session's declared `project_id`.
54
+ // 3. Otherwise → REJECT. Orphan sessions (no project_id and no batch)
55
+ // USED to be promoted to a fake "Orphan sessions" project, which
56
+ // violated the user's invariant: "Si se crea proyecto y se crean
57
+ // hus se asocian a proyecto. ES IMPOSIBLE QUE [no exista esa
58
+ // asociación]". Now we skip them entirely. The fix for the source
59
+ // (kj run always stamps project_id derived from cwd) lives in
60
+ // PR-F; this is the safety net so a leftover orphan can never
61
+ // again appear as a phantom project on the board.
62
+ const autoProjectId = `auto-${sessionId}`;
63
+ const db = getDb();
64
+ const existingBatch = db.prepare('SELECT id FROM projects WHERE id = ?').get(autoProjectId);
65
+ let projectId;
66
+ if (existingBatch) {
67
+ projectId = autoProjectId;
68
+ } else if (data.project_id) {
69
+ projectId = data.project_id;
70
+ } else {
71
+ // Orphan: log + skip. Never promote to "default".
72
+ console.warn(`[sync] Session ${sessionId} has no project_id and no matching batch — skipping (orphan). File: ${filePath}`);
73
+ return;
74
+ }
75
+
76
+ // Ensure the project row exists. Skip when an auto-batch owns it already
77
+ // (that row was written with richer metadata and we do
78
+ // not want to overwrite name/total_stories).
79
+ if (!existingBatch) {
80
+ upsertProject({
81
+ id: projectId,
82
+ name: projectId,
83
+ last_activity: data.updated_at || data.created_at || new Date().toISOString(),
84
+ total_stories: 0,
85
+ });
86
+ }
87
+
88
+ // Calculate iterations from checkpoints
89
+ const checkpoints = data.checkpoints || [];
90
+ const maxIteration = checkpoints.reduce((max, cp) => Math.max(max, cp.iteration || 0), 0);
91
+
92
+ // Calculate duration from first and last checkpoint
93
+ let durationMs = null;
94
+ if (data.created_at && data.updated_at) {
95
+ durationMs = new Date(data.updated_at).getTime() - new Date(data.created_at).getTime();
96
+ }
97
+
98
+ // Collect completed stages
99
+ const stagesSet = new Set();
100
+ for (const cp of checkpoints) {
101
+ if (cp.stage && !cp.stage.includes('attempt') && !cp.stage.includes('checkpoint')) {
102
+ stagesSet.add(cp.stage);
103
+ }
104
+ }
105
+
106
+ // Project row was created above (unless an auto-batch owns it); the
107
+ // session always has a project to attach to now.
108
+
109
+ upsertSession({
110
+ id: sessionId,
111
+ project_id: projectId,
112
+ task: data.task || null,
113
+ status: data.status || 'unknown',
114
+ created_at: data.created_at,
115
+ updated_at: data.updated_at,
116
+ iterations: maxIteration,
117
+ duration_ms: durationMs,
118
+ approved: data.status === 'approved' ? 1 : 0,
119
+ commits: data.commits ? JSON.stringify(data.commits) : null,
120
+ stages_completed: JSON.stringify([...stagesSet]),
121
+ checkpoints: JSON.stringify(checkpoints),
122
+ llm_calls: data.llm_calls ? JSON.stringify(data.llm_calls) : null,
123
+ config_snapshot: data.config_snapshot ? JSON.stringify(data.config_snapshot) : null,
124
+ budget: data.budget ? JSON.stringify(data.budget) : null,
125
+ });
126
+
127
+ console.log(`[sync] Synced session ${sessionId}: status=${data.status}, iterations=${maxIteration}`);
128
+ publishEvent({ type: 'session', projectId, sessionId });
129
+ } catch (err) {
130
+ console.error(`[sync] Error syncing session file ${filePath}:`, err.message);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Sync a v2 plan file (with embedded HUs) to SQLite.
136
+ * Plans are the authoritative source — replaces hu-stories for plan-based runs.
137
+ */
138
+ export function syncPlanFile(filePath) {
139
+ try {
140
+ const raw = readFileSync(filePath, 'utf-8');
141
+ const data = JSON.parse(raw);
142
+ // KJC-BUG-0124 (issue #1277): a strict `version !== 2` silently hid
143
+ // every plan whose version drifted — the board showed 0 stories with no
144
+ // trace, and users concluded their work was lost. Tolerant now: any
145
+ // v2+ plan syncs, with a LOUD warn on version drift (fired before the
146
+ // empty check so drift is visible even on empty plans). Pre-v2 plans
147
+ // keep their historical silent skip (they never synced and carry no
148
+ // hus[]), and an empty v2 plan has nothing to sync — skipping it
149
+ // loses nothing.
150
+ if (typeof data.version !== 'number' || data.version < 2) return;
151
+ if (data.version > 2) {
152
+ console.warn(`[sync] ${filePath}: plan version ${data.version} (expected 2) — syncing anyway; re-save with current kj to normalize`);
153
+ }
154
+ if (!Array.isArray(data.hus) || data.hus.length === 0) return;
155
+
156
+ // Bug fix: pre-patch, `projectId = data.planId` — every plan was a separate
157
+ // "project" on the board, so 2 464 plans across the same repo produced
158
+ // 2 464 boxes in the UI instead of one. Now we derive the project from
159
+ // the stamped `projectDir` so every plan for the same source tree groups
160
+ // under a single board entry. Legacy plans without `projectDir` still
161
+ // fall back to `planId` (conservative — they at least continue to show
162
+ // up, just each on its own card).
163
+ // KJC-BUG-0160: la identidad es el ROOT del repo — un plan creado desde
164
+ // un worktree/carril agrupa bajo el mismo proyecto que el árbol principal.
165
+ const canonicalDir = data.projectDir ? canonicalProjectDir(data.projectDir) : null;
166
+ const projectId = canonicalDir ? deriveProjectIdFromDir(canonicalDir) : data.planId;
167
+
168
+ if (skipIfTombstoned('plan', data.planId, filePath)) return;
169
+ // KJC-BUG-0055 (supersedes KJC-BUG-0050): gating por timestamp.
170
+ // El "fix" anterior quitaba el tombstone del proyecto en CUALQUIER
171
+ // llegada de plan — eso resucitaba proyectos borrados cuando un plan
172
+ // anterior reaparecía (DB nueva + plan-files viejos en disco) o
173
+ // cuando un fullScan posterior los re-encontraba. Comportamiento:
174
+ // • plan.updatedAt/createdAt > tombstone.deleted_at → el usuario
175
+ // ha generado el plan DESPUÉS de borrar el proyecto → revivir.
176
+ // • plan.updatedAt/createdAt <= tombstone.deleted_at → plan stale
177
+ // pre-existente al borrado → respetar el tombstone y borrar el
178
+ // fichero en disco para que no vuelva a re-importarse.
179
+ const projectTomb = getTombstone('project', projectId);
180
+ if (projectTomb) {
181
+ const planTime = data.updatedAt || data.createdAt || null;
182
+ const planTs = planTime ? Date.parse(planTime) : NaN;
183
+ const tombTs = Date.parse(projectTomb.deleted_at);
184
+ if (Number.isFinite(planTs) && planTs > tombTs) {
185
+ console.log(`[sync] Project ${projectId} resucitado: plan ${data.planId} es más reciente (${planTime}) que el tombstone (${projectTomb.deleted_at}).`);
186
+ removeTombstone('project', projectId);
187
+ } else {
188
+ console.log(`[sync] Plan ${data.planId} ignorado: proyecto ${projectId} tombstoned (${projectTomb.deleted_at}); borrando ${filePath}.`);
189
+ try { rmSync(filePath, { force: true }); } catch { /* best effort */ }
190
+ return;
191
+ }
192
+ }
193
+ // Human-friendly project name resolution order (most-specific first):
194
+ // 1. plan.name explicitly set by the user / planner ("Linux Assistant
195
+ // Orchestrator" — preserved across renames done from the board).
196
+ // 2. titleCaseBasename(projectDir) — derived from the cwd of the
197
+ // original kj plan invocation. NOTE: this is the SUBCARPETA of the
198
+ // projectDir, NOT necessarily where the work happens. If the user
199
+ // ran `kj plan` from inside `…/karajan-code/packages/hu-board/`
200
+ // with a SPEC about "Linux Assistant", the basename here would
201
+ // misleadingly say "Hu Board" — that's why plan.name wins.
202
+ // 3. slugToTitle of the task / projectId as a last resort.
203
+ // KJC-BUG-0162: the default backlog plan is named "brain-backlog" — a
204
+ // DEFAULT, not a user choice. Every project synced through it ended up
205
+ // named "brain-backlog", indistinguishable in the picker. Only a real,
206
+ // user-given plan name may override the repo-derived name.
207
+ const userGivenName = data.name && data.name !== 'brain-backlog' ? data.name : null;
208
+ const projectName = userGivenName
209
+ || (canonicalDir ? titleCaseBasename(basename(canonicalDir)) : null)
210
+ || slugToTitle(data.task || '')
211
+ || projectId;
212
+
213
+ // KJC-PRP-0002 PR3: stamp `is_shared` from the path (authoritative — works
214
+ // even for legacy plan files without the `shared:true` envelope marker).
215
+ // OR with existing value so a single shared plan promotes the project,
216
+ // while unsharing the LAST shared plan still leaves the flag set (we
217
+ // clear it lazily only when the user explicitly tombstones / re-creates).
218
+ upsertProject({
219
+ id: projectId,
220
+ name: projectName,
221
+ last_activity: data.updatedAt || data.createdAt || new Date().toISOString(),
222
+ total_stories: data.hus.length,
223
+ is_shared: isSharedPath(filePath) || data.shared === true ? 1 : undefined,
224
+ });
225
+
226
+ for (let i = 0; i < data.hus.length; i += 1) {
227
+ const hu = data.hus[i];
228
+ const acList = Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [];
229
+ // Only count tests that carry real content. `kj plan` stamps a
230
+ // placeholder like `["npx vitest run ..."]` on every HU — those
231
+ // DO count as 1 test each. But empty strings / null entries
232
+ // (which show up in half-baked plans) must not inflate the
233
+ // badge to "🧪 1 test" on a HU that effectively has none.
234
+ // Drop empty entries but accept BOTH legacy shapes and the
235
+ // v2.7.5 structured form. The original anti-junk filter only
236
+ // knew the legacy shape (`name|title|description|given|gherkin|
237
+ // scope`) and silently nuked every test produced by the
238
+ // tests-synthesizer (which emits `{type, content, file?}`),
239
+ // leaving every HU flagged "missing test contract" on the
240
+ // board even though the plan JSON had 4 tests apiece.
241
+ const testList = (Array.isArray(hu.acceptance_tests) ? hu.acceptance_tests : [])
242
+ .filter((t) => {
243
+ if (!t) return false;
244
+ if (typeof t === 'string') return t.trim().length > 0;
245
+ if (typeof t !== 'object') return false;
246
+ // v2.7.5 structured form — primary shape going forward.
247
+ if (typeof t.content === 'string' && t.content.trim().length > 0) return true;
248
+ // Legacy shapes kept for back-compat with hand-edited plans.
249
+ return Boolean(t.name || t.title || t.description || t.given || t.gherkin || t.scope);
250
+ });
251
+ const blockedByList = Array.isArray(hu.blocked_by) ? hu.blocked_by : [];
252
+ const acText = acList.length > 0 ? JSON.stringify(acList) : null;
253
+
254
+ upsertStory({
255
+ id: `${projectId}::${hu.id}`,
256
+ project_id: projectId,
257
+ session_id: projectId,
258
+ status: hu.status || 'pending',
259
+ title: hu.title || hu.id,
260
+ original_text: hu.scope || null,
261
+ certified_as: null,
262
+ certified_want: hu.scope || null,
263
+ certified_so_that: null,
264
+ quality_total: null,
265
+ quality_d1: null, quality_d2: null, quality_d3: null,
266
+ quality_d4: null, quality_d5: null, quality_d6: null,
267
+ antipatterns: null,
268
+ ac_format: acText ? 'list' : null,
269
+ acceptance_criteria: acText,
270
+ created_at: hu.createdAt,
271
+ updated_at: hu.updatedAt,
272
+ certified_at: null,
273
+ // Stamp plan_id so the PATCH / mark-ready endpoints can locate
274
+ // the source plan JSON without walking the plans dir.
275
+ plan_id: data.planId,
276
+ // Denormalised counters + deps so the card renders "3 ACs ·
277
+ // 2 tests · waits for lao-001" without every client parsing JSON.
278
+ ac_count: acList.length,
279
+ test_count: testList.length,
280
+ blocked_by: blockedByList.length > 0 ? JSON.stringify(blockedByList) : null,
281
+ // Full test list goes to SQLite too so the modal can show
282
+ // "🧪 2 tests" next to the list. Pre-patch the card showed
283
+ // test_count but the modal had nothing to render.
284
+ acceptance_tests: testList.length > 0 ? JSON.stringify(testList) : null,
285
+ // Index in plan.hus[] — used by getStoriesByProject to order
286
+ // cards topologically (roots first, dependents last).
287
+ plan_order: i,
288
+ // Spec mapping (PR C): "implements §5.3" trace pointer.
289
+ spec_section: typeof hu.spec_section === 'string' && hu.spec_section.trim() ? hu.spec_section.trim() : null,
290
+ // PR3: per-HU outcome (iterations, duration, commits,
291
+ // blockers, summary). Stored as a JSON string in SQLite —
292
+ // the front parses on read. Null until hu-sub-pipeline
293
+ // stamps it at the end of each HU.
294
+ outcome: hu.outcome ? JSON.stringify(hu.outcome) : null,
295
+ // KJC-TSK-0394: result (pass|fail|partial|null) ortogonal al status.
296
+ // Distinto de `outcome` (blob JSON) — esto es el enum corto para
297
+ // badge ✓/✗ en la card.
298
+ result: hu.result || null,
299
+ // KJC-TSK-0406: coder_model y reviewer_model por HU. Reflejan
300
+ // qué modelo se va a usar (o se usó) en este HU. El modal del
301
+ // board los muestra y permite cambiarlos antes de run.
302
+ coder_model: hu.coder_model || null,
303
+ reviewer_model: hu.reviewer_model || null,
304
+ coder_provider: hu.coder_provider || null,
305
+ reviewer_provider: hu.reviewer_provider || null,
306
+ // KJC-PRP-0002 PR6: optional handle of the owner of this HU
307
+ // (free-form string). Only surfaced in UI when project is_shared,
308
+ // but stored unconditionally so values survive share/unshare flips.
309
+ assignee: typeof hu.assignee === 'string' && hu.assignee.trim() ? hu.assignee.trim() : null,
310
+ });
311
+ }
312
+
313
+ console.log(`[sync] Plan ${projectId}: ${data.hus.length} HUs (${projectName})`);
314
+ publishEvent({ type: 'plan', projectId, planId: data.planId });
315
+ } catch (err) {
316
+ console.error(`[sync] Failed to sync plan ${filePath}: ${err.message}`);
317
+ }
318
+ }
@@ -0,0 +1,99 @@
1
+ // KJC-TSK-0542: how the sync names a project (id, canonical dir, display titles).
2
+ // sync.js re-exports the public functions, so callers keep importing from there.
3
+ import { execFileSync } from 'node:child_process';
4
+ import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
5
+
6
+ /**
7
+ * Derive a stable `project_id` from a plan's absolute `projectDir`.
8
+ * Used so every plan under the same source tree groups under one project
9
+ * on the board (instead of 1 project per plan — the 2 101-stories bug).
10
+ *
11
+ * @param {string} projectDir
12
+ * @returns {string}
13
+ */
14
+ export function deriveProjectIdFromDir(projectDir) {
15
+ if (!projectDir || typeof projectDir !== 'string') return 'unknown';
16
+ // Same slug rule the plan-store uses, so identifiers match if we ever
17
+ // cross-reference `~/.kj/plans/<slug>/` with this project_id.
18
+ return projectDir
19
+ .replace(/^\//, '')
20
+ .replace(/[/\\]/g, '_')
21
+ .replace(/[^a-zA-Z0-9_.-]/g, '')
22
+ .slice(0, 120);
23
+ }
24
+
25
+ /** git-common-dir del directorio, con el stderr silenciado (no-git = throw). */
26
+ const defaultGitCommonDir = (dir) =>
27
+ execFileSync('git', ['-C', dir, 'rev-parse', '--git-common-dir'], {
28
+ encoding: 'utf8',
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ }).trim();
31
+
32
+ const canonicalDirCache = new Map();
33
+
34
+ /**
35
+ * Resolve a projectDir to the REPO's identity (KJC-BUG-0160): a linked
36
+ * worktree (`.claude/worktrees/*`, kj lanes) used to register the same
37
+ * repo as a brand-new project — 13+ same-named entries in a real picker.
38
+ * `git rev-parse --git-common-dir` points every worktree at the main
39
+ * tree's `.git`; its parent is the canonical root. A non-git directory
40
+ * keeps its own path as identity, as always.
41
+ *
42
+ * @param {string} projectDir
43
+ * @param {(dir: string) => string} [gitCommonDir] Inyectable en tests.
44
+ * @returns {string}
45
+ */
46
+ export function canonicalProjectDir(projectDir, gitCommonDir = defaultGitCommonDir) {
47
+ if (!projectDir || typeof projectDir !== 'string') return projectDir;
48
+ if (canonicalDirCache.has(projectDir)) return canonicalDirCache.get(projectDir);
49
+ let canonical = projectDir;
50
+ try {
51
+ const common = gitCommonDir(projectDir);
52
+ const absCommon = isAbsolute(common) ? common : resolvePath(projectDir, common);
53
+ canonical = dirname(absCommon);
54
+ } catch {
55
+ // Sin git no hay más identidad que la ruta — comportamiento de siempre.
56
+ }
57
+ canonicalDirCache.set(projectDir, canonical);
58
+ return canonical;
59
+ }
60
+
61
+ /**
62
+ * Turn a directory basename into a human-friendly Title Case string,
63
+ * preserving every meaningful word. Unlike `slugToTitle`, this one does
64
+ * NOT apply stopword filtering — directory names are concise on purpose
65
+ * (e.g. "linux-assistant-orchestrator" → "Linux Assistant Orchestrator",
66
+ * "my-dev-tool" → "My Dev Tool").
67
+ *
68
+ * @param {string} name
69
+ * @returns {string}
70
+ */
71
+ export function titleCaseBasename(name) {
72
+ const words = String(name || '')
73
+ .replace(/[_\s]+/g, '-')
74
+ .split('-')
75
+ .map((w) => w.replace(/[^\p{L}\p{N}]/gu, ''))
76
+ .filter(Boolean);
77
+ if (words.length === 0) return String(name || '');
78
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
79
+ }
80
+
81
+ export function slugToTitle(text) {
82
+ const STOPWORDS = new Set([
83
+ 'a', 'an', 'the', 'and', 'or', 'with', 'for', 'to', 'of', 'in', 'on',
84
+ 'is', 'it', 'its', 'this', 'that', 'these', 'those', 'be', 'been', 'being',
85
+ 'build', 'create', 'implement', 'make', 'develop', 'add', 'set', 'up',
86
+ 'setup', 'write', 'code', 'new', 'complete', 'from', 'scratch',
87
+ 'application', 'app', 'tool', 'system', 'project', 'using', 'use',
88
+ 'full', 'full-stack', 'fullstack', 'stack', 'based', 'simple', 'basic',
89
+ ]);
90
+ const words = String(text || '')
91
+ .toLowerCase()
92
+ .replace(/[^\p{L}\p{N}\s-]/gu, ' ')
93
+ .split(/\s+/)
94
+ .filter((w) => w && !STOPWORDS.has(w));
95
+ const meaningful = words.slice(0, 6);
96
+ if (meaningful.length === 0) return String(text).slice(0, 60).trim();
97
+ const titled = meaningful.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
98
+ return titled.length > 60 ? titled.slice(0, 57) + '...' : titled;
99
+ }