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
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { isAbsolute, join } from "node:path";
10
10
  import { runCommand } from "../utils/process.js";
11
- import { checkVerdict, diffHash, pruneVerdicts } from "../review/verdict-store.js";
11
+ import { checkVerdict, diffHash, pruneVerdicts, saveVerdict } from "../review/verdict-store.js";
12
12
  import { runOneShotReview } from "../review/one-shot-review.js";
13
13
  import { runSolomonArbitration } from "../review/solomon-arbitration.js";
14
14
  import { ensureGateTrackable } from "../review/gate-gitignore.js";
@@ -188,119 +188,102 @@ export async function solomonCommand({ config, logger = null, flags = {} }) {
188
188
  return res;
189
189
  }
190
190
 
191
- export async function reviewGateCommand({ config, logger = null, flags = {} }) {
192
- const projectDir = config?.projectDir || process.cwd();
193
-
194
- if (flags.installGate) {
195
- const fs = await import("node:fs/promises");
196
- const marker = join(projectDir, ".karajan", "review-gate");
197
- await fs.mkdir(join(projectDir, ".karajan"), { recursive: true });
198
- await fs.writeFile(marker, "# Cross-AI review gate enabled (ENV-C1). Commit this file so the whole team inherits the gate.\n");
199
- // KJC-BUG-0447 (#2655): the marker declares the gate; only a pre-commit that runs it enforces it.
200
- const hook = gateHookStatus(projectDir);
201
- if (hook.enforced) console.log("✓ review gate enabled — commits now require an approved cross-AI verdict (kj review --staged)");
202
- else {
203
- console.log(`✗ review gate declared (.karajan/review-gate) but NOT enforced: ${hook.reason} — run \`kj harden\` so the pre-commit hook runs it`);
204
- process.exitCode = 1;
205
- }
206
- // KJC-TSK-0646: a `.karajan/` dir-exclude would silently keep the
207
- // contract out of git — rewrite it so the team actually inherits it.
208
- const gi = await ensureGateTrackable(projectDir);
209
- if (gi.changed) {
210
- console.log("✓ .gitignore adjusted: the gate contract (.karajan/review-gate, hooks) is now trackable — commit it");
211
- }
212
- return { installed: true, enforced: hook.enforced };
191
+ async function installGateAction(projectDir) {
192
+ const fs = await import("node:fs/promises");
193
+ const marker = join(projectDir, ".karajan", "review-gate");
194
+ await fs.mkdir(join(projectDir, ".karajan"), { recursive: true });
195
+ await fs.writeFile(marker, "# Cross-AI review gate enabled (ENV-C1). Commit this file so the whole team inherits the gate.\n");
196
+ // KJC-BUG-0447 (#2655): the marker declares the gate; only a pre-commit that runs it enforces it.
197
+ const hook = gateHookStatus(projectDir);
198
+ if (hook.enforced) console.log("✓ review gate enabled — commits now require an approved cross-AI verdict (kj review --staged)");
199
+ else {
200
+ console.log(`✗ review gate declared (.karajan/review-gate) but NOT enforced: ${hook.reason} — run \`kj harden\` so the pre-commit hook runs it`);
201
+ process.exitCode = 1;
213
202
  }
214
-
215
- // KJC-BUG-0173: manual GC of the verdict store. Opportunistic pruning runs
216
- // on every saved verdict; this lets a human sweep on demand, with --dry-run
217
- // to see what would go without deleting.
218
- if (flags.prune) {
219
- const dryRun = Boolean(flags.dryRun || flags.checkOnly);
220
- const days = Number(flags.pruneDays);
221
- const res = await pruneVerdicts({ projectDir, dryRun, ...(Number.isFinite(days) && days > 0 ? { maxAgeDays: days } : {}) });
222
- console.log(
223
- dryRun
224
- ? `kj review --prune (dry run): ${res.expired.length} of ${res.scanned} verdict(s) older than ${res.maxAgeDays}d would be removed`
225
- : `kj review --prune: removed ${res.removed} verdict(s) older than ${res.maxAgeDays}d (${res.scanned - res.removed} kept)`,
226
- );
227
- return { pruned: res.removed, dryRun };
203
+ // KJC-TSK-0646: a `.karajan/` dir-exclude would silently keep the
204
+ // contract out of git — rewrite it so the team actually inherits it.
205
+ const gi = await ensureGateTrackable(projectDir);
206
+ if (gi.changed) {
207
+ console.log("✓ .gitignore adjusted: the gate contract (.karajan/review-gate, hooks) is now trackable — commit it");
228
208
  }
209
+ return { installed: true, enforced: hook.enforced };
210
+ }
229
211
 
230
- const diff = await rawDiff(flags.range);
231
-
232
- const card = await enforceCardFirst({ config, projectDir });
233
- if (!card.ok) return { verdict: "rejected", reviewer: "card-first", issues: [{ severity: "high", description: card.reason }] };
234
-
235
- // KJC-TSK-1086: the Steward's block at commit too, not only where Edit starts work.
236
- const steward = stewardBlocking({ projectDir, config });
237
- if (steward) {
238
- console.log(`✗ ${steward.reason}`);
239
- process.exitCode = 1;
240
- return { verdict: "rejected", reviewer: "steward", issues: [{ severity: "high", description: steward.reason }] };
241
- }
212
+ // KJC-BUG-0173: manual GC of the verdict store. Opportunistic pruning runs
213
+ // on every saved verdict; this lets a human sweep on demand, with --dry-run
214
+ // to see what would go without deleting.
215
+ async function pruneAction(projectDir, flags) {
216
+ const dryRun = Boolean(flags.dryRun || flags.checkOnly);
217
+ const days = Number(flags.pruneDays);
218
+ const res = await pruneVerdicts({ projectDir, dryRun, ...(Number.isFinite(days) && days > 0 ? { maxAgeDays: days } : {}) });
219
+ console.log(
220
+ dryRun
221
+ ? `kj review --prune (dry run): ${res.expired.length} of ${res.scanned} verdict(s) older than ${res.maxAgeDays}d would be removed`
222
+ : `kj review --prune: removed ${res.removed} verdict(s) older than ${res.maxAgeDays}d (${res.scanned - res.removed} kept)`,
223
+ );
224
+ return { pruned: res.removed, dryRun };
225
+ }
242
226
 
243
- // KJC-TSK-0687 (MG-B): the verifiable half of TDD — sources without a
244
- // single test change warn (block via method_gates.tests_with_code).
245
- const changedFiles = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
246
- // KJC-BUG-0229: un fichero BORRADO no es una fuente que el scan pueda indexar
247
- // ni sobre la que el RAG pueda responder, asi que exigirlo era pedir lo
248
- // imposible: una PR que retiraba 45 ficheros se paro dos veces y acumulo tres
249
- // excepciones por la misma causa mecanica. Los requisitos de Sonar y de RAG
250
- // se evaluan sobre lo que sigue VIVO tras el cambio (A, C, M, R); policy, LOC
251
- // y tests-with-code siguen viendo el diff entero, porque ahi un borrado si
252
- // cuenta. El borrado danino lo cubre la review cruzada, que lee todo el diff.
253
- const liveFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=ACMR"])).split("\n").map((f) => f.trim()).filter(Boolean);
227
+ /** A gate's rejection: said, exit 1, and the record the caller returns (KJC-TSK-1257). */
228
+ function rejected(reviewer, issues) {
229
+ process.exitCode = 1;
230
+ return { verdict: "rejected", reviewer, issues };
231
+ }
254
232
 
255
- // KJC-TSK-0688 (MG-C) + KJC-TSK-0691 (MG-E): oversized-diff policy.
256
- // Field case: a 522-line PR sailed past the warning with a
257
- // rationalization — where the project wants teeth, pr_size: "block"
258
- // rejects deterministically with a NAMED escape. Default stays warn.
259
- // PL-C (KJC-TSK-0735), fuente única de umbrales: si la policy declara el
260
- // invariante net_lines_added, SU max es el umbral — el mismo número que
261
- // aplican kj policy check y el tier C de CI; method_gates queda de fallback.
262
- // KJC-BUG-0326 (#2085): two tiers. ~150 is the ideal (method_gates.pr_size_warn);
263
- // up to the limit (the policy's net_lines_added max, else method_gates.pr_size_max,
264
- // 200) is allowed without asking; above the limit, partition. KJC-TSK-1076: one
265
- // source, shared with the third-party PR checks.
266
- const pol = loadPolicy({ projectDir });
233
+ /**
234
+ * KJC-TSK-0688 (MG-C) + KJC-TSK-0691 (MG-E): oversized-diff policy.
235
+ * Field case: a 522-line PR sailed past the warning with a
236
+ * rationalization — where the project wants teeth, pr_size: "block"
237
+ * rejects deterministically with a NAMED escape. Default stays warn.
238
+ * PL-C (KJC-TSK-0735), fuente única de umbrales: si la policy declara el
239
+ * invariante net_lines_added, SU max es el umbral — el mismo número que
240
+ * aplican kj policy check y el tier C de CI; method_gates queda de fallback.
241
+ * KJC-BUG-0326 (#2085): two tiers. ~150 is the ideal (method_gates.pr_size_warn);
242
+ * up to the limit (the policy's net_lines_added max, else method_gates.pr_size_max,
243
+ * 200) is allowed without asking; above the limit, partition. KJC-TSK-1076: one
244
+ * source, shared with the third-party PR checks.
245
+ * @returns {Promise<object|null>} the rejection, or null to go on
246
+ */
247
+ async function prSizeGate({ projectDir, config, range }) {
267
248
  const { ideal: sizeIdeal, limit: sizeWarn, source: sizeSource } = prSizeLimit({ projectDir, config });
268
- if (sizeWarn > 0) {
269
- const numstat = await rawDiff(flags.range, ["--numstat"]);
270
- // KJC-BUG-0205: the budget caps CODE growth, so generated output and human
271
- // documentation are exempt — the same rule CI applies. Counting everything
272
- // here warned 234 lines on a doc-only PR that CI counted at 104, and two
273
- // implementations of one rule that disagree teach people to trust neither.
274
- // KJC-BUG-0434: measured NET, as CI and the policy's net_lines_added do — a rewrite in place is not growth.
275
- const { added, testAdded } = budgetedAdded(numstat);
276
- const { net, exempt } = budgetedNet(numstat);
277
- // Said out loud: an exemption nobody sees is indistinguishable from a gate
278
- // that is not looking.
279
- if (exempt > 0) console.log(`ℓ pr-size: ${exempt} line(s) exempt from the budget (generated output and human docs; AI-rule files always count)`);
280
- if (net > sizeIdeal && net <= sizeWarn) {
281
- console.log(`ℓ pr-size: ${net} net lines, over the ~${sizeIdeal} ideal, within the ${sizeWarn} limit: allowed, nothing to ask`);
282
- }
283
- if (net > sizeWarn) {
284
- // KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
285
- // partitioning is a decision, and it must never split code from its tests.
286
- const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} test lines added — partition by feature, never code from its tests)` : "";
287
- const sizePolicy = config?.method_gates?.pr_size || "warn";
288
- // ADR 0015: no env escape — an exception is the user's label on the PR, judged in CI.
289
- if (sizePolicy === "block") {
290
- const reason = `${net} net lines${split} is over the ${sizeWarn}-line limit (${sizeSource}; method_gates.pr_size: block) — partition the work; an exception is your user's large-pr-justified label on the PR`;
291
- console.log(`✗ pr-size gate: ${reason}`);
292
- process.exitCode = 1;
293
- return { verdict: "rejected", reviewer: "pr-size", issues: [{ severity: "high", description: reason }] };
294
- } else {
295
- console.log(`⚠ pr-size: ${net} net lines${split}, over the ${sizeWarn}-line limit (ideal ~${sizeIdeal}, source: ${sizeSource}) — partition the work before committing (method_gates.pr_size: block to harden)`);
296
- }
297
- }
249
+ if (sizeWarn <= 0) return null;
250
+ const numstat = await rawDiff(range, ["--numstat"]);
251
+ // KJC-BUG-0205: the budget caps CODE growth, so generated output and human
252
+ // documentation are exempt — the same rule CI applies. Counting everything
253
+ // here warned 234 lines on a doc-only PR that CI counted at 104, and two
254
+ // implementations of one rule that disagree teach people to trust neither.
255
+ // KJC-BUG-0434: measured NET, as CI and the policy's net_lines_added do — a rewrite in place is not growth.
256
+ const { added, testAdded } = budgetedAdded(numstat);
257
+ const { net, exempt } = budgetedNet(numstat);
258
+ // Said out loud: an exemption nobody sees is indistinguishable from a gate
259
+ // that is not looking.
260
+ if (exempt > 0) console.log(`ℓ pr-size: ${exempt} line(s) exempt from the budget (generated output and human docs; AI-rule files always count)`);
261
+ if (net > sizeIdeal && net <= sizeWarn) {
262
+ console.log(`ℓ pr-size: ${net} net lines, over the ~${sizeIdeal} ideal, within the ${sizeWarn} limit: allowed, nothing to ask`);
298
263
  }
299
- // KJC-TSK-0705 (PV-B): the outbound privacy boundary at commit time.
300
- // Denylist data rejects with no escape (ADR 0015: false positives go in the allow list); generic PII
301
- // warns (privacy.generic: "block" hardens). Added lines only — deletions
302
- // don't publish. In the SEA binary the privacy module is stubbed and
303
- // throws: the gate degrades with a note instead of crashing.
264
+ if (net <= sizeWarn) return null;
265
+ // KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
266
+ // partitioning is a decision, and it must never split code from its tests.
267
+ const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} test lines added — partition by feature, never code from its tests)` : "";
268
+ // ADR 0015: no env escape — an exception is the user's label on the PR, judged in CI.
269
+ if ((config?.method_gates?.pr_size || "warn") === "block") {
270
+ const reason = `${net} net lines${split} is over the ${sizeWarn}-line limit (${sizeSource}; method_gates.pr_size: block) — partition the work; an exception is your user's large-pr-justified label on the PR`;
271
+ console.log(`✗ pr-size gate: ${reason}`);
272
+ return rejected("pr-size", [{ severity: "high", description: reason }]);
273
+ }
274
+ console.log(`⚠ pr-size: ${net} net lines${split}, over the ${sizeWarn}-line limit (ideal ~${sizeIdeal}, source: ${sizeSource}) — partition the work before committing (method_gates.pr_size: block to harden)`);
275
+ return null;
276
+ }
277
+
278
+ /**
279
+ * KJC-TSK-0705 (PV-B): the outbound privacy boundary at commit time.
280
+ * Denylist data rejects with no escape (ADR 0015: false positives go in the allow list); generic PII
281
+ * warns (privacy.generic: "block" hardens). Added lines only — deletions
282
+ * don't publish. In the SEA binary the privacy module is stubbed and
283
+ * throws: the gate degrades with a note instead of crashing.
284
+ * @returns {object|null} the rejection, or null to go on
285
+ */
286
+ function privacyGate({ diff, config }) {
304
287
  try {
305
288
  // KJC-BUG-0203: per file, so a finding can name where it lives and so
306
289
  // build output is judged as what it is. Generic heuristics are silenced
@@ -315,53 +298,56 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
315
298
  const warns = findings.filter((f) => f.severity === "warn");
316
299
  for (const f of warns) console.log(`⚠ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked} — personal data? move it out before it ships`);
317
300
  const hardened = config?.privacy?.generic === "block" && warns.length > 0;
318
- if (blocks.length > 0 || hardened) {
319
- for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
320
- const reason = `${blocks.length || warns.length} personal-data finding(s) in the staged diff — this must not reach the repo (a confirmed false positive goes in the allow list of ~/.karajan/privacy.yml)`;
321
- console.log(`✗ privacy gate: ${reason}`);
322
- process.exitCode = 1;
323
- return { verdict: "rejected", reviewer: "privacy", issues: [{ severity: "high", description: reason }] };
324
- }
301
+ if (blocks.length === 0 && !hardened) return null;
302
+ for (const f of blocks) console.log(`✗ privacy: [${f.type}] ${f.source}:${f.line} → ${f.masked}`);
303
+ const reason = `${blocks.length || warns.length} personal-data finding(s) in the staged diff — this must not reach the repo (a confirmed false positive goes in the allow list of ~/.karajan/privacy.yml)`;
304
+ console.log(`✗ privacy gate: ${reason}`);
305
+ return rejected("privacy", [{ severity: "high", description: reason }]);
325
306
  } catch (err) {
326
307
  // Known degradation: the SEA stub throws its install-from-npm message.
327
308
  // Anything else fails CLOSED — a silent skip would defeat the guarantee.
328
309
  if (/standalone binary/i.test(err?.message || "")) {
329
310
  console.log("⚠ privacy gate unavailable in this build — skipping");
330
- } else {
331
- const reason = `privacy gate error: ${err.message} — refusing to pass the boundary unchecked`;
332
- console.log(`✗ ${reason}`);
333
- process.exitCode = 1;
334
- return { verdict: "rejected", reviewer: "privacy", issues: [{ severity: "high", description: reason }] };
311
+ return null;
335
312
  }
313
+ const reason = `privacy gate error: ${err.message} — refusing to pass the boundary unchecked`;
314
+ console.log(`✗ ${reason}`);
315
+ return rejected("privacy", [{ severity: "high", description: reason }]);
336
316
  }
317
+ }
337
318
 
