karajan-code 4.70.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/dir-setup.js +34 -1
  61. package/src/checks/native-build.js +21 -39
  62. package/src/checks/runner.js +128 -142
  63. package/src/cli/advanced-commands.js +6 -6
  64. package/src/cli/register-meta.js +42 -82
  65. package/src/cli/register-pipeline.js +8 -143
  66. package/src/cli/register-plan.js +9 -100
  67. package/src/cli/retired-commands.js +49 -0
  68. package/src/cli.js +18 -5
  69. package/src/commands/agent-run.js +40 -26
  70. package/src/commands/audit.js +127 -124
  71. package/src/commands/board.js +5 -9
  72. package/src/commands/check.js +29 -16
  73. package/src/commands/doctor.js +2 -0
  74. package/src/commands/env.js +176 -150
  75. package/src/commands/go.js +71 -53
  76. package/src/commands/harden.js +90 -81
  77. package/src/commands/hu-start.js +52 -0
  78. package/src/commands/hu.js +288 -221
  79. package/src/commands/identity.js +111 -78
  80. package/src/commands/init-atril.js +47 -0
  81. package/src/commands/init.js +11 -40
  82. package/src/commands/install-tools.js +70 -85
  83. package/src/commands/main-guard.js +78 -0
  84. package/src/commands/policy.js +226 -184
  85. package/src/commands/qmd.js +27 -21
  86. package/src/commands/rag.js +75 -48
  87. package/src/commands/review-gate.js +421 -341
  88. package/src/commands/start.js +4 -5
  89. package/src/commands/steward.js +96 -47
  90. package/src/commands/tournament.js +46 -37
  91. package/src/commands/worktree.js +94 -25
  92. package/src/config/loader.js +34 -17
  93. package/src/config/secrets.js +36 -0
  94. package/src/config.js +1 -0
  95. package/src/environment/adr.js +16 -2
  96. package/src/git/karajan-label.js +20 -8
  97. package/src/guards/main-guard.js +54 -0
  98. package/src/harden/hook-templates.js +16 -1
  99. package/src/harden/sentinel/posttooluse.mjs +18 -6
  100. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  101. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  102. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  103. package/src/harden/sentinel/sessionstart.mjs +3 -2
  104. package/src/harden/sentinel/stop.mjs +5 -4
  105. package/src/harden/workflow-templates.js +1 -1
  106. package/src/hu/board-lookup.js +29 -0
  107. package/src/hu/gh-identity.js +23 -0
  108. package/src/hu/github-board.js +4 -2
  109. package/src/hu/lane-prune.js +15 -7
  110. package/src/hu/lane-setup.js +65 -0
  111. package/src/hu/lanes.js +66 -0
  112. package/src/hu/team-board-mirror.js +3 -2
  113. package/src/infrastructure/command-runner.js +1 -1
  114. package/src/infrastructure/file-system-service.js +1 -1
  115. package/src/mcp/handlers/direct-handlers.js +2 -292
  116. package/src/mcp/handlers/hu-handlers.js +1 -35
  117. package/src/mcp/handlers/management-handlers.js +2 -42
  118. package/src/mcp/server-handlers.js +16 -22
  119. package/src/mcp/tools.js +1 -196
  120. package/src/plan/lane-plans.js +54 -0
  121. package/src/plan/plan-store.js +7 -4
  122. package/src/policy/add.js +26 -12
  123. package/src/policy/adr-number-check.js +31 -0
  124. package/src/prompts/audit.js +299 -339
  125. package/src/prompts/card-context.js +10 -5
  126. package/src/prompts/reviewer.js +5 -2
  127. package/src/rag/auto-update.js +56 -43
  128. package/src/rag/bootstrap-index.js +28 -54
  129. package/src/rag/migrate.js +1 -1
  130. package/src/rag/onnx-fallback.js +2 -2
  131. package/src/rag/pid-lease.js +70 -0
  132. package/src/rag/project-store.js +2 -12
  133. package/src/review/card-first.js +57 -46
  134. package/src/review/gate-gitignore.js +23 -4
  135. package/src/review/gate-hook.js +12 -4
  136. package/src/review/one-shot-review.js +45 -35
  137. package/src/review/publish-pr.js +14 -2
  138. package/src/review/repo-docs.js +73 -0
  139. package/src/review/repo-stack.js +59 -0
  140. package/src/review/reviewer-fallback.js +19 -4
  141. package/src/review/seal-commit.js +29 -0
  142. package/src/review/sonar-pregate.js +59 -44
  143. package/src/review/third-party-review.js +90 -16
  144. package/src/review/third-party-size.js +43 -0
  145. package/src/review/third-party-skills.js +54 -0
  146. package/src/review/verify-pr.js +1 -24
  147. package/src/roles/audit-role.js +106 -125
  148. package/src/skills/atril-skills.js +5 -4
  149. package/src/sonar/scanner.js +116 -101
  150. package/src/steward/invariants.js +5 -4
  151. package/src/steward/phantom-sweep.js +66 -0
  152. package/src/tournament/judge.js +60 -53
  153. package/src/utils/gh-account.js +37 -0
  154. package/src/utils/git.js +0 -13
  155. package/src/utils/home-migration.js +7 -1
  156. package/src/utils/main-checkout.js +53 -0
  157. package/src/utils/qmd-collection.js +3 -1
  158. package/src/utils/welcome.js +3 -3
  159. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  160. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  161. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  162. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  163. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  164. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  165. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  166. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  167. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  172. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  173. package/packages/hu-board/src/run-tracker.js +0 -121
  174. package/src/activity-log.js +0 -59
  175. package/src/agents/host-agent.js +0 -38
  176. package/src/agents/model-registry.js +0 -142
  177. package/src/audit/plan-adherence.js +0 -224
  178. package/src/autonomy/arbiter-role.js +0 -71
  179. package/src/autonomy/arbiter.js +0 -22
  180. package/src/autonomy/decider.js +0 -18
  181. package/src/autonomy/decision-resolver.js +0 -58
  182. package/src/autonomy/outcome-report.js +0 -55
  183. package/src/autonomy/stage-ask.js +0 -30
  184. package/src/brain/decision-tracker.js +0 -125
  185. package/src/brain/decisor.js +0 -243
  186. package/src/brain/solomon-consult.js +0 -171
  187. package/src/brain/standby-scheduler.js +0 -8
  188. package/src/budget/comparison.js +0 -96
  189. package/src/canvas/parse-md.js +0 -238
  190. package/src/canvas/schema.js +0 -101
  191. package/src/canvas/to-flat.js +0 -85
  192. package/src/canvas/validate.js +0 -163
  193. package/src/ci/dispatch.js +0 -99
  194. package/src/ci/index.js +0 -3
  195. package/src/ci/pr-diff.js +0 -26
  196. package/src/cli/register-standby.js +0 -26
  197. package/src/commands/architect.js +0 -111
  198. package/src/commands/autorun.js +0 -49
  199. package/src/commands/researcher.js +0 -54
  200. package/src/commands/resume.js +0 -72
  201. package/src/commands/review.js +0 -89
  202. package/src/commands/run.js +0 -185
  203. package/src/commands/standby.js +0 -117
  204. package/src/commands/status.js +0 -36
  205. package/src/commands/triage.js +0 -78
  206. package/src/commands/undo.js +0 -62
  207. package/src/domains/domain-loader.js +0 -245
  208. package/src/domains/domain-registry.js +0 -140
  209. package/src/domains/domain-synthesizer.js +0 -129
  210. package/src/git/automation.js +0 -433
  211. package/src/git/coder-commit-guard.js +0 -40
  212. package/src/git/hu-automation.js +0 -243
  213. package/src/git/hu-snapshot.js +0 -10
  214. package/src/git/symlink-targets.js +0 -57
  215. package/src/golden/asserter.js +0 -92
  216. package/src/golden/loader.js +0 -52
  217. package/src/golden/runner.js +0 -92
  218. package/src/golden/schema.js +0 -41
  219. package/src/guards/duplicate-members.js +0 -86
  220. package/src/guards/intent-guard.js +0 -161
  221. package/src/guards/perf-guard.js +0 -126
  222. package/src/guards/policy-guard.js +0 -90
  223. package/src/guards/policy-resolver.js +0 -85
  224. package/src/guards/secret-redactor.js +0 -71
  225. package/src/hu/acceptance-runner.js +0 -138
  226. package/src/hu/lazy-planner.js +0 -109
  227. package/src/hu/parallel-executor.js +0 -94
  228. package/src/hu/routing-metrics.js +0 -17
  229. package/src/hu/split-validator.js +0 -143
  230. package/src/hu/splitting-detector.js +0 -158
  231. package/src/hu/splitting-generator.js +0 -175
  232. package/src/hu/store.js +0 -394
  233. package/src/infrastructure/mocks.js +0 -182
  234. package/src/mcp/handlers/run-handler.js +0 -318
  235. package/src/mcp/sovereignty-guard.js +0 -113
  236. package/src/mcp/tool-arg-normalizers.js +0 -16
  237. package/src/mutate/reviewer-signal.js +0 -76
  238. package/src/orchestrator/agent-fallback.js +0 -81
  239. package/src/orchestrator/brain-coordinator.js +0 -186
  240. package/src/orchestrator/ci-integration.js +0 -139
  241. package/src/orchestrator/config-init.js +0 -426
  242. package/src/orchestrator/direct-actions.js +0 -217
  243. package/src/orchestrator/drivers/error-recovery.js +0 -287
  244. package/src/orchestrator/drivers/init-context.js +0 -238
  245. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  246. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  247. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  248. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  249. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  250. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  251. package/src/orchestrator/drivers/post-loop.js +0 -404
  252. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  253. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  254. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  255. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  256. package/src/orchestrator/drivers/pre-loop.js +0 -524
  257. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  258. package/src/orchestrator/feedback-enrichment.js +0 -140
  259. package/src/orchestrator/feedback-queue.js +0 -151
  260. package/src/orchestrator/flow-control.js +0 -170
  261. package/src/orchestrator/flow-runner.js +0 -365
  262. package/src/orchestrator/fs-leak-detector.js +0 -228
  263. package/src/orchestrator/hu-scheduler.js +0 -114
  264. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  265. package/src/orchestrator/iteration-gate.js +0 -73
  266. package/src/orchestrator/iteration-stages.js +0 -9
  267. package/src/orchestrator/parallel-limiter.js +0 -74
  268. package/src/orchestrator/pipeline-context.js +0 -72
  269. package/src/orchestrator/post-loop-stages.js +0 -468
  270. package/src/orchestrator/pre-loop-stages.js +0 -11
  271. package/src/orchestrator/preflight-checks.js +0 -487
  272. package/src/orchestrator/repair/repair-detector.js +0 -117
  273. package/src/orchestrator/repair/repair-runner.js +0 -142
  274. package/src/orchestrator/reviewer-fallback.js +0 -45
  275. package/src/orchestrator/role-output-compressor.js +0 -189
  276. package/src/orchestrator/session-journal.js +0 -308
  277. package/src/orchestrator/solomon-escalation.js +0 -331
  278. package/src/orchestrator/solomon-rules.js +0 -212
  279. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  280. package/src/orchestrator/stages/architect-stage.js +0 -172
  281. package/src/orchestrator/stages/coder-stage.js +0 -506
  282. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  283. package/src/orchestrator/stages/format-stage.js +0 -61
  284. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  285. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  286. package/src/orchestrator/stages/perf-stage.js +0 -140
  287. package/src/orchestrator/stages/planner-stage.js +0 -85
  288. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  289. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  290. package/src/orchestrator/stages/research-stage.js +0 -69
  291. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  292. package/src/orchestrator/stages/sonar-stage.js +0 -439
  293. package/src/orchestrator/stages/stage-classes.js +0 -84
  294. package/src/orchestrator/stages/stage-executor.js +0 -155
  295. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  296. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  297. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  298. package/src/orchestrator/stages/triage-stage.js +0 -172
  299. package/src/orchestrator/standby.js +0 -71
  300. package/src/orchestrator/verification-gate.js +0 -183
  301. package/src/orchestrator.js +0 -22
  302. package/src/plan/adr-generator.js +0 -170
  303. package/src/plan/adr-loader.js +0 -60
  304. package/src/plan/plan-executor.js +0 -100
  305. package/src/plan/plan-validation.js +0 -4
  306. package/src/prompts/acceptance-coverage.js +0 -32
  307. package/src/prompts/arbiter.js +0 -38
  308. package/src/prompts/architect.js +0 -103
  309. package/src/prompts/hu-reviewer.js +0 -244
  310. package/src/prompts/tool-correctness-judge.js +0 -31
  311. package/src/prompts/triage.js +0 -57
  312. package/src/rag/embedder.js +0 -2
  313. package/src/rag/embedders/_cloud-base.js +0 -2
  314. package/src/rag/embedders/cohere.js +0 -2
  315. package/src/rag/embedders/mistral.js +0 -2
  316. package/src/rag/embedders/onnx.js +0 -2
  317. package/src/rag/embedders/openai.js +0 -2
  318. package/src/rag/embedders/voyage.js +0 -2
  319. package/src/rag/rerank.js +0 -4
  320. package/src/rag/where-parser.js +0 -4
  321. package/src/repeat-detector.js +0 -88
  322. package/src/review/diff-generator.js +0 -112
  323. package/src/review/git-stash-helper.js +0 -104
  324. package/src/review/schema.js +0 -31
  325. package/src/review/scope-filter.js +0 -152
  326. package/src/review/snapshot-diff.js +0 -122
  327. package/src/review/tdd-discipline.js +0 -90
  328. package/src/review/tdd-policy.js +0 -69
  329. package/src/roles/acceptance-coverage-role.js +0 -78
  330. package/src/roles/architect-role.js +0 -77
  331. package/src/roles/coder-role.js +0 -113
  332. package/src/roles/commiter-role.js +0 -113
  333. package/src/roles/domain-curator-role.js +0 -134
  334. package/src/roles/hu-reviewer-role.js +0 -64
  335. package/src/roles/impeccable-role.js +0 -85
  336. package/src/roles/index.js +0 -18
  337. package/src/roles/karajan-brain-role.js +0 -134
  338. package/src/roles/perf-role.js +0 -122
  339. package/src/roles/planner-role.js +0 -151
  340. package/src/roles/refactorer-role.js +0 -30
  341. package/src/roles/repairer-role.js +0 -181
  342. package/src/roles/researcher-role.js +0 -70
  343. package/src/roles/reviewer-role.js +0 -115
  344. package/src/roles/security-role.js +0 -73
  345. package/src/roles/solomon-role.js +0 -271
  346. package/src/roles/sonar-role.js +0 -72
  347. package/src/roles/tester-role.js +0 -235
  348. package/src/roles/tool-correctness-judge-role.js +0 -78
  349. package/src/roles/triage-role.js +0 -99
  350. package/src/session/journal/decisions-writer.js +0 -200
  351. package/src/session/journal/iteration-logger.js +0 -194
  352. package/src/session/journal/summary-writer.js +0 -320
  353. package/src/session/journal/tree-writer.js +0 -164
  354. package/src/session/journal/write-all.js +0 -89
  355. package/src/session-cleanup.js +0 -116
  356. package/src/skills/semantic-detector.js +0 -167
  357. package/src/skills/skill-detector.js +0 -385
  358. package/src/sonar/cloud-scanner.js +0 -78
  359. package/src/tracker-bootstrap.js +0 -44
  360. package/src/types/index.js +0 -38
  361. package/src/types/policy.js +0 -43
  362. package/src/types/solomon.js +0 -87
  363. package/src/utils/async-lock.js +0 -31
  364. package/src/utils/budget.js +0 -229
  365. package/src/utils/cwd-confirm.js +0 -39
  366. package/src/utils/display/event-handlers.js +0 -403
  367. package/src/utils/display/formatters.js +0 -132
  368. package/src/utils/display/header.js +0 -47
  369. package/src/utils/display/session-summary.js +0 -118
  370. package/src/utils/display/solomon.js +0 -42
  371. package/src/utils/pricing.js +0 -29
  372. package/src/utils/quota-simulator.js +0 -56
  373. package/src/utils/role-assigner.js +0 -137
  374. package/src/utils/rtk-wrapper.js +0 -87
  375. package/src/utils/run-registry.js +0 -9
  376. package/src/utils/status-dashboard.js +0 -254
  377. package/src/utils/tool-call-extractor.js +0 -56
  378. package/src/webperf/devtools-detect.js +0 -112
  379. package/src/webperf/webperf-role-integration.js +0 -66
  380. package/templates/skills/kj-run.md +0 -73
@@ -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);