karajan-code 4.70.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +34 -3
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -1,22 +1,17 @@
1
1
  import { watch } from 'chokidar';
2
- import { execFileSync } from 'node:child_process';
3
2
  import { readFileSync, readdirSync, existsSync, rmSync } from 'node:fs';
4
- import { dirname, join, basename, isAbsolute, resolve as resolvePath } from 'node:path';
3
+ import { join } from 'node:path';
5
4
  import { homedir } from 'node:os';
6
- import {
7
- getKjHome,
8
- upsertProject,
9
- upsertStory,
10
- upsertSession,
11
- insertContextRequest,
12
- getDb,
13
- isTombstoned,
14
- getTombstone,
15
- removeTombstone,
16
- } from './db.js';
5
+ import { getKjHome, getDb, isTombstoned } from './db.js';
17
6
  import { publish as publishEvent } from './event-bus.js';
18
- import { getSharedPlansDir, SHARED_DIR_NAME, isSharedPath } from 'karajan-core/shared-paths';
7
+ import { getSharedPlansDir, SHARED_DIR_NAME } from 'karajan-core/shared-paths';
19
8
  import { readConfig } from './config-yaml.js';
9
+ import { syncSessionFile, syncPlanFile } from './sync-files.js';
10
+
11
+ // KJC-TSK-0542: the project naming lives in sync-project.js and the per-file sync in
12
+ // sync-files.js; callers keep importing them from here.
13
+ export { deriveProjectIdFromDir, canonicalProjectDir } from './sync-project.js';
14
+ export { syncPlanFile };
20
15
 
