karajan-code 4.70.4 → 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 (380) 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/retired-commands.js +49 -0
  68. package/src/cli.js +18 -5
  69. package/src/commands/agent-run.js +40 -26
  70. package/src/commands/audit.js +127 -124
  71. package/src/commands/board.js +5 -9
  72. package/src/commands/check.js +29 -16
  73. package/src/commands/doctor.js +2 -0
  74. package/src/commands/env.js +176 -150
  75. package/src/commands/go.js +71 -53
  76. package/src/commands/harden.js +90 -81
  77. package/src/commands/hu-start.js +52 -0
  78. package/src/commands/hu.js +288 -221
  79. package/src/commands/identity.js +111 -78
  80. package/src/commands/init-atril.js +47 -0
  81. package/src/commands/init.js +11 -40
  82. package/src/commands/install-tools.js +70 -85
  83. package/src/commands/main-guard.js +78 -0
  84. package/src/commands/policy.js +226 -184
  85. package/src/commands/qmd.js +27 -21
  86. package/src/commands/rag.js +75 -48
  87. package/src/commands/review-gate.js +421 -341
  88. package/src/commands/start.js +4 -5
  89. package/src/commands/steward.js +96 -47
  90. package/src/commands/tournament.js +46 -37
  91. package/src/commands/worktree.js +94 -25
  92. package/src/config/loader.js +34 -17
  93. package/src/config/secrets.js +36 -0
  94. package/src/config.js +1 -0
  95. package/src/environment/adr.js +16 -2
  96. package/src/git/karajan-label.js +20 -8
  97. package/src/guards/main-guard.js +54 -0
  98. package/src/harden/hook-templates.js +16 -1
  99. package/src/harden/sentinel/posttooluse.mjs +18 -6
  100. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  101. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  102. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  103. package/src/harden/sentinel/sessionstart.mjs +3 -2
  104. package/src/harden/sentinel/stop.mjs +5 -4
  105. package/src/harden/workflow-templates.js +1 -1
  106. package/src/hu/board-lookup.js +29 -0
  107. package/src/hu/gh-identity.js +23 -0
  108. package/src/hu/github-board.js +4 -2
  109. package/src/hu/lane-prune.js +15 -7
  110. package/src/hu/lane-setup.js +65 -0
  111. package/src/hu/lanes.js +66 -0
  112. package/src/hu/team-board-mirror.js +3 -2
  113. package/src/infrastructure/command-runner.js +1 -1
  114. package/src/infrastructure/file-system-service.js +1 -1
  115. package/src/mcp/handlers/direct-handlers.js +2 -292
  116. package/src/mcp/handlers/hu-handlers.js +1 -35
  117. package/src/mcp/handlers/management-handlers.js +2 -42
  118. package/src/mcp/server-handlers.js +16 -22
  119. package/src/mcp/tools.js +1 -196
  120. package/src/plan/lane-plans.js +54 -0
  121. package/src/plan/plan-store.js +7 -4
  122. package/src/policy/add.js +26 -12
  123. package/src/policy/adr-number-check.js +31 -0
  124. package/src/prompts/audit.js +299 -339
  125. package/src/prompts/card-context.js +10 -5
  126. package/src/prompts/reviewer.js +5 -2
  127. package/src/rag/auto-update.js +56 -43
  128. package/src/rag/bootstrap-index.js +28 -54
  129. package/src/rag/migrate.js +1 -1
  130. package/src/rag/onnx-fallback.js +2 -2
  131. package/src/rag/pid-lease.js +70 -0
  132. package/src/rag/project-store.js +2 -12
  133. package/src/review/card-first.js +57 -46
  134. package/src/review/gate-gitignore.js +23 -4
  135. package/src/review/gate-hook.js +12 -4
  136. package/src/review/one-shot-review.js +45 -35
  137. package/src/review/publish-pr.js +14 -2
  138. package/src/review/repo-docs.js +73 -0
  139. package/src/review/repo-stack.js +59 -0
  140. package/src/review/reviewer-fallback.js +19 -4
  141. package/src/review/seal-commit.js +29 -0
  142. package/src/review/sonar-pregate.js +59 -44
  143. package/src/review/third-party-review.js +90 -16
  144. package/src/review/third-party-size.js +43 -0
  145. package/src/review/third-party-skills.js +54 -0
  146. package/src/review/verify-pr.js +1 -24
  147. package/src/roles/audit-role.js +106 -125
  148. package/src/skills/atril-skills.js +5 -4
  149. package/src/sonar/scanner.js +116 -101
  150. package/src/steward/invariants.js +5 -4
  151. package/src/steward/phantom-sweep.js +66 -0
  152. package/src/tournament/judge.js +60 -53
  153. package/src/utils/gh-account.js +37 -0
  154. package/src/utils/git.js +0 -13
  155. package/src/utils/home-migration.js +7 -1
  156. package/src/utils/main-checkout.js +53 -0
  157. package/src/utils/qmd-collection.js +3 -1
  158. package/src/utils/welcome.js +3 -3
  159. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  160. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  161. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  162. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  163. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  164. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  165. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  166. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  167. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  172. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  173. package/packages/hu-board/src/run-tracker.js +0 -121
  174. package/src/activity-log.js +0 -59
  175. package/src/agents/host-agent.js +0 -38
  176. package/src/agents/model-registry.js +0 -142
  177. package/src/audit/plan-adherence.js +0 -224
  178. package/src/autonomy/arbiter-role.js +0 -71
  179. package/src/autonomy/arbiter.js +0 -22
  180. package/src/autonomy/decider.js +0 -18
  181. package/src/autonomy/decision-resolver.js +0 -58
  182. package/src/autonomy/outcome-report.js +0 -55
  183. package/src/autonomy/stage-ask.js +0 -30
  184. package/src/brain/decision-tracker.js +0 -125
  185. package/src/brain/decisor.js +0 -243
  186. package/src/brain/solomon-consult.js +0 -171
  187. package/src/brain/standby-scheduler.js +0 -8
  188. package/src/budget/comparison.js +0 -96
  189. package/src/canvas/parse-md.js +0 -238
  190. package/src/canvas/schema.js +0 -101
  191. package/src/canvas/to-flat.js +0 -85
  192. package/src/canvas/validate.js +0 -163
  193. package/src/ci/dispatch.js +0 -99
  194. package/src/ci/index.js +0 -3
  195. package/src/ci/pr-diff.js +0 -26
  196. package/src/cli/register-standby.js +0 -26
  197. package/src/commands/architect.js +0 -111
  198. package/src/commands/autorun.js +0 -49
  199. package/src/commands/researcher.js +0 -54
  200. package/src/commands/resume.js +0 -72
  201. package/src/commands/review.js +0 -89
  202. package/src/commands/run.js +0 -185
  203. package/src/commands/standby.js +0 -117
  204. package/src/commands/status.js +0 -36
  205. package/src/commands/triage.js +0 -78
  206. package/src/commands/undo.js +0 -62
  207. package/src/domains/domain-loader.js +0 -245
  208. package/src/domains/domain-registry.js +0 -140
  209. package/src/domains/domain-synthesizer.js +0 -129
  210. package/src/git/automation.js +0 -433
  211. package/src/git/coder-commit-guard.js +0 -40
  212. package/src/git/hu-automation.js +0 -243
  213. package/src/git/hu-snapshot.js +0 -10
  214. package/src/git/symlink-targets.js +0 -57
  215. package/src/golden/asserter.js +0 -92
  216. package/src/golden/loader.js +0 -52
  217. package/src/golden/runner.js +0 -92
  218. package/src/golden/schema.js +0 -41
  219. package/src/guards/duplicate-members.js +0 -86
  220. package/src/guards/intent-guard.js +0 -161
  221. package/src/guards/perf-guard.js +0 -126
  222. package/src/guards/policy-guard.js +0 -90
  223. package/src/guards/policy-resolver.js +0 -85
  224. package/src/guards/secret-redactor.js +0 -71
  225. package/src/hu/acceptance-runner.js +0 -138
  226. package/src/hu/lazy-planner.js +0 -109
  227. package/src/hu/parallel-executor.js +0 -94
  228. package/src/hu/routing-metrics.js +0 -17
  229. package/src/hu/split-validator.js +0 -143
  230. package/src/hu/splitting-detector.js +0 -158
  231. package/src/hu/splitting-generator.js +0 -175
  232. package/src/hu/store.js +0 -394
  233. package/src/infrastructure/mocks.js +0 -182
  234. package/src/mcp/handlers/run-handler.js +0 -318
  235. package/src/mcp/sovereignty-guard.js +0 -113
  236. package/src/mcp/tool-arg-normalizers.js +0 -16
  237. package/src/mutate/reviewer-signal.js +0 -76
  238. package/src/orchestrator/agent-fallback.js +0 -81
  239. package/src/orchestrator/brain-coordinator.js +0 -186
  240. package/src/orchestrator/ci-integration.js +0 -139
  241. package/src/orchestrator/config-init.js +0 -426
  242. package/src/orchestrator/direct-actions.js +0 -217
  243. package/src/orchestrator/drivers/error-recovery.js +0 -287
  244. package/src/orchestrator/drivers/init-context.js +0 -238
  245. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  246. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  247. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  248. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  249. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  250. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  251. package/src/orchestrator/drivers/post-loop.js +0 -404
  252. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  253. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  254. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  255. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  256. package/src/orchestrator/drivers/pre-loop.js +0 -524
  257. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  258. package/src/orchestrator/feedback-enrichment.js +0 -140
  259. package/src/orchestrator/feedback-queue.js +0 -151
  260. package/src/orchestrator/flow-control.js +0 -170
  261. package/src/orchestrator/flow-runner.js +0 -365
  262. package/src/orchestrator/fs-leak-detector.js +0 -228
  263. package/src/orchestrator/hu-scheduler.js +0 -114
  264. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  265. package/src/orchestrator/iteration-gate.js +0 -73
  266. package/src/orchestrator/iteration-stages.js +0 -9
  267. package/src/orchestrator/parallel-limiter.js +0 -74
  268. package/src/orchestrator/pipeline-context.js +0 -72
  269. package/src/orchestrator/post-loop-stages.js +0 -468
  270. package/src/orchestrator/pre-loop-stages.js +0 -11
  271. package/src/orchestrator/preflight-checks.js +0 -487
  272. package/src/orchestrator/repair/repair-detector.js +0 -117
  273. package/src/orchestrator/repair/repair-runner.js +0 -142
  274. package/src/orchestrator/reviewer-fallback.js +0 -45
  275. package/src/orchestrator/role-output-compressor.js +0 -189
  276. package/src/orchestrator/session-journal.js +0 -308
  277. package/src/orchestrator/solomon-escalation.js +0 -331
  278. package/src/orchestrator/solomon-rules.js +0 -212
  279. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  280. package/src/orchestrator/stages/architect-stage.js +0 -172
  281. package/src/orchestrator/stages/coder-stage.js +0 -506
  282. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  283. package/src/orchestrator/stages/format-stage.js +0 -61
  284. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  285. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  286. package/src/orchestrator/stages/perf-stage.js +0 -140
  287. package/src/orchestrator/stages/planner-stage.js +0 -85
  288. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  289. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  290. package/src/orchestrator/stages/research-stage.js +0 -69
  291. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  292. package/src/orchestrator/stages/sonar-stage.js +0 -439
  293. package/src/orchestrator/stages/stage-classes.js +0 -84
  294. package/src/orchestrator/stages/stage-executor.js +0 -155
  295. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  296. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  297. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  298. package/src/orchestrator/stages/triage-stage.js +0 -172
  299. package/src/orchestrator/standby.js +0 -71
  300. package/src/orchestrator/verification-gate.js +0 -183
  301. package/src/orchestrator.js +0 -22
  302. package/src/plan/adr-generator.js +0 -170
  303. package/src/plan/adr-loader.js +0 -60
  304. package/src/plan/plan-executor.js +0 -100
  305. package/src/plan/plan-validation.js +0 -4
  306. package/src/prompts/acceptance-coverage.js +0 -32
  307. package/src/prompts/arbiter.js +0 -38
  308. package/src/prompts/architect.js +0 -103
  309. package/src/prompts/hu-reviewer.js +0 -244
  310. package/src/prompts/tool-correctness-judge.js +0 -31
  311. package/src/prompts/triage.js +0 -57
  312. package/src/rag/embedder.js +0 -2
  313. package/src/rag/embedders/_cloud-base.js +0 -2
  314. package/src/rag/embedders/cohere.js +0 -2
  315. package/src/rag/embedders/mistral.js +0 -2
  316. package/src/rag/embedders/onnx.js +0 -2
  317. package/src/rag/embedders/openai.js +0 -2
  318. package/src/rag/embedders/voyage.js +0 -2
  319. package/src/rag/rerank.js +0 -4
  320. package/src/rag/where-parser.js +0 -4
  321. package/src/repeat-detector.js +0 -88
  322. package/src/review/diff-generator.js +0 -112
  323. package/src/review/git-stash-helper.js +0 -104
  324. package/src/review/schema.js +0 -31
  325. package/src/review/scope-filter.js +0 -152
  326. package/src/review/snapshot-diff.js +0 -122
  327. package/src/review/tdd-discipline.js +0 -90
  328. package/src/review/tdd-policy.js +0 -69
  329. package/src/roles/acceptance-coverage-role.js +0 -78
  330. package/src/roles/architect-role.js +0 -77
  331. package/src/roles/coder-role.js +0 -113
  332. package/src/roles/commiter-role.js +0 -113
  333. package/src/roles/domain-curator-role.js +0 -134
  334. package/src/roles/hu-reviewer-role.js +0 -64
  335. package/src/roles/impeccable-role.js +0 -85
  336. package/src/roles/index.js +0 -18
  337. package/src/roles/karajan-brain-role.js +0 -134
  338. package/src/roles/perf-role.js +0 -122
  339. package/src/roles/planner-role.js +0 -151
  340. package/src/roles/refactorer-role.js +0 -30
  341. package/src/roles/repairer-role.js +0 -181
  342. package/src/roles/researcher-role.js +0 -70
  343. package/src/roles/reviewer-role.js +0 -115
  344. package/src/roles/security-role.js +0 -73
  345. package/src/roles/solomon-role.js +0 -271
  346. package/src/roles/sonar-role.js +0 -72
  347. package/src/roles/tester-role.js +0 -235
  348. package/src/roles/tool-correctness-judge-role.js +0 -78
  349. package/src/roles/triage-role.js +0 -99
  350. package/src/session/journal/decisions-writer.js +0 -200
  351. package/src/session/journal/iteration-logger.js +0 -194
  352. package/src/session/journal/summary-writer.js +0 -320
  353. package/src/session/journal/tree-writer.js +0 -164
  354. package/src/session/journal/write-all.js +0 -89
  355. package/src/session-cleanup.js +0 -116
  356. package/src/skills/semantic-detector.js +0 -167
  357. package/src/skills/skill-detector.js +0 -385
  358. package/src/sonar/cloud-scanner.js +0 -78
  359. package/src/tracker-bootstrap.js +0 -44
  360. package/src/types/index.js +0 -38
  361. package/src/types/policy.js +0 -43
  362. package/src/types/solomon.js +0 -87
  363. package/src/utils/async-lock.js +0 -31
  364. package/src/utils/budget.js +0 -229
  365. package/src/utils/cwd-confirm.js +0 -39
  366. package/src/utils/display/event-handlers.js +0 -403
  367. package/src/utils/display/formatters.js +0 -132
  368. package/src/utils/display/header.js +0 -47
  369. package/src/utils/display/session-summary.js +0 -118
  370. package/src/utils/display/solomon.js +0 -42
  371. package/src/utils/pricing.js +0 -29
  372. package/src/utils/quota-simulator.js +0 -56
  373. package/src/utils/role-assigner.js +0 -137
  374. package/src/utils/rtk-wrapper.js +0 -87
  375. package/src/utils/run-registry.js +0 -9
  376. package/src/utils/status-dashboard.js +0 -254
  377. package/src/utils/tool-call-extractor.js +0 -56
  378. package/src/webperf/devtools-detect.js +0 -112
  379. package/src/webperf/webperf-role-integration.js +0 -66
  380. package/templates/skills/kj-run.md +0 -73
