karajan-code 4.70.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +34 -3
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -21,91 +21,115 @@ import { activeGhUser, effectiveGitEmail } from "../identity/detect.js";
21
21
  import { compareIdentity } from "../identity/compare.js";
22
22
  import { createWizard, isTTY } from "../utils/wizard.js";
23
23
 
24
- export async function identityCommand({ action = "show", config, kjVersion = "unknown", flags = {}, deps = {} }) {
25
- const projectDir = config?.projectDir || process.cwd();
26
- const log = deps.log || console.log;
27
- const tty = deps.isTTY || isTTY;
28
- const detectOpts = { hostsPath: deps.hostsPath, gitFn: deps.gitFn };
29
- const effective = {
30
- gh_user: activeGhUser(detectOpts.hostsPath ? { hostsPath: detectOpts.hostsPath } : {}),
31
- git_email: effectiveGitEmail(projectDir, detectOpts.gitFn ? { gitFn: detectOpts.gitFn } : {}),
32
- };
24
+ const SIGNERS_FILE = ".karajan/supervisor-signers.json";
33
25
 
34
- // KJC-TSK-0822: enrola la clave PÚBLICA del móvil (la privada nunca toca esta máquina).
35
- if (action === "enroll-phone") {
36
- let recoveryCode;
37
- try {
38
- // KJC-TSK-0966 (HUM-C, ADR 0018): enrolar es un acto del catálogo; hasta hoy
39
- // solo lo frenaba el Sentinel, y un kj fuera de Claude no tiene Sentinel.
40
- humanAct("phone-enroll", { env: deps.env ?? process.env, tty: deps.ttyHuman ?? process.stdout.isTTY, ancestry: deps.ancestry ?? {}, confirm: deps.confirm });
41
- // KJC-TSK-0998 (HUM-D, ADR 0018): a phone of the roster signs the key that
42
- // enters; the FIRST one has nobody to sign it (bootstrap) and runs on the four layers.
43
- const key = validatePhonePublicKey(flags.publicKeyBase64);
44
- const signers = ".karajan/supervisor-signers.json";
45
- // KJC-TSK-0999 (HUM-B2, ADR 0018): a lost phone is replaced with the recovery
46
- // code, never by reinstalling: its proof is the admission and the lost phone
47
- // is not asked to sign. The code is checked BEFORE anything else happens.
48
- const recovered = flags.recovery ? redeemRecoveryCode(flags.recovery, { projectDir }) : null;
49
- // The lost phone is REVOKED by the same code: the one named with --replaces, or the one this machine had enrolled.
50
- const lost = recovered ? (flags.replaces ?? (isPhoneEnrolled({ home: deps.home }) ? readEnrolledKey({ home: deps.home }) : null)) : null;
51
- if (recovered && !lost) throw new Error("di qué móvil se pierde: --replaces <su clave pública> (esta máquina no tiene ninguno enrolado)");
52
- if (recovered) log(`recuperación con el código: el móvil perdido (${lost.slice(0, 8)}…) queda revocado y no firma; el código gastado es la admisión del nuevo`);
53
- const { signature } = await signAct("phone-enroll", {
54
- projectDir, home: deps.home, kjVersion, logger: { warn: log, info: log },
55
- deps: recovered ? { ...deps, phone: { enrolled: () => false } } : deps,
56
- bootstrap: recovered !== null || readEnrolledKeys({ projectDir, home: deps.home }).size === 0,
57
- files: [{ file: signers, sha256: createHash("sha256").update(key).digest("hex") }],
58
- changes: [{ file: signers, status: "modified", added: 1, removed: 0, summary: `Entra la clave ${flags.label ?? "(sin etiqueta)"}`, diff: `+ ${key}`, truncated: false }],
59
- });
60
- // KJC-TSK-0831 (ADR 0010): the versioned roster, now with the admission CI
61
- // verifies (HUM-B); the legacy key in ~/.karajan stays.
62
- recoveryCode = admitSigner({ projectDir, publicKey: key, label: flags.label ?? null, admission: recovered ?? signature, replaces: lost }).recoveryCode;
63
- enrollPhone(key, { home: deps.home });
64
- } catch (err) {
65
- log(`kj identity enroll-phone: ${err.message}`);
66
- return 1;
67
- }
68
- log("Móvil enrolado: la clave pública se añadió al padrón .karajan/supervisor-signers.json (commítealo) y a ~/.karajan/supervisor-phone.json. Sellar el supervisor pedirá la firma de un móvil del padrón.");
69
- if (recoveryCode) {
70
- log(`\nCÓDIGO DE RECUPERACIÓN, se enseña UNA sola vez. Guárdalo fuera de esta máquina:\n\n ${recoveryCode}\n`);
71
- log("Sirve para enrolar un móvil nuevo si pierdes este: kj identity enroll-phone <clave nueva> --recovery <código>. Es de un solo uso: al usarlo se emite otro. Sin móvil y sin código no hay recuperación: el padrón se rehace por fuera de CI, a la vista de todos (ADR 0018).");
72
- }
73
- return 0;
26
+ // Reading the effective identity never throws: both readers answer null when unknown.
27
+ const effectiveIdentity = (projectDir, deps) => ({
28
+ gh_user: activeGhUser(deps.hostsPath ? { hostsPath: deps.hostsPath } : {}),
29
+ git_email: effectiveGitEmail(projectDir, deps.gitFn ? { gitFn: deps.gitFn } : {}),
30
+ });
31
+
32
+ // KJC-TSK-0999 (HUM-B2, ADR 0018): a lost phone is replaced with the recovery
33
+ // code, never by reinstalling: its proof is the admission and the lost phone
34
+ // is not asked to sign. The code is checked BEFORE anything else happens.
35
+ // The lost phone is REVOKED by the same code: the one named with --replaces, or the one this machine had enrolled.
36
+ function recoveryOf({ projectDir, log, flags, deps }) {
37
+ const recovered = flags.recovery ? redeemRecoveryCode(flags.recovery, { projectDir }) : null;
38
+ if (!recovered) return { recovered: null, lost: null };
39
+ const lost = flags.replaces ?? (isPhoneEnrolled({ home: deps.home }) ? readEnrolledKey({ home: deps.home }) : null);
40
+ if (!lost) throw new Error("di qué móvil se pierde: --replaces <su clave pública> (esta máquina no tiene ninguno enrolado)");
41
+ log(`recuperación con el código: el móvil perdido (${lost.slice(0, 8)}…) queda revocado y no firma; el código gastado es la admisión del nuevo`);
42
+ return { recovered, lost };
43
+ }
44
+
45
+ // Returns the recovery code the roster issues for the new key.
46
+ async function admitPhone(ctx) {
47
+ const { projectDir, log, kjVersion, flags, deps } = ctx;
48
+ // KJC-TSK-0966 (HUM-C, ADR 0018): enrolar es un acto del catálogo; hasta hoy
49
+ // solo lo frenaba el Sentinel, y un kj fuera de Claude no tiene Sentinel.
50
+ humanAct("phone-enroll", { env: deps.env ?? process.env, tty: deps.ttyHuman ?? process.stdout.isTTY, ancestry: deps.ancestry ?? {}, confirm: deps.confirm });
51
+ // KJC-TSK-0998 (HUM-D, ADR 0018): a phone of the roster signs the key that
52
+ // enters; the FIRST one has nobody to sign it (bootstrap) and runs on the four layers.
53
+ const key = validatePhonePublicKey(flags.publicKeyBase64);
54
+ const { recovered, lost } = recoveryOf(ctx);
55
+ const { signature } = await signAct("phone-enroll", {
56
+ projectDir, home: deps.home, kjVersion, logger: { warn: log, info: log },
57
+ deps: recovered ? { ...deps, phone: { enrolled: () => false } } : deps,
58
+ bootstrap: recovered !== null || readEnrolledKeys({ projectDir, home: deps.home }).size === 0,
59
+ files: [{ file: SIGNERS_FILE, sha256: createHash("sha256").update(key).digest("hex") }],
60
+ changes: [{ file: SIGNERS_FILE, status: "modified", added: 1, removed: 0, summary: `Entra la clave ${flags.label ?? "(sin etiqueta)"}`, diff: `+ ${key}`, truncated: false }],
61
+ });
62
+ // KJC-TSK-0831 (ADR 0010): the versioned roster, now with the admission CI
63
+ // verifies (HUM-B); the legacy key in ~/.karajan stays.
64
+ const { recoveryCode } = admitSigner({ projectDir, publicKey: key, label: flags.label ?? null, admission: recovered ?? signature, replaces: lost });
65
+ enrollPhone(key, { home: deps.home });
66
+ return recoveryCode;
67
+ }
68
+
69
+ // KJC-TSK-0822: enrola la clave PÚBLICA del móvil (la privada nunca toca esta máquina).
70
+ async function enrollPhoneAction(ctx) {
71
+ const { log } = ctx;
72
+ let recoveryCode;
73
+ try {
74
+ recoveryCode = await admitPhone(ctx);
75
+ } catch (err) {
76
+ log(`kj identity enroll-phone: ${err.message}`);
77
+ return 1;
78
+ }
79
+ log("Móvil enrolado: la clave pública se añadió al padrón .karajan/supervisor-signers.json (commítealo) y a ~/.karajan/supervisor-phone.json. Sellar el supervisor pedirá la firma de un móvil del padrón.");
80
+ if (recoveryCode) {
81
+ log(`\nCÓDIGO DE RECUPERACIÓN, se enseña UNA sola vez. Guárdalo fuera de esta máquina:\n\n ${recoveryCode}\n`);
82
+ log("Sirve para enrolar un móvil nuevo si pierdes este: kj identity enroll-phone <clave nueva> --recovery <código>. Es de un solo uso: al usarlo se emite otro. Sin móvil y sin código no hay recuperación: el padrón se rehace por fuera de CI, a la vista de todos (ADR 0018).");
74
83
  }
84
+ return 0;
85
+ }
75
86
 
76
- // KJC-TSK-0986 (HUM-F): the once-per-version invitation to enroll the phone,
77
- // printed for the SessionStart hook (which cannot import kj) and for scripts.
78
- if (action === "phone-invite") {
79
- // KJC-BUG-0360: the text depends on the level; at max (the default) the acts are refused.
80
- const { level } = readSecurityLevel({ projectDir: deps.projectDir ?? process.cwd(), home: deps.home });
81
- const text = phoneInvite({ home: deps.home, version: flags.version, level });
82
- if (text) log(text);
83
- return 0;
87
+ // KJC-TSK-0986 (HUM-F): the once-per-version invitation to enroll the phone,
88
+ // printed for the SessionStart hook (which cannot import kj) and for scripts.
89
+ function phoneInviteAction({ log, flags, deps }) {
90
+ // KJC-BUG-0360: the text depends on the level; at max (the default) the acts are refused.
91
+ const { level } = readSecurityLevel({ projectDir: deps.projectDir ?? process.cwd(), home: deps.home });
92
+ const text = phoneInvite({ home: deps.home, version: flags.version, level });
93
+ if (text) log(text);
94
+ return 0;
95
+ }
96
+
97
+ // Resolves true when the binding may go ahead.
98
+ async function confirmBinding(candidate, { log, flags, deps }) {
99
+ const tty = deps.isTTY || isTTY;
100
+ if (tty() && !flags.yes) {
101
+ const wizard = (deps.makeWizard || createWizard)();
102
+ try {
103
+ const ok = await wizard.confirm(`Bind this clone to gh ${candidate.gh_user} / git ${candidate.git_email}?`, true);
104
+ if (!ok) log("kj identity: not declared.");
105
+ return ok;
106
+ } finally { wizard.close(); }
84
107
  }
108
+ if (!flags.gh || !flags.email) {
109
+ // Proven live on day one: the active gh session had been flipped by
110
+ // ANOTHER session, and --yes bound the clone to the wrong account.
111
+ // Binding without confirmation is allowed, but never silent.
112
+ log("kj identity: WARNING — binding the ACTIVE session as-is without confirmation (another session may have switched it); prefer --gh/--email explicitly and verify with `kj identity show`.");
113
+ }
114
+ return true;
115
+ }
85
116
 
86
- if (action === "set") {
87
- const candidate = { gh_user: flags.gh || effective.gh_user, git_email: flags.email || effective.git_email };
88
- if (!candidate.gh_user || !candidate.git_email) {
89
- log(`kj identity: cannot declare — gh account: ${candidate.gh_user ?? "no session"}, git email: ${candidate.git_email ?? "none"}. Log in / set git config, or pass --gh <user> --email <email>.`);
90
- return 1;
91
- }
92
- if (tty() && !flags.yes) {
93
- const wizard = (deps.makeWizard || createWizard)();
94
- try {
95
- const ok = await wizard.confirm(`Bind this clone to gh ${candidate.gh_user} / git ${candidate.git_email}?`, true);
96
- if (!ok) { log("kj identity: not declared."); return 1; }
97
- } finally { wizard.close(); }
98
- } else if (!flags.gh || !flags.email) {
99
- // Proven live on day one: the active gh session had been flipped by
100
- // ANOTHER session, and --yes bound the clone to the wrong account.
101
- // Binding without confirmation is allowed, but never silent.
102
- log("kj identity: WARNING — binding the ACTIVE session as-is without confirmation (another session may have switched it); prefer --gh/--email explicitly and verify with `kj identity show`.");
103
- }
104
- writeIdentity(projectDir, candidate);
105
- log(`kj identity: declared gh ${candidate.gh_user} / git ${candidate.git_email} → .karajan/identity.local.yml (ignored by git).`);
106
- return 0;
117
+ async function setAction(ctx) {
118
+ const { projectDir, log, flags, deps } = ctx;
119
+ const effective = effectiveIdentity(projectDir, deps);
120
+ const candidate = { gh_user: flags.gh || effective.gh_user, git_email: flags.email || effective.git_email };
121
+ if (!candidate.gh_user || !candidate.git_email) {
122
+ log(`kj identity: cannot declare — gh account: ${candidate.gh_user ?? "no session"}, git email: ${candidate.git_email ?? "none"}. Log in / set git config, or pass --gh <user> --email <email>.`);
123
+ return 1;
107
124
  }
125
+ if (!(await confirmBinding(candidate, ctx))) return 1;
126
+ writeIdentity(projectDir, candidate);
127
+ log(`kj identity: declared gh ${candidate.gh_user} / git ${candidate.git_email} → .karajan/identity.local.yml (ignored by git).`);
128
+ return 0;
129
+ }
108
130
 
131
+ function showAction({ projectDir, log, deps }) {
132
+ const effective = effectiveIdentity(projectDir, deps);
109
133
  const declared = readIdentity(projectDir);
110
134
  const result = compareIdentity(declared, effective);
111
135
  if (!declared) {
@@ -118,3 +142,12 @@ export async function identityCommand({ action = "show", config, kjVersion = "un
118
142
  for (const m of result.mismatches) log(` → ${m.remedy}`);
119
143
  return result.ok ? 0 : 2;
120
144
  }
145
+
146
+ // Any other action shows the identity, as before.
147
+ const ACTIONS = { "enroll-phone": enrollPhoneAction, "phone-invite": phoneInviteAction, set: setAction };
148
+
149
+ export async function identityCommand({ action = "show", config, kjVersion = "unknown", flags = {}, deps = {} }) {
150
+ const ctx = { projectDir: config?.projectDir || process.cwd(), log: deps.log || console.log, kjVersion, flags, deps };
151
+ const run = Object.hasOwn(ACTIONS, action) ? ACTIONS[action] : showAction;
152
+ return run(ctx);
153
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * KJC-TSK-1219 (ADR 0021, step 15d): kj init installs Karajan's kj-* commands into atril's
3
+ * store under `.karajan/`, on demand (ADR 0021's default answer), instead of copying them into
4
+ * `.claude/commands/`. With rulesync, `atril sync` takes the bridge and the router rule to every
5
+ * CLI; without it nothing persistent is installed, and kj says how to get there.
6
+ */
7
+ import { readdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { generate, install, sync } from "@karajan-family/atril";
10
+ import { ATRIL_ROOT } from "../skills/atril-skills.js";
11
+ import { detectRulesync } from "../utils/rulesync.js";
12
+ import { getTemplatesRoot } from "../utils/templates-root.js";
13
+
14
+ /**
15
+ * @param {{ projectDir: string, logger: { info: Function, warn: Function }, env?: object, offerRulesync?: boolean }} opts
16
+ * offerRulesync false: the user declined rulesync (--no-rulesync), so kj does not suggest it again
17
+ * @returns {{ installed: number, synced: boolean }}
18
+ */
19
+ export function installKarajanCommands({ projectDir, logger, env = process.env, offerRulesync = true }) {
20
+ const catalog = join(getTemplatesRoot(), "atril", "commands");
21
+ const atril = { root: ATRIL_ROOT, projectDir, env };
22
+ let installed = 0;
23
+ let unchanged = 0;
24
+ for (const file of readdirSync(catalog).filter((name) => name.endsWith(".md")).toSorted()) {
25
+ try {
26
+ // install() answers "installed" or "unchanged" (same content already there) and throws on a refusal.
27
+ const { status } = install(join(catalog, file), { ...atril, kind: "command" });
28
+ if (status === "installed") installed += 1;
29
+ else unchanged += 1;
30
+ } catch (err) {
31
+ logger.warn(`kj: ${file} was not installed in ${ATRIL_ROOT}: ${err.message}`);
32
+ }
33
+ }
34
+ logger.info(`kj: ${installed} Karajan command(s) installed in ${ATRIL_ROOT}/commands, on demand (${unchanged} already there)`);
35
+ if (!detectRulesync(projectDir)) {
36
+ if (offerRulesync) logger.info(`kj: to reach every CLI through /atril, run rulesync init and then atril sync --root ${ATRIL_ROOT}`);
37
+ return { installed, synced: false };
38
+ }
39
+ try {
40
+ for (const { action, path } of sync(atril).filter((step) => step.action !== "unchanged")) logger.info(`kj: atril ${action} .rulesync/${path}`);
41
+ if (!generate(atril)) logger.info("kj: run rulesync generate to reach your agents, or declare rulesync.generate: true in .karajan/atril.json");
42
+ } catch (err) {
43
+ logger.warn(`kj: atril sync failed (${err.message}); fix it and run atril sync --root ${ATRIL_ROOT}`);
44
+ return { installed, synced: false };
45
+ }
46
+ return { installed, synced: true };
47
+ }
@@ -7,7 +7,7 @@ import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
7
7
  import { ollamaUp, waitForOllamaReady, normalizeOllamaConfig } from "../rag/ollama-manager.js";
8
8
  import { checkOllamaCapability, pullOllamaModel } from "../rag/ollama-capability.js";
9
9
  import { exists, ensureDir } from "../utils/fs.js";
10
- import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
10
+ import { ensureContractBlockPresent, ensureGateTrackable } from "../review/gate-gitignore.js";
11
11
  import { getKarajanHome } from "../utils/paths.js";
12
12
  import { getTemplatesRoot } from "../utils/templates-root.js";
13
13
  import { detectAvailableAgents } from "../utils/agent-detect.js";
@@ -28,6 +28,7 @@ import { registerQmdCollections } from "../utils/qmd-collection.js";
28
28
  import { isGitRepo } from "../harden/harden-engine.js";
29
29
  import { isPhoneEnrolled } from "../harden/phone-sign.js";
30
30
  import { identityCommand } from "./identity.js";
31
+ import { installKarajanCommands } from "./init-atril.js";
31
32
  import { hardenCommand } from "./harden.js";
32
33
  import { contractChanges } from "../environment/contract-commit.js";
33
34
  import { detectProjectStack } from "../utils/stack-detect.js";
@@ -680,54 +681,22 @@ async function bootstrapOllama({ flags, config, logger, interactive }) {
680
681
  }
681
682
  }
682
683
 
683
- async function installSkills(logger, interactive) {
684
- const projectDir = process.cwd();
685
- const commandsDir = path.join(projectDir, ".claude", "commands");
686
- const skillsTemplateDir = path.join(getTemplatesRoot(), "skills");
687
-
684
+ // KJC-TSK-1219 (ADR 0021, step 15d): Karajan's commands go to atril's store, not .claude/commands.
685
+ async function installSkills(logger, interactive, offerRulesync) {
688
686
  let doInstall = true;
689
687
  if (interactive) {
690
688
  const wizard = createWizard();
691
689
  try {
692
- doInstall = await wizard.confirm("Install Karajan skills as slash commands (/kj-code, /kj-review, etc.)?", true);
690
+ doInstall = await wizard.confirm("Install Karajan's commands (kj-code, kj-review, kj-audit...) into atril's store, on demand?", true);
693
691
  } finally {
694
692
  wizard.close();
695
693
  }
696
694
  }
697
-
698
695
  if (!doInstall) {
699
- logger.info("Skills installation skipped.");
700
- return;
701
- }
702
-
703
- await ensureDir(commandsDir);
704
-
705
- let installed = 0;
706
- try {
707
- const files = await fs.readdir(skillsTemplateDir);
708
- for (const file of files) {
709
- if (!file.endsWith(".md")) continue;
710
- const src = path.join(skillsTemplateDir, file);
711
- const dest = path.join(commandsDir, file);
712
- if (await exists(dest)) {
713
- logger.info(` ${file} already exists — skipping`);
714
- continue;
715
- }
716
- const content = await fs.readFile(src, "utf8");
717
- await fs.writeFile(dest, content, "utf8");
718
- installed += 1;
719
- }
720
- } catch (err) {
721
- logger.warn(`Could not install skills: ${err.message}`);
696
+ logger.info("Karajan's commands skipped.");
722
697
  return;
723
698
  }
724
-
725
- if (installed > 0) {
726
- logger.info(`Installed ${installed} Karajan skill(s) in .claude/commands/`);
727
- logger.info("Available as slash commands: /kj-run, /kj-code, /kj-review, /kj-test, /kj-security, /kj-discover, /kj-architect, /kj-sonar, /kj-rag-query");
728
- } else {
729
- logger.info("All skills already installed.");
730
- }
699
+ installKarajanCommands({ projectDir: process.cwd(), logger, offerRulesync });
731
700
  }
732
701
 
733
702
  const GITIGNORE_ENTRIES = [
@@ -764,6 +733,8 @@ async function ensureGitignoreEntries(projectDir, logger) {
764
733
  // review-gate/hooks/adrs the whole team inherits (KJC-TSK-0646).
765
734
  const res = await ensureContractBlockPresent(projectDir);
766
735
  if (res.changed) logger.info("Added the .karajan contract block to .gitignore (verdicts stay local; gate/hooks/adrs tracked)");
736
+ // ADR 0032: an existing project gets the block completed with what the team now shares.
737
+ else if ((await ensureGateTrackable(projectDir)).changed) logger.info("Completed the .karajan contract block in .gitignore with the team's shared state (ADR 0032)");
767
738
  }
768
739
 
769
740
  /**
@@ -876,7 +847,7 @@ export async function initCommand({ logger, flags = {} }) {
876
847
  await ensureReviewRules(reviewRulesPath, logger);
877
848
  await ensureCoderRules(coderRulesPath, logger);
878
849
  await ensureGitignoreEntries(process.cwd(), logger);
879
- await installSkills(logger, interactive);
850
+ await installSkills(logger, interactive, flags.rulesync !== false);
880
851
  await bootstrapOllama({ flags, config, logger, interactive });
881
852
 
882
853
  // Auto-detect project stack and preconfigure
@@ -1046,7 +1017,7 @@ export async function initCommand({ logger, flags = {} }) {
1046
1017
  logger.info(hardened || skipHarden
1047
1018
  ? "✓ Karajan is set up in this project."
1048
1019
  : "⚠ Karajan is set up EXCEPT the quality harness — without it there are no gates.");
1049
- logger.info(' Next: kj run "describe what to build or fix"');
1020
+ logger.info(" Next: ask your agent for the work; it follows the method kj installed");
1050
1021
  logger.info(" More: kj --help · kj doctor (check your setup)");
1051
1022
  }
1052
1023
 
@@ -294,6 +294,74 @@ async function handleStandalone({ tool, standalone, manualUrl, dryRun, yes, logg
294
294
  return { tool, action: "failed", via: "binary", error: r.error };
295
295
  }
296
296
 
297
+ // Sonar: plan the container, then start it on a yes.
298
+ async function startSonar({ available, dryRun, yes, logger }) {
299
+ const result = await handleSonar({ available, dryRun });
300
+ if (result.action === "manual") logger.warn?.(`✗ sonar: ${result.reason}`);
301
+ else logger.info?.(`▸ sonar: ${result.command}`);
302
+ if (dryRun || result.action !== "ready") return result;
303
+ const proceed = yes || await promptYesNo(`Start SonarQube with: ${result.command}?`);
304
+ if (!proceed) return result;
305
+ const r = await runInstall(result.command);
306
+ result.action = r.ok ? "installed" : "failed";
307
+ if (!r.ok) result.error = r.error;
308
+ return result;
309
+ }
310
+
311
+ async function handleOptimizer({ tool, dryRun, yes, logger }) {
312
+ if (dryRun) {
313
+ logger.info?.(`▸ ${tool}: would install (init installer)`);
314
+ return { tool, action: "planned", command: `kj install-tools --only ${tool}` };
315
+ }
316
+ const proceed = yes || await promptYesNo(`Install ${tool}?`);
317
+ if (!proceed) return { tool, action: "declined" };
318
+ const install = await OPTIMIZER_INSTALLERS[tool]();
319
+ const r = await install(logger);
320
+ return r.ok ? { tool, action: "installed", version: r.version } : { tool, action: "failed", error: r.error };
321
+ }
322
+
323
+ // No package manager matched: try a standalone route (static binary for
324
+ // osv-scanner, a concrete command for semgrep) before giving up.
325
+ async function handleNoManager({ tool, hint, available, dryRun, yes, logger }) {
326
+ const standalone = resolveStandalone(tool, available);
327
+ if (standalone) return handleStandalone({ tool, standalone, manualUrl: hint.manualUrl, dryRun, yes, logger });
328
+ logger.warn?.(`✗ ${tool}: no automatic install route here. Run \`kj doctor\` for options, or install manually: ${hint.manualUrl}`);
329
+ return { tool, action: "manual", reason: "no compatible package manager found", manualUrl: hint.manualUrl, suggested: hint.command };
330
+ }
331
+
332
+ // semgrep / osv-scanner / lighthouse — package-manager driven.
333
+ async function handlePackageManaged(ctx) {
334
+ const { tool, available, dryRun, yes, logger } = ctx;
335
+ const hint = await getInstallHint(tool, available);
336
+ if (!hint.command || !hint.manager) return handleNoManager({ ...ctx, hint });
337
+ if (dryRun) {
338
+ logger.info?.(`▸ ${tool}: would run \`${hint.command}\` (manager: ${hint.manager})`);
339
+ return { tool, action: "dry-run", command: hint.command, manager: hint.manager };
340
+ }
341
+ const proceed = yes || await promptYesNo(`Install ${tool} with: ${hint.command}?`);
342
+ if (!proceed) {
343
+ logger.info?.(`⊘ ${tool}: declined`);
344
+ return { tool, action: "declined", command: hint.command };
345
+ }
346
+ logger.info?.(`▸ ${tool}: running \`${hint.command}\`...`);
347
+ const r = await runInstall(hint.command);
348
+ if (r.ok) {
349
+ logger.info?.(`✓ ${tool}: installed via ${hint.manager}`);
350
+ return { tool, action: "installed", command: hint.command, manager: hint.manager };
351
+ }
352
+ logger.warn?.(`✗ ${tool}: install failed (${r.error})`);
353
+ return { tool, action: "failed", command: hint.command, error: r.error };
354
+ }
355
+
356
+ // Tools with their own route; any other one goes through the package manager.
357
+ const TOOL_HANDLERS = {
358
+ sonar: startSonar,
359
+ git: handleGit,
360
+ "agent-cli": handleAgentCli,
361
+ docker: handleDocker,
362
+ ...Object.fromEntries(Object.keys(OPTIMIZER_INSTALLERS).map((tool) => [tool, handleOptimizer])),
363
+ };
364
+
297
365
  /**
298
366
  * Plan + (optionally execute) the install of every requested tool.
299
367
  *
@@ -330,91 +398,8 @@ export async function installToolsCommand(opts = {}) {
330
398
  continue;
331
399
  }
332
400
 
333
- if (tool === "sonar") {
334
- const result = await handleSonar({ available, dryRun });
335
- results.push(result);
336
- if (result.action === "manual") logger.warn?.(`✗ sonar: ${result.reason}`);
337
- else logger.info?.(`▸ sonar: ${result.command}`);
338
- if (!dryRun && result.action === "ready") {
339
- const proceed = yes || await promptYesNo(`Start SonarQube with: ${result.command}?`);
340
- if (proceed) {
341
- const r = await runInstall(result.command);
342
- result.action = r.ok ? "installed" : "failed";
343
- if (!r.ok) result.error = r.error;
344
- }
345
- }
346
- continue;
347
- }
348
-
349
- if (OPTIMIZER_INSTALLERS[tool]) {
350
- if (dryRun) {
351
- results.push({ tool, action: "planned", command: `kj install-tools --only ${tool}` });
352
- logger.info?.(`▸ ${tool}: would install (init installer)`);
353
- continue;
354
- }
355
- const proceed = yes || await promptYesNo(`Install ${tool}?`);
356
- if (!proceed) { results.push({ tool, action: "declined" }); continue; }
357
- const install = await OPTIMIZER_INSTALLERS[tool]();
358
- const r = await install(logger);
359
- results.push(r.ok
360
- ? { tool, action: "installed", version: r.version }
361
- : { tool, action: "failed", error: r.error });
362
- continue;
363
- }
364
-
365
- if (tool === "git") {
366
- const result = await handleGit({ available, dryRun, yes, logger });
367
- results.push(result);
368
- continue;
369
- }
370
-
371
- if (tool === "agent-cli") {
372
- const result = await handleAgentCli({ available, dryRun, yes, logger });
373
- results.push(result);
374
- continue;
375
- }
376
-
377
- if (tool === "docker") {
378
- const result = await handleDocker({ available, dryRun, yes, logger });
379
- results.push(result);
380
- continue;
381
- }
382
-
383
- // semgrep / osv-scanner / lighthouse — package-manager driven.
384
- const hint = await getInstallHint(tool, available);
385
- if (!hint.command || !hint.manager) {
386
- // No package manager matched — try a standalone route (static binary
387
- // for osv-scanner, a concrete command for semgrep) before giving up.
388
- const standalone = resolveStandalone(tool, available);
389
- if (standalone) {
390
- const result = await handleStandalone({ tool, standalone, manualUrl: hint.manualUrl, dryRun, yes, logger });
391
- results.push(result);
392
- continue;
393
- }
394
- results.push({ tool, action: "manual", reason: "no compatible package manager found", manualUrl: hint.manualUrl, suggested: hint.command });
395
- logger.warn?.(`✗ ${tool}: no automatic install route here. Run \`kj doctor\` for options, or install manually: ${hint.manualUrl}`);
396
- continue;
397
- }
398
- if (dryRun) {
399
- results.push({ tool, action: "dry-run", command: hint.command, manager: hint.manager });
400
- logger.info?.(`▸ ${tool}: would run \`${hint.command}\` (manager: ${hint.manager})`);
401
- continue;
402
- }
403
- const proceed = yes || await promptYesNo(`Install ${tool} with: ${hint.command}?`);
404
- if (!proceed) {
405
- results.push({ tool, action: "declined", command: hint.command });
406
- logger.info?.(`⊘ ${tool}: declined`);
407
- continue;
408
- }
409
- logger.info?.(`▸ ${tool}: running \`${hint.command}\`...`);
410
- const r = await runInstall(hint.command);
411
- if (r.ok) {
412
- results.push({ tool, action: "installed", command: hint.command, manager: hint.manager });
413
- logger.info?.(`✓ ${tool}: installed via ${hint.manager}`);
414
- } else {
415
- results.push({ tool, action: "failed", command: hint.command, error: r.error });
416
- logger.warn?.(`✗ ${tool}: install failed (${r.error})`);
417
- }
401
+ const install = Object.hasOwn(TOOL_HANDLERS, tool) ? TOOL_HANDLERS[tool] : handlePackageManaged;
402
+ results.push(await install({ tool, available, dryRun, yes, logger }));
418
403
  }
419
404
 
420
405
  // KJC-TSK-0659 stop-on-sudo: anything that still needs the user's hands
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `kj main-guard` (KJC-TSK-1369, ADR 0037 layer 2): audits a push to the protected branch
3
+ * against GitHub and exits 1 with a markdown report when a merge skipped its required checks
4
+ * or the protection no longer enforces them on admins. The sealed workflow runs it on every
5
+ * push to main and posts the report as the alarm.
6
+ *
7
+ * Pull requests and check results are read with the token gh has (the workflow's GITHUB_TOKEN).
8
+ * Reading the branch protection needs Administration, so it uses MAIN_GUARD_PROTECTION_TOKEN,
9
+ * a read-only secret (the maintainer's choice, 2026-11-01).
10
+ */
11
+ import { execFileSync } from "node:child_process";
12
+
13
+ import { auditMain } from "../guards/main-guard.js";
14
+
15
+ const NEW_BRANCH = /^0+$/;
16
+
17
+ const gitIn = (projectDir) => (args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8" });
18
+
19
+ /** The commits the push put on the branch, newest first: its first-parent line. */
20
+ export function pushedCommits({ before, after, git }) {
21
+ if (!before || NEW_BRANCH.test(before)) return [after];
22
+ return git(["rev-list", "--first-parent", `${before}..${after}`]).split("\n").filter(Boolean);
23
+ }
24
+
25
+ /** `gh api` as JSON; a token, when given, replaces gh's own for that call only. */
26
+ function ghJson(path, token) {
27
+ const env = token ? { ...process.env, GH_TOKEN: token } : process.env;
28
+ return JSON.parse(execFileSync("gh", ["api", path], { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] }));
29
+ }
30
+
31
+ /** GitHub as main-guard's api. */
32
+ export function githubApi({ repo, branch, protectionToken }) {
33
+ return {
34
+ // The commit's pull list leaves merged_by out; a merged one is read in full to name who merged it.
35
+ pullsOf: async (sha) => ghJson(`repos/${repo}/commits/${sha}/pulls`).map((p) => (p.merged_at ? ghJson(`repos/${repo}/pulls/${p.number}`) : p)),
36
+ async checksOf(sha) {
37
+ const runs = ghJson(`repos/${repo}/commits/${sha}/check-runs?per_page=100`).check_runs ?? [];
38
+ const statuses = ghJson(`repos/${repo}/commits/${sha}/status`).statuses ?? [];
39
+ return [...runs.map((r) => ({ name: r.name, conclusion: r.conclusion })), ...statuses.map((s) => ({ name: s.context, conclusion: s.state }))];
40
+ },
41
+ async protection() {
42
+ if (!protectionToken) throw new Error("main-guard: MAIN_GUARD_PROTECTION_TOKEN is not set; the protection cannot be read");
43
+ try {
44
+ return ghJson(`repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, protectionToken);
45
+ } catch (err) {
46
+ if (/Branch not protected/i.test(`${err.stdout ?? ""}${err.stderr ?? ""}`)) return null;
47
+ throw err;
48
+ }
49
+ },
50
+ };
51
+ }
52
+
53
+ /** The alarm as markdown. */
54
+ export function renderReport(result, { repo, branch }) {
55
+ if (result.ok) return `main-guard: nothing reached ${branch} without its checks, and the protection of ${repo} is as declared.`;
56
+ const lines = [`## main-guard: ${repo}@${branch} needs a look`, ""];
57
+ for (const m of result.merges) {
58
+ lines.push(m.kind === "direct-push"
59
+ ? `- \`${m.sha.slice(0, 7)}\`: direct push, ${m.detail}`
60
+ : `- \`${m.sha.slice(0, 7)}\`: PR #${m.pr} merged by @${m.mergedBy} without ${m.missing.join(", ")}`);
61
+ }
62
+ for (const change of result.protection) lines.push(`- protection: ${change}`);
63
+ return lines.join("\n");
64
+ }
65
+
66
+ /** @returns {Promise<number>} the exit code */
67
+ export async function runMainGuard({ repo, branch, before, after, require, git, audit = auditMain, api, print = console.log }) {
68
+ const commits = pushedCommits({ before, after, git });
69
+ const result = await audit({ branch, commits, requiredChecks: require, api });
70
+ print(renderReport(result, { repo, branch }));
71
+ return result.ok ? 0 : 1;
72
+ }
73
+
74
+ export async function mainGuardCommand({ flags, projectDir = process.cwd() }) {
75
+ const { repo, branch, before, after } = flags;
76
+ const api = githubApi({ repo, branch, protectionToken: process.env.MAIN_GUARD_PROTECTION_TOKEN });
77
+ process.exitCode = await runMainGuard({ repo, branch, before, after, require: flags.require, git: gitIn(projectDir), api });
78
+ }