karajan-code 4.70.4 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/dir-setup.js +34 -1
  61. package/src/checks/native-build.js +21 -39
  62. package/src/checks/runner.js +128 -142
  63. package/src/cli/advanced-commands.js +6 -6
  64. package/src/cli/register-meta.js +42 -82
  65. package/src/cli/register-pipeline.js +8 -143
  66. package/src/cli/register-plan.js +9 -100
  67. package/src/cli/register-roles-skills.js +1 -1
  68. package/src/cli/retired-commands.js +49 -0
  69. package/src/cli.js +18 -5
  70. package/src/commands/agent-run.js +40 -26
  71. package/src/commands/audit.js +127 -124
  72. package/src/commands/board.js +5 -9
  73. package/src/commands/check.js +29 -16
  74. package/src/commands/doctor.js +2 -0
  75. package/src/commands/env.js +176 -150
  76. package/src/commands/go.js +71 -53
  77. package/src/commands/harden.js +90 -81
  78. package/src/commands/hu-start.js +52 -0
  79. package/src/commands/hu.js +288 -221
  80. package/src/commands/identity.js +111 -78
  81. package/src/commands/init-atril.js +47 -0
  82. package/src/commands/init.js +11 -40
  83. package/src/commands/install-tools.js +70 -85
  84. package/src/commands/main-guard.js +78 -0
  85. package/src/commands/policy.js +226 -184
  86. package/src/commands/qmd.js +27 -21
  87. package/src/commands/rag.js +121 -55
  88. package/src/commands/review-gate.js +421 -341
  89. package/src/commands/skills.js +4 -1
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +107 -26
  94. package/src/config/defaults.js +1 -0
  95. package/src/config/loader.js +34 -17
  96. package/src/config/role-resolver.js +1 -0
  97. package/src/config/schema.js +1 -0
  98. package/src/config/secrets.js +36 -0
  99. package/src/config.js +1 -0
  100. package/src/environment/adr.js +16 -2
  101. package/src/git/karajan-label.js +20 -8
  102. package/src/guards/main-guard.js +54 -0
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  106. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  107. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-templates.js +1 -1
  111. package/src/hu/board-lookup.js +29 -0
  112. package/src/hu/gh-identity.js +23 -0
  113. package/src/hu/github-board.js +4 -2
  114. package/src/hu/lane-prune.js +83 -13
  115. package/src/hu/lane-setup.js +65 -0
  116. package/src/hu/lanes.js +66 -0
  117. package/src/hu/team-board-mirror.js +3 -2
  118. package/src/infrastructure/command-runner.js +1 -1
  119. package/src/infrastructure/file-system-service.js +1 -1
  120. package/src/mcp/handlers/direct-handlers.js +2 -292
  121. package/src/mcp/handlers/hu-handlers.js +1 -35
  122. package/src/mcp/handlers/management-handlers.js +2 -42
  123. package/src/mcp/handlers/rag-handler.js +9 -1
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +1 -196
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +71 -44
  134. package/src/rag/bootstrap-index.js +46 -54
  135. package/src/rag/indexer.js +9 -4
  136. package/src/rag/migrate.js +1 -1
  137. package/src/rag/onnx-fallback.js +2 -2
  138. package/src/rag/pid-lease.js +181 -0
  139. package/src/rag/project-store.js +2 -12
  140. package/src/review/card-first.js +57 -46
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +12 -4
  143. package/src/review/one-shot-review.js +45 -35
  144. package/src/review/publish-pr.js +14 -2
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +19 -4
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +1 -24
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-cli.js +19 -1
  156. package/src/skills/atril-skills.js +5 -4
  157. package/src/skills/skills-search.js +72 -0
  158. package/src/skills/translate.js +62 -0
  159. package/src/sonar/scanner.js +116 -101
  160. package/src/steward/invariants.js +5 -4
  161. package/src/steward/phantom-sweep.js +66 -0
  162. package/src/tournament/judge.js +60 -53
  163. package/src/utils/gh-account.js +37 -0
  164. package/src/utils/git.js +0 -13
  165. package/src/utils/home-migration.js +7 -1
  166. package/src/utils/main-checkout.js +53 -0
  167. package/src/utils/qmd-collection.js +3 -1
  168. package/src/utils/welcome.js +3 -3
  169. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  180. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  181. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  182. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  183. package/packages/hu-board/src/run-tracker.js +0 -121
  184. package/src/activity-log.js +0 -59
  185. package/src/agents/host-agent.js +0 -38
  186. package/src/agents/model-registry.js +0 -142
  187. package/src/audit/plan-adherence.js +0 -224
  188. package/src/autonomy/arbiter-role.js +0 -71
  189. package/src/autonomy/arbiter.js +0 -22
  190. package/src/autonomy/decider.js +0 -18
  191. package/src/autonomy/decision-resolver.js +0 -58
  192. package/src/autonomy/outcome-report.js +0 -55
  193. package/src/autonomy/stage-ask.js +0 -30
  194. package/src/brain/decision-tracker.js +0 -125
  195. package/src/brain/decisor.js +0 -243
  196. package/src/brain/solomon-consult.js +0 -171
  197. package/src/brain/standby-scheduler.js +0 -8
  198. package/src/budget/comparison.js +0 -96
  199. package/src/canvas/parse-md.js +0 -238
  200. package/src/canvas/schema.js +0 -101
  201. package/src/canvas/to-flat.js +0 -85
  202. package/src/canvas/validate.js +0 -163
  203. package/src/ci/dispatch.js +0 -99
  204. package/src/ci/index.js +0 -3
  205. package/src/ci/pr-diff.js +0 -26
  206. package/src/cli/register-standby.js +0 -26
  207. package/src/commands/architect.js +0 -111
  208. package/src/commands/autorun.js +0 -49
  209. package/src/commands/researcher.js +0 -54
  210. package/src/commands/resume.js +0 -72
  211. package/src/commands/review.js +0 -89
  212. package/src/commands/run.js +0 -185
  213. package/src/commands/standby.js +0 -117
  214. package/src/commands/status.js +0 -36
  215. package/src/commands/triage.js +0 -78
  216. package/src/commands/undo.js +0 -62
  217. package/src/domains/domain-loader.js +0 -245
  218. package/src/domains/domain-registry.js +0 -140
  219. package/src/domains/domain-synthesizer.js +0 -129
  220. package/src/git/automation.js +0 -433
  221. package/src/git/coder-commit-guard.js +0 -40
  222. package/src/git/hu-automation.js +0 -243
  223. package/src/git/hu-snapshot.js +0 -10
  224. package/src/git/symlink-targets.js +0 -57
  225. package/src/golden/asserter.js +0 -92
  226. package/src/golden/loader.js +0 -52
  227. package/src/golden/runner.js +0 -92
  228. package/src/golden/schema.js +0 -41
  229. package/src/guards/duplicate-members.js +0 -86
  230. package/src/guards/intent-guard.js +0 -161
  231. package/src/guards/perf-guard.js +0 -126
  232. package/src/guards/policy-guard.js +0 -90
  233. package/src/guards/policy-resolver.js +0 -85
  234. package/src/guards/secret-redactor.js +0 -71
  235. package/src/hu/acceptance-runner.js +0 -138
  236. package/src/hu/lazy-planner.js +0 -109
  237. package/src/hu/parallel-executor.js +0 -94
  238. package/src/hu/routing-metrics.js +0 -17
  239. package/src/hu/split-validator.js +0 -143
  240. package/src/hu/splitting-detector.js +0 -158
  241. package/src/hu/splitting-generator.js +0 -175
  242. package/src/hu/store.js +0 -394
  243. package/src/infrastructure/mocks.js +0 -182
  244. package/src/mcp/handlers/run-handler.js +0 -318
  245. package/src/mcp/sovereignty-guard.js +0 -113
  246. package/src/mcp/tool-arg-normalizers.js +0 -16
  247. package/src/mutate/reviewer-signal.js +0 -76
  248. package/src/orchestrator/agent-fallback.js +0 -81
  249. package/src/orchestrator/brain-coordinator.js +0 -186
  250. package/src/orchestrator/ci-integration.js +0 -139
  251. package/src/orchestrator/config-init.js +0 -426
  252. package/src/orchestrator/direct-actions.js +0 -217
  253. package/src/orchestrator/drivers/error-recovery.js +0 -287
  254. package/src/orchestrator/drivers/init-context.js +0 -238
  255. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  256. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  257. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  258. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  259. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  260. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  261. package/src/orchestrator/drivers/post-loop.js +0 -404
  262. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  263. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  264. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  265. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  266. package/src/orchestrator/drivers/pre-loop.js +0 -524
  267. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  268. package/src/orchestrator/feedback-enrichment.js +0 -140
  269. package/src/orchestrator/feedback-queue.js +0 -151
  270. package/src/orchestrator/flow-control.js +0 -170
  271. package/src/orchestrator/flow-runner.js +0 -365
  272. package/src/orchestrator/fs-leak-detector.js +0 -228
  273. package/src/orchestrator/hu-scheduler.js +0 -114
  274. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  275. package/src/orchestrator/iteration-gate.js +0 -73
  276. package/src/orchestrator/iteration-stages.js +0 -9
  277. package/src/orchestrator/parallel-limiter.js +0 -74
  278. package/src/orchestrator/pipeline-context.js +0 -72
  279. package/src/orchestrator/post-loop-stages.js +0 -468
  280. package/src/orchestrator/pre-loop-stages.js +0 -11
  281. package/src/orchestrator/preflight-checks.js +0 -487
  282. package/src/orchestrator/repair/repair-detector.js +0 -117
  283. package/src/orchestrator/repair/repair-runner.js +0 -142
  284. package/src/orchestrator/reviewer-fallback.js +0 -45
  285. package/src/orchestrator/role-output-compressor.js +0 -189
  286. package/src/orchestrator/session-journal.js +0 -308
  287. package/src/orchestrator/solomon-escalation.js +0 -331
  288. package/src/orchestrator/solomon-rules.js +0 -212
  289. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  290. package/src/orchestrator/stages/architect-stage.js +0 -172
  291. package/src/orchestrator/stages/coder-stage.js +0 -506
  292. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  293. package/src/orchestrator/stages/format-stage.js +0 -61
  294. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  295. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  296. package/src/orchestrator/stages/perf-stage.js +0 -140
  297. package/src/orchestrator/stages/planner-stage.js +0 -85
  298. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  299. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  300. package/src/orchestrator/stages/research-stage.js +0 -69
  301. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  302. package/src/orchestrator/stages/sonar-stage.js +0 -439
  303. package/src/orchestrator/stages/stage-classes.js +0 -84
  304. package/src/orchestrator/stages/stage-executor.js +0 -155
  305. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  306. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  307. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  308. package/src/orchestrator/stages/triage-stage.js +0 -172
  309. package/src/orchestrator/standby.js +0 -71
  310. package/src/orchestrator/verification-gate.js +0 -183
  311. package/src/orchestrator.js +0 -22
  312. package/src/plan/adr-generator.js +0 -170
  313. package/src/plan/adr-loader.js +0 -60
  314. package/src/plan/plan-executor.js +0 -100
  315. package/src/plan/plan-validation.js +0 -4
  316. package/src/prompts/acceptance-coverage.js +0 -32
  317. package/src/prompts/arbiter.js +0 -38
  318. package/src/prompts/architect.js +0 -103
  319. package/src/prompts/hu-reviewer.js +0 -244
  320. package/src/prompts/tool-correctness-judge.js +0 -31
  321. package/src/prompts/triage.js +0 -57
  322. package/src/rag/embedder.js +0 -2
  323. package/src/rag/embedders/_cloud-base.js +0 -2
  324. package/src/rag/embedders/cohere.js +0 -2
  325. package/src/rag/embedders/mistral.js +0 -2
  326. package/src/rag/embedders/onnx.js +0 -2
  327. package/src/rag/embedders/openai.js +0 -2
  328. package/src/rag/embedders/voyage.js +0 -2
  329. package/src/rag/rerank.js +0 -4
  330. package/src/rag/where-parser.js +0 -4
  331. package/src/repeat-detector.js +0 -88
  332. package/src/review/diff-generator.js +0 -112
  333. package/src/review/git-stash-helper.js +0 -104
  334. package/src/review/schema.js +0 -31
  335. package/src/review/scope-filter.js +0 -152
  336. package/src/review/snapshot-diff.js +0 -122
  337. package/src/review/tdd-discipline.js +0 -90
  338. package/src/review/tdd-policy.js +0 -69
  339. package/src/roles/acceptance-coverage-role.js +0 -78
  340. package/src/roles/architect-role.js +0 -77
  341. package/src/roles/coder-role.js +0 -113
  342. package/src/roles/commiter-role.js +0 -113
  343. package/src/roles/domain-curator-role.js +0 -134
  344. package/src/roles/hu-reviewer-role.js +0 -64
  345. package/src/roles/impeccable-role.js +0 -85
  346. package/src/roles/index.js +0 -18
  347. package/src/roles/karajan-brain-role.js +0 -134
  348. package/src/roles/perf-role.js +0 -122
  349. package/src/roles/planner-role.js +0 -151
  350. package/src/roles/refactorer-role.js +0 -30
  351. package/src/roles/repairer-role.js +0 -181
  352. package/src/roles/researcher-role.js +0 -70
  353. package/src/roles/reviewer-role.js +0 -115
  354. package/src/roles/security-role.js +0 -73
  355. package/src/roles/solomon-role.js +0 -271
  356. package/src/roles/sonar-role.js +0 -72
  357. package/src/roles/tester-role.js +0 -235
  358. package/src/roles/tool-correctness-judge-role.js +0 -78
  359. package/src/roles/triage-role.js +0 -99
  360. package/src/session/journal/decisions-writer.js +0 -200
  361. package/src/session/journal/iteration-logger.js +0 -194
  362. package/src/session/journal/summary-writer.js +0 -320
  363. package/src/session/journal/tree-writer.js +0 -164
  364. package/src/session/journal/write-all.js +0 -89
  365. package/src/session-cleanup.js +0 -116
  366. package/src/skills/semantic-detector.js +0 -167
  367. package/src/skills/skill-detector.js +0 -385
  368. package/src/sonar/cloud-scanner.js +0 -78
  369. package/src/tracker-bootstrap.js +0 -44
  370. package/src/types/index.js +0 -38
  371. package/src/types/policy.js +0 -43
  372. package/src/types/solomon.js +0 -87
  373. package/src/utils/async-lock.js +0 -31
  374. package/src/utils/budget.js +0 -229
  375. package/src/utils/cwd-confirm.js +0 -39
  376. package/src/utils/display/event-handlers.js +0 -403
  377. package/src/utils/display/formatters.js +0 -132
  378. package/src/utils/display/header.js +0 -47
  379. package/src/utils/display/session-summary.js +0 -118
  380. package/src/utils/display/solomon.js +0 -42
  381. package/src/utils/pricing.js +0 -29
  382. package/src/utils/quota-simulator.js +0 -56
  383. package/src/utils/role-assigner.js +0 -137
  384. package/src/utils/rtk-wrapper.js +0 -87
  385. package/src/utils/run-registry.js +0 -9
  386. package/src/utils/status-dashboard.js +0 -254
  387. package/src/utils/tool-call-extractor.js +0 -56
  388. package/src/webperf/devtools-detect.js +0 -112
  389. package/src/webperf/webperf-role-integration.js +0 -66
  390. package/templates/skills/kj-run.md +0 -73