@@ -12,7 +12,7 @@
12
12
  * and says so: the coder learns the card exists and where to ask for it,
13
13
  * instead of receiving a silently empty context.
14
14
  */
15
- import { getHu } from "../hu/store.js";
15
+ import { findBoardHu } from "../hu/board-lookup.js";
16
16
 
17
17
  /** Ids minted by the HU Board; anything else belongs to an external board. */
18
18
  const LOCAL_HU = /^HU-/i;
@@ -25,9 +25,14 @@ const externalSection = (ref) =>
25
25
  "Treat the task below as the card's statement, and if something the card should answer is missing, ASK instead of guessing.",
26
26
  ].join("\n");
27
27
 
28
+ // KJC-BUG-0454: a board card keeps its criteria as a list; older HUs as one string.
29
+ const criteriaOf = (hu) => (Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria.join("\n") : hu.acceptanceCriteria ?? "").trim();
30
+ const idOf = (hu) => hu.short_id || hu.id;
31
+
28
32
  const localSection = (hu) => {
29
- const lines = [`## Card ${hu.id} — ${hu.title}`, ""];
33
+ const lines = [`## Card ${idOf(hu)} — ${hu.title}`, ""];
30
34
  if (hu.description?.trim()) lines.push(hu.description.trim(), "");
35
+ if (hu.scope?.trim()) lines.push(`Scope: ${hu.scope.trim()}`, "");
31
36
  lines.push(`Status on the board: ${hu.status}. The card is the contract: done means its statement is literally true.`);
32
37
  return lines.join("\n");
33
38
  };