338
- // KJC-TSK-0795 AC1: hand the gate the numbers so a delete-only diff is
339
- // exempt — deleting code adds no behavior to test.
340
- const numstat = (await rawDiff(flags.range, ["--numstat"])).split("\n").map((l) => l.trim()).filter(Boolean)
319
+ /**
320
+ * KJC-TSK-0795 AC1: hand the gate the numbers so a delete-only diff is
321
+ * exempt — deleting code adds no behavior to test.
322
+ * @returns {Promise<object|null>} the rejection, or null to go on
323
+ */
324
+ async function testsWithCodeGate({ config, projectDir, range, changedFiles }) {
325
+ const numstat = (await rawDiff(range, ["--numstat"])).split("\n").map((l) => l.trim()).filter(Boolean)
341
326
  .map((l) => { const [a, r, ...f] = l.split(/\s+/); return { file: f.join(" "), added: a === "-" ? 1 : Number(a) || 0, removed: r === "-" ? 0 : Number(r) || 0 }; });
342
- await markCommentOnlyAdditions(numstat, { range: flags.range, projectDir });
327
+ await markCommentOnlyAdditions(numstat, { range, projectDir });
343
328
  const tests = checkTestsWithCode({ config, stagedFiles: changedFiles, numstat });
344
329
  if (tests.mode === "delete-only") console.log(`⚠ tests-with-code: exempt — ${tests.reason}`);
345
330
  if (tests.mode === "warn") console.log(`⚠ tests-with-code: ${tests.reason}`);
346
331
  if (tests.mode === "exempt") console.log(`⚠ tests-with-code exempt: ${tests.reason}`);
347
- if (!tests.ok) {
348
- console.log(`✗ tests-with-code gate: ${tests.reason}`);
349
- process.exitCode = 1;
350
- return { verdict: "rejected", reviewer: "tests-with-code", issues: [{ severity: "high", description: tests.reason }] };
351
- }
332
+ if (tests.ok) return null;
333
+ console.log(`✗ tests-with-code gate: ${tests.reason}`);
334
+ return rejected("tests-with-code", [{ severity: "high", description: tests.reason }]);
335
+ }
352
336
 
