karajan-code 4.70.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +34 -3
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -7,6 +7,7 @@
7
7
  * Uso:
8
8
  * node scripts/dual-publish.mjs --pack-only # genera+verifica el tarball scoped
9
9
  * node scripts/dual-publish.mjs --publish --otp=XXX # publica el scoped (tras publicar el legacy)
10
+ * node scripts/dual-publish.mjs --publish --provenance --no-verify # desde CI (ADR 0035), tras --pack-only en otro job
10
11
  *
11
12
  * El swap del name es EN SITIO con restauración en finally: un crash a mitad
12
13
  * no deja el manifest renombrado. La verificación es el MISMO verify-pack
@@ -43,6 +44,22 @@ export async function withScopedName(repoRoot, fn) {
43
44
  }
44
45
  }
45
46
 
47
+ /** KJC-TSK-1359 (ADR 0035): npm publish arguments; from CI, provenance and no OTP. */
48
+ export function publishArgs(args) {
49
+ const otp = args.find((a) => a.startsWith("--otp="))?.slice(6);
50
+ return ["publish", "--ignore-scripts", ...(args.includes("--provenance") ? ["--provenance"] : []), ...(otp ? [`--otp=${otp}`] : [])];
51
+ }
52
+
53
+ /**
54
+ * KJC-TSK-1359: the CI publish job holds the OIDC token and installs nothing, so it skips
55
+ * verify-pack (which installs the tarball and runs its scripts); the verify job ran --pack-only.
56
+ */
57
+ export function shouldVerify(args) {
58
+ if (!args.includes("--no-verify")) return true;
59
+ if (!args.includes("--publish")) throw new Error("dual-publish: --no-verify only goes with --publish; --pack-only always verifies");
60
+ return false;
61
+ }
62
+
46
63
  // argv[1] normalizado (catch de codex): node lo absolutiza hoy, pero el
47
64
  // guard no debe depender de ese detalle de implementación.
48
65
  const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