@@ -45,10 +50,10 @@ export async function resolveCardContext({ projectDir, ref, deps = {} }) {
45
50
  }
46
51
  // A missing local HU is an error, never an empty context: the caller asked
47
52
  // for a card by id and kj either delivers it or says it does not exist.
48
- const hu = await (deps.getHu || getHu)(projectDir, id);
49
- const criteria = hu.acceptanceCriteria?.trim();
53
+ const hu = await (deps.getHu || findBoardHu)(projectDir, id);
54
+ const criteria = criteriaOf(hu);
50
55
  return {
51
- huId: hu.id,
56
+ huId: idOf(hu),
52
57
  title: hu.title,
53
58
  section: localSection(hu),
54
59
  acceptanceTests: criteria ? [{ type: "gherkin", content: criteria }] : null,
@@ -35,7 +35,7 @@ const SERENA_INSTRUCTIONS = [
35
35
  * on it; the per-review content (task context + git diff, up to 12 KB
36
36
  * and always different) goes last.
37
37
  */
38
- export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null }) {
38
+ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null, roleSkillBlock = null }) {
39
39
  const { body: clippedDiff, note: clipNote } = clipDiff(diff);
40
40
  // KJC-BUG-0138: full diff on purpose — the moved-to side survives clipping.
41
41
  const splitSignal = buildSplitSignal(diff);
@@ -44,7 +44,10 @@ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode,
44
44
  const rtkSnippet = buildRtkInstructions({ rtkAvailable });
45
45
  // KJC-TSK-1065: the reviewer gets the skills of its role and of the task, up to 3.
46
46
  let skillSection = null;
47
- if (projectDir) {
47
+ // KJC-TSK-1272: a block the caller resolved (a third-party repo's stack) wins over the project's.
48
+ if (roleSkillBlock) {
49
+ skillSection = buildRoleSkillSection(roleSkillBlock, { provider }) || null;
50
+ } else if (projectDir) {
48
51
  skillSection = buildRoleSkillSection(await roleSkills({ role: "reviewer", task, projectDir }), { provider }) || null;
49
52
  }
50
53
 
@@ -31,6 +31,57 @@ const countSources = (db, slug, projectDir) => {
31
31
  export const DEFAULT_MAX_DRIFT_FILES = 25;
32
32
  const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
33
33
 
34
+ // KJC-TSK-1174 (RAG-A2): no index yet. It is built in the background, once; the session
35
+ // does not wait. A recent attempt that died (no Ollama) is not retried blindly.
36
+ function bootstrapInBackground({ projectDir, slug, head, logger }) {
37
+ const boot = startBootstrap(projectDir);
38
+ if (boot.busy) {
39
+ warnOnce(logger, `bootstrap-busy:${projectDir}`, `[rag] ${slug} has no index yet: another background index is running (${boot.busy.slug ?? "?"}, pid ${boot.busy.pid ?? "?"}); it is tried again on the next command`);
40
+ return { skipped: true, reason: "bootstrap-queued", holder: boot.busy, head };
41
+ }
42
+ if (boot.recent) {
43
+ warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index and the background indexing did not finish: run kj rag index --with-sources`);
44
+ return { skipped: true, reason: "bootstrap-failed", head };
45
+ }
46
+ warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index yet: indexing it in the background (pid ${boot.pid}); queries answer from it once it is done`);
47
+ return { skipped: true, reason: "bootstrapping", pid: boot.pid, head };
48
+ }
49
+
50
+ // KJC-TSK-1171 (RAG-B): an index built before the embedded header holds vectors a
51
+ // query by file or symbol name cannot reach. Small, it is rebuilt here; large, the
52
+ // query answers from it and says once how to rebuild.
53
+ async function rebuildOldSchema({ db, slug, projectDir, config, logger, head, maxFiles }, schema) {
54
+ const sources = countSources(db, slug, projectDir);
55
+ if (sources > maxFiles) {
56
+ warnOnce(logger, projectDir, `[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}): answering from it as it is; rebuild it with kj rag index --with-sources`);
57
+ return { skipped: true, reason: "embed-schema-old", sources, head };
58
+ }
59
+ logger.info?.(`[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}); rebuilding ${sources} source(s)`);
60
+ const totals = await indexProject(projectDir, { db, embedder: makeGovernedEmbedder(config), karajanHome: getKarajanHome(), logger, withSources: true, exclude: ragExclude(config) });
61
+ if (totals.head) setLastIndexedCommit(db, slug, totals.head);
62
+ return { ran: true, rebuilt: true, totals };
63
+ }
64
+
65
+ // KJC-TSK-1174 (RAG-A1): the drift is measured up to the files on disk, untracked ones
66
+ // included, so a query sees what the session is writing before it is committed.
67
+ // The dirty paths of the last refresh count too: reverted or deleted since, they hold stale chunks.
68
+ async function deltaUpdate({ db, slug, projectDir, config, logger, head, maxFiles }, since) {
69
+ const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-only", "-z", since]);
70
+ const dirty = [...stdout.split("\0").filter(Boolean), ...(await untrackedFiles(projectDir))];
71
+ const reconcile = readRefreshedPaths(projectDir).filter((p) => !dirty.includes(p));
72
+ const changed = dirty.length + reconcile.length;
73
+ if (changed === 0) return { skipped: true, head };
74
+ if (changed > maxFiles) {
75
+ logger.warn?.(`[rag] ${changed} file(s) changed since the last index (more than ${maxFiles}): answering from the index as it is; bring it up to date with kj rag index --since auto`);
76
+ return { skipped: true, reason: "drift-too-large", changed, head };
77
+ }
78
+ logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}, ${changed} file(s)); running delta update`);
79
+ const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config), worktree: true, reconcile });
80
+ if (totals.head) setLastIndexedCommit(db, slug, totals.head);
81
+ writeRefreshedPaths(projectDir, dirty);
82
+ return { ran: true, totals };
83
+ }
84
+
34
85
  export async function maybeAutoUpdate({ projectDir, config, logger = console, flags = {} } = {}) {
35
86
  // commander turns `--no-rag-update` into flags.ragUpdate === false.
36
87
  if (flags?.ragUpdate === false || config?.rag?.autoUpdate?.onRun === false) return { skipped: true };
@@ -42,53 +93,15 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
42
93
  const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
43
94
  try {
44
95
  const since = getLastIndexedCommit(db, slug);
45
- if (!since) {
46
- // KJC-TSK-1174 (RAG-A2): no index yet. It is built in the background, once; the session
47
- // does not wait. A recent attempt that died (no Ollama) is not retried blindly.
48
- const boot = startBootstrap(projectDir);
49
- if (boot.recent) {
50
- warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index and the background indexing did not finish: run kj rag index --with-sources`);
51
- return { skipped: true, reason: "bootstrap-failed", head };
52
- }
53
- warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index yet: indexing it in the background (pid ${boot.pid}); queries answer from it once it is done`);
54
- return { skipped: true, reason: "bootstrapping", pid: boot.pid, head };
55
- }
96
+ if (!since) return bootstrapInBackground({ projectDir, slug, head, logger });
56
97
  // KJC-BUG-0304: a drift of hundreds of files (an index weeks old) reindexed the
57
98
  // repo for half an hour before the query answered. Past the bound the query
58
99
  // answers from the index as it is, and says how to bring it up to date.
59
- const maxFiles = config?.rag?.autoUpdate?.maxFiles ?? DEFAULT_MAX_DRIFT_FILES;
60
- // KJC-TSK-1171 (RAG-B): an index built before the embedded header holds vectors a
61
- // query by file or symbol name cannot reach. Small, it is rebuilt here; large, the
62
- // query answers from it and says once how to rebuild.
100
+ const ctx = { db, slug, projectDir, config, logger, head, maxFiles: config?.rag?.autoUpdate?.maxFiles ?? DEFAULT_MAX_DRIFT_FILES };
63
101
  const schema = readEmbedSchema(projectDir);
64
- if (schema < EMBED_SCHEMA) {
65
- const sources = countSources(db, slug, projectDir);
66
- if (sources > maxFiles) {
67
- warnOnce(logger, projectDir, `[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}): answering from it as it is; rebuild it with kj rag index --with-sources`);
68
- return { skipped: true, reason: "embed-schema-old", sources, head };
69
- }
70
- logger.info?.(`[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}); rebuilding ${sources} source(s)`);
71
- const totals = await indexProject(projectDir, { db, embedder: makeGovernedEmbedder(config), karajanHome: getKarajanHome(), logger, withSources: true, exclude: ragExclude(config) });
72
- if (totals.head) setLastIndexedCommit(db, slug, totals.head);
73
- return { ran: true, rebuilt: true, totals };
74
- }
75
- // KJC-TSK-1174 (RAG-A1): the drift is measured up to the files on disk, untracked ones
76
- // included, so a query sees what the session is writing before it is committed.
77
- // The dirty paths of the last refresh count too: reverted or deleted since, they hold stale chunks.
78
- const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-only", "-z", since]);
79
- const dirty = [...stdout.split("\0").filter(Boolean), ...(await untrackedFiles(projectDir))];
80
- const reconcile = readRefreshedPaths(projectDir).filter((p) => !dirty.includes(p));
81
- const changed = dirty.length + reconcile.length;
82
- if (changed === 0) return { skipped: true, head };
83
- if (changed > maxFiles) {
84
- logger.warn?.(`[rag] ${changed} file(s) changed since the last index (more than ${maxFiles}): answering from the index as it is; bring it up to date with kj rag index --since auto`);
85
- return { skipped: true, reason: "drift-too-large", changed, head };
86
- }
87
- logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}, ${changed} file(s)); running delta update`);
88
- const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config), worktree: true, reconcile });
89
- if (totals.head) setLastIndexedCommit(db, slug, totals.head);
90
- writeRefreshedPaths(projectDir, dirty);
91
- return { ran: true, totals };
102
+ // Awaited here, inside the try: the catch must see their errors and the finally must not close the store under them.
103
+ if (schema < EMBED_SCHEMA) return await rebuildOldSchema(ctx, schema);
104
+ return await deltaUpdate(ctx, since);
92
105
  } catch (err) {
93
106
  // Said once per process: an embedder that is down would repeat it on every query otherwise.
94
107
  warnOnce(logger, `failed:${projectDir}`, `[rag] auto-update failed (${err.message}); continuing without refresh`);
@@ -1,52 +1,28 @@
1
1
  // KJC-TSK-1174 (RAG-A2): a project with no index gets one in the background, once, at the first
2
2
  // command that needs it. The session goes on; queries answer from the index when it is done.
3
3
  import { spawn } from "node:child_process";
4
- import { mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, rmdirSync, unlinkSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { projectDbPath } from "./project-store.js";
8
+ import { projectSlug } from "./vec-store.js";
9
+ import { GRACE_MS, MAX_RUN_MS, indexLeasePath, readLease, takeLock, unlessGone, withMutex, writeLease } from "./pid-lease.js";
8
10
 
9
11
  const KJ_BIN = resolve(fileURLToPath(import.meta.url), "../../../bin/kj.js");
10
12
  // An attempt that died (no Ollama, no model) is not repeated on every command: this long apart.
11
13
  export const RETRY_MS = 10 * 60 * 1000;
12
- // A marker that does not parse, or a lock, older than this was left by a crashed command: taken over.
13
- export const GRACE_MS = 30 * 1000;
14
- // An indexer is not trusted to run longer than this: past it, its pid may belong to someone else.
15
- export const MAX_RUN_MS = 60 * 60 * 1000;
14
+ export { GRACE_MS, MAX_RUN_MS };
16
15
  const markerPath = (projectDir) => `${projectDbPath(projectDir)}.bootstrap`;
17
16
 
18
- const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
19
-
20
17
  /** What the marker says of the last attempt: `{ pid, at, running }`, or null when none was ever made. */
21
- const unlessGone = (fn) => {
22
- try { return fn(); }
23
- catch (err) {
24
- if (err.code === "ENOENT") { return null; }
25
- throw err;
26
- }
27
- };
28
-
29
- export function readBootstrap(projectDir, { now = Date.now() } = {}) {
30
- const file = markerPath(projectDir);
31
- // Read first, never check-then-read: another command may set the marker aside in between.
32
- const raw = unlessGone(() => readFileSync(file, "utf8"));
33
- if (raw === null) return null;
34
- try {
35
- const note = JSON.parse(raw);
36
- return { ...note, running: alive(note.pid) && now - note.at < MAX_RUN_MS };
37
- } catch {
38
- // Another command is writing it this very moment: running for a moment; past the grace, a crash.
39
- const stat = unlessGone(() => statSync(file));
40
- return stat === null ? null : { pid: null, at: 0, running: now - stat.mtimeMs < GRACE_MS };
41
- }
42
- }
18
+ export const readBootstrap = (projectDir, o) => readLease(markerPath(projectDir), o);
43
19
 
44
20
  /**
45
21
  * Starts `kj rag index --with-sources` detached, or reuses the one running. Returns
46
22
  * `{ started, pid }`, or `{ started: false, recent: true }` when the last attempt died less
47
23
  * than RETRY_MS ago: the index is then asked for by hand instead of being retried blindly.
48
24
  */
49
- export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now = Date.now() } = {}) {
25
+ export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now = Date.now(), lease = indexLeasePath() } = {}) {
50
26
  const file = markerPath(projectDir);
51
27
  mkdirSync(dirname(file), { recursive: true });
52
28
  // One claimant at a time: the lock directory is created atomically, so of two commands arriving
@@ -58,29 +34,27 @@ export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now
58
34
  const last = readBootstrap(projectDir, { now });
59
35
  if (last?.running) return { started: false, pid: last.pid };
60
36
  if (last && now - last.at < RETRY_MS) return { started: false, recent: true, pid: null };
61
- // The marker lands whole each time (written apart, then renamed): first with the claimant's
62
- // pid, so it reads as running meanwhile, then with the indexer's.
63
- const note = (pid) => { const tmp = `${file}.${process.pid}.tmp`; writeFileSync(tmp, JSON.stringify({ pid, at: now }), "utf8"); renameSync(tmp, file); };
64
- note(process.pid);
65
- const child = spawnFn(process.execPath, [bin, "rag", "index", "--with-sources"], { cwd: projectDir, detached: true, stdio: "ignore" });
66
- child.unref?.();
67
- note(child.pid);
68
- return { started: true, pid: child.pid };
37
+ // One indexer per machine: the lease is taken inside the project mutex, before the marker is
38
+ // written, so a queued project does not spend its RETRY_MS. Both mutexes yield, never wait.
39
+ const slug = projectSlug(projectDir);
40
+ const res = withMutex(lease, now, () => {
41
+ const holder = readLease(lease, { now });
42
+ if (holder?.running && holder.pid !== process.pid) return { started: false, busy: { projectDir: holder.projectDir, slug: holder.slug, pid: holder.pid } };
43
+ // Marker and lease land whole each time: first with the claimant's pid, so they read as
44
+ // running meanwhile, then with the indexer's.
45
+ const note = (pid) => { writeLease(file, { pid, at: now }); writeLease(lease, { pid, at: now, projectDir, slug }); };
46
+ note(process.pid);
47
+ try {
48
+ const child = spawnFn(process.execPath, [bin, "rag", "index", "--with-sources"], { cwd: projectDir, detached: true, stdio: "ignore" });
49
+ child.unref?.();
50
+ note(child.pid);
51
+ return { started: true, pid: child.pid };
52
+ } catch (err) {
53
+ // No indexer exists: neither the marker nor the lease may keep naming this (maybe long-lived) process.
54
+ unlessGone(() => unlinkSync(file)); unlessGone(() => unlinkSync(lease));
55
+ throw err;
56
+ }
57
+ });
58
+ return res.busy === true ? { started: false, busy: {} } : res;
69
59
  } finally { unlessGone(() => rmdirSync(lock)); }