@@ -27,6 +27,7 @@ import { getKarajanHome } from "../utils/paths.js";
27
27
  import { safeParseConfig, formatConfigIssues } from "./schema.js";
28
28
  import { DEFAULTS } from "./defaults.js";
29
29
  import { resolveTestHarness } from "./test-harness.js";
30
+ import { SECRET_PATHS } from "./secrets.js";
30
31
 
31
32
  /**
32
33
  * Parse a YAML config file with an actionable error message.
@@ -74,13 +75,18 @@ export function getProjectConfigPath(projectDir = process.cwd()) {
74
75
  return path.join(projectDir, ".karajan", "kj.config.yml");
75
76
  }
76
77
 
78
+ /** ADR 0032: a member's own preferences, ignored by git, on top of the team's project config. */
79
+ export function getLocalConfigPath(projectDir = process.cwd()) {
80
+ return path.join(projectDir, ".karajan", "kj.config.local.yml");
81
+ }
82
+
83
+ async function readYamlIfPresent(filePath) {
84
+ if (!(await exists(filePath))) return null;
85
+ return parseYamlOrThrow(await fs.readFile(filePath, "utf8"), filePath);
86
+ }
87
+
77
88
  export async function loadProjectConfig(projectDir = process.cwd()) {
78
- const projectConfigPath = getProjectConfigPath(projectDir);
79
- if (!(await exists(projectConfigPath))) {
80
- return null;
81
- }
82
- const raw = await fs.readFile(projectConfigPath, "utf8");
83
- return parseYamlOrThrow(raw, projectConfigPath);
89
+ return readYamlIfPresent(getProjectConfigPath(projectDir));
84
90
  }