@@ -50,13 +67,12 @@ if (isMain) {
50
67
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
51
68
  const args = process.argv.slice(2);
52
69
  const publish = args.includes("--publish");
53
- const otp = args.find((a) => a.startsWith("--otp="))?.slice(6);
54
70
  await withScopedName(repoRoot, async () => {
55
71
  console.log(`dual-publish: manifest → ${SCOPED_NAME} (temporal)`);
56
72
  // La E2E real: empaqueta el scoped, instálalo aislado y corre kj.
57
- execFileSync("node", [join(repoRoot, "scripts", "verify-pack.mjs")], { cwd: repoRoot, stdio: "inherit" });
73
+ if (shouldVerify(args)) execFileSync("node", [join(repoRoot, "scripts", "verify-pack.mjs")], { cwd: repoRoot, stdio: "inherit" });
58
74
  if (publish) {
59
- const pubArgs = ["publish", "--ignore-scripts", ...(otp ? [`--otp=${otp}`] : [])];
75
+ const pubArgs = publishArgs(args);
60
76
  console.log(`dual-publish: npm ${pubArgs.join(" ").replace(/--otp=\S+/, "--otp=***")}`);
61
77
  execFileSync("npm", pubArgs, { cwd: repoRoot, stdio: "inherit" });
62
78
  } else {
@@ -7,6 +7,7 @@
7
7
  * - Idempotent: safe to run many times.
8
8
  * - Never fails hard (exits 0 even on error) so `npm install` is not blocked.
9
9
  */
10
+ import { existsSync } from "node:fs";
10
11
  import fs from "node:fs/promises";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
@@ -38,10 +39,22 @@ const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
38
39
  const isPackageDirHome = (home) => /node_modules[\\/]karajan-code[\\/]\.karajan[\\/]?$/.test(home)
39
40
  || path.resolve(home) === path.join(ROOT_DIR, ".karajan");
40
41
 
42
+ // KJC-BUG-0404: the .karajan of a git working tree is a project's own state (ADR 0032). Taken as
43
+ // the home, every project's home data (audits, stories, board) landed in that repo.
44
+ function isProjectKarajanDir(home) {
45
+ const dir = path.resolve(home);
46
+ if (path.basename(dir) !== ".karajan" || path.dirname(dir) === os.homedir()) return false;
47
+ if (!existsSync(path.join(path.dirname(dir), ".git"))) return false;
48
+ process.stderr.write(`karajan-mcp: ${dir} is a project's .karajan, not a Karajan home; ignoring it\n`);
49
+ return true;
50
+ }
51
+
52
+ const usableHome = (home) => Boolean(home) && !isPackageDirHome(home) && !isProjectKarajanDir(home);
53
+
41
54
  /** The home a previous registration carried, if the user (not this script) set it. */
42
55
  function homeFromExistingEntry(entry) {
43
56
  const previous = entry?.env?.KARAJAN_HOME || entry?.env?.KJ_HOME;
44
- return previous && !isPackageDirHome(previous) ? previous : null;
57
+ return usableHome(previous) ? previous : null;
45
58
  }
46
59
 
47
60
  /**
@@ -58,7 +71,7 @@ async function resolveKjHome(existingEntry) {
58
71
  const names = Object.keys(registry.instances || {});
59
72
  if (names.length > 0) {
60
73
  const first = registry.instances[names.includes("default") ? "default" : names[0]];
61
- if (first?.kjHome) return first.kjHome;
74
+ if (usableHome(first?.kjHome)) return first.kjHome;
62
75
  }
63
76
  } catch {
64
77
  // No registry yet — use default
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * MCP smoke for verify-pack (KJC-TSK-0844) — the karajan-mcp server of the
3
3
  * INSTALLED tarball is started over stdio with the SDK client and exercised
4
- * with kj_status, kj_config and kj_review, no LLM involved. The MCP surface
4
+ * with kj_config and kj_code, no LLM involved; kj_run, retired in 5.0
5
+ * (KJC-TSK-1318), must answer with its retirement notice. The MCP surface
5
6
  * was only proved by tests with every seam mocked and dogfooded by no
6
7
  * development session; KJC-BUG-0175 (taskFile promised, never read) and
7
8
  * KJC-BUG-0176 (kjHome declared, ignored) both shipped through that gap.
@@ -17,7 +18,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
18
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
18
19
 
19
20
  const FATAL = [/Missing required field/i, /Config file not found/i, /taskFile read failed/i, /Unknown tool/i];
20
- const TOOLS = ["kj_status", "kj_config", "kj_review"];
21
+ const TOOLS = ["kj_config", "kj_code"];
21
22
 
22
23
  /** The text of a tool answer, and whether it is a failure of the contract (not of the environment). */
23
24
  export function classifyAnswer(tool, text) {
@@ -58,15 +59,18 @@ export async function mcpSmoke({ serverPath, kjHome, projectDir, taskFile, env =
58
59
  const listed = new Set((await client.listTools()).tools.map((t) => t.name));
59
60
  for (const t of TOOLS) if (!listed.has(t)) findings.push(`${t} is not listed by the server`);
60
61
  if (findings.length > 0) return { ok: false, findings, answers };
61
- // A project that never ran has no run log: that answer is the contract, not a refusal.
62
- const status = await call("kj_status", { kjHome, projectDir, lines: 5 });
63
- if (status.gated && !/no active run log/i.test(status.reason)) findings.push(`kj_status refused: ${status.reason}`);
64
62
  const config = await call("kj_config", { kjHome, json: true });
65
63
  if (config.gated) findings.push(`kj_config refused: ${config.reason}`);
66
64
  // The task travels ONLY as a file: the handler must read it before any
67
- // gate. The reviewer is a name no machine has, so the gate stops the call
65
+ // gate. The coder is a name no machine has, so the gate stops the call
68
66
  // before a single LLM token, on a laptop with real agents as much as in CI.
69
- await call("kj_review", { kjHome, projectDir, taskFile, reviewer: "kj-verify-stub" });
67
+ const code = await call("kj_code", { kjHome, projectDir, taskFile, coder: "kj-verify-stub" });
68
+ // Its gate: the bootstrap on a runner with no agent CLI (CI), the unknown coder anywhere else.
69
+ // Both come after the taskFile is read; an answer that is not a refusal ran something.
70
+ if (!code.gated || !/Unsupported agent: kj-verify-stub|BOOTSTRAP FAILED/.test(code.reason)) findings.push(`kj_code did not stop at its gate with the stub coder: ${code.reason ?? "it answered ok"}`);
71
+ // KJC-TSK-1318: a retired tool says so and points at the migration guide.
72
+ const retired = await call("kj_run", { kjHome, projectDir, task: "x" });
73
+ if (!/^kj run was retired in 5\.0/.test(retired.reason ?? "")) findings.push(`kj_run did not answer with its retirement notice: ${retired.reason}`);
70
74
  } catch (err) {
71
75
  findings.push(`MCP session failed: ${err.message}`);
72
76
  } finally {
@@ -253,10 +253,10 @@ try {
253
253
  // (a symlinked virtual store), so it can break resolution of the bundled
254
254
  // karajan-core the way npm packaging breakage did before (KJC-BUG-0082/0086).
255
255
  // Verify a pnpm install resolves the JS tree and `kj --version` boots — every
256
- // release catches a pnpm packaging regression. (pnpm also blocks
257
- // better-sqlite3's native build by default, so DB-backed commands need
258
- // `pnpm approve-builds`; that's surfaced as a doctor warning, KJC-BUG-0092 —
259
- // not a gate failure, since it's inherent to pnpm.) Auto-skips without pnpm.
256
+ // release catches a pnpm packaging regression. (pnpm also blocks node-pty's
257
+ // native build by default, so the HU Board terminal needs `pnpm approve-builds`;
258
+ // that's surfaced as a doctor warning, KJC-BUG-0092 / KJC-TSK-1353 — not a gate
259
+ // failure, since it's inherent to pnpm.) Auto-skips without pnpm.
260
260
  // KJC-BUG-0135: `npm run` leaks npm_config_* into the child env, and pnpm
261
261
  // maps those variables onto its own config — which can re-anchor it to THIS
262
262
  // repo (7-aug incident: the repo's node_modules converted to pnpm layout,
@@ -1,7 +1,8 @@
1
1
  // KJC-TSK-0472 — Per-project audit history (sqlite at .karajan/audit-history.db).
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
- import Database from "better-sqlite3";
4
+ // KJC-TSK-0618 (SQL-D): karajan-core's adapter over node:sqlite, built into Node.
5
+ import { openDatabase } from "karajan-core/db";
5
6
 
6
7
  const SCHEMA_VERSION = 1;
7
8
 
@@ -12,7 +13,7 @@ export function getAuditHistoryDbPath(projectDir) {
12
13
  export function openAuditHistoryDb(projectDir) {
13
14
  const dbPath = getAuditHistoryDbPath(projectDir);
14
15
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
15
- const db = new Database(dbPath);
16
+ const db = openDatabase(dbPath);
16
17
  db.pragma("journal_mode = WAL");
17
18
  migrate(db);
18
19
  return db;
@@ -93,6 +93,68 @@ function resolveKnipBin() {
93
93
  return path.join(knipRoot, "bin", "knip.js");
94
94
  }
95
95
 
96
+ // The ephemeral knip config for the entrypoints kj can vouch for. A project with
97
+ // its OWN knip config already decided — it is respected. Declaring is
98
+ // best-effort: on any error the default scan still runs.
99
+ async function declareEntrypoints(projectDir, config) {
100
+ try {
101
+ const eps = await resolveProjectEntrypoints(projectDir, config);
102
+ if (eps && !(await hasOwnKnipConfig(projectDir))) {
103
+ const cfgPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "kj-knip-")), "knip.json");
104
+ await fs.writeFile(cfgPath, JSON.stringify(buildEphemeralKnipConfig(eps)));
105
+ return { configArgs: ["--config", cfgPath], declaredEntrypoints: eps };
106
+ }
107
+ } catch { /* declaring is best-effort; the default scan still runs */ }
108
+ return { configArgs: [], declaredEntrypoints: null };
109
+ }
110
+
111
+ async function runKnip(args, projectDir, logger) {
112
+ try {
113
+ const { stdout } = await execFileAsync(
114
+ process.execPath,
115
+ args,
116
+ { cwd: projectDir, timeout: SCAN_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 },
117
+ );
118
+ return { raw: stdout };
119
+ } catch (err) {
120
+ // knip exits non-zero when issues are found; with --no-exit-code that
121
+ // should not happen, but old configs might still trigger it. If the
122
+ // process produced JSON on stdout we still try to parse it.
123
+ const raw = err?.stdout || "";
124
+ if (raw) return { raw };
125
+ logger?.warn?.(`audit/dead-exports: knip failed (${err?.message || err})`);
126
+ return { failure: { available: false, reason: `knip run failed: ${err?.shortMessage || err?.message || err}` } };
127
+ }
128
+ }
129
+
130
+ // One knip file entry → its unused exports, then its unused types.
131
+ function deadExportIssuesOf(fi) {
132
+ const file = fi.file || fi.filePath || "";
133
+ if (!file) return [];
134
+ const exportIssues = (fi.exports || []).map((ex) => ({
135
+ tool: "knip",
136
+ rule: "unused-exports",
137
+ component: file,
138
+ path: file,
139
+ line: ex.line || 1,
140
+ col: ex.col,
141
+ name: ex.name,
142
+ severity: "MINOR",
143
+ message: `Unused export \`${ex.name}\` in ${file}`,
144
+ }));
145
+ const typeIssues = (fi.types || []).map((t) => ({
146
+ tool: "knip",
147
+ rule: "unused-types",
148
+ component: file,
149
+ path: file,
150
+ line: t.line || 1,
151
+ name: t.name,
152
+ severity: "MINOR",
153
+ message: `Unused type \`${t.name}\` in ${file}`,
154
+ }));
155
+ return [...exportIssues, ...typeIssues];
156
+ }
157
+
96
158
  /**
97
159
  * Collect dead-exports findings for a project.
98
160
  *
@@ -116,36 +178,12 @@ export async function collectDeadExports(projectDir, stack, config = {}, logger
116
178
  }
117
179
 
118
180
  const args = [knipBin, "--reporter", "json", "--no-progress", "--no-exit-code"];
119
- let declaredEntrypoints = null;
120
- try {
121
- const eps = await resolveProjectEntrypoints(projectDir, config);
122
- // a project with its OWN knip config already decided — it is respected
123
- if (eps && !(await hasOwnKnipConfig(projectDir))) {
124
- const cfgPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "kj-knip-")), "knip.json");
125
- await fs.writeFile(cfgPath, JSON.stringify(buildEphemeralKnipConfig(eps)));
126
- args.push("--config", cfgPath);
127
- declaredEntrypoints = eps;
128
- }
129
- } catch { /* declaring is best-effort; the default scan still runs */ }
181
+ const { configArgs, declaredEntrypoints } = await declareEntrypoints(projectDir, config);
182
+ args.push(...configArgs);
130
183
 
131
- let raw;
132
- try {
133
- const { stdout } = await execFileAsync(
134
- process.execPath,
135
- args,
136
- { cwd: projectDir, timeout: SCAN_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 },
137
- );
138
- raw = stdout;
139
- } catch (err) {
140
- // knip exits non-zero when issues are found; with --no-exit-code that
141
- // should not happen, but old configs might still trigger it. If the
142
- // process produced JSON on stdout we still try to parse it.
143
- raw = err?.stdout || "";
144
- if (!raw) {
145
- logger?.warn?.(`audit/dead-exports: knip failed (${err?.message || err})`);
146
- return { available: false, reason: `knip run failed: ${err?.shortMessage || err?.message || err}` };
147
- }
148
- }
184
+ const run = await runKnip(args, projectDir, logger);
185
+ if (run.failure) return run.failure;
186
+ const { raw } = run;
149
187
 
150
188
  let report;
151
189
  try {
@@ -158,36 +196,7 @@ export async function collectDeadExports(projectDir, stack, config = {}, logger
158
196
  const unusedFiles = Array.isArray(report.files) ? report.files : [];
159
197
  const fileIssues = Array.isArray(report.issues) ? report.issues : [];
160
198
 
161
- const deadExportIssues = [];
162
- for (const fi of fileIssues) {
163
- const file = fi.file || fi.filePath || "";
164
- if (!file) continue;
165
- for (const ex of fi.exports || []) {
166
- deadExportIssues.push({
167
- tool: "knip",
168
- rule: "unused-exports",
169
- component: file,
170
- path: file,
171
- line: ex.line || 1,
172
- col: ex.col,
173
- name: ex.name,
174
- severity: "MINOR",
175
- message: `Unused export \`${ex.name}\` in ${file}`,
176
- });
177
- }
178
- for (const t of fi.types || []) {
179
- deadExportIssues.push({
180
- tool: "knip",
181
- rule: "unused-types",
182
- component: file,
183
- path: file,
184
- line: t.line || 1,
185
- name: t.name,
186
- severity: "MINOR",
187
- message: `Unused type \`${t.name}\` in ${file}`,
188
- });
189
- }
190
- }
199
+ const deadExportIssues = fileIssues.flatMap(deadExportIssuesOf);
191
200
 
192
201
  const unusedFileIssues = unusedFiles.map((f) => ({
193
202
  tool: "knip",
@@ -40,6 +40,7 @@ export function formatDeterministicSummary(ctx) {
40
40
  if (ctx.osvFindings) lines.push(...formatOsvBlock(ctx.osvFindings));
41
41
  if (ctx.semgrepFindings) lines.push(...formatSemgrepBlock(ctx.semgrepFindings));
42
42
  if (ctx.infraFindings) lines.push(...formatInfraBlock(ctx.infraFindings));
43
+ if (ctx.npmSignatures) lines.push(...formatNpmSignaturesBlock(ctx.npmSignatures));
43
44
  if (ctx.circularDeps) lines.push(...formatCircularDepsBlock(ctx.circularDeps));
44
45
  if (ctx.deadExports) lines.push(...formatDeadExportsBlock(ctx.deadExports));
45
46
  if (ctx.injectionFindings) lines.push(...formatInjectionBlock(ctx.injectionFindings));
@@ -159,6 +160,22 @@ function formatSemgrepBlock(semgrepFindings) {
159
160
 
160
161
  // INF-C (KJC-TSK-0760): checkov misconfigs — same shape as the semgrep
161
162
  // block; a not-applicable repo (no infra markers) declares it in one line.
163
+ // KJC-TSK-1253: an invalid signature is a tampered package; a missing one, a package the registry cannot vouch for.
164
+ function formatNpmSignaturesBlock(sigs) {
165
+ if (!sigs.available) return ["### npm signatures", `- Status: not available — ${sigs.reason}`, ""];
166
+ const lines = ["### npm signatures"];
167
+ const list = (title, pkgs) => {
168
+ if (!pkgs.length) return;
169
+ lines.push(` - ${title} (${pkgs.length}):`);
170
+ for (const p of pkgs.slice(0, MAX_SAMPLE_SEMGREP_PER_SEVERITY * 3)) lines.push(` - ${p.name}@${p.version}`);
171
+ };
172
+ if (!sigs.invalid.length && !sigs.missing.length) lines.push("- Every installed dependency has a valid registry signature");
173
+ list("Invalid signature", sigs.invalid);
174
+ list("No signature", sigs.missing);
175
+ lines.push("");
176
+ return lines;
177
+ }
178
+
162
179
  function formatInfraBlock(infraFindings) {
163
180
  if (!infraFindings.available) {
164
181
  return ["### Infra misconfigurations (checkov)", `- Status: not available — ${infraFindings.reason || "checkov not installed"}`, ""];
@@ -350,17 +367,20 @@ function formatWebperfBlock(webperf) {
350
367
  */
351
368
  export function deterministicContextHasFindings(ctx) {
352
369
  if (!ctx) return false;
353
- if (ctx.basalCost?.deadExports?.length > 0) return true;
354
- if (ctx.basalCost?.unusedDependencies?.unused?.length > 0) return true;
355
- if (ctx.sonarFindings?.available && (ctx.sonarFindings.total ?? 0) > 0) return true;
356
- if (ctx.sonarFindings?.qualityGate?.status === "ERROR") return true;
357
- if (ctx.osvFindings?.available && (ctx.osvFindings.total ?? 0) > 0) return true;
358
- if (ctx.semgrepFindings?.available && (ctx.semgrepFindings.total ?? 0) > 0) return true;
359
- if (ctx.infraFindings?.available && (ctx.infraFindings.total ?? 0) > 0) return true;
360
- if (ctx.circularDeps?.available && (ctx.circularDeps.total ?? 0) > 0) return true;
361
- if (ctx.deadExports?.available && (ctx.deadExports.total ?? 0) > 0) return true;
362
- if (ctx.growthDelta && (Math.abs(ctx.growthDelta.lines || 0) > 100 || Math.abs(ctx.growthDelta.deps || 0) > 0)) return true;
363
- if (ctx.aiSlop?.available && (ctx.aiSlop.total ?? 0) > 0) return true;
364
- if (ctx.envKeys?.available && ((ctx.envKeys.divergent?.length ?? 0) + (ctx.envKeys.incomplete?.length ?? 0)) > 0) return true;
365
- return false;
370
+ return FINDING_SIGNALS.some((signal) => signal(ctx));
366
371
  }
372
+
373
+ // Sections that report a `total` once collected.
374
+ const COUNTED_SECTIONS = ["sonarFindings", "osvFindings", "semgrepFindings", "infraFindings", "circularDeps", "deadExports", "aiSlop"];
375
+ const hasCount = (section) => Boolean(section?.available) && (section.total ?? 0) > 0;
376
+ const grewNoticeably = (delta) => Boolean(delta) && (Math.abs(delta.lines || 0) > 100 || Math.abs(delta.deps || 0) > 0);
377
+ const envKeysDiverge = (envKeys) => Boolean(envKeys?.available) && ((envKeys.divergent?.length ?? 0) + (envKeys.incomplete?.length ?? 0)) > 0;
378
+
379
+ const FINDING_SIGNALS = [
380
+ (ctx) => ctx.basalCost?.deadExports?.length > 0,
381
+ (ctx) => ctx.basalCost?.unusedDependencies?.unused?.length > 0,
382
+ (ctx) => ctx.sonarFindings?.qualityGate?.status === "ERROR",
383
+ (ctx) => COUNTED_SECTIONS.some((key) => hasCount(ctx[key])),
384
+ (ctx) => grewNoticeably(ctx.growthDelta),
385
+ (ctx) => envKeysDiverge(ctx.envKeys),
386
+ ];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * KJC-TSK-1253: npm registry signatures and provenance of the installed dependencies, for
3
+ * `kj audit` and `kj audit --security`. `npm audit signatures` checks every installed package
4
+ * against the registry's signing keys (and its Sigstore provenance when published with it):
5
+ * an invalid signature means a tampered package, a missing one a package the registry cannot
6
+ * vouch for. Same best-effort shape as osv-findings: when it does not apply or cannot run, the
7
+ * section says why and the audit goes on.
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { runGoverned } from "../utils/tool-governor.js";
12
+
13
+ const TIMEOUT_MS = 60_000;
14
+ const pick = (list) => (Array.isArray(list) ? list.map((p) => ({ name: p?.name ?? "?", version: p?.version ?? "?" })) : []);
15
+
16
+ /** @returns {Promise<{available: boolean, reason?: string, invalid?: {name, version}[], missing?: {name, version}[]}>} */
17
+ export async function collectNpmSignatures(projectDir, logger = null) {
18
+ if (!existsSync(join(projectDir, "package-lock.json")) || !existsSync(join(projectDir, "node_modules"))) {
19
+ return { available: false, reason: "not an installed npm project (needs package-lock.json and node_modules)" };
20
+ }
21
+ // Same kill-switch as the other scanners: never from the unit suite without the opt-in.
22
+ if (process.env.VITEST && process.env.KJ_ALLOW_REAL_SCANS !== "1") {
23
+ return { available: false, reason: "real scans disabled under the test runner (set KJ_ALLOW_REAL_SCANS=1 to opt in)" };
24
+ }
25
+ let stdout;
26
+ try {
27
+ ({ stdout } = await runGoverned("npm", ["audit", "signatures", "--json"], { cwd: projectDir, timeout: TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }));
28
+ } catch (err) {
29
+ // npm exits non-zero when a signature is invalid or missing: its JSON is still the answer.
30
+ if (err.code === "ENOENT") return { available: false, reason: "npm not found in PATH" };
31
+ if (err.killed) return { available: false, reason: `npm audit signatures timed out after ${TIMEOUT_MS}ms` };
32
+ stdout = err.stdout;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(stdout);
36
+ return { available: true, invalid: pick(parsed.invalid), missing: pick(parsed.missing) };
37
+ } catch {
38
+ logger?.warn?.("npm audit signatures: output not parseable as JSON");
39
+ return { available: false, reason: "npm audit signatures output not parseable as JSON" };
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * KJC-TSK-1255: kj audit --sbom <file>. The CycloneDX SBOM of an npm project's production
3
+ * dependencies, as npm itself builds it from the lockfile (no node_modules needed). Written
4
+ * atomically: a failed run leaves no file, never a half one.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { rename, writeFile } from "node:fs/promises";
8
+ import { join, resolve } from "node:path";
9
+ import { runGoverned } from "../utils/tool-governor.js";
10
+
11
+ const TIMEOUT_MS = 60_000;
12
+ const firstLine = (text) => String(text ?? "").trim().split("\n")[0];
13
+
14
+ /** @returns {Promise<{file: string, components: number}>} */
15
+ export async function writeSbom({ projectDir, file, run = runGoverned }) {
16
+ if (!existsSync(join(projectDir, "package-lock.json"))) {
17
+ throw new Error("--sbom needs an npm project with package-lock.json; other ecosystems are not covered yet");
18
+ }
19
+ let stdout;
20
+ try {
21
+ ({ stdout } = await run("npm", ["sbom", "--sbom-format", "cyclonedx", "--omit", "dev", "--package-lock-only"], { cwd: projectDir, timeout: TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 }));
22
+ } catch (err) {
23
+ let why = firstLine(err.stderr) || err.message;
24
+ if (err.code === "ENOENT") why = "npm not found in PATH";
25
+ else if (err.killed) why = `timed out after ${TIMEOUT_MS}ms`;
26
+ throw new Error(`npm sbom failed: ${why}`, { cause: err });
27
+ }
28
+ let doc;
29
+ try { doc = JSON.parse(stdout); } catch { doc = null; }
30
+ if (doc?.bomFormat !== "CycloneDX") throw new Error("npm sbom output is not a CycloneDX document");
31
+ const target = resolve(projectDir, file);
32
+ await writeFile(`${target}.tmp`, stdout);
33
+ await rename(`${target}.tmp`, target);
34
+ return { file: target, components: Array.isArray(doc.components) ? doc.components.length : 0 };
35
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Agent token check (KJC-TSK-1268, ADR 0037).
3
+ *
4
+ * The agent works with whatever token `gh` uses. ADR 0037 asks for a fine-grained token without
5
+ * Administration or Deployments, so the agent can neither change branch protection nor approve
6
+ * a protected environment such as npm-publish. Two probes that change nothing tell:
7
+ * - reading the default branch's protection needs Administration;
8
+ * - reviewing pending deployments of a run that does not exist is refused (403) without
9
+ * Deployments write, and answers 404/422 with it.
10
+ */
11
+
12
+ import { execFileSync } from "node:child_process";
13
+
14
+ import { STRATEGY } from "./types.js";
15
+
16
+ const FIX = "Give the agent a fine-grained token with no Administration, Environments, Secrets, Variables or Deployments "
17
+ + "(github.com/settings/personal-access-tokens/new), load it with `gh auth login --with-token`, revoke the old one "
18
+ + "and do admin acts in the browser (ADR 0037)";
19
+
20
+ const OWNER_REPO = /^\/?([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
21
+
22
+ /** owner/repo of a GitHub remote, or null: exact host github.com, or an ssh alias github.com-<name>. */
23
+ export function parseGithubRepo(url) {
24
+ const text = String(url ?? "").trim();
25
+ const scp = /^[\w.-]+@github\.com(?:-[\w.-]+)?:(.+)$/.exec(text);
26
+ let path = scp?.[1];
27
+ if (!path) {
28
+ try {
29
+ const parsed = new URL(text);
30
+ if (!["https:", "ssh:"].includes(parsed.protocol) || !/^github\.com(?:-[\w.-]+)?$/.test(parsed.hostname)) return null;
31
+ if (parsed.protocol === "https:" && parsed.hostname !== "github.com") return null;
32
+ path = parsed.pathname;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ const match = OWNER_REPO.exec(path);
38
+ return match ? `${match[1]}/${match[2]}` : null;
39
+ }
40
+
41
+ function defaultOriginUrl(projectDir) {
42
+ try {
43
+ return execFileSync("git", ["remote", "get-url", "origin"], { cwd: projectDir, encoding: "utf8", timeout: 10_000 });
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /** `gh api` as data: the HTTP status gh reports and the JSON body, success or not. */
50
+ async function defaultGh(args) {
51
+ try {
52
+ return { status: 200, body: JSON.parse(execFileSync("gh", ["api", ...args], { encoding: "utf8", timeout: 20_000, stdio: "pipe" }) || "{}") };
53
+ } catch (err) {
54
+ const status = Number(/\(HTTP (\d{3})\)/.exec(String(err.stderr ?? ""))?.[1]);
55
+ if (!status) throw err;
56
+ let body = {};
57
+ try { body = JSON.parse(String(err.stdout ?? "") || "{}"); } catch { /* gh printed no JSON body */ }
58
+ return { status, body };
59
+ }
60
+ }
61
+
62
+ /** true/false for the answers each probe expects; anything else (401, 5xx...) cannot be read. */
63
+ function hasAdmin({ status, body }) {
64
+ if (status === 200) return true;
65
+ if (status === 404) return /not protected/i.test(body?.message ?? "");
66
+ if (status === 403) return false;
67
+ throw new Error(`branch protection probe answered HTTP ${status}`);
68
+ }
69
+
70
+ function canApprove({ status }) {
71
+ if (status === 404 || status === 422) return true;
72
+ if (status === 403) return false;
73
+ throw new Error(`deployment review probe answered HTTP ${status}`);
74
+ }
75
+
76
+ async function defaultBranchOf(gh, repo) {
77
+ const { status, body } = await gh([`repos/${repo}`]);
78
+ if (status !== 200 || !body?.default_branch) throw new Error(`reading ${repo} answered HTTP ${status}`);
79
+ return body.default_branch;
80
+ }
81
+
82
+ export function createAgentTokenCheck({ projectDir = process.cwd(), originUrl = () => defaultOriginUrl(projectDir), gh = defaultGh } = {}) {
83
+ return {
84
+ name: "agent-token",
85
+ label: "agent's GitHub token (no admin power)",
86
+ strategy: STRATEGY.MANUAL,
87
+ async detect() {
88
+ const repo = parseGithubRepo(originUrl());
89
+ if (!repo) return { ok: true, severity: "info", detail: "no GitHub origin: nothing to check" };
90
+ try {
91
+ const branch = await defaultBranchOf(gh, repo);
92
+ const powers = [];
93
+ if (hasAdmin(await gh([`repos/${repo}/branches/${encodeURIComponent(branch)}/protection`]))) {
94
+ powers.push("it holds the Administration permission (it reads branch protection; ADR 0037 allows it at no level, since write changes protection and settings)");
95
+ }
96
+ const probe = ["-X", "POST", `repos/${repo}/actions/runs/1/pending_deployments`, "-F", "environment_ids[]=0", "-f", "state=rejected", "-f", "comment=kj doctor probe"];
97
+ if (canApprove(await gh(probe))) powers.push("it can approve protected environments (Deployments write)");
98
+ if (powers.length === 0) return { ok: true, severity: "info", detail: `the token gh uses has no admin power over ${repo}` };
99
+ return { ok: false, severity: "warn", detail: `the token gh uses on ${repo}: ${powers.join("; ")}`, fix: FIX };
100
+ } catch (err) {
101
+ const reason = `${err?.code ?? ""} ${err?.stderr ?? ""} ${err?.message ?? err}`;
102
+ // No gh, or gh with no token: the agent holds no GitHub power to check.
103
+ if (/ENOENT|gh auth login|not logged in/i.test(reason)) return { ok: true, severity: "info", detail: "gh has no GitHub token here: nothing to check" };
104
+ return { ok: false, severity: "warn", detail: `could not check the token gh uses: ${String(err?.message || err).split("\n")[0]}`, fix: FIX };
105
+ }
106
+ },
107
+ };
108
+ }
109
+
110
+ export function getAgentTokenChecks({ projectDir } = {}) {
111
+ return [createAgentTokenCheck({ projectDir })];
112
+ }
@@ -11,11 +11,13 @@
11
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
14
+ import { claudeConfigFile, codexHome } from "../utils/agent-home.js";
14
15
 
15
- const sources = (projectDir, home) => [
16
+ // KJC-TSK-1344: claude and codex configs of the account CLAUDE_CONFIG_DIR / CODEX_HOME select.
17
+ const sources = (projectDir, home, env = process.env) => [
16
18
  { file: path.join(projectDir, ".mcp.json"), format: "json" },
17
- { file: path.join(home, ".claude.json"), format: "json" },
18
- { file: path.join(home, ".codex", "config.toml"), format: "toml" },
19
+ { file: claudeConfigFile({ home, env }), format: "json" },
20
+ { file: path.join(codexHome({ home, env }), "config.toml"), format: "toml" },
19
21
  { file: path.join(home, ".gemini", "settings.json"), format: "json" },
20
22
  ];
21
23
 
@@ -34,9 +36,9 @@ const tomlServerNames = (text) =>
34
36
  new Set([...text.matchAll(/^\s*\[mcp_servers\.["']?([^\]"']+)["']?\]/gm)].map((m) => m[1]));
35
37
 
36
38
  /** Sorted `name (config-file)` entries for every MCP reachable in this project. */
37
- export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir() } = {}) {
39
+ export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir(), env = process.env } = {}) {
38
40
  const surface = new Set();
39
- for (const { file, format } of sources(projectDir, home)) {
41
+ for (const { file, format } of sources(projectDir, home, env)) {
40
42
  if (!existsSync(file)) continue;
41
43
  let text;
42
44
  try { text = readFileSync(file, "utf8"); } catch { continue; }
@@ -50,11 +52,11 @@ export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir
50
52
  * Diff the current surface against the last-seen snapshot and persist the
51
53
  * new state. First run records the baseline silently.
52
54
  */
53
- export function checkAiSurface({ projectDir = process.cwd(), home = os.homedir(), statePath = path.join(os.homedir(), ".karajan", "ai-surface.json"), extraSurface = [] } = {}) {
55
+ export function checkAiSurface({ projectDir = process.cwd(), home = os.homedir(), env = process.env, statePath = path.join(os.homedir(), ".karajan", "ai-surface.json"), extraSurface = [] } = {}) {
54
56
  // KJC-TSK-0728: extraSurface carries entries the (async) caller collected
55
57
  // outside config files — e.g. observed agent CLIs as "grok (cli)" — so
56
58
  // they ride the same snapshot and the same "NEW since last check" drift.
57
- const surface = [...new Set([...collectAiSurface({ projectDir, home }), ...extraSurface])].sort();
59
+ const surface = [...new Set([...collectAiSurface({ projectDir, home, env }), ...extraSurface])].sort();
58
60
  let state = {};
59
61
  try { state = JSON.parse(readFileSync(statePath, "utf8")); } catch { /* first run or corrupt → baseline */ }
60
62
  const prev = Array.isArray(state[projectDir]) ? state[projectDir] : null;