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
@@ -0,0 +1,36 @@
1
+ /**
2
+ * KJC-TSK-1234 (ADR 0032): the project's kj.config.yml travels with the repo, so it must never
3
+ * hold a secret. It is judged by its parsed YAML (escaped keys, flow mappings, anchors and merge
4
+ * keys all resolved), never by its raw text. The parse is strict: a duplicated key fails, since a
5
+ * second empty value could hide a secret written before it.
6
+ */
7
+ import fs from "node:fs";
8
+ import yaml from "js-yaml";
9
+
10
+ export const SECRET_PATHS = [["sonarqube", "token"], ["sonarqube", "admin_password"], ["sonarcloud", "token"]];
11
+
12
+ /** @returns {string[]} the secret paths that hold a value (empty, null and absent are fine) */
13
+ export function configSecrets(text) {
14
+ const doc = yaml.load(text) ?? {};
15
+ return SECRET_PATHS
16
+ .filter(([section, key]) => {
17
+ const value = doc?.[section]?.[key];
18
+ return value !== undefined && value !== null && value !== "";
19
+ })
20
+ .map((path) => path.join("."));
21
+ }
22
+
23
+ /** `kj config --check-secrets <file|->`: exit 1 naming each secret, or when the file cannot be read. */
24
+ export function checkSecretsCommand(file, { logger = console } = {}) {
25
+ let found;
26
+ try {
27
+ found = configSecrets(fs.readFileSync(file === "-" ? 0 : file, "utf8"));
28
+ } catch (err) {
29
+ logger.error(`kj: cannot check ${file} for secrets (${err.message})`);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ if (found.length === 0) return;
34
+ logger.error(`kj: ${file} holds a secret (${found.join(", ")}): move it to .karajan/kj.config.local.yml or ~/.karajan`);
35
+ process.exitCode = 1;
36
+ }
package/src/config.js CHANGED
@@ -25,6 +25,7 @@ export { DEFAULTS } from "./config/defaults.js";
25
25
  export {
26
26
  getConfigPath,
27
27
  getProjectConfigPath,
28
+ getLocalConfigPath,
28
29
  loadProjectConfig,
29
30
  loadConfig,
30
31
  writeConfig,
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
9
11
 
10
12
  const ADR_DIR = path.join(".karajan", "adrs");
11
13
  // KJC-BUG-0272: whoever records a decision proposes it. Accepting is the user's:
@@ -28,12 +30,24 @@ export async function listAdrs(projectDir) {
28
30
  return adrs;
29
31
  }
30
32
 
31
- export async function addAdr(projectDir, { title, decision, context = "", consequences = "" }) {
33
+ const realGit = (cwd) => async (args) => (await promisify(execFile)("git", args, { cwd })).stdout;
34
+
35
+ // HU-2792: numbers that open branches hold, read from the local origin/* refs
36
+ // (no network). Not a git repo, or no remotes: nothing taken, as before.
37
+ async function remoteAdrNumbers(run) {
38
+ try {
39
+ const refs = (await run(["for-each-ref", "--format=%(refname)", "refs/remotes/origin"])).split("\n").filter(Boolean);
40
+ const trees = await Promise.all(refs.map((ref) => run(["ls-tree", "--name-only", ref, `${ADR_DIR}/`]).catch(() => "")));
41
+ return trees.flatMap((t) => t.split("\n")).map((f) => /\/(\d{4})-[^/]*\.md$/.exec(f)?.[1]).filter(Boolean).map(Number);
42
+ } catch { return []; }
43
+ }
44
+
45
+ export async function addAdr(projectDir, { title, decision, context = "", consequences = "" }, run = realGit(projectDir)) {
32
46
  if (!title?.trim() || !decision?.trim()) {
33
47
  throw new Error("kj adr add requires a title and --decision");
34
48
  }
35
49
  const existing = await listAdrs(projectDir);
36
- const number = (existing.at(-1)?.number || 0) + 1;
50
+ const number = Math.max(0, ...existing.map((a) => a.number), ...await remoteAdrNumbers(run)) + 1;
37
51
  const file = path.join(ADR_DIR, `${String(number).padStart(4, "0")}-${slugify(title)}.md`);
38
52
  const body = [
39
53
  `# ${title}`, "",
@@ -14,6 +14,7 @@
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
+ import { claudeConfigFile, codexHome } from "../utils/agent-home.js";
17
18
 
18
19
  const TOKEN_CONVENTIONS = {
19
20
  linear: ["LINEAR_API_KEY"],
@@ -23,18 +24,19 @@ const TOKEN_CONVENTIONS = {
23
24
  asana: ["ASANA_ACCESS_TOKEN"],
24
25
  };
25
26
 
26
- function mcpConfigPaths(projectDir, home) {
27
+ // KJC-TSK-1344: claude and codex configs of the account CLAUDE_CONFIG_DIR / CODEX_HOME select.
28
+ function mcpConfigPaths(projectDir, home, env = process.env) {
27
29
  return [
28
30
  path.join(projectDir, ".mcp.json"),
29
- path.join(home, ".claude.json"),
30
- path.join(home, ".codex", "config.toml"),
31
+ claudeConfigFile({ home, env }),
32
+ path.join(codexHome({ home, env }), "config.toml"),
31
33
  path.join(home, ".gemini", "settings.json"),
32
34
  ];
33
35
  }
34
36
 
35
- function mcpConfigsMention(slug, projectDir, home) {
37
+ function mcpConfigsMention(slug, projectDir, home, env) {
36
38
  const needle = slug.toLowerCase();
37
- for (const p of mcpConfigPaths(projectDir, home)) {
39
+ for (const p of mcpConfigPaths(projectDir, home, env)) {
38
40
  try {
39
41
  if (existsSync(p) && readFileSync(p, "utf8").toLowerCase().includes(needle)) return p;
40
42
  } catch { /* unreadable config — keep looking */ }
@@ -50,13 +52,13 @@ function mcpConfigsMention(slug, projectDir, home) {
50
52
  */
51
53
  export function detectAvailableBoards({ projectDir = process.cwd(), env = process.env, home = os.homedir() } = {}) {
52
54
  const boards = [{ value: "hu-board", label: "HU Board (bundled, zero setup)" }];
53
- if (mcpConfigsMention("planning-game", projectDir, home)) {
55
+ if (mcpConfigsMention("planning-game", projectDir, home, env)) {
54
56
  boards.push({ value: "planning-game", label: "Planning Game (MCP detected)" });
55
57
  }
56
58
  for (const [slug, envs] of Object.entries(TOKEN_CONVENTIONS)) {
57
59
  const token = envs.find((k) => env[k]);
58
60
  if (token) boards.push({ value: `external:${slug}`, label: `${slug} (token ${token})` });
59
- else if (mcpConfigsMention(slug, projectDir, home)) boards.push({ value: `external:${slug}`, label: `${slug} (MCP detected)` });
61
+ else if (mcpConfigsMention(slug, projectDir, home, env)) boards.push({ value: `external:${slug}`, label: `${slug} (MCP detected)` });
60
62
  }
61
63
  return boards;
62
64
  }
@@ -65,7 +67,7 @@ export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env
65
67
  const backend = config.state_backend || "hu-board";
66
68
 
67
69
  if (backend === "planning-game") {
68
- const hit = mcpConfigsMention("planning-game", projectDir, home);
70
+ const hit = mcpConfigsMention("planning-game", projectDir, home, env);
69
71
  if (hit) return { ok: true, backend, via: `planning-game MCP (${hit})` };
70
72
  return {
71
73
  ok: false, backend,
@@ -77,7 +79,7 @@ export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env
77
79
  const name = String(config.board?.name || "").trim();
78
80
  if (!name) return { ok: false, backend, needed: ["declare board.name in kj.config.yml (e.g. Linear, Trello, Jira, GitHub Issues)"] };
79
81
  const slug = name.toLowerCase().split(/\s+/)[0];
80
- const hit = mcpConfigsMention(slug, projectDir, home);
82
+ const hit = mcpConfigsMention(slug, projectDir, home, env);
81
83
  if (hit) return { ok: true, backend, via: `${name} MCP detected (${hit})` };
82
84
  const tokenEnvs = config.board?.token_env ? [config.board.token_env] : (TOKEN_CONVENTIONS[slug] || []);
83
85
  const found = tokenEnvs.find((k) => env[k]);
@@ -1,10 +1,11 @@
1
1
  /**
2
- * KJC-TSK-1123 (#2315): every PR Karajan opens carries the «Karajan» label, so
2
+ * KJC-TSK-1123 (#2315): every PR Karajan governs carries the «Karajan» label, so
3
3
  * a team can measure how much of its change goes through the governor. It means
4
4
  * "went through Karajan", never "written by an AI": Karajan governs AIs, and the
5
5
  * PR may be a person's work it reviewed. The signal lives only in the PR's
6
6
  * metadata, never in commits or the PR body. Labelling never costs the PR: a
7
- * failure is logged and the work goes on.
7
+ * failure is logged and the work goes on. KJC-BUG-0461: since 5.0 (no kj run opening PRs)
8
+ * kj review --publish-pr adds it, the step of the method that runs on every PR.
8
9
  */
9
10
  import { runCommand } from "../utils/process.js";
10
11
 
@@ -12,20 +13,31 @@ export const KARAJAN_LABEL = "Karajan";
12
13
  const LABEL_COLOR = "1f6feb";
13
14
  const LABEL_DESCRIPTION = "Went through Karajan (governed and reviewed); not a claim that an AI wrote it";
14
15
 
15
- /** owner/repo of a GitHub PR URL, or null when the URL has another shape. */
16
- const repoOf = (url) => /^https:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/\d+/.exec(url)?.[1] ?? null;
16
+ /** The PR a URL or a bare number names: { repo: "owner/repo" or null for the repo gh runs in, number }. */
17
+ function prOf(ref) {
18
+ const url = /^https:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/(\d+)/.exec(String(ref ?? ""));
19
+ if (url) return { repo: url[1], number: url[2] };
20
+ return /^\d+$/.test(String(ref ?? "")) ? { repo: null, number: String(ref) } : null;
21
+ }
17
22
 
18
23
  /**
19
24
  * Add the label to the PR, creating it first when the repo lacks it.
25
+ * @param {{url: string|number}} args a PR URL, or its number in the repo gh runs in
20
26
  * @returns {Promise<boolean>} true when the PR carries the label.
21
27
  */
22
28
  export async function labelKarajanPr({ url, cwd = null, run = runCommand, warn = (msg) => process.stderr.write(`${msg}\n`) }) {
23
29
  const opts = cwd ? { cwd } : {};
24
- const repo = repoOf(url);
30
+ const pr = prOf(url);
31
+ if (!pr) {
32
+ warn(`kj: could not add the Karajan label to ${url}: not a pull request URL or number`);
33
+ return false;
34
+ }
25
35
  try {
26
- // Fails when the label exists; the edit below says whether the PR got it.
27
- await run("gh", ["label", "create", KARAJAN_LABEL, "--color", LABEL_COLOR, "--description", LABEL_DESCRIPTION, ...(repo ? ["--repo", repo] : [])], opts);
28
- const res = await run("gh", ["pr", "edit", url, "--add-label", KARAJAN_LABEL], opts);
36
+ // Fails when the label exists; the call below says whether the PR got it.
37
+ await run("gh", ["label", "create", KARAJAN_LABEL, "--color", LABEL_COLOR, "--description", LABEL_DESCRIPTION, ...(pr.repo ? ["--repo", pr.repo] : [])], opts);
38
+ // KJC-BUG-0462: REST. `gh pr edit --add-label` asks GraphQL for the PR's classic project
39
+ // cards and fails since GitHub sunset Projects (classic), so no PR got the label.
40
+ const res = await run("gh", ["api", "-X", "POST", `repos/${pr.repo ?? "{owner}/{repo}"}/issues/${pr.number}/labels`, "-f", `labels[]=${KARAJAN_LABEL}`], opts);
29
41
  if (res.exitCode === 0) return true;
30
42
  warn(`kj: could not add the Karajan label to ${url}: ${(res.stderr || res.stdout || "").trim()}`);
31
43
  } catch (err) {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * main-guard (KJC-TSK-1368, ADR 0037 layer 2): after a push to the protected branch, what
3
+ * reached it without its required checks, and whether the protection still enforces them on
4
+ * admins. It cannot prevent an abuse by someone holding the maintainer's credentials; it makes
5
+ * it visible. GitHub is reached through an injected api, so the logic runs without network.
6
+ *
7
+ * api.pullsOf(sha) → pull requests associated with a commit (GitHub's commits/{sha}/pulls)
8
+ * api.checksOf(sha) → [{ name, conclusion }] for a commit (check runs and statuses)
9
+ * api.protection() → the branch protection, or null when the branch is not protected
10
+ */
11
+
12
+ // GitHub counts these as passing for a required check.
13
+ const PASSING = new Set(["success", "neutral", "skipped"]);
14
+
15
+ /** The pull request that merged this commit into the branch, if any. */
16
+ function mergedInto(pulls, branch) {
17
+ return pulls.find((p) => p.merged_at && p.base?.ref === branch) ?? null;
18
+ }
19
+
20
+ async function auditCommit(sha, { branch, requiredChecks, api }) {
21
+ const pull = mergedInto(await api.pullsOf(sha), branch);
22
+ if (!pull) return { sha, kind: "direct-push", detail: `no pull request merged into ${branch}` };
23
+ const results = await api.checksOf(pull.head.sha);
24
+ const passed = new Set(results.filter((c) => PASSING.has(c.conclusion)).map((c) => c.name));
25
+ const missing = requiredChecks.filter((name) => !passed.has(name));
26
+ if (missing.length === 0) return null;
27
+ return { sha, kind: "checks-skipped", pr: pull.number, mergedBy: pull.merged_by?.login ?? "unknown", missing };
28
+ }
29
+
30
+ /** What changed in the protection against what is declared. */
31
+ function auditProtection(protection, { branch, requiredChecks }) {
32
+ if (!protection) return [`${branch} is not protected`];
33
+ const changes = [];
34
+ if (!protection.enforce_admins?.enabled) changes.push("enforce_admins is off: admins can merge past the required checks");
35
+ const rules = protection.required_status_checks ?? {};
36
+ const enforced = new Set([...(rules.contexts ?? []), ...(rules.checks ?? []).map((c) => c.context)]);
37
+ for (const name of requiredChecks) if (!enforced.has(name)) changes.push(`required check removed: ${name}`);
38
+ return changes;
39
+ }
40
+
41
+ /**
42
+ * @param {{ branch: string, commits: string[], requiredChecks: string[], api: object }} opts
43
+ * @returns {Promise<{ ok: boolean, merges: object[], protection: string[] }>}
44
+ */
45
+ export async function auditMain({ branch, commits, requiredChecks, api }) {
46
+ if (!requiredChecks?.length) throw new Error("main-guard: declare at least one required check to audit against");
47
+ const merges = [];
48
+ for (const sha of commits) {
49
+ const finding = await auditCommit(sha, { branch, requiredChecks, api });
50
+ if (finding) merges.push(finding);
51
+ }
52
+ const protection = auditProtection(await api.protection(), { branch, requiredChecks });
53
+ return { ok: merges.length === 0 && protection.length === 0, merges, protection };
54
+ }
@@ -43,6 +43,34 @@ const addCommand = ({ missing, ...target }) =>
43
43
  `gh api -X POST ${protectionPath(target)}/required_status_checks/contexts ${missing.map((c) => `-f ${shellQuote(`contexts[]=${c}`)}`).join(" ")}`;
44
44
 
45
45
  const settingsUrl = ({ owner, repo }) => `https://github.com/${owner}/${repo}/settings/branches`;
46
+ const rulesUrl = ({ owner, repo }) => `https://github.com/${owner}/${repo}/settings/rules`;
47
+
48
+ /**
49
+ * KJC-BUG-0449 (#2787): every rule that applies to the branch, rulesets included.
50
+ * [] when none applies (a 404 included); null when it cannot be read.
51
+ */
52
+ async function branchRules({ owner, repo, branch }, run) {
53
+ let res;
54
+ try { res = await run("gh", ["api", `repos/${owner}/${repo}/rules/branches/${encodeURIComponent(branch)}`]); } catch { return null; }
55
+ if (res.exitCode !== 0) return /404/.test(`${res.stderr}${res.stdout}`) ? [] : null;
56
+ try {
57
+ const rules = JSON.parse(res.stdout || "[]");
58
+ return Array.isArray(rules) ? rules : null;
59
+ } catch { return null; }
60
+ }
61
+
62
+ const rulesetChecks = (rules) => rules
63
+ .filter((r) => r?.type === "required_status_checks")
64
+ .flatMap((r) => (r.parameters?.required_status_checks ?? []).map((c) => c.context));
65
+
66
+ /** No classic protection: the rulesets decide, and they are fixed in their own page, never with a PUT. */
67
+ function rulesetAdvice({ rules, checks, target }) {
68
+ if (rules === null) return { status: "unverified", reason: "the branch rules could not be read", settings: rulesUrl(target), missing: checks };
69
+ if (rules.length === 0) return { status: "unprotected", command: protectCommand({ ...target, checks }) };
70
+ const required = new Set(rulesetChecks(rules));
71
+ const missing = checks.filter((c) => !required.has(c));
72
+ return missing.length > 0 ? { status: "ruleset-missing", missing, settings: rulesUrl(target) } : { status: "ok", via: "ruleset" };
73
+ }
46
74
 
47
75
  /**
48
76
  * What the base branch needs for the checks to be required, and how. A command
@@ -61,14 +89,18 @@ export async function protectionAdvice({ owner, repo, branch, checks, run = runC
61
89
  return { status: "unverified", reason: err.message, settings: settingsUrl(target), missing: checks };
62
90
  }
63
91
  if (res.exitCode !== 0) {
64
- if (/404|not protected/i.test(`${res.stderr}${res.stdout}`)) return { status: "unprotected", command: protectCommand({ ...target, checks }) };
92
+ if (/404|not protected/i.test(`${res.stderr}${res.stdout}`)) return rulesetAdvice({ rules: await branchRules(target, run), checks, target });
65
93
  return { status: "unverified", reason: (res.stderr || res.stdout || "gh failed").trim(), settings: settingsUrl(target), missing: checks };
66
94
  }
67
95
  const protection = JSON.parse(res.stdout || "{}");
96
+ const rules = await branchRules(target, run);
97
+ const fromRules = rulesetChecks(rules ?? []);
68
98
  const statusChecks = protection.required_status_checks;
69
- if (!statusChecks) return { status: "no-checks", settings: settingsUrl(target), missing: checks };
70
- const existing = new Set(statusChecks.contexts ?? []);
99
+ const existing = new Set([...(statusChecks?.contexts ?? []), ...fromRules]);
71
100
  const missing = checks.filter((c) => !existing.has(c));
101
+ // Unreadable rules: a ruleset may require what the classic layer lacks, so nothing is "missing" for sure.
102
+ if (rules === null && missing.length > 0) return { status: "unverified", reason: "the branch rules could not be read", settings: rulesUrl(target), missing };
103
+ if (!statusChecks && fromRules.length === 0) return { status: "no-checks", settings: settingsUrl(target), missing: checks };
72
104
  // KJC-BUG-0401: required checks an admin skips are not required for an agent holding an admin's token.
73
105
  const bindAdmins = protection.enforce_admins?.enabled === false ? `gh api -X POST ${protectionPath(target)}/enforce_admins` : null;
74
106
  if (missing.length > 0) return { status: "missing", missing, command: [addCommand({ ...target, missing }), bindAdmins].filter(Boolean).join(" && ") };
@@ -105,6 +137,7 @@ export async function adviseBranchProtection({ projectDir, branch, logger, run =
105
137
  missing: `${branch} does not require ${advice.missing?.length} kj check(s) (${list}). An admin runs (kj never does):`,
106
138
  "no-checks": `${branch} is protected but requires no status checks. An admin enables "Require status checks" and adds ${list} in:`,
107
139
  unverified: `could not read the protection of ${branch} (${advice.reason}). An admin checks that ${list} are required in:`,
140
+ "ruleset-missing": `${branch} is protected by a ruleset that does not require ${advice.missing?.length} kj check(s) (${list}). An admin adds them to the ruleset (no second protection layer) in:`,
108
141
  "admins-exempt": `${branch} requires the kj checks, but admins can merge without them, and an agent working with an admin's token would. An admin runs (kj never does):`,
109
142
  }[advice.status];
110
143
  logger.warn?.(` ⚠ branch protection: ${why}`);
@@ -109,9 +109,17 @@ function identityGuard(hook) {
109
109
  " fi",
110
110
  " kj_declared_gh=$(sed -n 's/^gh_user:[[:space:]]*//p' .karajan/identity.local.yml | head -n1)",
111
111
  ' kj_hosts="${GH_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/gh}/hosts.yml"',
112
+ // KJC-BUG-0459: a per-account gh (gh-tribbu, ADR 0040) as the credential helper is the account
113
+ // git pushes as. The accounts guide makes gh-<account> use ~/.config/gh-<account>: read it.
114
+ " kj_helper=$(git config --get-urlmatch credential.helper https://github.com 2>/dev/null)",
115
+ ' case "$kj_helper" in',
116
+ " '!'*/gh-*' auth git-credential'*|'!'gh-*' auth git-credential'*)",
117
+ ' kj_ghbin=${kj_helper#!}; kj_ghbin=${kj_ghbin%% *}',
118
+ ' kj_hosts="${XDG_CONFIG_HOME:-$HOME/.config}/${kj_ghbin##*/}/hosts.yml" ;;',
119
+ " esac",
112
120
  " kj_active_gh=$(awk '/^github.com:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]*user:/{sub(/^[[:space:]]*user:[[:space:]]*/,\"\");print;exit}' \"$kj_hosts\" 2>/dev/null)",
113
121
  ' if [ -n "$kj_declared_gh" ] && [ "$kj_active_gh" != "$kj_declared_gh" ]; then',
114
- ' echo "kj harden: identity lock — gh session is ${kj_active_gh:-none} but this clone is declared as $kj_declared_gh — run: gh auth switch --user $kj_declared_gh"; exit 1',
122
+ ' echo "kj harden: identity lock — gh session is ${kj_active_gh:-none} but this clone is declared as $kj_declared_gh — run: gh auth switch --user $kj_declared_gh (or set the gh of that account, e.g. gh-<account>, as the credential helper)"; exit 1',
115
123
  " fi",
116
124
  ...tail,
117
125
  ];
@@ -147,6 +155,13 @@ export function hookBody(hook, cmds = {}, { baseBranch = null } = {}) {
147
155
  "if git diff --cached --name-only --diff-filter=d | grep -qE '^(\\.claude/[^/]*\\.lock|\\.claude/scheduled_tasks\\.lock|\\.kj/|\\.karajan/identity\\.local\\.yml|\\.karajan/harness/sentinel-state\\.json)'; then",
148
156
  " echo 'kj harden: agent runtime files are staged (.claude/*.lock, .kj/, identity.local.yml, sentinel-state.json) — unstage them: git restore --staged <path>'; exit 1",
149
157
  "fi",
158
+ "# KJC-TSK-1234 (ADR 0032): the project's kj.config.yml travels with the repo, so its staged",
159
+ "# version is checked for secrets by kj, which parses the YAML (raw text can spell a key in",
160
+ "# too many ways). Without kj nothing can check it, so it does not enter.",
161
+ "if git diff --cached --name-only --diff-filter=d | grep -qx '.karajan/kj.config.yml'; then",
162
+ " command -v kj >/dev/null 2>&1 || { echo 'kj harden: .karajan/kj.config.yml is staged but kj is not installed to check it holds no secret'; exit 1; }",
163
+ " git show ':.karajan/kj.config.yml' | kj config --check-secrets - || { echo 'kj harden: keep tokens and passwords out of the shared kj.config.yml'; exit 1; }",
164
+ "fi",
150
165
  "# KJC-TSK-1205 (ADR 0030): a tampered Sentinel is caught before each commit,",
151
166
  "# not only when the turn ends. Only where it is installed; no kj, no check.",
152
167
  "if [ -d .karajan/harness ] && command -v kj >/dev/null 2>&1; then",
@@ -7,13 +7,14 @@ import process from "node:process";
7
7
  import { realpathSync } from "node:fs";
8
8
  import { basename, dirname, join, relative } from "node:path";
9
9
  import { spawnSync } from "node:child_process";
10
- import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
10
+ import { CODE, TESTS, ROOT, CARD, branchOf, enterLane, laneFor, laneTop, load, save, session } from "./sentinel-lib.mjs";
11
11
  import { remindersFor } from "./sentinel-reminders.mjs";
12
12
  let raw = "";
13
13
  process.stdin.on("data", (d) => { raw += d; });
14
14
  process.stdin.on("end", () => {
15
15
  try {
16
- const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, tool_response: response = {} } = JSON.parse(raw);
16
+ const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, tool_response: response = {}, cwd = null } = JSON.parse(raw);
17
+ enterLane(load(), sid, cwd);
17
18
  // KJC-TSK-0765 board-sync: a merge that gh CONFIRMS leaves its card pending
18
19
  // in the tracker (the PreToolUse gate then refuses to advance until it is
19
20
  // moved). Recorded here, after the fact, so a failed merge never blocks.
@@ -76,7 +77,10 @@ process.stdin.on("end", () => {
76
77
  // Sources are read STRUCTURALLY, never through the stringified response:
77
78
  // a JSON document (the MCP {content:[{text}]} parts, the CLI --json array)
78
79
  // is parsed and its hits[].source / [].source collected.
79
- const sourcesOf = (doc) => {
80
+ // KJC-BUG-0440 (#2830): the MCP tool may wrap its answer again (the part's
81
+ // text is itself {content:[parts]}); unwrapped up to three levels.
82
+ const sourcesOf = (doc, depth = 0) => {
83
+ if (Array.isArray(doc?.content) && depth < 3) return doc.content.flatMap((p) => sourcesOf(parseJson(String(p?.text || "")), depth + 1));
80
84
  const list = Array.isArray(doc) ? doc : Array.isArray(doc?.hits) ? doc.hits : [];
81
85
  return list.map((h) => h?.source).filter((x) => typeof x === "string");
82
86
  };
@@ -88,7 +92,7 @@ process.stdin.on("end", () => {
88
92
  let found = parts.flatMap((p) => sourcesOf(parseJson(String(p?.text || ""))));
89
93
  // KJC-BUG-0365 (#2208): an answer cut short (whole code chunks) is no
90
94
  // longer valid JSON, but every "source" it carries still names a hit.
91
- if (found.length === 0) found = [...text.matchAll(/\\?"source\\?"\s*:\s*\\?"([^"\\]+)\\?"/g)].map((m) => m[1]);
95
+ if (found.length === 0) found = [...text.matchAll(/\\*"source\\*"\s*:\s*\\*"([^"\\]+)\\*"/g)].map((m) => m[1]);
92
96
  recordRag(input.text, found, parts.map((p) => String(p?.text || "")).join(String.fromCharCode(10)));
93
97
  process.exit(0);
94
98
  }
@@ -184,10 +188,18 @@ process.stdin.on("end", () => {
184
188
  const state = load();
185
189
  const s = session(state, sid);
186
190
  s.at = Date.now();
187
- const rel = relative(ROOT, file).replaceAll("\\", "/");
191
+ // ADR 0034 (KJC-TSK-1245): a write in another lane is recorded under that lane, relative to it,
192
+ // so the Stop gate judges it with that lane's branch and tree.
193
+ const top = laneFor(file);
194
+ const rec = top === laneTop() ? s : (((s.lanes ||= {})[top]) ||= { edited_sources: [], edited_tests: [] });
195
+ const rel = relative(top, file).replaceAll("\\", "/");
188
196
  // KJC-BUG-0279 (#1981): a file outside the repository is not one of its sources.
189
197
  const inside = rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/");
190
- const bucket = !inside ? null : TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
198
+ const bucketOf = () => {
199
+ if (TESTS.test(rel)) return rec.edited_tests;
200
+ return CODE.test(rel) ? rec.edited_sources : null;
201
+ };
202
+ const bucket = inside ? bucketOf() : null;
191
203
  if (bucket && !bucket.includes(rel)) bucket.push(rel);
192
204
  // KJC-TSK-0910: the branch size while it is written, with the CI budget
193
205
  // (kj pr-size), said once per threshold crossed. Context, never a block.