karajan-code 4.70.4 → 5.0.1

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