karajan-code 4.70.4 → 5.0.1

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