353
- // KJC-TSK-0734 (PL-B): la policy declarativa como gate determinista, en
354
- // --staged Y en --check — el pre-commit hereda los dientes sin regenerar
355
- // hooks. enforcement=warn avisa; deny cierra sin excepción por diff (ADR
356
- // 0015); class=security cierra además sin arbitraje.
337
+ const atFile = (v) => (v.file ? ` (${v.file})` : "");
338
+
339
+ /**
340
+ * KJC-TSK-0734 (PL-B): la policy declarativa como gate determinista, en
341
+ * --staged Y en --check — el pre-commit hereda los dientes sin regenerar
342
+ * hooks. enforcement=warn avisa; deny cierra sin excepción por diff (ADR
343
+ * 0015); class=security cierra además sin arbitraje.
344
+ * @returns {Promise<{rejection: object|null, gate: object, seal: Function}>} `seal` records the chokepoint's decisions
345
+ */
346
+ async function policyGate({ projectDir, diff, pol, changedFiles, std, range, check }) {
357
347
  // KJC-BUG-0208: el mismo modulo que decide el presupuesto decide el metrico
358
348
  // del invariante. Sumar aqui el numstat entero daba dos cifras para la misma
359
349
  // regla en dos lineas seguidas del mismo comando.
360
- const { net } = budgetedNet(await rawDiff(flags.range, ["--numstat"]));
361
- // GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
362
- // eximen su regla mientras viven; el descarte de líneas corruptas se dice.
363
- const std = loadStandingExceptions(projectDir);
364
- if (std.discarded > 0) console.log(`⚠ policy: ${std.discarded} línea(s) corruptas descartadas en policy-exceptions.jsonl — revísalo`);
350
+ const { net } = budgetedNet(await rawDiff(range, ["--numstat"]));
365
351
  const gate = evaluatePolicyGate({
366
352
  policy: pol.policy, errors: pol.errors, files: changedFiles, netLinesAdded: net,
367
353
  diffHashValue: diffHash(diff),
@@ -371,195 +357,222 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
371
357
  // ADR 0009 (KJC-BUG-0161): la exención ESTRUCTURAL del supervisor — un
372
358
  // diff de .karajan/hooks respaldado por la provenance sellada Y el render
373
359
  // canónico no es una edición: es la regeneración de kj harden --commit.
374
- {
375
- const res = liftSealedSupervisorViolations({ projectDir, violations: gate.denials });
376
- if (res.lifted > 0) {
377
- console.log(`✓ policy: ${res.lifted} fichero(s) de supervisor verificados contra la provenance sellada (ADR 0009) — regeneración, no edición`);
378
- gate.denials = res.violations;
379
- gate.ok = gate.denials.length === 0 && !gate.invalid;
380
- }
360
+ const lifted = liftSealedSupervisorViolations({ projectDir, violations: gate.denials });
361
+ if (lifted.lifted > 0) {
362
+ console.log(`✓ policy: ${lifted.lifted} fichero(s) de supervisor verificados contra la provenance sellada (ADR 0009) — regeneración, no edición`);
363
+ gate.denials = lifted.violations;
364
+ gate.ok = gate.denials.length === 0 && !gate.invalid;
381
365
  }
382
- const at = (v) => (v.file ? ` (${v.file})` : "");
383
- for (const w of gate.warns) console.log(`⚠ policy [${w.rule_id}] ${w.reason}${at(w)}`);
366
+ for (const w of gate.warns) console.log(`⚠ policy [${w.rule_id}] ${w.reason}${atFile(w)}`);
384
367
  for (const e of gate.exempted) console.log(formatExempted(e));
385
368
  // GOV-C (KJC-TSK-0747): las decisiones de chokepoint dejan rastro
386
369
  // hash-encadenado — cada deny, cada excepción, y el allow del commit.
387
- const chokepoint = flags.check ? "commit" : "review";
370
+ const chokepoint = check ? "commit" : "review";
388
371
  const seal = (decision, extra = {}) =>
389
372
  recordGateDecision(projectDir, { decision, chokepoint, policy_hash: policyFileHash(projectDir), artifact_hash: diffHash(diff), ...extra });
390
373
  if (gate.exempted.length > 0) seal("exempt", { rule_ids: gate.exempted.map((e) => e.rule_id) });
391
- if (!gate.ok) {
392
- for (const d of gate.denials) {
393
- const sec = d.class === "security" ? " [security — sin escape ni arbitraje]" : "";
394
- console.log(`✗ policy [${d.rule_id}]${sec} ${d.reason}${at(d)}`);
395
- }
396
- seal("deny", { rule_ids: gate.denials.map((d) => d.rule_id) });
397
- const reason = gate.invalid
398
- ? "policy.yml inválida — una regla que miente es peor que ninguna: corrígela"
399
- : `${gate.denials.length} violación(es) de policy con enforcement=deny — el diff no entra`;
400
- console.log(`✗ policy gate: ${reason}`);
401
- process.exitCode = 1;
402
- return { verdict: "rejected", reviewer: "policy", issues: gate.denials.map((d) => ({ severity: "high", file: d.file, description: `[${d.rule_id}] ${d.reason}` })) };
374
+ if (gate.ok) return { rejection: null, gate, seal };
375
+ for (const d of gate.denials) {
376
+ const sec = d.class === "security" ? " [security — sin escape ni arbitraje]" : "";
377
+ console.log(`✗ policy [${d.rule_id}]${sec} ${d.reason}${atFile(d)}`);
403
378
  }
379
+ seal("deny", { rule_ids: gate.denials.map((d) => d.rule_id) });
380
+ const reason = gate.invalid
381
+ ? "policy.yml inválida — una regla que miente es peor que ninguna: corrígela"
382
+ : `${gate.denials.length} violación(es) de policy con enforcement=deny — el diff no entra`;
383
+ console.log(`✗ policy gate: ${reason}`);
384
+ const rejection = rejected("policy", gate.denials.map((d) => ({ severity: "high", file: d.file, description: `[${d.rule_id}] ${d.reason}` })));
385
+ return { rejection, gate, seal };
386
+ }
404
387
 
405
- if (flags.check) {
406
- // KJC-BUG-0132 (issue #1344): a PURE merge commit stages no content of
407
- // its own — everything it brings reached the parent branches already
408
- // reviewed, and there is no diff to bind a verdict to, so demanding one
409
- // deadlocks the merge. A merge WITH conflict resolutions stages real
410
- // content and still requires its verdict below.
411
- if (!flags.range && !diff.trim()) {
412
- const gitDirRaw = (await runCommand("git", ["rev-parse", "--git-dir"])).stdout?.trim();
413
- const gitDir = gitDirRaw && !isAbsolute(gitDirRaw) ? join(projectDir, gitDirRaw) : gitDirRaw;
414
- if (gitDir && existsSync(join(gitDir, "MERGE_HEAD"))) {
415
- console.log("✓ pure merge commit — empty staged diff, parents already reviewed; gate passes with trail");
416
- process.exitCode = 0;
417
- return { ok: true, merge: true, reason: "pure merge commit (MERGE_HEAD, empty staged diff)" };
418
- }
419
- }
420
- let res = await checkVerdict(projectDir, diff);
421
- if (res.note) console.log(`ℹ ${res.note}`);
422
- // KJC-TSK-0838: a reviewed diff with code enters only if the verdict's
423
- // sonar block proves the analysis ran and covered every staged source.
424
- // The headless pipeline stamps its verdict after its own sonar stage and
425
- // carries no block yet (step 3 of the card) — said, never assumed silent.
426
- if (res.ok && res.verdict.host === "kj-pipeline") {
427
- // The pipeline scans the whole project (no per-file proof yet), so its
428
- // stamp must at least show the stage RAN; no block or a skipped stage
429
- // is refused like any other unanalysed code.
430
- const stage = res.verdict.sonar;
431
- if (!stage?.ran) {
432
- const why = stage ? `the pipeline's sonar stage did not run: ${stage.reason || "unknown reason"}` : "the pipeline verdict carries no sonar block";
433
- res = { ok: false, verdict: res.verdict, reason: `Sonar is mandatory for code — ${why}` };
434
- }
435
- } else if (res.ok) {
436
- const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: res.verdict.sonar, standingExceptions: std.standing });
437
- if (!req.ok) res = { ok: false, verdict: res.verdict, reason: req.reason };
438
- else if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
439
- }
440
- // KJC-TSK-0849 (ADR 0010, RAG-C): the verdict's rag block is the evidence
441
- // that the session consulted the RAG about every staged source. The one
442
- // place the rule cannot be verified is a pipeline verdict: the headless
443
- // pipeline keeps no session ledger yet. It WARNS and the allow seals the
444
- // warn under the rule id, so the exception is auditable (kj policy report)
445
- // and gains teeth with data — never accepted in silence.
446
- const warnIds = gate.warns.map((w) => w.rule_id);
447
- if (res.ok && res.verdict.host === "kj-pipeline") {
448
- console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
449
- if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
450
- } else if (res.ok) {
451
- const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
452
- const rreq = checkRagVerdict({ config, stagedFiles: liveFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
453
- if (!rreq.ok) res = { ok: false, verdict: res.verdict, reason: rreq.reason };
454
- else if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
455
- }
456
- console.log(res.ok
457
- ? `✓ verdict ok — approved by ${res.verdict.reviewer} (diff ${res.verdict.diffHash.slice(0, 12)})`
458
- : `✗ ${res.reason}`);
459
- // GOV-C: el allow del chokepoint de COMMIT es evidencia — se sella. PL-E
460
- // (KJC-TSK-0767): con las reglas que AVISARON, para que "nace avisando y
461
- // gana dientes" se decida con datos (kj policy report), no a ciegas.
462
- if (res.ok) seal("allow", warnIds.length > 0 ? { warn_rule_ids: warnIds } : {});
463
- process.exitCode = res.ok ? 0 : 1;
388
+ /**
389
+ * KJC-BUG-0132 (issue #1344): a PURE merge commit stages no content of
390
+ * its own — everything it brings reached the parent branches already
391
+ * reviewed, and there is no diff to bind a verdict to, so demanding one
392
+ * deadlocks the merge. A merge WITH conflict resolutions stages real
393
+ * content and still requires its verdict.
394
+ */
395
+ async function isPureMerge({ projectDir, diff, range }) {
396
+ if (range || diff.trim()) return false;
397
+ const gitDirRaw = (await runCommand("git", ["rev-parse", "--git-dir"])).stdout?.trim();
398
+ const gitDir = gitDirRaw && !isAbsolute(gitDirRaw) ? join(projectDir, gitDirRaw) : gitDirRaw;
399
+ return Boolean(gitDir && existsSync(join(gitDir, "MERGE_HEAD")));
400
+ }
401
+
402
+ /**
403
+ * KJC-TSK-0838: a reviewed diff with code enters only if the verdict's
404
+ * sonar block proves the analysis ran and covered every staged source.
405
+ * The headless pipeline stamps its verdict after its own sonar stage and
406
+ * carries no block yet (step 3 of the card) — said, never assumed silent.
407
+ */
408
+ function withSonarProof(res, { config, liveFiles, std }) {
409
+ if (res.verdict.host === "kj-pipeline") {
410
+ // The pipeline scans the whole project (no per-file proof yet), so its
411
+ // stamp must at least show the stage RAN; no block or a skipped stage
412
+ // is refused like any other unanalysed code.
413
+ const stage = res.verdict.sonar;
414
+ if (stage?.ran) return res;
415
+ const why = stage ? `the pipeline's sonar stage did not run: ${stage.reason || "unknown reason"}` : "the pipeline verdict carries no sonar block";
416
+ return { ok: false, verdict: res.verdict, reason: `Sonar is mandatory for code — ${why}` };
417
+ }
418
+ const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: res.verdict.sonar, standingExceptions: std.standing });
419
+ if (!req.ok) return { ok: false, verdict: res.verdict, reason: req.reason };
420
+ if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
421
+ return res;
422
+ }
423
+
424
+ /**
425
+ * KJC-TSK-0849 (ADR 0010, RAG-C): the verdict's rag block is the evidence
426
+ * that the session consulted the RAG about every staged source. The one
427
+ * place the rule cannot be verified is a pipeline verdict: the headless
428
+ * pipeline keeps no session ledger yet. It WARNS and the allow seals the
429
+ * warn under the rule id, so the exception is auditable (kj policy report)
430
+ * and gains teeth with data — never accepted in silence.
431
+ */
432
+ function withRagProof(res, { config, liveFiles, std, projectDir, warnIds }) {
433
+ if (res.verdict.host === "kj-pipeline") {
434
+ console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
435
+ if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
464
436
  return res;
465
437
  }
438
+ const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
439
+ const rreq = checkRagVerdict({ config, stagedFiles: liveFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
440
+ if (!rreq.ok) return { ok: false, verdict: res.verdict, reason: rreq.reason };
441
+ if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
442
+ return res;
443
+ }
466
444
 
467
- // KJC-TSK-0676: deterministic pre-gate — sonar findings on the changed
468
- // files come back BEFORE any AI opinion. Blocking severities reject
469
- // without spending reviewer tokens; the rest travel with the task so
470
- // the cross-AI reviewer weighs them. Unavailable sonar degrades loudly.
471
- let task = flags.task;
472
- // KJC-TSK-0838: what sonar saw travels INSIDE the verdict, bound to the
473
- // diff hash — a reviewed diff and a reviewed AND analysed diff must be
474
- // distinguishable by --check and by the method report.
475
- let sonarRecord = { ran: false, reason: "--no-sonar: the pre-gate was skipped by flag" };
476
- if (flags.sonar !== false) {
477
- // KJC-TSK-0795 AC3: only issues on lines this diff ADDS may veto.
478
- const touchedLines = addedLinesByFile(await rawDiff(flags.range, ["--unified=0"]));
479
- const pre = await runSonarPregate({ config, stagedFiles: liveFiles, touchedLines, logger });
480
- if (!pre.available) {
481
- // KJC-BUG-0156 (issue #1543): a scan that FAILED must not read like one
482
- // more config warning — it hid a dead quality gate for 8 straight PRs.
483
- // Still fail-open (a laptop without Docker still commits), but SAID.
484
- const chosen = /disabled in config/.test(pre.reason || "");
485
- console.log(chosen
486
- ? `⚠ sonar pre-gate skipped: ${pre.reason}`
487
- : `✗ sonar pre-gate UNAVAILABLE — the quality gate did NOT run on this diff: ${pre.reason}`);
488
- sonarRecord = { ran: false, reason: pre.reason };
489
- } else {
490
- sonarRecord = {
491
- ran: true, projectKey: pre.projectKey, covered: pre.covered ?? [], uncovered: pre.uncovered ?? [],
492
- blocking: pre.blocking.length, advisory: pre.advisory.length,
493
- };
494
- if (sonarRecord.uncovered.length > 0) {
495
- console.log(`⚠ sonar coverage: ${sonarRecord.uncovered.length} staged source(s) were NOT in the analysis (${sonarRecord.uncovered.slice(0, 5).join(", ")}${sonarRecord.uncovered.length > 5 ? "…" : ""}) — the scan did not see them`);
496
- }
497
- const found = [...pre.blocking, ...pre.advisory];
498
- if (found.length > 0) {
499
- console.log(`Sonar on the changed lines — ${pre.blocking.length} blocking, ${pre.advisory.length} advisory (project total: ${pre.totalProject}):`);
500
- for (const f of found) console.log(` - ${formatSonarFinding(f)}`);
501
- }
502
- if ((pre.preexisting || []).length > 0) {
503
- console.log(`⚠ sonar trend: ${pre.preexisting.length} preexisting issue(s) on untouched lines of these files — not this PR's veto, but the debt is real`);
504
- }
505
- if (pre.blocking.length > 0) {
506
- console.log("✗ REJECTED by sonar (deterministic) — fix the blocking findings; the cross-AI reviewer was not invoked.");
507
- process.exitCode = 1;
508
- return {
509
- verdict: "rejected", reviewer: "sonar",
510
- issues: pre.blocking.map((i) => ({ severity: i.severity, file: undefined, description: formatSonarFinding(i) })),
511
- };
512
- }
513
- if (pre.advisory.length > 0) {
514
- task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
515
- + `Deterministic Sonar findings on these files (fold them into your review):\n`
516
- + pre.advisory.map((f) => `- ${formatSonarFinding(f)}`).join("\n");
517
- }
518
- }
445
+ /** --check (the pre-commit): the staged diff enters only with an approved verdict that carries its proofs. */
446
+ async function checkPath({ projectDir, config, diff, range, liveFiles, std, gate, seal }) {
447
+ if (await isPureMerge({ projectDir, diff, range })) {
448
+ console.log("✓ pure merge commit — empty staged diff, parents already reviewed; gate passes with trail");
449
+ process.exitCode = 0;
450
+ return { ok: true, merge: true, reason: "pure merge commit (MERGE_HEAD, empty staged diff)" };
519
451
  }
452
+ let res = await checkVerdict(projectDir, diff);
453
+ if (res.note) console.log(`ℹ ${res.note}`);
454
+ if (res.ok) res = withSonarProof(res, { config, liveFiles, std });
455
+ const warnIds = gate.warns.map((w) => w.rule_id);
456
+ if (res.ok) res = withRagProof(res, { config, liveFiles, std, projectDir, warnIds });
457
+ console.log(res.ok
458
+ ? `✓ verdict ok — approved by ${res.verdict.reviewer} (diff ${res.verdict.diffHash.slice(0, 12)})`
459
+ : `✗ ${res.reason}`);
460
+ // GOV-C: el allow del chokepoint de COMMIT es evidencia — se sella. PL-E
461
+ // (KJC-TSK-0767): con las reglas que AVISARON, para que "nace avisando y
462
+ // gana dientes" se decida con datos (kj policy report), no a ciegas.
463
+ if (res.ok) seal("allow", warnIds.length > 0 ? { warn_rule_ids: warnIds } : {});
464
+ process.exitCode = res.ok ? 0 : 1;
465
+ return res;
466
+ }
520
467
 
521
- // KJC-TSK-0838: fail-CLOSED for code. Sonar disabled, down, skipped by
522
- // flag, or blind to a staged source — the diff does not reach the reviewer.
523
- // Docs-only diffs pass; a live human grant on the rule is the only escape.
524
- const sonarReq = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: sonarRecord, standingExceptions: std.standing });
525
- if (!sonarReq.ok) {
526
- console.log(`✗ ${sonarReq.reason}`);
527
- process.exitCode = 1;
528
- return { verdict: "rejected", reviewer: "sonar", issues: [{ severity: "high", file: undefined, description: sonarReq.reason }] };
468
+ const DEFAULT_REVIEW_TASK = "Review the following diff for correctness, security and maintainability.";
469
+ /** Appends a gate's finding for the reviewer to the task (the default task when there is none). */
470
+ const withNote = (task, note) => (note ? `${task || DEFAULT_REVIEW_TASK}\n\n${note}` : task);
471
+ const listed = (items) => `${items.slice(0, 5).join(", ")}${items.length > 5 ? "…" : ""}`;
472
+
473
+ /**
474
+ * KJC-TSK-0676: deterministic pre-gate — sonar findings on the changed
475
+ * files come back BEFORE any AI opinion. Blocking severities reject
476
+ * without spending reviewer tokens; the rest travel with the task so
477
+ * the cross-AI reviewer weighs them. Unavailable sonar degrades loudly.
478
+ * KJC-TSK-0838: what sonar saw travels INSIDE the verdict (`record`), bound to the
479
+ * diff hash — a reviewed diff and a reviewed AND analysed diff must be
480
+ * distinguishable by --check and by the method report.
481
+ */
482
+ async function sonarPregate({ config, logger, range, liveFiles, enabled }) {
483
+ if (!enabled) return { record: { ran: false, reason: "--no-sonar: the pre-gate was skipped by flag" } };
484
+ // KJC-TSK-0795 AC3: only issues on lines this diff ADDS may veto.
485
+ const touchedLines = addedLinesByFile(await rawDiff(range, ["--unified=0"]));
486
+ const pre = await runSonarPregate({ config, stagedFiles: liveFiles, touchedLines, logger });
487
+ if (!pre.available) {
488
+ // KJC-BUG-0156 (issue #1543): a scan that FAILED must not read like one
489
+ // more config warning — it hid a dead quality gate for 8 straight PRs.
490
+ // Still fail-open (a laptop without Docker still commits), but SAID.
491
+ const chosen = /disabled in config/.test(pre.reason || "");
492
+ console.log(chosen
493
+ ? `⚠ sonar pre-gate skipped: ${pre.reason}`
494
+ : `✗ sonar pre-gate UNAVAILABLE — the quality gate did NOT run on this diff: ${pre.reason}`);
495
+ return { record: { ran: false, reason: pre.reason } };
529
496
  }
530
- if (sonarReq.mode === "granted") console.log(formatSonarGrant(sonarReq.grant));
531
- // The verdict says on what grounds the requirement was met (proof, docs-only,
532
- // human grant) — the method report reads it back.
533
- sonarRecord.mode = sonarReq.mode;
534
-
535
- // KJC-TSK-0849 (ADR 0010, RAG-C): the session's RAG ledger must show the
536
- // RAG answered about every staged source (file or sibling), and the TWINS
537
- // it returned that the diff does not touch travel to the reviewer by name —
538
- // the 16-sep bugs were exactly the twin nobody touched.
539
- const ledger = readRagLedger(projectDir);
540
- const newFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=A"])).split("\n").map((f) => f.trim()).filter(Boolean);
541
- const ragReq = checkRagRequirement({ config, stagedFiles: liveFiles, newFiles, ledger, standingExceptions: std.standing });
542
- if (!ragReq.ok) {
543
- console.log(`✗ ${ragReq.reason}`);
544
- process.exitCode = 1;
545
- return { verdict: "rejected", reviewer: "rag-first", issues: [{ severity: "high", file: undefined, description: ragReq.reason }] };
497
+ const record = {
498
+ ran: true, projectKey: pre.projectKey, covered: pre.covered ?? [], uncovered: pre.uncovered ?? [],
499
+ blocking: pre.blocking.length, advisory: pre.advisory.length,
500
+ };
501
+ printSonarPregate(pre, record);
502
+ if (pre.blocking.length > 0) {
503
+ console.log("✗ REJECTED by sonar (deterministic) — fix the blocking findings; the cross-AI reviewer was not invoked.");
504
+ return { record, rejection: rejected("sonar", pre.blocking.map((i) => ({ severity: i.severity, file: undefined, description: formatSonarFinding(i) }))) };
505
+ }
506
+ const findings = pre.advisory.map((f) => `- ${formatSonarFinding(f)}`).join("\n");
507
+ const note = pre.advisory.length > 0 ? `Deterministic Sonar findings on these files (fold them into your review):\n${findings}` : null;
508
+ return { record, note };
509
+ }
510
+
511
+ function printSonarPregate(pre, record) {
512
+ if (record.uncovered.length > 0) {
513
+ console.log(`⚠ sonar coverage: ${record.uncovered.length} staged source(s) were NOT in the analysis (${listed(record.uncovered)}) — the scan did not see them`);
514
+ }
515
+ const found = [...pre.blocking, ...pre.advisory];
516
+ if (found.length > 0) {
517
+ console.log(`Sonar on the changed lines — ${pre.blocking.length} blocking, ${pre.advisory.length} advisory (project total: ${pre.totalProject}):`);
518
+ for (const f of found) console.log(` - ${formatSonarFinding(f)}`);
546
519
  }
547
- if (ragReq.mode === "granted") console.log(formatRagGrant(ragReq.grant));
548
- if (ragReq.mode === "no-harness") console.log("⚠ rag: no Sentinel harness in this tree — no session ledger to check (kj harden installs it)");
549
- const ragRecord = ragBlock(ragReq, ledger);
550
- if (ragRecord.twinsUntouched.length > 0) {
551
- const twins = ragRecord.twinsUntouched;
552
- console.log(`ℹ rag twins: this session's queries also returned ${twins.length} file(s) the diff does not touch — ${twins.slice(0, 5).join(", ")}${twins.length > 5 ? "…" : ""}`);
553
- task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
554
- + "The session's RAG queries also returned these files, which the diff does NOT touch — the same concept may live there, so check the change is complete, not only correct:\n"
555
- + twins.map((t) => `- ${t}`).join("\n");
520
+ if ((pre.preexisting || []).length > 0) {
521
+ console.log(`⚠ sonar trend: ${pre.preexisting.length} preexisting issue(s) on untouched lines of these files — not this PR's veto, but the debt is real`);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * KJC-TSK-0838: fail-CLOSED for code. Sonar disabled, down, skipped by
527
+ * flag, or blind to a staged source — the diff does not reach the reviewer.
528
+ * Docs-only diffs pass; a live human grant on the rule is the only escape.
529
+ * The verdict says on what grounds the requirement was met (proof, docs-only,
530
+ * human grant): `record.mode`, read back by the method report.
531
+ */
532
+ function sonarRequirementGate({ config, liveFiles, std, record }) {
533
+ const req = checkSonarRequirement({ config, stagedFiles: liveFiles, sonar: record, standingExceptions: std.standing });
534
+ if (!req.ok) {
535
+ console.log(`✗ ${req.reason}`);
536
+ return rejected("sonar", [{ severity: "high", file: undefined, description: req.reason }]);
556
537
  }
538
+ if (req.mode === "granted") console.log(formatSonarGrant(req.grant));
539
+ record.mode = req.mode;
540
+ return null;
541
+ }
557
542
 
558
- // BOOT-D (KJC-TSK-0863): green is not proof for what a person SEES. kj does
559
- // not drive the browser (the host has it); it demands the walkthrough and
560
- // records it in the verdict, bound to the diff like sonar and rag. It WARNS:
561
- // the last proof, never the only one, and a gate that fires often teaches
562
- // people to skip gates.
543
+ /**
544
+ * KJC-TSK-0849 (ADR 0010, RAG-C): the session's RAG ledger must show the
545
+ * RAG answered about every staged source (file or sibling), and the TWINS
546
+ * it returned that the diff does not touch travel to the reviewer by name —
547
+ * the 16-sep bugs were exactly the twin nobody touched.
548
+ */
549
+ async function ragGate({ config, projectDir, range, liveFiles, std }) {
550
+ const ledger = readRagLedger(projectDir);
551
+ const newFiles = (await rawDiff(range, ["--name-only", "--diff-filter=A"])).split("\n").map((f) => f.trim()).filter(Boolean);
552
+ const req = checkRagRequirement({ config, stagedFiles: liveFiles, newFiles, ledger, standingExceptions: std.standing });
553
+ if (!req.ok) {
554
+ console.log(`✗ ${req.reason}`);
555
+ return { rejection: rejected("rag-first", [{ severity: "high", file: undefined, description: req.reason }]) };
556
+ }
557
+ if (req.mode === "granted") console.log(formatRagGrant(req.grant));
558
+ if (req.mode === "no-harness") console.log("⚠ rag: no Sentinel harness in this tree — no session ledger to check (kj harden installs it)");
559
+ const record = ragBlock(req, ledger);
560
+ const twins = record.twinsUntouched;
561
+ if (twins.length === 0) return { record };
562
+ console.log(`ℹ rag twins: this session's queries also returned ${twins.length} file(s) the diff does not touch — ${listed(twins)}`);
563
+ const note = "The session's RAG queries also returned these files, which the diff does NOT touch — the same concept may live there, so check the change is complete, not only correct:\n"
564
+ + twins.map((t) => `- ${t}`).join("\n");
565
+ return { record, note };
566
+ }
567
+
568
+ /**
569
+ * BOOT-D (KJC-TSK-0863): green is not proof for what a person SEES. kj does
570
+ * not drive the browser (the host has it); it demands the walkthrough and
571
+ * records it in the verdict, bound to the diff like sonar and rag. It WARNS:
572
+ * the last proof, never the only one, and a gate that fires often teaches
573
+ * people to skip gates.
574
+ */
575
+ function uiEvidence({ changedFiles, flags, std }) {
563
576
  const walked = Array.isArray(flags.walked) ? flags.walked : [];
564
577
  const uiReq = checkUiEvidence({
565
578
  stagedFiles: changedFiles,
@@ -567,51 +580,118 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
567
580
  standingExceptions: std.standing,
568
581
  });
569
582
  if (uiReq.warn) console.log(`⚠ ${uiReq.reason}`);
570
- const uiRecord = uiBlock(uiReq);
583
+ return uiBlock(uiReq);
584
+ }
571
585
 
572
- // MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
573
- // SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
574
- // el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
575
- // cierra ANTES de gastar reviewer; warn viaja como advisory en su task.
586
+ /**
587
+ * MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
588
+ * SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
589
+ * el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
590
+ * cierra ANTES de gastar reviewer; warn viaja como advisory en su task.
591
+ */
592
+ async function mutationGate({ config, projectDir, flags }) {
576
593
  const mut = flags.staged && !flags.range ? await runMutationPregate({ config, projectDir }) : { enabled: false, ok: true };
577
- if (mut.enabled) {
578
- if (mut.available === false) {
579
- console.log(`⚠ mutation pre-gate no disponible: ${mut.reason} — degradado avisando (method_gates.mutation)`);
580
- } else if (mut.survived.length > 0) {
581
- console.log(`Mutantes supervivientes en el diff — score ${mut.score ?? "n/a"}%:`);
582
- for (const m of mut.survived) console.log(` ✗ ${formatSurvivor(m)}`);
583
- if (!mut.ok) {
584
- console.log("✗ mutation gate: la suite verde no PRUEBA el cambio — mata los supervivientes antes del review (method_gates.mutation: block).");
585
- process.exitCode = 1;
586
- return { verdict: "rejected", reviewer: "mutation", issues: mut.survived.map((m) => ({ severity: "high", file: m.file, description: formatSurvivor(m) })) };
587
- }
588
- task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
589
- + `Surviving mutants on the changed lines (weak asserts — fold into your review):\n`
590
- + mut.survived.map((m) => `- ${formatSurvivor(m)}`).join("\n");
591
- }
594
+ if (!mut.enabled) return {};
595
+ if (mut.available === false) {
596
+ console.log(`⚠ mutation pre-gate no disponible: ${mut.reason} — degradado avisando (method_gates.mutation)`);
597
+ return {};
592
598
  }
593
-
594
- // KJC-TSK-1052 (TEAM-F): another branch in progress touching the same files is said, never blocks.
595
- if (flags.staged && !flags.range) {
596
- for (const line of formatOverlaps(await branchOverlaps({ projectDir, stagedFiles: changedFiles, base: config?.base_branch || "main" }))) console.log(line);
599
+ if (mut.survived.length === 0) return {};
600
+ console.log(`Mutantes supervivientes en el diff — score ${mut.score ?? "n/a"}%:`);
601
+ for (const m of mut.survived) console.log(` ✗ ${formatSurvivor(m)}`);
602
+ if (!mut.ok) {
603
+ console.log("✗ mutation gate: la suite verde no PRUEBA el cambio — mata los supervivientes antes del review (method_gates.mutation: block).");
604
+ return { rejection: rejected("mutation", mut.survived.map((m) => ({ severity: "high", file: m.file, description: formatSurvivor(m) }))) };
597
605
  }
606
+ const survivors = mut.survived.map((m) => `- ${formatSurvivor(m)}`).join("\n");
607
+ return { note: `Surviving mutants on the changed lines (weak asserts — fold into your review):\n${survivors}` };
608
+ }
598
609
 
599
- // KJC-TSK-0722: a dependency published under the cooldown is said, and weighed by the reviewer.
610
+ /**
611
+ * KJC-TSK-0722: a dependency published under the cooldown is said, and weighed by the reviewer.
612
+ * KJC-TSK-1056: method_gates.dep_cooldown: block stops it here, before any reviewer token.
613
+ */
614
+ async function cooldownStep({ diff, config, std }) {
600
615
  const cooldownHours = config?.supply_chain?.cooldown_hours ?? DEFAULT_COOLDOWN_HOURS;
601
616
  const cooldownResult = await cooldownAdvisories(diff, { thresholdHours: cooldownHours });
602
617
  const cooldown = formatCooldown(cooldownResult, cooldownHours);
603
618
  for (const line of cooldown.lines) console.log(line);
604
- // KJC-TSK-1056: method_gates.dep_cooldown: block stops it here, before any reviewer token.
605
- const cooldownStop = cooldownGate({ result: cooldownResult, mode: config?.method_gates?.dep_cooldown, standingExceptions: std.standing });
606
- if (cooldownStop.grant) console.log(`⚠ dep_cooldown lifted by a HUMAN grant [method.dep_cooldown] until ${cooldownStop.grant.expiresAt}`);
607
- if (cooldownStop.block) {
608
- console.log(`✗ ${cooldownStop.reason}`);
619
+ const stop = cooldownGate({ result: cooldownResult, mode: config?.method_gates?.dep_cooldown, standingExceptions: std.standing });
620
+ if (stop.grant) console.log(`⚠ dep_cooldown lifted by a HUMAN grant [method.dep_cooldown] until ${stop.grant.expiresAt}`);
621
+ if (stop.block) {
622
+ console.log(`✗ ${stop.reason}`);
623
+ return { rejection: rejected("dep-cooldown", cooldownResult.young.map((p) => ({ severity: "high", description: `${p.name}@${p.version} published ${p.hours} h ago` }))) };
624
+ }
625
+ return { note: cooldown.note || null };
626
+ }
627
+
628
+ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
629
+ const projectDir = config?.projectDir || process.cwd();
630
+ if (flags.installGate) return installGateAction(projectDir);
631
+ if (flags.prune) return pruneAction(projectDir, flags);
632
+
633
+ const diff = await rawDiff(flags.range);
634
+
635
+ const card = await enforceCardFirst({ config, projectDir });
636
+ if (!card.ok) return { verdict: "rejected", reviewer: "card-first", issues: [{ severity: "high", description: card.reason }] };
637
+
638
+ // KJC-TSK-1086: the Steward's block at commit too, not only where Edit starts work.
639
+ const steward = stewardBlocking({ projectDir, config });
640
+ if (steward) {
641
+ console.log(`✗ ${steward.reason}`);
609
642
  process.exitCode = 1;
610
- return { verdict: "rejected", reviewer: "dep-cooldown", issues: cooldownResult.young.map((p) => ({ severity: "high", description: `${p.name}@${p.version} published ${p.hours} h ago` })) };
643
+ return { verdict: "rejected", reviewer: "steward", issues: [{ severity: "high", description: steward.reason }] };
644
+ }
645
+
646
+ // KJC-TSK-0687 (MG-B): the verifiable half of TDD — sources without a
647
+ // single test change warn (block via method_gates.tests_with_code).
648
+ const changedFiles = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
649
+ // KJC-BUG-0229: un fichero BORRADO no es una fuente que el scan pueda indexar
650
+ // ni sobre la que el RAG pueda responder, asi que exigirlo era pedir lo
651
+ // imposible: una PR que retiraba 45 ficheros se paro dos veces y acumulo tres
652
+ // excepciones por la misma causa mecanica. Los requisitos de Sonar y de RAG
653
+ // se evaluan sobre lo que sigue VIVO tras el cambio (A, C, M, R); policy, LOC
654
+ // y tests-with-code siguen viendo el diff entero, porque ahi un borrado si
655
+ // cuenta. El borrado danino lo cubre la review cruzada, que lee todo el diff.
656
+ const liveFiles = (await rawDiff(flags.range, ["--name-only", "--diff-filter=ACMR"])).split("\n").map((f) => f.trim()).filter(Boolean);
657
+
658
+ const pol = loadPolicy({ projectDir });
659
+ // KJC-TSK-1257: the pre-gates in their order; the first rejection ends the review.
660
+ const preRejection = await prSizeGate({ projectDir, config, range: flags.range })
661
+ ?? privacyGate({ diff, config })
662
+ ?? await testsWithCodeGate({ config, projectDir, range: flags.range, changedFiles });
663
+ if (preRejection) return preRejection;
664
+
665
+ // GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
666
+ // eximen su regla mientras viven; el descarte de líneas corruptas se dice.
667
+ const std = loadStandingExceptions(projectDir);
668
+ if (std.discarded > 0) console.log(`⚠ policy: ${std.discarded} línea(s) corruptas descartadas en policy-exceptions.jsonl — revísalo`);
669
+ const { rejection: policyRejection, gate, seal } = await policyGate({ projectDir, diff, pol, changedFiles, std, range: flags.range, check: flags.check });
670
+ if (policyRejection) return policyRejection;
671
+ if (flags.check) return checkPath({ projectDir, config, diff, range: flags.range, liveFiles, std, gate, seal });
672
+
673
+ // KJC-TSK-1257: the review path's gates in their order; each may reject, and what they found
674
+ // that does not reject travels to the reviewer inside the task. KJC-BUG-0423 (#2665): --check
675
+ // does not run these, so their rejection is recorded for the diff, replacing an earlier APPROVED.
676
+ const recorded = (rejection) => saveVerdict(projectDir, diff, rejection).then(() => rejection);
677
+ const sonar = await sonarPregate({ config, logger, range: flags.range, liveFiles, enabled: flags.sonar !== false });
678
+ if (sonar.rejection) return recorded(sonar.rejection);
679
+ const sonarRejection = sonarRequirementGate({ config, liveFiles, std, record: sonar.record });
680
+ if (sonarRejection) return recorded(sonarRejection);
681
+ const rag = await ragGate({ config, projectDir, range: flags.range, liveFiles, std });
682
+ if (rag.rejection) return recorded(rag.rejection);
683
+ const uiRecord = uiEvidence({ changedFiles, flags, std });
684
+ const mutation = await mutationGate({ config, projectDir, flags });
685
+ if (mutation.rejection) return recorded(mutation.rejection);
686
+ // KJC-TSK-1052 (TEAM-F): another branch in progress touching the same files is said, never blocks.
687
+ if (flags.staged && !flags.range) {
688
+ for (const line of formatOverlaps(await branchOverlaps({ projectDir, stagedFiles: changedFiles, base: config?.base_branch || "main" }))) console.log(line);
611
689
  }
612
- if (cooldown.note) task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n${cooldown.note}`;
690
+ const cooldown = await cooldownStep({ diff, config, std });
691
+ if (cooldown.rejection) return recorded(cooldown.rejection);
692
+ const task = [sonar.note, rag.note, mutation.note, cooldown.note].reduce(withNote, flags.task);
613
693
 
614
- const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonarRecord, rag: ragRecord, ui: uiRecord });
694
+ const record = await runOneShotReview({ diff, task, config, logger, projectDir, sonar: sonar.record, rag: rag.record, ui: uiRecord });
615
695
  printVerdict(record, record.verdict === "approved" ? await reviewCorrectionRounds(projectDir, record) : 0);
616
696
  process.exitCode = record.verdict === "approved" ? 0 : 1;
617
697
  return record;