karajan-code 4.70.4 → 5.0.0

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