21
16
  /**
22
17
  * KJC-PRP-0002 PR5: resolve the conflict policy that decides who wins
@@ -46,536 +41,13 @@ function getSharedConflictPolicy() {
46
41
  return 'shared-wins';
47
42
  }
48
43
 
49
- /**
50
- * Skip + best-effort fs cleanup when the resource is tombstoned. Returns
51
- * true if the caller should bail out (resource is dead).
52
- *
53
- * KJC-TSK-0380: without this, every fullScan/chokidar event resurrects
54
- * resources the user already deleted via the API.
55
- */
56
- function skipIfTombstoned(resourceType, resourceId, fsPathToRemove) {
57
- if (!isTombstoned(resourceType, resourceId)) return false;
58
- if (fsPathToRemove) {
59
- try { rmSync(fsPathToRemove, { recursive: true, force: true }); } catch { /* best effort */ }
60
- }
61
- return true;
62
- }
63
-
64
- /**
65
- * Derive a human-readable project name from batch data.
66
- * Priority: explicit data.project_name → extracted from story text → sessionId.
67
- */
68
- function deriveProjectName(data, fallbackId) {
69
- if (data.project_name) return data.project_name;
70
- // Try to find "Part of: <originalTask>" in any story's certified text
71
- const stories = data.stories || [];
72
- for (const story of stories) {
73
- const text = story.certified?.text || '';
74
- const match = text.match(/Part of:\s*([^\n]+)/);
75
- if (match) {
76
- return slugToTitle(match[1].trim());
77
- }
78
- }
79
- // Try first story's original text as a fallback
80
- if (stories[0]?.original?.text) {
81
- return slugToTitle(stories[0].original.text);
82
- }
83
- return fallbackId;
84
- }
85
-
86
- /**
87
- * Turn a free-text goal into a short title-cased name (max 60 chars).
88
- */
89
- /**
90
- * Derive a stable `project_id` from a plan's absolute `projectDir`.
91
- * Used so every plan under the same source tree groups under one project
92
- * on the board (instead of 1 project per plan — the 2 101-stories bug).
93
- *
94
- * @param {string} projectDir
95
- * @returns {string}
96
- */
97
- export function deriveProjectIdFromDir(projectDir) {
98
- if (!projectDir || typeof projectDir !== 'string') return 'unknown';
99
- // Same slug rule the plan-store uses, so identifiers match if we ever
100
- // cross-reference `~/.kj/plans/<slug>/` with this project_id.
101
- return projectDir
102
- .replace(/^\//, '')
103
- .replace(/[/\\]/g, '_')
104
- .replace(/[^a-zA-Z0-9_.-]/g, '')
105
- .slice(0, 120);
106
- }
107
-
108
- /** git-common-dir del directorio, con el stderr silenciado (no-git = throw). */
109
- const defaultGitCommonDir = (dir) =>
110
- execFileSync('git', ['-C', dir, 'rev-parse', '--git-common-dir'], {
111
- encoding: 'utf8',
112
- stdio: ['ignore', 'pipe', 'ignore'],
113
- }).trim();
114
-
115
- const canonicalDirCache = new Map();
116
-
117
- /**
118
- * Resolve a projectDir to the REPO's identity (KJC-BUG-0160): a linked
119
- * worktree (`.claude/worktrees/*`, kj lanes) used to register the same
120
- * repo as a brand-new project — 13+ same-named entries in a real picker.
121
- * `git rev-parse --git-common-dir` points every worktree at the main
122
- * tree's `.git`; its parent is the canonical root. A non-git directory
123
- * keeps its own path as identity, as always.
124
- *
125
- * @param {string} projectDir
126
- * @param {(dir: string) => string} [gitCommonDir] Inyectable en tests.
127
- * @returns {string}
128
- */
129
- export function canonicalProjectDir(projectDir, gitCommonDir = defaultGitCommonDir) {
130
- if (!projectDir || typeof projectDir !== 'string') return projectDir;
131
- if (canonicalDirCache.has(projectDir)) return canonicalDirCache.get(projectDir);
132
- let canonical = projectDir;
133
- try {
134
- const common = gitCommonDir(projectDir);
135
- const absCommon = isAbsolute(common) ? common : resolvePath(projectDir, common);
136
- canonical = dirname(absCommon);
137
- } catch {
138
- // Sin git no hay más identidad que la ruta — comportamiento de siempre.
139
- }
140
- canonicalDirCache.set(projectDir, canonical);
141
- return canonical;
142
- }
143
-
144
- /**
145
- * Turn a directory basename into a human-friendly Title Case string,
146
- * preserving every meaningful word. Unlike `slugToTitle`, this one does
147
- * NOT apply stopword filtering — directory names are concise on purpose
148
- * (e.g. "linux-assistant-orchestrator" → "Linux Assistant Orchestrator",
149
- * "my-dev-tool" → "My Dev Tool").
150
- *
151
- * @param {string} name
152
- * @returns {string}
153
- */
154
- function titleCaseBasename(name) {
155
- const words = String(name || '')
156
- .replace(/[_\s]+/g, '-')
157
- .split('-')
158
- .map((w) => w.replace(/[^\p{L}\p{N}]/gu, ''))
159
- .filter(Boolean);
160
- if (words.length === 0) return String(name || '');
161
- return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
162
- }
163
-
164
- function slugToTitle(text) {
165
- const STOPWORDS = new Set([
166
- 'a', 'an', 'the', 'and', 'or', 'with', 'for', 'to', 'of', 'in', 'on',
167
- 'is', 'it', 'its', 'this', 'that', 'these', 'those', 'be', 'been', 'being',
168
- 'build', 'create', 'implement', 'make', 'develop', 'add', 'set', 'up',
169
- 'setup', 'write', 'code', 'new', 'complete', 'from', 'scratch',
170
- 'application', 'app', 'tool', 'system', 'project', 'using', 'use',
171
- 'full', 'full-stack', 'fullstack', 'stack', 'based', 'simple', 'basic',
172
- ]);
173
- const words = String(text || '')
174
- .toLowerCase()
175
- .replace(/[^\p{L}\p{N}\s-]/gu, ' ')
176
- .split(/\s+/)
177
- .filter((w) => w && !STOPWORDS.has(w));
178
- const meaningful = words.slice(0, 6);
179
- if (meaningful.length === 0) return String(text).slice(0, 60).trim();
180
- const titled = meaningful.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
181
- return titled.length > 60 ? titled.slice(0, 57) + '...' : titled;
182
- }
183
-
184
- /**
185
- * Parses a batch.json HU stories file and syncs to SQLite.
186
- * @param {string} filePath - Absolute path to the batch.json file
187
- */
188
- function syncStoryFile(filePath) {
189
- try {
190
- const raw = readFileSync(filePath, 'utf-8');
191
- const data = JSON.parse(raw);
192
- const sessionId = data.session_id || basename(join(filePath, '..'));
193
- const projectId = data.project_id || sessionId;
194
-
195
- // Tombstone gate: if the project or its batch is buried, drop the
196
- // file from disk and skip. Without this, fullScan resurrects them.
197
- if (skipIfTombstoned('project', projectId, dirname(filePath))) return;
198
- if (skipIfTombstoned('story', sessionId, dirname(filePath))) return;
199
-
200
- // Skip if this is a non-auto batch and an auto- version already exists.
201
- // Prevents duplicate projects when both auto-s_xxx/ and s_xxx/ exist.
202
- if (!projectId.startsWith('auto-')) {
203
- const db = getDb();
204
- const autoExists = db.prepare('SELECT id FROM projects WHERE id = ?').get(`auto-${projectId}`);
205
- if (autoExists) {
206
- console.log(`[sync] Skipping ${projectId} — auto-${projectId} already covers this session`);
207
- return;
208
- }
209
- }
210
-
211
- upsertProject({
212
- id: projectId,
213
- name: deriveProjectName(data, projectId),
214
- last_activity: data.created_at || new Date().toISOString(),
215
- total_stories: (data.stories || []).length,
216
- });
217
-
218
- for (const story of data.stories || []) {
219
- const certified = story.certified || {};
220
- const quality = story.quality || {};
221
- const dimensions = quality.dimensions || {};
222
-
223
- // acceptance_criteria: support both array (auto-generator) and object (hu-reviewer)
224
- let acText = null;
225
- if (Array.isArray(story.acceptance_criteria)) {
226
- acText = JSON.stringify(story.acceptance_criteria);
227
- } else if (story.acceptance_criteria?.criteria) {
228
- acText = JSON.stringify(story.acceptance_criteria.criteria);
229
- }
230
-
231
- upsertStory({
232
- id: story.id,
233
- project_id: projectId,
234
- session_id: sessionId,
235
- status: story.status || 'pending',
236
- title: extractTitle(story),
237
- original_text: story.original?.text || certified.text || null,
238
- certified_as: certified.as || null,
239
- certified_want: certified.want || (certified.text ? certified.text.slice(0, 500) : null),
240
- certified_so_that: certified.so_that || null,
241
- quality_total: quality.total ?? null,
242
- quality_d1: dimensions.d1 ?? null,
243
- quality_d2: dimensions.d2 ?? null,
244
- quality_d3: dimensions.d3 ?? null,
245
- quality_d4: dimensions.d4 ?? null,
246
- quality_d5: dimensions.d5 ?? null,
247
- quality_d6: dimensions.d6 ?? null,
248
- antipatterns: quality.antipatterns ? JSON.stringify(quality.antipatterns) : null,
249
- ac_format: story.acceptance_criteria?.format || (Array.isArray(story.acceptance_criteria) ? 'list' : null),
250
- acceptance_criteria: acText,
251
- created_at: story.created_at,
252
- updated_at: story.updated_at,
253
- certified_at: story.certified_at || null,
254
- });
255
-
256
- // Sync context requests
257
- for (const ctx of story.context_requests || []) {
258
- insertContextRequest({
259
- story_id: story.id,
260
- fields_needed: ctx.fields_needed ? JSON.stringify(ctx.fields_needed) : null,
261
- question: ctx.question || null,
262
- answer: ctx.answer || null,
263
- requested_at: ctx.requested_at || null,
264
- answered_at: ctx.answered_at || null,
265
- });
266
- }
267
- }
268
-
269
- console.log(`[sync] Synced stories from ${basename(join(filePath, '..'))}: ${(data.stories || []).length} stories`);
270
- publishEvent({ type: 'batch', projectId, path: filePath });
271
- } catch (err) {
272
- console.error(`[sync] Error syncing story file ${filePath}:`, err.message);
273
- }
274
- }
275
-
276
- /**
277
- * Extracts a short title from a story.
278
- * @param {object} story
279
- * @returns {string}
280
- */
281
- function extractTitle(story) {
282
- // Auto-generated HUs have story.title directly
283
- if (story.title && story.title !== story.id) return story.title.slice(0, 120);
284
- // HU-reviewer format: certified.want
285
- if (story.certified?.want) return story.certified.want.slice(0, 120);
286
- // Fallback: original text or id
287
- const text = story.original?.text || '';
288
- return text.slice(0, 120) || story.id;
289
- }
290
-
291
- /**
292
- * Parses a session.json file and syncs to SQLite.
293
- * @param {string} filePath - Absolute path to session.json
294
- */
295
- function syncSessionFile(filePath) {
296
- try {
297
- const raw = readFileSync(filePath, 'utf-8');
298
- const data = JSON.parse(raw);
299
- const sessionId = data.id || basename(join(filePath, '..'));
300
-
301
- if (skipIfTombstoned('session', sessionId, dirname(filePath))) return;
302
-
303
- // Resolve which project this session belongs to:
304
- // 1. If a plan / HU batch already created `auto-<sessionId>`, reuse it
305
- // so plan-based runs group with their HU batch.
306
- // 2. Otherwise fall back to the session's declared `project_id`.
307
- // 3. Otherwise → REJECT. Orphan sessions (no project_id and no batch)
308
- // USED to be promoted to a fake "Orphan sessions" project, which
309
- // violated the user's invariant: "Si se crea proyecto y se crean
310
- // hus se asocian a proyecto. ES IMPOSIBLE QUE [no exista esa
311
- // asociación]". Now we skip them entirely. The fix for the source
312
- // (kj run always stamps project_id derived from cwd) lives in
313
- // PR-F; this is the safety net so a leftover orphan can never
314
- // again appear as a phantom project on the board.
315
- const autoProjectId = `auto-${sessionId}`;
316
- const db = getDb();
317
- const existingBatch = db.prepare('SELECT id FROM projects WHERE id = ?').get(autoProjectId);
318
- let projectId;
319
- if (existingBatch) {
320
- projectId = autoProjectId;
321
- } else if (data.project_id) {
322
- projectId = data.project_id;
323
- } else {
324
- // Orphan: log + skip. Never promote to "default".
325
- console.warn(`[sync] Session ${sessionId} has no project_id and no matching batch — skipping (orphan). File: ${filePath}`);
326
- return;
327
- }
328
-
329
- // Ensure the project row exists. Skip when an auto-batch owns it already
330
- // (that row was written by syncStoryFile with richer metadata and we do
331
- // not want to overwrite name/total_stories).
332
- if (!existingBatch) {
333
- upsertProject({
334
- id: projectId,
335
- name: projectId,
336
- last_activity: data.updated_at || data.created_at || new Date().toISOString(),
337
- total_stories: 0,
338
- });
339
- }
340
-
341
- // Calculate iterations from checkpoints
342
- const checkpoints = data.checkpoints || [];
343
- const maxIteration = checkpoints.reduce((max, cp) => Math.max(max, cp.iteration || 0), 0);
344
-
345
- // Calculate duration from first and last checkpoint
346
- let durationMs = null;
347
- if (data.created_at && data.updated_at) {
348
- durationMs = new Date(data.updated_at).getTime() - new Date(data.created_at).getTime();
349
- }
350
-
351
- // Collect completed stages
352
- const stagesSet = new Set();
353
- for (const cp of checkpoints) {
354
- if (cp.stage && !cp.stage.includes('attempt') && !cp.stage.includes('checkpoint')) {
355
- stagesSet.add(cp.stage);
356
- }
357
- }
358
-
359
- // Project row was created above (unless an auto-batch owns it); the
360
- // session always has a project to attach to now.
361
-
362
- upsertSession({
363
- id: sessionId,
364
- project_id: projectId,
365
- task: data.task || null,
366
- status: data.status || 'unknown',
367
- created_at: data.created_at,
368
- updated_at: data.updated_at,
369
- iterations: maxIteration,
370
- duration_ms: durationMs,
371
- approved: data.status === 'approved' ? 1 : 0,
372
- commits: data.commits ? JSON.stringify(data.commits) : null,
373
- stages_completed: JSON.stringify([...stagesSet]),
374
- checkpoints: JSON.stringify(checkpoints),
375
- llm_calls: data.llm_calls ? JSON.stringify(data.llm_calls) : null,
376
- config_snapshot: data.config_snapshot ? JSON.stringify(data.config_snapshot) : null,
377
- budget: data.budget ? JSON.stringify(data.budget) : null,
378
- });
379
-
380
- console.log(`[sync] Synced session ${sessionId}: status=${data.status}, iterations=${maxIteration}`);
381
- publishEvent({ type: 'session', projectId, sessionId });
382
- } catch (err) {
383
- console.error(`[sync] Error syncing session file ${filePath}:`, err.message);
384
- }
385
- }
386
-
387
44
  /**
388
45
  * Performs a full scan of all existing JSON files.
389
46
  */
390
- /**
391
- * Sync a v2 plan file (with embedded HUs) to SQLite.
392
- * Plans are the authoritative source — replaces hu-stories for plan-based runs.
393
- */
394
- export function syncPlanFile(filePath) {
395
- try {
396
- const raw = readFileSync(filePath, 'utf-8');
397
- const data = JSON.parse(raw);
398
- // KJC-BUG-0124 (issue #1277): a strict `version !== 2` silently hid
399
- // every plan whose version drifted — the board showed 0 stories with no
400
- // trace, and users concluded their work was lost. Tolerant now: any
401
- // v2+ plan syncs, with a LOUD warn on version drift (fired before the
402
- // empty check so drift is visible even on empty plans). Pre-v2 plans
403
- // keep their historical silent skip (they never synced and carry no
404
- // hus[]), and an empty v2 plan has nothing to sync — skipping it
405
- // loses nothing.
406
- if (typeof data.version !== 'number' || data.version < 2) return;
407
- if (data.version > 2) {
408
- console.warn(`[sync] ${filePath}: plan version ${data.version} (expected 2) — syncing anyway; re-save with current kj to normalize`);
409
- }
410
- if (!Array.isArray(data.hus) || data.hus.length === 0) return;
411
-
412
- // Bug fix: pre-patch, `projectId = data.planId` — every plan was a separate
413
- // "project" on the board, so 2 464 plans across the same repo produced
414
- // 2 464 boxes in the UI instead of one. Now we derive the project from
415
- // the stamped `projectDir` so every plan for the same source tree groups
416
- // under a single board entry. Legacy plans without `projectDir` still
417
- // fall back to `planId` (conservative — they at least continue to show
418
- // up, just each on its own card).
419
- // KJC-BUG-0160: la identidad es el ROOT del repo — un plan creado desde
420
- // un worktree/carril agrupa bajo el mismo proyecto que el árbol principal.
421
- const canonicalDir = data.projectDir ? canonicalProjectDir(data.projectDir) : null;
422
- const projectId = canonicalDir ? deriveProjectIdFromDir(canonicalDir) : data.planId;
423
-
424
- if (skipIfTombstoned('plan', data.planId, filePath)) return;
425
- // KJC-BUG-0055 (supersedes KJC-BUG-0050): gating por timestamp.
426
- // El "fix" anterior quitaba el tombstone del proyecto en CUALQUIER
427
- // llegada de plan — eso resucitaba proyectos borrados cuando un plan
428
- // anterior reaparecía (DB nueva + plan-files viejos en disco) o
429
- // cuando un fullScan posterior los re-encontraba. Comportamiento:
430
- // • plan.updatedAt/createdAt > tombstone.deleted_at → el usuario
431
- // ha generado el plan DESPUÉS de borrar el proyecto → revivir.
432
- // • plan.updatedAt/createdAt <= tombstone.deleted_at → plan stale
433
- // pre-existente al borrado → respetar el tombstone y borrar el
434
- // fichero en disco para que no vuelva a re-importarse.
435
- const projectTomb = getTombstone('project', projectId);
436
- if (projectTomb) {
437
- const planTime = data.updatedAt || data.createdAt || null;
438
- const planTs = planTime ? Date.parse(planTime) : NaN;
439
- const tombTs = Date.parse(projectTomb.deleted_at);
440
- if (Number.isFinite(planTs) && planTs > tombTs) {
441
- console.log(`[sync] Project ${projectId} resucitado: plan ${data.planId} es más reciente (${planTime}) que el tombstone (${projectTomb.deleted_at}).`);
442
- removeTombstone('project', projectId);
443
- } else {
444
- console.log(`[sync] Plan ${data.planId} ignorado: proyecto ${projectId} tombstoned (${projectTomb.deleted_at}); borrando ${filePath}.`);
445
- try { rmSync(filePath, { force: true }); } catch { /* best effort */ }
446
- return;
447
- }
448
- }
449
- // Human-friendly project name resolution order (most-specific first):
450
- // 1. plan.name explicitly set by the user / planner ("Linux Assistant
451
- // Orchestrator" — preserved across renames done from the board).
452
- // 2. titleCaseBasename(projectDir) — derived from the cwd of the
453
- // original kj plan invocation. NOTE: this is the SUBCARPETA of the
454
- // projectDir, NOT necessarily where the work happens. If the user
455
- // ran `kj plan` from inside `…/karajan-code/packages/hu-board/`
456
- // with a SPEC about "Linux Assistant", the basename here would
457
- // misleadingly say "Hu Board" — that's why plan.name wins.
458
- // 3. slugToTitle of the task / projectId as a last resort.
459
- // KJC-BUG-0162: the default backlog plan is named "brain-backlog" — a
460
- // DEFAULT, not a user choice. Every project synced through it ended up
461
- // named "brain-backlog", indistinguishable in the picker. Only a real,
462
- // user-given plan name may override the repo-derived name.
463
- const userGivenName = data.name && data.name !== 'brain-backlog' ? data.name : null;
464
- const projectName = userGivenName
465
- || (canonicalDir ? titleCaseBasename(basename(canonicalDir)) : null)
466
- || slugToTitle(data.task || '')
467
- || projectId;
468
-
469
- // KJC-PRP-0002 PR3: stamp `is_shared` from the path (authoritative — works
470
- // even for legacy plan files without the `shared:true` envelope marker).
471
- // OR with existing value so a single shared plan promotes the project,
472
- // while unsharing the LAST shared plan still leaves the flag set (we
473
- // clear it lazily only when the user explicitly tombstones / re-creates).
474
- upsertProject({
475
- id: projectId,
476
- name: projectName,
477
- last_activity: data.updatedAt || data.createdAt || new Date().toISOString(),
478
- total_stories: data.hus.length,
479
- is_shared: isSharedPath(filePath) || data.shared === true ? 1 : undefined,
480
- });
481
-
482
- for (let i = 0; i < data.hus.length; i += 1) {
483
- const hu = data.hus[i];
484
- const acList = Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [];
485
- // Only count tests that carry real content. `kj plan` stamps a
486
- // placeholder like `["npx vitest run ..."]` on every HU — those
487
- // DO count as 1 test each. But empty strings / null entries
488
- // (which show up in half-baked plans) must not inflate the
489
- // badge to "🧪 1 test" on a HU that effectively has none.
490
- // Drop empty entries but accept BOTH legacy shapes and the
491
- // v2.7.5 structured form. The original anti-junk filter only
492
- // knew the legacy shape (`name|title|description|given|gherkin|
493
- // scope`) and silently nuked every test produced by the
494
- // tests-synthesizer (which emits `{type, content, file?}`),
495
- // leaving every HU flagged "missing test contract" on the
496
- // board even though the plan JSON had 4 tests apiece.
497
- const testList = (Array.isArray(hu.acceptance_tests) ? hu.acceptance_tests : [])
498
- .filter((t) => {
499
- if (!t) return false;
500
- if (typeof t === 'string') return t.trim().length > 0;
501
- if (typeof t !== 'object') return false;
502
- // v2.7.5 structured form — primary shape going forward.
503
- if (typeof t.content === 'string' && t.content.trim().length > 0) return true;
504
- // Legacy shapes kept for back-compat with hand-edited plans.
505
- return Boolean(t.name || t.title || t.description || t.given || t.gherkin || t.scope);
506
- });
507
- const blockedByList = Array.isArray(hu.blocked_by) ? hu.blocked_by : [];
508
- const acText = acList.length > 0 ? JSON.stringify(acList) : null;
509
-
510
- upsertStory({
511
- id: `${projectId}::${hu.id}`,
512
- project_id: projectId,
513
- session_id: projectId,
514
- status: hu.status || 'pending',
515
- title: hu.title || hu.id,
516
- original_text: hu.scope || null,
517
- certified_as: null,
518
- certified_want: hu.scope || null,
519
- certified_so_that: null,
520
- quality_total: null,
521
- quality_d1: null, quality_d2: null, quality_d3: null,
522
- quality_d4: null, quality_d5: null, quality_d6: null,
523
- antipatterns: null,
524
- ac_format: acText ? 'list' : null,
525
- acceptance_criteria: acText,
526
- created_at: hu.createdAt,
527
- updated_at: hu.updatedAt,
528
- certified_at: null,
529
- // Stamp plan_id so the PATCH / mark-ready endpoints can locate
530
- // the source plan JSON without walking the plans dir.
531
- plan_id: data.planId,
532
- // Denormalised counters + deps so the card renders "3 ACs ·
533
- // 2 tests · waits for lao-001" without every client parsing JSON.
534
- ac_count: acList.length,
535
- test_count: testList.length,
536
- blocked_by: blockedByList.length > 0 ? JSON.stringify(blockedByList) : null,
537
- // Full test list goes to SQLite too so the modal can show
538
- // "🧪 2 tests" next to the list. Pre-patch the card showed
539
- // test_count but the modal had nothing to render.
540
- acceptance_tests: testList.length > 0 ? JSON.stringify(testList) : null,
541
- // Index in plan.hus[] — used by getStoriesByProject to order
542
- // cards topologically (roots first, dependents last).
543
- plan_order: i,
544
- // Spec mapping (PR C): "implements §5.3" trace pointer.
545
- spec_section: typeof hu.spec_section === 'string' && hu.spec_section.trim() ? hu.spec_section.trim() : null,
546
- // PR3: per-HU outcome (iterations, duration, commits,
547
- // blockers, summary). Stored as a JSON string in SQLite —
548
- // the front parses on read. Null until hu-sub-pipeline
549
- // stamps it at the end of each HU.
550
- outcome: hu.outcome ? JSON.stringify(hu.outcome) : null,
551
- // KJC-TSK-0394: result (pass|fail|partial|null) ortogonal al status.
552
- // Distinto de `outcome` (blob JSON) — esto es el enum corto para
553
- // badge ✓/✗ en la card.
554
- result: hu.result || null,
555
- // KJC-TSK-0406: coder_model y reviewer_model por HU. Reflejan
556
- // qué modelo se va a usar (o se usó) en este HU. El modal del
557
- // board los muestra y permite cambiarlos antes de run.
558
- coder_model: hu.coder_model || null,
559
- reviewer_model: hu.reviewer_model || null,
560
- coder_provider: hu.coder_provider || null,
561
- reviewer_provider: hu.reviewer_provider || null,
562
- // KJC-PRP-0002 PR6: optional handle of the owner of this HU
563
- // (free-form string). Only surfaced in UI when project is_shared,
564
- // but stored unconditionally so values survive share/unshare flips.
565
- assignee: typeof hu.assignee === 'string' && hu.assignee.trim() ? hu.assignee.trim() : null,
566
- });
567
- }
568
-
569
- console.log(`[sync] Plan ${projectId}: ${data.hus.length} HUs (${projectName})`);
570
- publishEvent({ type: 'plan', projectId, planId: data.planId });
571
- } catch (err) {
572
- console.error(`[sync] Failed to sync plan ${filePath}: ${err.message}`);
573
- }
574
- }
575
-
576
47
  export function fullScan() {
577
48
  const kjHome = getKjHome();
578
- const storiesDir = join(kjHome, 'hu-stories');
49
+ // KJC-TSK-1347: ~/.karajan/hu-stories (the retired kj run store) is no longer
50
+ // read; the board's HUs come from the plan backlog and the shared board.
579
51
  const sessionsDir = join(kjHome, 'sessions');
580
52
 
581
53
  // Clear existing data for a clean rebuild
@@ -585,29 +57,15 @@ export function fullScan() {
585
57
  db.exec('DELETE FROM sessions');
586
58
  db.exec('DELETE FROM projects');
587
59
 
588
- // KJC-BUG-0055: garbage-collect on-disk story/session artefacts whose
589
- // ids carry an active tombstone. Without this, a tombstone is useless
590
- // against a stray ~/.karajan/hu-stories/<id>/ that survived a manual
591
- // DB wipe — the next pass would re-import it as a ghost project.
592
- // (Plan dirs are NOT pre-purged here: a tombstoned project may hold a
593
- // newer plan from a legit projectDir re-use; `syncPlanFile`'s
594
- // timestamp gate decides per-file.)
595
- for (const [type, root] of [['story', storiesDir], ['session', sessionsDir]]) {
596
- if (!existsSync(root)) continue;
597
- for (const name of readdirSync(root)) {
598
- if (!isTombstoned(type, name)) continue;
599
- try { rmSync(join(root, name), { recursive: true, force: true }); } catch { /* best effort */ }
600
- }
601
- }
602
-
603
- // Scan stories
604
- if (existsSync(storiesDir)) {
605
- const dirs = readdirSync(storiesDir);
606
- for (const dir of dirs) {
607
- const batchPath = join(storiesDir, dir, 'batch.json');
608
- if (existsSync(batchPath)) {
609
- syncStoryFile(batchPath);
610
- }
60
+ // KJC-BUG-0055: garbage-collect on-disk session artefacts whose ids carry
61
+ // an active tombstone, or a stray one that survived a manual DB wipe would
62
+ // come back as a ghost. (Plan dirs are NOT pre-purged here: a tombstoned
63
+ // project may hold a newer plan from a legit projectDir re-use;
64
+ // `syncPlanFile`'s timestamp gate decides per-file.)
65
+ if (existsSync(sessionsDir)) {
66
+ for (const name of readdirSync(sessionsDir)) {
67
+ if (!isTombstoned('session', name)) continue;
68
+ try { rmSync(join(sessionsDir, name), { recursive: true, force: true }); } catch { /* best effort */ }
611
69
  }
612
70
  }
613
71
 
@@ -694,7 +152,6 @@ function scanSharedPlans() {
694
152
  */
695
153
  export function startWatcher() {
696
154
  const kjHome = getKjHome();
697
- const storiesGlob = join(kjHome, 'hu-stories', '*', 'batch.json');
698
155
  const sessionsGlob = join(kjHome, 'sessions', '*', 'session.json');
699
156
  const promptsGlob = join(kjHome, 'prompts', '*.json');
700
157
  // Plans live under KJ_PLANS_DIR or `~/.karajan/plans/` after
@@ -709,7 +166,7 @@ export function startWatcher() {
709
166
  const plansGlob = join(plansRoot, '*', 'plan-*.json');
710
167
  const legacyPlansGlob = legacyPlansRoot ? join(legacyPlansRoot, '*', 'plan-*.json') : null;
711
168
 
712
- const watchTargets = [storiesGlob, sessionsGlob, plansGlob, promptsGlob];
169
+ const watchTargets = [sessionsGlob, plansGlob, promptsGlob];
713
170
  if (legacyPlansGlob) watchTargets.push(legacyPlansGlob);
714
171
  // KJC-PRP-0002 PR2: live-watch shared plans for the board's cwd + any
715
172
  // dir listed in KJ_SHARED_PROJECT_DIRS. Without this, `kj plan share`
@@ -728,8 +185,7 @@ export function startWatcher() {
728
185
  });
729
186
 
730
187
  const syncByPath = (p, eventName) => {
731
- if (p.includes('hu-stories')) syncStoryFile(p);
732
- else if (p.includes('sessions')) syncSessionFile(p);
188
+ if (p.includes('sessions')) syncSessionFile(p);
733
189
  else if (p.includes(`${plansRoot}${plansRoot.endsWith('/') ? '' : '/'}`) || p.includes('/plans/')) {
734
190
  // Catches both per-user `~/.karajan/plans/<slug>/plan-*.json` and
735
191
  // team-shared `<projectDir>/.karajan-shared/plans/plan-*.json` — the
@@ -172,7 +172,7 @@ export function markSessionFailedOnDisk(sessionId, reason, reapedAtIso, opts = {
172
172
  * sessions so the server can log them.
173
173
  *
174
174
  * @param {object} deps
175
- * @param {object} deps.db better-sqlite3 Database handle
175
+ * @param {object} deps.db SQLite handle (karajan-core/db adapter)
176
176
  * @param {object} [deps.opts] threshold + sessionsDir overrides
177
177
  * @param {Function} [deps.now] () => number, for tests
178
178
  * @returns {Array<{ id: string, status_before: string, reason: string, disk_persisted: boolean }>}