85
91
 
86
92
  async function loadProjectPricingOverrides(projectDir = process.cwd()) {
@@ -113,6 +119,7 @@ export async function loadConfig(projectDir) {
113
119
 
114
120
  // Load project config (.karajan/kj.config.yml)
115
121
  const projectConfig = await loadProjectConfig(projectDir);
122
+ const localConfig = await readYamlIfPresent(getLocalConfigPath(projectDir));
116
123
 
117
124
  // KJC-TSK-0395: enforce the local-overrides-global hierarchy. A project
118
125
  // config with no global counterpart is almost always user error (copied
@@ -120,19 +127,24 @@ export async function loadConfig(projectDir) {
120
127
  // would silently behave like a global config without the defaults
121
128
  // people expect. Refuse with an actionable message.
122
129
  // KJC-BUG-0428 (issue #2694): a CI runner never has a global config; there the
123
- // team's committed project config sits on the defaults instead.
124
- if (projectConfig && !globalExists && !process.env.CI) {
130
+ // team's committed project config sits on the defaults instead. A member's local
131
+ // config is never committed, so without a global it is still refused, in CI too.
132
+ if (!globalExists && (localConfig || (projectConfig && !process.env.CI))) {
133
+ const found = projectConfig ? getProjectConfigPath(projectDir) : getLocalConfigPath(projectDir);
125
134
  throw new Error(
126
- `Local config found at ${getProjectConfigPath(projectDir)} but no global config exists.\n` +
135
+ `Local config found at ${found} but no global config exists.\n` +
127
136
  "Local configs are merged ON TOP of the global one — run `kj init --global` first to create the base."
128
137
  );
129
138
  }
130
139
 
131
- // Merge: DEFAULTS < global < project
140
+ // Merge: DEFAULTS < global < project (the team's) < local (the member's, ADR 0032)
132
141
  let merged = mergeDeep(DEFAULTS, globalConfig);
133
142
  if (projectConfig) {
134
143
  merged = mergeDeep(merged, projectConfig);
135
144
  }
145
+ if (localConfig) {
146
+ merged = mergeDeep(merged, localConfig);
147
+ }
136
148
 
137
149
  if (projectPricing) {
138
150
  merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
@@ -199,19 +211,24 @@ function stripRuntimeOnlyKeys(config) {
199
211
  // KJC-BUG-0253: a project's .karajan/kj.config.yml is versioned with the repo,
200
212
  // so the SonarQube credentials never go there; they live in the global config
201
213
  // (~/.karajan), where the token bootstrap already saves them.
202
- const SECRET_KEYS = ["token", "admin_password"];
203
214
  const isProjectConfig = (configPath) =>
204
215
  path.basename(configPath) === "kj.config.yml"
205
216
  && path.basename(path.dirname(configPath)) === ".karajan"
206
217
  && path.resolve(configPath) !== path.resolve(getConfigPath());
207
218
 
219
+ // KJC-TSK-1234 (ADR 0032): the same secret paths the pre-commit check refuses (SonarCloud's token
220
+ // included), one list for both, so writing never keeps what committing would reject.
208
221
  function stripSecrets(config) {
209
- if (!config?.sonarqube || typeof config.sonarqube !== "object") return { out: config, stripped: [] };
210
- const stripped = SECRET_KEYS.filter((k) => config.sonarqube[k] != null);
211
- if (!stripped.length) return { out: config, stripped };
212
- const sonarqube = { ...config.sonarqube };
213
- for (const k of stripped) delete sonarqube[k];
214
- return { out: { ...config, sonarqube }, stripped: stripped.map((k) => `sonarqube.${k}`) };
222
+ let out = config;
223
+ const stripped = [];
224
+ for (const [section, key] of SECRET_PATHS) {
225
+ const block = out?.[section];
226
+ if (!block || typeof block !== "object" || block[key] == null) continue;
227
+ const { [key]: _secret, ...kept } = block;
228
+ out = { ...out, [section]: kept };
229
+ stripped.push(`${section}.${key}`);
230
+ }
231
+ return { out, stripped };
215
232
  }
216
233
 
217
234
  /** @returns {Promise<{strippedSecrets: string[]}>} the secret keys kept out of a project config */
@@ -65,6 +65,7 @@ const CODER_INHERITED_ROLES = new Set([
65
65
  "impeccable", "triage", "discover", "architect", "audit",
66
66
  "hu_reviewer", "hu-reviewer",
67
67
  "spec_reviewer", "spec-reviewer",
68
+ "translator",
68
69
  ]);
69
70
 
70
71
  function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
@@ -90,6 +90,7 @@ const RolesMap = v.optional(v.looseObject({
90
90
  discover: v.optional(RoleEntry),
91
91
  architect: v.optional(RoleEntry),
92
92
  hu_reviewer: v.optional(RoleEntry),
93
+ translator: v.optional(RoleEntry),
93
94
  start: v.optional(RoleEntry),
94
95
  }));
95
96
 
@@ -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}`, "",
@@ -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
+ }
@@ -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.
@@ -7,11 +7,11 @@
7
7
  import console from "node:console";
8
8
  import process from "node:process";
9
9
  import { dirname, join, relative, resolve } from "node:path";
10
- import { appendFileSync, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
10
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { homedir } from "node:os";
13
13
  import { fileURLToPath } from "node:url";
14
- import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, STATE, branchOf, foreignLane, load, save, session, boardGate, pendingMoves, pendingText, stagedVerdict } from "./sentinel-lib.mjs";
14
+ import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, STATE, branchOf, enterLane, holdLane, laneFor, laneRefusal, laneTop, load, save, session, boardGate, pendingMoves, pendingText, stagedVerdict } from "./sentinel-lib.mjs";
15
15
  // KJC-TSK-0915 (ADR 0014): the shell reader is a real, unit-tested module copied here as is.
16
16
  import { commitStaging, isGhIssueCreate, isPublishCommand, isReadOnlyCommand, optionValues, shellSegments, stripInertQuotes, stripTextOptionValues, stripTextOnlyCommandValues } from "./sentinel-shell.mjs";
17
17
  import { DISCARD_VERBS, discardOf, foreignLost, removedFiles } from "./sentinel-discard.mjs";
@@ -26,7 +26,8 @@ let raw = "";
26
26
  process.stdin.on("data", (d) => { raw += d; });
27
27
  process.stdin.on("end", () => {
28
28
  try {
29
- const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, transcript_path: transcript = null } = JSON.parse(raw);
29
+ const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, transcript_path: transcript = null, cwd = null } = JSON.parse(raw);
30
+ enterLane(load(), sid, cwd);
30
31
  // KJC-TSK-0920 (SNT-E): every deny ends with the same line, whatever gate it was.
31
32
  // KJC-TSK-0968 (HUM-E, ADR 0018): and leaves a record next to the state, with the
32
33
  // gate's own words: kj handoff takes the why from here, never from the agent.
@@ -50,7 +51,7 @@ process.stdin.on("end", () => {
50
51
  // hasOwn + defineProperty: a file named __proto__ or toString is a path, not a prototype key.
51
52
  if (Object.hasOwn(touch, relT)) return;
52
53
  // --ignored: a user's ignored .env is theirs too (git clean -x deletes it).
53
- const gs = spawnSync("git", ["-C", ROOT, "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
54
+ const gs = spawnSync("git", ["-C", laneTop(), "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
54
55
  const value = gs.status === 0 && String(gs.stdout).trim() === "" ? "clean" : "dirty";
55
56
  Object.defineProperty(touch, relT, { value, enumerable: true, writable: true, configurable: true });
56
57
  save(st);
@@ -60,7 +61,9 @@ process.stdin.on("end", () => {
60
61
  // only the human, editing outside it.
61
62
  if (EDIT_TOOLS.includes(tool)) {
62
63
  const target = input.file_path || input.notebook_path;
63
- const relT = target ? relative(ROOT, String(target)).replaceAll("\\", "/") : "";
64
+ // ADR 0034: judged in the lane the write lands in (its supervisor, its config).
65
+ const targetTop = target ? laneFor(target) : laneTop();
66
+ const relT = target ? relative(targetTop, String(target)).replaceAll("\\", "/") : "";
64
67
  if (relT && PROTECTED.test(relT)) {
65
68
  console.error("karajan sentinel: ese fichero es parte del supervisor (" + relT + ") — solo el humano desmonta el sentinel, editalo fuera de la sesion." + doc("supervisor"));
66
69
  process.exit(2);
@@ -74,7 +77,8 @@ process.stdin.on("end", () => {
74
77
  console.error("karajan sentinel: " + relT + " es configuracion de gobierno (politicas, gates, exclusiones): la cambia tu usuario, no la sesion. Si un gate te parece injusto, proponselo a tu usuario o usa kj report-issue." + doc("governance"));
75
78
  process.exit(2);
76
79
  }
77
- if (relT && !relT.startsWith("..") && !relT.startsWith("/")) noteFirstTouch(relT);
80
+ // The ledger is keyed by path in the session's lane: a write the lane guard will deny records nothing.
81
+ if (targetTop === laneTop() && relT && !relT.startsWith("..") && !relT.startsWith("/")) noteFirstTouch(relT);
78
82
  }
79
83
  // KJC-BUG-0261: rm / git rm is the session touching those files, noted before they go.
80
84
  if (tool === "Bash") for (const words of shellSegments(String(input.command || ""))) for (const f of removedFiles(words, ROOT)) noteFirstTouch(f);
@@ -202,9 +206,9 @@ process.stdin.on("end", () => {
202
206
  };
203
207
  const declaredGh = idLine("gh_user");
204
208
  const ghHostsPath = join(process.env.GH_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "gh"), "hosts.yml");
205
- const activeGh = () => {
206
- if (!existsSync(ghHostsPath)) return null;
207
- const lines = readFileSync(ghHostsPath, "utf8").split(ESC_NL);
209
+ const activeGh = (hostsPath = ghHostsPath) => {
210
+ if (!existsSync(hostsPath)) return null;
211
+ const lines = readFileSync(hostsPath, "utf8").split(ESC_NL);
208
212
  const start = lines.findIndex((l) => l.startsWith("github.com:"));
209
213
  if (start < 0) return null;
210
214
  for (let k = start + 1; k < lines.length; k += 1) {
@@ -216,6 +220,19 @@ process.stdin.on("end", () => {
216
220
  };
217
221
  const words = (seg) => seg.trim().split(" ").filter((w) => w.length > 0 && w !== ESC_TAB);
218
222
  const headOf = (ws) => { let k = 0; while (k < ws.length && ws[k].indexOf("=") > 0 && ws[k].indexOf("=") < ws[k].length) k += 1; return ws.slice(k); };
223
+ // KJC-BUG-0459: `GH_CONFIG_DIR=<dir> gh ...` reads <dir>, not the hook's config.
224
+ const hostsOf = (ws) => {
225
+ const a = ws.slice(0, ws.length - headOf(ws).length).findLast((w) => w.startsWith("GH_CONFIG_DIR="));
226
+ if (!a) return null; // no assignment of its own (not the same as one naming the default dir)
227
+ const d = bare(a.slice("GH_CONFIG_DIR=".length)).replace(/^(~|\$HOME|\$\{HOME\})(?=\/|$)/, homedir());
228
+ return join(d, "hosts.yml");
229
+ };
230
+ // KJC-BUG-0459: a per-account gh (gh-tribbu, gh-ox: ADR 0040). The accounts guide makes
231
+ // gh-<account> run gh with GH_CONFIG_DIR=~/.config/gh-<account>: its config is READ, the
232
+ // wrapper is never executed by the hook.
233
+ const wrapperName = (w) => w.slice(w.lastIndexOf("/") + 1);
234
+ const isGhWrapper = (w) => /^gh-[A-Za-z0-9_.-]+$/.test(wrapperName(w));
235
+ const wrapperHosts = (w) => join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), wrapperName(w), "hosts.yml");
219
236
  // Wrapper shells hide the payload in a string (review catch): scan inside.
220
237
  const WRAPPERS =["sh", "bash", "zsh", "dash", "eval", "env", "xargs", "sudo", "nohup", "time", "exec"];
221
238
  let switchedTo = null;
@@ -223,12 +240,21 @@ process.stdin.on("end", () => {
223
240
  // $( and backticks are separators too (review catch): their payload is its own segment.
224
241
  const SEPS = [";", "&&", "||", "|", "$(", String.fromCharCode(96), "(", ")"];
225
242
  const bare = (w) => w.replaceAll(String.fromCharCode(39), "").replaceAll(String.fromCharCode(34), "");
226
- const scan = (text, depth) => { if (depth > 3) return; for (const seg of SEPS.reduce((acc, s) => acc.flatMap((l) => l.split(s)), text.split(ESC_NL))) checkSeg(headOf(words(seg)), depth); };
227
- const checkSeg = (ws, depth) => {
243
+ // A rescanned payload inherits the config resolved outside it (`GH_CONFIG_DIR=<dir> env gh ...`).
244
+ const scan = (text, depth, inherited = null) => {
245
+ if (depth > 3) return;
246
+ for (const seg of SEPS.reduce((acc, s) => acc.flatMap((l) => l.split(s)), text.split(ESC_NL))) {
247
+ checkSeg(headOf(words(seg)), depth, hostsOf(words(seg)) ?? inherited);
248
+ }
249
+ };
250
+ // hostsPath: the config an explicit GH_CONFIG_DIR assignment selects, or null without one.
251
+ const checkSeg = (ws, depth, hostsPath = null) => {
228
252
  if (ws.length === 0) return;
229
253
  if (WRAPPERS.includes(ws[0])) { // wrapper args have arity (sudo -u bob): scan from the first gh/wrapper token (review catch)
230
- const j = ws.findIndex((w, i) => i > 0 && ["gh", ...WRAPPERS].includes(bare(w)));
231
- if (j > 0) scan(ws.slice(j).map(bare).join(" "), depth + 1);
254
+ const j = ws.findIndex((w, i) => i > 0 && (["gh", ...WRAPPERS].includes(bare(w)) || isGhWrapper(bare(w))));
255
+ // `env GH_CONFIG_DIR=<dir> gh ...`: the assignments before the command go with it (review catch).
256
+ const assigns = ws.slice(1, Math.max(j, 1)).map(bare).filter((w) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
257
+ if (j > 0) scan([...assigns, ...ws.slice(j).map(bare)].join(" "), depth + 1, hostsPath);
232
258
  return;
233
259
  }
234
260
  if (ws[0] === "gh") {
@@ -237,8 +263,15 @@ process.stdin.on("end", () => {
237
263
  return; // session commands ARE the remedy — never blocked
238
264
  }
239
265
  if (!declaredGh) { problems.push("gh without a declared identity for this clone — run: kj identity set"); return; }
240
- const eff = switchedTo || activeGh();
241
- if (eff !== declaredGh) problems.push("gh as '" + (eff || "no session") + "' but this clone is declared as '" + declaredGh + "' — prefix the command: gh auth switch --user " + declaredGh + " && ...");
266
+ const eff = hostsPath ? activeGh(hostsPath) : (switchedTo || activeGh());
267
+ if (eff !== declaredGh) problems.push("gh as '" + (eff || "no session") + "' but this clone is declared as '" + declaredGh + "' — prefix the command: gh auth switch --user " + declaredGh + " && ... (or use the gh of that account, e.g. gh-<account>)");
268
+ }
269
+ if (isGhWrapper(ws[0])) {
270
+ if (ws[1] === "auth") return; // its session commands are the remedy too
271
+ if (!declaredGh) { problems.push(ws[0] + " without a declared identity for this clone — run: kj identity set"); return; }
272
+ // Its own config even with an inline GH_CONFIG_DIR: the wrapper sets that variable itself.
273
+ const eff = activeGh(wrapperHosts(ws[0]));
274
+ if (eff !== declaredGh) problems.push(wrapperName(ws[0]) + " runs as '" + (eff || "no session") + "' (" + wrapperHosts(ws[0]) + ") but this clone is declared as '" + declaredGh + "' — use the gh of that account");
242
275
  }
243
276
  // KJC-TSK-1085 (ADR 0015): git commit/tag/rebase/push are no longer read here:
244
277
  // pre-commit checks the author, pre-push every pushed commit and the gh session.
@@ -249,24 +282,18 @@ process.stdin.on("end", () => {
249
282
  process.exit(2);
250
283
  }
251
284
  }
252
- // MONO-0 (KJC-TSK-0737, ADR 0002): cada sesion muta solo SU worktree;
253
- // otro carril del MISMO repo se deniega ANTES del dano. Lectura libre.
285
+ // MONO-0 (ADR 0002) under ADR 0034 (KJC-TSK-1244): a write in another lane of the SAME
286
+ // repo is allowed while no other live session holds that lane; the session then holds it.
287
+ // A held lane is denied BEFORE the damage. Reads are free.
288
+ // Another lane of this repo => its toplevel; the session's own lane or no repo => null.
254
289
  const laneOf = (p) => {
255
- // Sube al primer ancestro EXISTENTE (un subdir aun no creado escapaba).
256
- let d = String(p);
257
- if (!(existsSync(d) && statSync(d).isDirectory())) d = dirname(d);
258
- while (d && !existsSync(d)) {
259
- const up = dirname(d);
260
- if (up === d) return null;
261
- d = up;
262
- }
263
- // Canonicaliza (reviewer catch: un symlink local hacia otro carril
264
- // esquivaba la clasificacion por ruta literal).
265
- try { d = realpathSync(d); } catch { /* se clasifica tal cual */ }
266
- return d ? foreignLane(d) : null;
290
+ const top = laneFor(p);
291
+ return top === laneTop() ? null : top;
267
292
  };
268
293
  const laneDeny = (what, lane) => {
269
- console.error("karajan sentinel: " + what + " vive en otro carril (" + lane + ") de este repo — cada sesion muta solo SU worktree (MONO-0). Sin escape (ADR 0015): un cruce deliberado lo hace tu usuario en su terminal." + doc("cross-lane"));
294
+ const why = holdLane(lane);
295
+ if (!why) return false;
296
+ console.error("karajan sentinel: " + what + " vive en otro carril (" + lane + ") de este repo " + why + (laneRefusal() ? " " + laneRefusal() : "") + ". Sin escape (ADR 0015): un cruce deliberado lo hace tu usuario en su terminal." + doc("cross-lane"));
270
297
  return true;
271
298
  };
272
299
  if (EDIT_TOOLS.includes(tool)) {
@@ -523,10 +550,12 @@ process.stdin.on("end", () => {
523
550
  }
524
551
  } catch { /* no report or unreadable — the Steward is not adopted here */ }
525
552
  const file = input.file_path || input.notebook_path;
526
- const rel = file ? relative(ROOT, String(file)).replaceAll("\\", "/") : "";
553
+ // ADR 0034: the card, the branch and the RAG of the lane the write lands in.
554
+ const top = file ? laneFor(file) : laneTop();
555
+ const rel = file ? relative(top, String(file)).replaceAll("\\", "/") : "";
527
556
  // KJC-BUG-0279 (#1981): outside the repository there is no source to govern.
528
557
  if (rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/") && CODE.test(rel) && !TESTS.test(rel)) {
529
- const branch = branchOf();
558
+ const branch = branchOf(top);
530
559
  const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
531
560
  // ADR 0015 (KJC-TSK-0925): no escape. Without a card there is no work.
532
561
  if (why) {
@@ -549,7 +578,7 @@ process.stdin.on("end", () => {
549
578
  // Solo se pregunta a git en el camino lento, cuando el gate iba a
550
579
  // denegar: exit 1 = git responde que no lo conoce; cualquier otro
551
580
  // resultado se comporta como hasta ahora y bloquea.
552
- const fresh = () => !existsSync(String(file)) || spawnSync("git", ["-C", ROOT, "ls-files", "--error-unmatch", "--", rel], { encoding: "utf8" }).status === 1;
581
+ const fresh = () => !existsSync(String(file)) || spawnSync("git", ["-C", top, "ls-files", "--error-unmatch", "--", rel], { encoding: "utf8" }).status === 1;
553
582
  // A path outside this tree is another lane's business (the lane guard
554
583
  // above rules on it) and the ledger only holds in-repo paths.
555
584
  // KJC-BUG-0190: with nothing indexed, no query can cover anything —
@@ -570,7 +599,7 @@ process.stdin.on("end", () => {
570
599
  // esa: un indice vacio o por detras si tiene arreglo (kj rag index)
571
600
  // y sigue bloqueando, porque si no el gate desapareceria en
572
601
  // cualquier proyecto sin indexar. Nada legible = se bloquea.
573
- const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: ROOT, encoding: "utf8" });
602
+ const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: top, encoding: "utf8" });
574
603
  let blind = null;
575
604
  let absent = null;
576
605
  try {
@@ -640,8 +669,7 @@ process.stdin.on("end", () => {
640
669
  // tail that hid its failure, let a commit through with no verdict at all.
641
670
  const plan = commitStaging(cmd);
642
671
  if (plan.commits) {
643
- // 4.x: the hook payload carries no cwd; the session works in the project root.
644
- const verdict = stagedVerdict(plan.dir ? resolve(ROOT, plan.dir) : ROOT);
672
+ const verdict = stagedVerdict(plan.dir ? resolve(cwd || ROOT, plan.dir) : (cwd || ROOT));
645
673
  if (verdict.gate && plan.stages) {
646
674
  console.error("karajan sentinel: stagea y commitea en comandos separados — el veredicto se comprueba sobre lo que ya esta stageado, y este comando stagea a la vez que commitea (git add/rm/mv, -a o rutas en git commit). Stagea por nombre, pasa kj review --staged y luego git commit." + doc("commit-verdict"));
647
675
  process.exit(2);