70
60
  }
71
-
72
- const takeLock = (lock, now) => {
73
- for (let attempt = 0; attempt < 2; attempt += 1) {
74
- try { mkdirSync(lock); return true; }
75
- catch (err) {
76
- if (err.code !== "EEXIST") throw err;
77
- const held = unlessGone(() => statSync(lock));
78
- if (held && now - held.mtimeMs < GRACE_MS) return false;
79
- // The holder crashed. The lock is set aside under a unique name first: only the one caller
80
- // whose rename succeeds removes it, so nobody removes a lock another caller has just taken.
81
- const aside = `${lock}.${process.pid}.${now}.stale`;
82
- if (unlessGone(() => renameSync(lock, aside)) !== null) rmdirSync(aside);
83
- }
84
- }
85
- return false;
86
- };
@@ -10,7 +10,7 @@ import { existsSync } from "node:fs";
10
10
  import { sep } from "node:path";
11
11
 
12
12
  import { insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
13
- import { openVecStoreExplained as openVecStore } from "./project-store.js";
13
+ import { openVecStore } from "./vec-store.js";
14
14
 
15
15
  export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
16
16
 
@@ -9,7 +9,7 @@
9
9
  import fs from "node:fs/promises";
10
10
  import { existsSync, rmSync } from "node:fs";
11
11
  import path from "node:path";
12
- import Database from "better-sqlite3";
12
+ import { openDatabase } from "karajan-core/db";
13
13
  import { parseDocument } from "yaml";
14
14
  import { getProjectConfigPath } from "../config/loader.js";
15
15
  import { projectDbPath } from "./project-store.js";
@@ -31,7 +31,7 @@ export function resetEmptyStore(projectDir = process.cwd()) {
31
31
  if (!existsSync(storePath)) return true;
32
32
  // Read-only, schema-agnostic inspection: no DDL, no vec extension, no
33
33
  // dimension assumptions — the store is only ever OPENED to be counted.
34
- const db = new Database(storePath, { readonly: true });
34
+ const db = openDatabase(storePath, { readOnly: true });
35
35
  try {
36
36
  const hasChunksTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'").get();
37
37
  if (hasChunksTable && db.prepare("SELECT COUNT(*) AS n FROM chunks").get().n > 0) return false;
@@ -0,0 +1,70 @@
1
+ // HU-2757: a lease names the pid that holds a resource and when it took it. Nobody releases it:
2
+ // a dead pid, or an age of MAX_RUN_MS, means free. A mkdir mutex makes the decision atomic.
3
+ import { mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { getKarajanHome } from "../utils/paths.js";
6
+
7
+ // A lease that does not parse, or a lock, older than this was left by a crashed command: taken over.
8
+ export const GRACE_MS = 30 * 1000;
9
+ // An indexer is not trusted to run longer than this: past it, its pid may belong to someone else.
10
+ export const MAX_RUN_MS = 60 * 60 * 1000;
11
+
12
+ export const indexLeasePath = () => join(getKarajanHome(), "locks", "rag-index.json");
13
+
14
+ // EPERM (a pid of another user) counts as dead: a single-user machine has no such indexer.
15
+ const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
16
+
17
+ export const unlessGone = (fn) => {
18
+ try { return fn(); }
19
+ catch (err) {
20
+ if (err.code === "ENOENT") { return null; }
21
+ throw err;
22
+ }
23
+ };
24
+
25
+ /** What the lease says: `{ pid, at, running, ... }`, or null when none was ever written. */
26
+ export function readLease(file, { now = Date.now() } = {}) {
27
+ // Read first, never check-then-read: another command may set the file aside in between.
28
+ const raw = unlessGone(() => readFileSync(file, "utf8"));
29
+ if (raw === null) return null;
30
+ try {
31
+ const note = JSON.parse(raw);
32
+ return { ...note, running: alive(note.pid) && now - note.at < MAX_RUN_MS };
33
+ } catch {
34
+ // Another command is writing it this very moment: running for a moment; past the grace, a crash.
35
+ const stat = unlessGone(() => statSync(file));
36
+ return stat === null ? null : { pid: null, at: 0, running: now - stat.mtimeMs < GRACE_MS };
37
+ }
38
+ }
39
+
40
+ /** The lease lands whole: written apart, then renamed. */
41
+ export function writeLease(file, record) {
42
+ mkdirSync(dirname(file), { recursive: true });
43
+ const tmp = `${file}.${process.pid}.tmp`;
44
+ writeFileSync(tmp, JSON.stringify(record), "utf8");
45
+ renameSync(tmp, file);
46
+ }
47
+
48
+ export const takeLock = (lock, now) => {
49
+ for (let attempt = 0; attempt < 2; attempt += 1) {
50
+ try { mkdirSync(lock); return true; }
51
+ catch (err) {
52
+ if (err.code !== "EEXIST") throw err;
53
+ const held = unlessGone(() => statSync(lock));
54
+ if (held && now - held.mtimeMs < GRACE_MS) return false;
55
+ // The holder crashed. The lock is set aside under a unique name first: only the one caller
56
+ // whose rename succeeds removes it, so nobody removes a lock another caller has just taken.
57
+ const aside = `${lock}.${process.pid}.${now}.stale`;
58
+ if (unlessGone(() => renameSync(lock, aside)) !== null) rmdirSync(aside);
59
+ }
60
+ }
61
+ return false;
62
+ };
63
+
64
+ /** Runs `fn` under the mutex `${file}.lock`, or returns `{ busy: true }` when another command holds it. */
65
+ export function withMutex(file, now, fn) {
66
+ const lock = `${file}.lock`;
67
+ mkdirSync(dirname(lock), { recursive: true });
68
+ if (!takeLock(lock, now)) return { busy: true };
69
+ try { return fn(); } finally { unlessGone(() => rmdirSync(lock)); }
70
+ }
@@ -17,7 +17,6 @@ import { execFileSync } from "node:child_process";
17
17
  import path from "node:path";
18
18
 
19
19
  import { openVecStore } from "./vec-store.js";
20
- import { explainNativeLoadError } from "../checks/native-build.js";
21
20
  import { getKarajanHome } from "../utils/paths.js";
22
21
 
23
22
  function projectRoot(dir) {
@@ -40,20 +39,11 @@ export function libraryDbPath() {
40
39
  return process.env.KJ_LIBRARY_DB || path.join(getKarajanHome(), "library.db");
41
40
  }
42
41
 
43
- // KJC-BUG-0426: an unbuilt better-sqlite3 addon says how to build it, not "bindings file".
44
- // Every opening of a RAG store goes through here.
45
- export function openVecStoreExplained(opts) {
46
- try {
47
- return openVecStore(opts);
48
- } catch (err) {
49
- throw explainNativeLoadError(err);
50
- }
51
- }
52
42
 
53
43
  export function openLibraryStore({ dim = 768 } = {}) {
54
- return openVecStoreExplained({ dim, path: libraryDbPath() });
44
+ return openVecStore({ dim, path: libraryDbPath() });
55
45
  }
56
46
 
57
47
  export function openProjectStore({ projectDir = process.cwd(), dim = 768 } = {}) {
58
- return openVecStoreExplained({ dim, path: projectDbPath(projectDir) });
48
+ return openVecStore({ dim, path: projectDbPath(projectDir) });
59
49
  }
@@ -41,7 +41,8 @@ async function findLiveCardInBranch(projectDir, branch) {
41
41
  const plan = await loadPlan(projectDir, meta.planId);
42
42
  for (const h of plan.hus || []) {
43
43
  // KJC-BUG-0353: an HU is also named by the id of its replica in an external tracker.
44
- for (const ref of [h.short_id, h.id, h.external_ref]) {
44
+ // KJC-BUG-0443: a shared-board HU carries "repo#n · ref"; each part names it.
45
+ for (const ref of [h.short_id, h.id, ...String(h.external_ref ?? "").split(" · ")]) {
45
46
  if (ref && refInBranch(ref, needle)) {
46
47
  if (LIVE_STATUSES.has(h.status)) return { ref, live: true, status: h.status };
47
48
  closedHit = { ref, live: false, status: h.status };
@@ -52,7 +53,12 @@ async function findLiveCardInBranch(projectDir, branch) {
52
53
  return closedHit;
53
54
  }
54
55
 
55
- export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, deps = {} }) {
56
+ // A failed check under the effective policy: blocks, or passes with a warning.
57
+ const failedCheck = (effective, reason, extra = {}) => (effective === "block"
58
+ ? { ok: false, mode: "block", reason, ...extra }
59
+ : { ok: true, mode: "warn", reason, ...extra });
60
+
61
+ function exemption(config, branch) {
56
62
  const exempt = config.method_gates?.card_first_exempt_branches || DEFAULT_EXEMPT_PREFIXES;
57
63
  if (exempt.some((p) => branch.startsWith(p))) {
58
64
  return { ok: true, mode: "exempt", reason: `branch prefix exempt (${branch.split("/")[0]}/…)` };
@@ -62,59 +68,64 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
62
68
  if ([config.base_branch || "main", "main", "master", "HEAD"].includes(branch)) {
63
69
  return { ok: true, mode: "exempt", reason: "base branch — the branch-first gate owns this case" };
64
70
  }
71
+ return null;
72
+ }
65
73
 
66
- const backend = config.state_backend || "hu-board";
67
- const policy = config.method_gates?.card_first || "auto";
74
+ async function huBoardCheck({ projectDir, branch, policy }) {
75
+ const hit = await findLiveCardInBranch(projectDir, branch);
76
+ if (hit?.live) return { ok: true, mode: "pass", ref: hit.ref };
77
+ const reason = hit
78
+ ? `branch references ${hit.ref} but its card is "${hit.status}" — card-first needs a live card (reopen it or create one: kj hu add)`
79
+ : `no live card referenced in branch "${branch}" — create it first (kj hu add "…" --id <REF>) and name the branch after it`;
80
+ return failedCheck(policy === "auto" ? "block" : policy, reason);
81
+ }
68
82
 
69
- if (backend === "hu-board") {
70
- const hit = await findLiveCardInBranch(projectDir, branch);
71
- if (hit?.live) return { ok: true, mode: "pass", ref: hit.ref };
72
- const effective = policy === "auto" ? "block" : policy;
73
- const reason = hit
74
- ? `branch references ${hit.ref} but its card is "${hit.status}" — card-first needs a live card (reopen it or create one: kj hu add)`
75
- : `no live card referenced in branch "${branch}" — create it first (kj hu add "…" --id <REF>) and name the branch after it`;
76
- return effective === "block"
77
- ? { ok: false, mode: "block", reason }
78
- : { ok: true, mode: "warn", reason };
83
+ // A card-shaped reference in the branch, verified against the tracker when an adapter is declared.
84
+ async function trackerRefCheck({ config, projectDir, branch, deps, boardName, effective }) {
85
+ const ref = branch.match(CARD_REF_RE)[0];
86
+ const { verifyFn = verifyCardAlive } = deps;
87
+ const v = await verifyFn({ ref, verifyCmd: config.board?.verify_cmd, projectDir });
88
+ if (v.verified === false) {
89
+ const reason = `branch references ${ref} but ${boardName} reports it "${v.status}" — card-first needs a live card in the tracker (create/reopen it there)`;
90
+ return failedCheck(effective, reason, { level: v.level });
79
91
  }
92
+ // KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
93
+ // esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
94
+ // decide si la primera para, y el mensaje dice cual de las dos es: un gate
95
+ // que aprueba lo que no ha comprobado da una confianza que no ha ganado.
96
+ // Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
97
+ // quien aun no tiene verify_cmd.
98
+ if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
99
+ return {
100
+ ok: false, mode: "block", level: v.level,
101
+ reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
102
+ };
103
+ }
104
+ const huNote = `no live HU on the HU Board links ${ref}: kj hu add "…" --ref ${ref}`;
105
+ return { ok: true, mode: "pass", ref, level: v.level, note: v.note ? `${v.note}; ${huNote}` : huNote };
106
+ }
80
107
 
81
- // planning-game / external: presence check, upgraded to LIVENESS against
82
- // the real tracker when board.verify_cmd declares an adapter
83
- // (KJC-TSK-0732, issue #1371). Unverifiable degrades honestly — the
84
- // result names its level so nobody mistakes branch-ref for tracker.
108
+ // planning-game / external: presence check, upgraded to LIVENESS against
109
+ // the real tracker when board.verify_cmd declares an adapter
110
+ // (KJC-TSK-0732, issue #1371). Unverifiable degrades honestly — the
111
+ // result names its level so nobody mistakes branch-ref for tracker.
112
+ async function externalBoardCheck({ config, projectDir, branch, deps, backend, policy }) {
85
113
  const boardName = config.board?.name || backend;
86
114
  const effective = policy === "auto" ? "warn" : policy;
87
115
  // KJC-BUG-0353: the HU Board is always kj's board (KJC-BUG-0305) — a live HU
88
116
  // the branch names, or whose replica it names, is checked here, not guessed.
89
117
  const huHit = await findLiveCardInBranch(projectDir, branch);
90
118
  if (huHit?.live) return { ok: true, mode: "pass", ref: huHit.ref, level: "hu-board" };
91
- if (CARD_REF_RE.test(branch)) {
92
- const ref = branch.match(CARD_REF_RE)[0];
93
- const { verifyFn = verifyCardAlive } = deps;
94
- const v = await verifyFn({ ref, verifyCmd: config.board?.verify_cmd, projectDir });
95
- if (v.verified === false) {
96
- const reason = `branch references ${ref} but ${boardName} reports it "${v.status}" — card-first needs a live card in the tracker (create/reopen it there)`;
97
- return effective === "block"
98
- ? { ok: false, mode: "block", reason, level: v.level }
99
- : { ok: true, mode: "warn", reason, level: v.level };
100
- }
101
- // KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
102
- // esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
103
- // decide si la primera para, y el mensaje dice cual de las dos es: un gate
104
- // que aprueba lo que no ha comprobado da una confianza que no ha ganado.
105
- // Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
106
- // quien aun no tiene verify_cmd.
107
- if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
108
- return {
109
- ok: false, mode: "block", level: v.level,
110
- reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
111
- };
112
- }
113
- const huNote = `no live HU on the HU Board links ${ref}: kj hu add "…" --ref ${ref}`;
114
- return { ok: true, mode: "pass", ref, level: v.level, note: v.note ? `${v.note}; ${huNote}` : huNote };
115
- }
119
+ if (CARD_REF_RE.test(branch)) return trackerRefCheck({ config, projectDir, branch, deps, boardName, effective });
116
120
  const reason = `no card reference in branch "${branch}" — card-first on ${boardName}: create the card there and name the branch after it (e.g. feat/ABC-123-summary)`;
117
- return effective === "block"
118
- ? { ok: false, mode: "block", reason }
119
- : { ok: true, mode: "warn", reason };
121
+ return failedCheck(effective, reason);
122
+ }
123
+
124
+ export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, deps = {} }) {
125
+ const exempt = exemption(config, branch);
126
+ if (exempt) return exempt;
127
+ const backend = config.state_backend || "hu-board";
128
+ const policy = config.method_gates?.card_first || "auto";
129
+ if (backend === "hu-board") return huBoardCheck({ projectDir, branch, policy });
130
+ return externalBoardCheck({ config, projectDir, branch, deps, backend, policy });
120
131
  }
@@ -46,6 +46,14 @@ export const CONTRACT_BLOCK = [
46
46
  "!.karajan/security-level.json",
47
47
  // KJC-TSK-1163 (ADR 0025): which HU Board the repo uses; the team shares it.
48
48
  "!.karajan/board.yml",
49
+ // ADR 0032 (KJC-TSK-1231): the team's own state, as the board: the rules the roles
50
+ // apply and atril's config with its lockfile. Shared plans travel in .karajan-shared/.
51
+ "!.karajan/coder-rules.md",
52
+ "!.karajan/review-rules.md",
53
+ // KJC-TSK-1234: the project config, once the pre-commit refuses it with a secret in it.
54
+ "!.karajan/kj.config.yml",
55
+ "!.karajan/atril.json",
56
+ "!.karajan/atril.lock.json",
49
57
  ];
50
58
 
51
59
  /**
@@ -117,6 +125,8 @@ export async function excludeLocalArtifacts(projectDir, run = runCommand) {
117
125
  }
118
126
  }
119
127
 
128
+ const SHADOWING = new Set(["review-rules.md", "coder-rules.md"]);
129
+
120
130
  export async function ensureGateTrackable(projectDir) {
121
131
  const file = path.join(projectDir, ".gitignore");
122
132
  let text;
@@ -126,14 +136,22 @@ export async function ensureGateTrackable(projectDir) {
126
136
  // a partial state (e.g. a hand-written marker re-include without the hook
127
137
  // ones) must still be completed, not silently accepted.
128
138
  const reIncludes = CONTRACT_BLOCK.filter((l) => l.startsWith("!"));
129
- if (reIncludes.every((l) => text.includes(l))) return { changed: false };
130
-
131
139
  const isRootExclude = (l) => {
132
140
  const t = l.trim();
133
141
  return t === ".karajan/" || t === ".karajan" || t === ".karajan/*";
134
142
  };
135
143
  const lines = text.split("\n");
136
144
  const idx = lines.findIndex(isRootExclude);
145
+ // ADR 0032: a bare rules line an older kj wrote after its re-include (or with none) undoes
146
+ // it, since the last match wins: it goes. One after the root exclude but before its own
147
+ // re-include was already overridden, so it is kept and moved ahead of the block, where it
148
+ // stays overridden. Lines before the root exclude are never touched.
149
+ const placed = (l, i) => {
150
+ if (idx === -1 || i <= idx || !SHADOWING.has(l.trim())) return "keep";
151
+ const reInclude = lines.findLastIndex((x) => x.trim() === `!.karajan/${l.trim()}`);
152
+ return reInclude === -1 || i > reInclude ? "drop" : "move";
153
+ };
154
+ if (reIncludes.every((l) => text.includes(l)) && lines.every((l, i) => placed(l, i) === "keep")) return { changed: false };
137
155
  if (idx === -1) return { changed: false };
138
156
 
139
157
  // Drop EVERY .karajan root exclude (a later duplicate would override the
@@ -142,9 +160,10 @@ export async function ensureGateTrackable(projectDir) {
142
160
  // position of the first exclude. Exactly one coherent block, idempotent.
143
161
  const partials = new Set(CONTRACT_BLOCK);
144
162
  const marker = "@@kj-contract-block@@";
163
+ const moved = lines.filter((l, i) => placed(l, i) === "move");
145
164
  const cleaned = lines.map((l, i) => (i === idx ? marker : l))
146
- .filter((l) => l === marker || (!isRootExclude(l) && !partials.has(l.trim())));
147
- const result = cleaned.flatMap((l) => (l === marker ? CONTRACT_BLOCK : [l]));
165
+ .filter((l, i) => l === marker || (!isRootExclude(l) && !partials.has(l.trim()) && placed(l, i) === "keep"));
166
+ const result = cleaned.flatMap((l) => (l === marker ? [...moved, ...CONTRACT_BLOCK] : [l]));
148
167
  await fs.writeFile(file, result.join("\n"));
149
168
  return { changed: true };
150
169
  }