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
@@ -85,7 +85,9 @@ export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
85
85
  if (scanner.scm_disabled || where.linkedWorktree) opts.push("-Dsonar.scm.disabled=true");
86
86
  // KJC-TSK-0838: verbose is what makes the scanner name every indexed file.
87
87
  if (scanner.verbose) opts.push("-Dsonar.verbose=true");
88
+ if (scanner.project_name) opts.push(`-Dsonar.projectName=${scanner.project_name}`);
88
89
  if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
90
+ if (scanner.tests) opts.push(`-Dsonar.tests=${scanner.tests}`);
89
91
  if (scanner.exclusions) opts.push(`-Dsonar.exclusions=${scanner.exclusions}`);
90
92
  if (scanner.test_inclusions) opts.push(`-Dsonar.test.inclusions=${scanner.test_inclusions}`);
91
93
  if (scanner.coverage_exclusions) opts.push(`-Dsonar.coverage.exclusions=${scanner.coverage_exclusions}`);
@@ -107,14 +109,15 @@ export function buildScannerOpts(projectKey, scanner = {}, where = {}) {
107
109
  return opts.join(" ");
108
110
  }
109
111
 
110
- function normalizeScannerConfig(scanner = {}) {
112
+ // The folders are checked in the scanned directory (baseDir), not in process.cwd().
113
+ function normalizeScannerConfig(scanner = {}, baseDir = process.cwd()) {
111
114
  const out = { ...scanner };
112
115
  if (typeof out.sources === "string" && out.sources.trim()) {
113
116
  const existing = out.sources
114
117
  .split(",")
115
118
  .map((s) => s.trim())
116
119
  .filter(Boolean)
117
- .filter((entry) => fs.existsSync(entry));
120
+ .filter((entry) => fs.existsSync(path.resolve(baseDir, entry)));
118
121
 
119
122
  if (existing.length > 0) {
120
123
  out.sources = existing.join(",");
@@ -238,7 +241,7 @@ export async function resolveSonarTokenWithFallback(config, apiHost) {
238
241
  export function respectRepoProperties(scanner = {}, props = {}) {
239
242
  if (!props.existed) return scanner;
240
243
  const kept = { ...scanner };
241
- for (const k of ["sources", "exclusions", "test_inclusions", "coverage_exclusions", "javascript_lcov_report_paths"]) delete kept[k];
244
+ for (const k of ["project_name", "sources", "tests", "exclusions", "test_inclusions", "coverage_exclusions", "javascript_lcov_report_paths"]) delete kept[k];
242
245
  return kept;
243
246
  }
244
247
 
@@ -271,100 +274,145 @@ export async function ensureSonarProjectProperties(cwd = process.cwd()) {
271
274
  const wanted = SOURCE_DIRS.map((d) => `${d}/`).join(", ");
272
275
  return { existed: false, declaredKey: null, written: false, nothingToScan: `no ${wanted} folder to analyse: write a sonar-project.properties declaring sonar.sources when the code arrives` };
273
276
  }
274
- const projectKey = (pkg.name || path.basename(cwd)).replaceAll(/[^a-zA-Z0-9_.-]/g, "_");
275
- const layout = [`sonar.sources=${sources.join(",")}`, ...(tests.length ? [`sonar.tests=${tests.join(",")}`] : [])];
276
- const props = [
277
- `sonar.projectKey=${projectKey}`,
278
- `sonar.projectName=${pkg.name || path.basename(cwd)}`,
279
- ...layout,
280
- `sonar.javascript.lcov.reportPaths=coverage/lcov.info`,
281
- `sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/coverage/**`,
282
- ].join("\n");
283
- await fsPromises.writeFile(propsPath, props + "\n", "utf8");
284
- // Said out loud: the file is new in the working tree and the team should see it.
285
- const note = `sonar: wrote sonar-project.properties (${layout.join(", ")}): review it and commit it`;
286
- return { existed: false, declaredKey: null, written: true, note };
277
+ // KJC-BUG-0445 (#2854): nothing is written in the tree (a lane kept the file
278
+ // untracked); the layout reaches the scanner as -D opts, under kj's config.
279
+ const layout = {
280
+ project_name: pkg.name || path.basename(cwd),
281
+ sources: sources.join(","),
282
+ ...(tests.length ? { tests: tests.join(",") } : {}),
283
+ exclusions: "**/node_modules/**,**/dist/**,**/build/**,**/coverage/**",
284
+ };
285
+ const declared = [`sonar.sources=${layout.sources}`, ...(layout.tests ? [`sonar.tests=${layout.tests}`] : [])];
286
+ const note = `sonar: no sonar-project.properties, kj scanned with ${declared.join(", ")}: commit one to own the layout`;
287
+ return { existed: false, declaredKey: null, written: false, layout, note };
287
288
  }
288
289
  }
289
290
 
290
- export async function runSonarScan(config, projectKey = null, { verbose = false, cwd = null } = {}) {
291
- const scanCwd = cwd || process.cwd();
292
- // KJC-TSK-0838 step 3 (monorepo): a nested package scans from its own
293
- // directory under the key its properties declare — so the proof of
294
- // coverage is about that package, not the root's `sonar.sources`.
295
- const repoProps = await ensureSonarProjectProperties(scanCwd);
296
- if (repoProps.nothingToScan) {
297
- return { ok: false, skipped: true, projectKey: null, stdout: "", stderr: `sonar: ${repoProps.nothingToScan}`, exitCode: 0 };
298
- }
299
- let effectiveProjectKey;
300
- if (cwd && !projectKey && repoProps.declaredKey) {
301
- effectiveProjectKey = repoProps.declaredKey;
302
- } else {
303
- try {
304
- effectiveProjectKey = await resolveSonarProjectKey(config, { projectKey });
305
- } catch (error) {
306
- return {
307
- ok: false,
308
- projectKey: null,
309
- stdout: "",
310
- stderr: error?.message || String(error),
311
- exitCode: 1
312
- };
313
- }
314
- }
315
- const sonarConfig = config?.sonarqube || {};
291
+ function scanSettings(sonarConfig) {
316
292
  const rawHost = sonarConfig.host || "http://localhost:9000";
317
- const isExternalSonar = sonarConfig.external === true;
318
- const scannerTimeout = Number(sonarConfig?.timeouts?.scanner_ms) > 0
319
- ? Number(sonarConfig.timeouts.scanner_ms)
320
- : 15 * 60 * 1000;
321
- const sonarNetwork = sonarConfig.network || "karajan_sonar_net";
322
- const apiHost = resolveSonarHost(rawHost);
323
293
  const isLocalHost = /localhost|127\.0\.0\.1/.test(rawHost);
324
- const host = isLocalHost ? rawHost.replaceAll(/localhost|127\.0\.0\.1/g, "host.docker.internal") : rawHost;
294
+ return {
295
+ rawHost,
296
+ isExternalSonar: sonarConfig.external === true,
297
+ scannerTimeout: Number(sonarConfig?.timeouts?.scanner_ms) > 0 ? Number(sonarConfig.timeouts.scanner_ms) : 15 * 60 * 1000,
298
+ sonarNetwork: sonarConfig.network || "karajan_sonar_net",
299
+ apiHost: resolveSonarHost(rawHost),
300
+ isLocalHost,
301
+ host: isLocalHost ? rawHost.replaceAll(/localhost|127\.0\.0\.1/g, "host.docker.internal") : rawHost,
302
+ };
303
+ }
304
+
305
+ const repoPropertiesNote = (repoProps, sonarConfig) => (repoProps.existed && sonarConfig.scanner?.sources
306
+ ? "sonar: the repo's sonar-project.properties wins over sonarqube.scanner.sources — kj passed only its ignore rules"
307
+ : null);
325
308
 
309
+ // Server up, token resolved (and exported for the scanner), coverage generated:
310
+ // the first step that fails is the scan's result.
311
+ async function prepareServer(config, rawHost, apiHost) {
326
312
  const start = await sonarUp(rawHost);
327
313
  if (start.exitCode !== 0) {
328
- return {
329
- ok: false,
330
- stdout: start.stdout || "",
331
- stderr: start.stderr || "Failed to start SonarQube service",
332
- exitCode: start.exitCode
333
- };
314
+ return { failure: { ok: false, stdout: start.stdout || "", stderr: start.stderr || "Failed to start SonarQube service", exitCode: start.exitCode } };
334
315
  }
335
- // KJC-BUG-0156: kj's -Dsonar.projectKey stays (scan and query must use the
336
- // SAME key), but the repo's properties own the LAYOUT from here on.
337
316
  const token = await resolveSonarTokenWithFallback(config, apiHost);
338
317
  if (!token) {
339
- return {
318
+ return { failure: {
340
319
  ok: false,
341
320
  stdout: "",
342
321
  stderr:
343
322
  "Unable to resolve Sonar token. Fix: (1) run 'kj init' to configure it, (2) set KJ_SONAR_TOKEN env var, or (3) add sonarqube.token to ~/.karajan/kj.config.yml.",
344
323
  exitCode: 1
345
- };
324
+ } };
346
325
  }
347
326
  process.env.KJ_SONAR_TOKEN = token;
348
327
  const coverage = await maybeRunCoverage(config);
349
328
  if (!coverage.ok) {
350
- return {
329
+ return { failure: {
351
330
  ok: false,
352
331
  stdout: coverage.stdout || "",
353
332
  stderr: coverage.stderr || "Failed to generate coverage report for SonarQube",
354
333
  exitCode: coverage.exitCode || 1
334
+ } };
335
+ }
336
+ return { token, coverage };
337
+ }
338
+
339
+ // A nested package (explicit cwd, no explicit key) scans under the key its properties declare.
340
+ function scanProjectKey(config, projectKey, cwd, repoProps) {
341
+ if (cwd && !projectKey && repoProps.declaredKey) return repoProps.declaredKey;
342
+ return resolveSonarProjectKey(config, { projectKey });
343
+ }
344
+
345
+ function scannerCommand(pick, { scanCwd, isLocalHost, isExternalSonar, sonarNetwork }) {
346
+ if (pick.type === "native") return { cmd: pick.bin, args: [] };
347
+ return {
348
+ cmd: "docker",
349
+ args: [
350
+ "run",
351
+ "--rm",
352
+ "-v",
353
+ `${scanCwd}:/usr/src`,
354
+ ...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
355
+ ...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
356
+ "-e", "SONAR_HOST_URL",
357
+ "-e", "SONAR_TOKEN",
358
+ "-e", "SONAR_SCANNER_OPTS",
359
+ "sonarsource/sonar-scanner-cli"
360
+ ]
361
+ };
362
+ }
363
+
364
+ // KJC-BUG-0263: the scan is done when the server has processed it; whoever reads
365
+ // the issues next would otherwise get the previous analysis. Said out loud when
366
+ // the log names no task to wait for.
367
+ async function serverAnalysis(result, apiConfig) {
368
+ const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
369
+ if (!taskId) {
370
+ return { ok: true, analysisNote: "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis" };
371
+ }
372
+ const done = await waitForAnalysis(apiConfig, taskId);
373
+ if (done.ok) return { ok: true, analysisNote: null };
374
+ return { ok: false, analysisNote: `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis` };
375
+ }
376
+
377
+ export async function runSonarScan(config, projectKey = null, { verbose = false, cwd = null } = {}) {
378
+ const scanCwd = cwd || process.cwd();
379
+ // KJC-TSK-0838 step 3 (monorepo): a nested package scans from its own
380
+ // directory under the key its properties declare — so the proof of
381
+ // coverage is about that package, not the root's `sonar.sources`.
382
+ const repoProps = await ensureSonarProjectProperties(scanCwd);
383
+ if (repoProps.nothingToScan) {
384
+ return { ok: false, skipped: true, projectKey: null, stdout: "", stderr: `sonar: ${repoProps.nothingToScan}`, exitCode: 0 };
385
+ }
386
+ let effectiveProjectKey;
387
+ try {
388
+ effectiveProjectKey = await scanProjectKey(config, projectKey, cwd, repoProps);
389
+ } catch (error) {
390
+ return {
391
+ ok: false,
392
+ projectKey: null,
393
+ stdout: "",
394
+ stderr: error?.message || String(error),
395
+ exitCode: 1
355
396
  };
356
397
  }
398
+ const sonarConfig = config?.sonarqube || {};
399
+ const { rawHost, isExternalSonar, scannerTimeout, sonarNetwork, apiHost, isLocalHost, host } = scanSettings(sonarConfig);
400
+
401
+ const prepared = await prepareServer(config, rawHost, apiHost);
402
+ if (prepared.failure) return prepared.failure;
403
+ const { token, coverage } = prepared;
404
+ // KJC-BUG-0156: kj's -Dsonar.projectKey stays (scan and query must use the
405
+ // SAME key), but the repo's properties own the LAYOUT from here on.
357
406
  const scannerConfig = respectRepoProperties(
358
407
  normalizeScannerConfig({
408
+ ...repoProps.layout,
359
409
  ...sonarConfig.scanner,
360
410
  ...coverage.scannerPatch,
361
411
  ...(verbose ? { verbose: true } : {})
362
- }),
412
+ }, scanCwd),
363
413
  repoProps
364
414
  );
365
- const note = repoProps.existed && sonarConfig.scanner?.sources
366
- ? "sonar: the repo's sonar-project.properties wins over sonarqube.scanner.sources — kj passed only its ignore rules"
367
- : null;
415
+ const note = repoPropertiesNote(repoProps, sonarConfig);
368
416
 
369
417
  const pick = await pickSonarScanner(sonarConfig.scanner);
370
418
  // KJC-BUG-0225: en un carril, el plugin SCM del scanner (JGit) no sabe abrir
@@ -379,44 +427,11 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
379
427
  SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig, { linkedWorktree })
380
428
  };
381
429
 
382
- let cmd, args;
383
- if (pick.type === "native") {
384
- cmd = pick.bin;
385
- args = [];
386
- } else {
387
- cmd = "docker";
388
- args = [
389
- "run",
390
- "--rm",
391
- "-v",
392
- `${scanCwd}:/usr/src`,
393
- ...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
394
- ...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
395
- "-e", "SONAR_HOST_URL",
396
- "-e", "SONAR_TOKEN",
397
- "-e", "SONAR_SCANNER_OPTS",
398
- "sonarsource/sonar-scanner-cli"
399
- ];
400
- }
401
-
430
+ const { cmd, args } = scannerCommand(pick, { scanCwd, isLocalHost, isExternalSonar, sonarNetwork });
402
431
  const result = await runCommand(cmd, args, { timeout: scannerTimeout, env, cwd: scanCwd });
403
- // KJC-BUG-0263: the scan is done when the server has processed it; whoever reads
404
- // the issues next would otherwise get the previous analysis. Said out loud when
405
- // the log names no task to wait for.
406
- let ok = result.exitCode === 0;
407
- let analysisNote = null;
408
- if (ok) {
409
- const taskId = /api\/ce\/task\?id=([\w-]+)/.exec(String(result.stdout || ""))?.[1];
410
- if (taskId) {
411
- const done = await waitForAnalysis({ ...config, sonarqube: { ...sonarConfig, host: apiHost } }, taskId);
412
- if (!done.ok) {
413
- ok = false;
414
- analysisNote = `sonar: the server did not finish analysis ${taskId} (${done.status}${done.timedOut ? ", timed out" : ""}) — its issues would be the previous analysis`;
415
- }
416
- } else {
417
- analysisNote = "sonar: the scanner log names no analysis task, so kj could not wait for the server — issues may belong to the previous analysis";
418
- }
419
- }
432
+ const { ok, analysisNote } = result.exitCode === 0
433
+ ? await serverAnalysis(result, { ...config, sonarqube: { ...sonarConfig, host: apiHost } })
434
+ : { ok: false, analysisNote: null };
420
435
  return {
421
436
  ok,
422
437
  projectKey: effectiveProjectKey,
@@ -154,11 +154,12 @@ export function evaluateCoverageConfig({ projectDir }) {
154
154
  }
155
155
  return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: "no coverage threshold configured anywhere — nothing measures the level", remedy: "instrument: configure a coverage threshold (any value the team stands behind — no 80% is demanded)" };
156
156
  }
157
- /** AC3 — phantom coverage: the two detectors exist (KJC-TSK-0800) but the
158
- * sweep does not yet discover test↔source pairs, so their output is INJECTED.
159
- * No output → not observable: never ok by absence of the detector's run. */
160
- export function evaluatePhantomCoverage({ phantoms } = {}) {
157
+ /** AC3 — phantom coverage: the sweep (KJC-TSK-1343) runs the two detectors
158
+ * (KJC-TSK-0800) over the test↔source pairs and hands their output in.
159
+ * No output → not observable, with the sweep's reason: never ok by absence. */
160
+ export function evaluatePhantomCoverage({ phantoms, reason } = {}) {
161
161
  if (!Array.isArray(phantoms)) {
162
+ if (reason) return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: reason, remedy: "the detectors read Lit components in a git repository; until the project has them this invariant cannot be observed" };
162
163
  return { verdict: VERDICTS.NOT_OBSERVABLE, evidence: "the phantom detectors were not run", remedy: "instrument: run the detectors (steward/phantom-coverage) over the suite's test↔source pairs and feed their output" };
163
164
  }
164
165
  if (phantoms.length > 0) {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Phantom sweep (KJC-TSK-1343) — finds the test↔source pairs the phantom
3
+ * detectors (KJC-TSK-0800) need and runs them. Only Lit components sit inside
4
+ * the reachability perimeter, so only they can make a test a phantom; anywhere
5
+ * else the answer is NOT OBSERVABLE with its reason, never a clean bill.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { analyzeMemberReachability } from "../audit/member-reachability.js";
10
+ import { detectPhantomUnit, detectPhantomE2E } from "./phantom-coverage.js";
11
+ import { listProjectFiles } from "../rag/indexable.js";
12
+
13
+ const CODE = /\.(m?js|jsx|ts|tsx|mts)$/;
14
+ const TEST = /\.(test|spec)\.(m?js|jsx|ts|tsx|mts)$/;
15
+ const RELATIVE_IMPORT = /(?:from\s*|import\s*\(\s*)["'](\.{1,2}\/[^"']+)["']/g;
16
+ const SUFFIXES = ["", ".js", ".mjs", ".ts", ".tsx", ".jsx", "/index.js", "/index.ts"];
17
+
18
+ /** The candidate sources a test imports through relative specifiers. */
19
+ function importedCandidates(test, text, candidates) {
20
+ const out = new Set();
21
+ for (const [, spec] of text.matchAll(RELATIVE_IMPORT)) {
22
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(test), spec));
23
+ const hit = SUFFIXES.map((s) => base + s).find((p) => candidates.has(p));
24
+ if (hit) out.add(hit);
25
+ }
26
+ return out;
27
+ }
28
+
29
+ const defaultRead = (projectDir) => (rel) => fs.readFileSync(path.join(projectDir, rel), "utf8");
30
+
31
+ /**
32
+ * @param {{projectDir: string, files?: string[]|null, read?: (rel: string) => string}} opts
33
+ * @returns {{observable: boolean, reason?: string, phantoms?: Array<object>}}
34
+ */
35
+ export function sweepPhantoms({ projectDir, files = listProjectFiles(projectDir), read = defaultRead(projectDir) }) {
36
+ if (!Array.isArray(files)) return { observable: false, reason: "not a git repository: the project's tests and sources cannot be listed" };
37
+ const texts = new Map();
38
+ for (const f of files.filter((x) => CODE.test(x))) {
39
+ try { texts.set(f, read(f)); } catch { /* listed but gone from the tree: nothing to read */ }
40
+ }
41
+ const tests = [...texts.keys()].filter((f) => TEST.test(f));
42
+ const sources = [...texts.keys()].filter((f) => !TEST.test(f));
43
+ const analyzed = sources
44
+ .filter((f) => texts.get(f).includes("LitElement"))
45
+ .map((file) => ({ file, analysis: analyzeMemberReachability(texts.get(file), { file }) }))
46
+ .filter(({ analysis }) => analysis.observable && analysis.classes.some((c) => c.observable));
47
+ if (analyzed.length === 0) {
48
+ return { observable: false, reason: "no observable Lit component (LitElement): the reachability analysis only vouches for Lit" };
49
+ }
50
+ const candidates = new Map(analyzed
51
+ .filter(({ analysis }) => analysis.classes.some((c) => c.observable && c.unreachable.length > 0))
52
+ .map((c) => [c.file, c.analysis]));
53
+ const phantoms = [];
54
+ for (const test of tests) {
55
+ const testSource = texts.get(test);
56
+ for (const file of importedCandidates(test, testSource, candidates)) {
57
+ phantoms.push(...detectPhantomUnit({ sourceAnalysis: candidates.get(file), testSource, file: test }).phantoms);
58
+ }
59
+ for (const [file, analysis] of candidates) {
60
+ const found = detectPhantomE2E({ sourceText: texts.get(file), sourceAnalysis: analysis, testSource, file: test }).phantoms;
61
+ // a literal another source also carries may be alive there: never accused
62
+ phantoms.push(...found.filter((p) => !sources.some((s) => s !== file && texts.get(s).includes(p.literal))));
63
+ }
64
+ }
65
+ return { observable: true, phantoms };
66
+ }
@@ -58,33 +58,15 @@ function finalistBlock(board, torDir, coder) {
58
58
  ].filter(Boolean).join("\n");
59
59
  }
60
60
 
61
- /**
62
- * Judge a scored tournament: rank finalists, escalate on tie/disagreement.
63
- * @returns {Promise<{winner: string, judge: string|null, escalated: boolean, walkover?: boolean}>}
64
- */
65
- export async function judgeTournament({ tournamentDir, config = {}, logger = console, deps = {} }) {
66
- const { judgeFn = defaultJudge, agentFn = defaultAgent, solomonFn = null } = deps;
67
- const board = JSON.parse(fs.readFileSync(path.join(tournamentDir, "scoreboard.json"), "utf8"));
68
- const finalists = board.rows.filter((r) => r.finalist).map((r) => r.coder);
69
- const participants = board.rows.map((r) => r.coder);
70
-
71
- if (finalists.length === 0) throw new Error("tournament: sin finalistas (todas las suites rojas) — no hay nada que juzgar");
72
- if (finalists.length === 1) {
73
- const judgement = { winner: finalists[0], judge: null, escalated: false, walkover: true, board_ranking: board.ranking };
74
- fs.writeFileSync(path.join(tournamentDir, "judgement.json"), JSON.stringify(judgement, null, 2));
75
- logger?.info?.(`torneo ${board.id}: ganador por walkover — ${finalists[0]} (único finalista)`);
76
- return judgement;
77
- }
78
-
79
- const judge = await judgeFn({ config, participants });
80
- if (!judge) {
81
- throw new Error(
82
- "tournament: sin juez limpio — todos los agentes disponibles compitieron o son el host. " +
83
- "Un participante jamás juzga su propio torneo: quita un coder o añade un agente."
84
- );
85
- }
61
+ function walkover({ board, tournamentDir, logger, winner }) {
62
+ const judgement = { winner, judge: null, escalated: false, walkover: true, board_ranking: board.ranking };
63
+ fs.writeFileSync(path.join(tournamentDir, "judgement.json"), JSON.stringify(judgement, null, 2));
64
+ logger?.info?.(`torneo ${board.id}: ganador por walkover — ${winner} (único finalista)`);
65
+ return judgement;
66
+ }
86
67
 
87
- const prompt = [
68
+ function judgePrompt(board, finalists, tournamentDir) {
69
+ return [
88
70
  "Eres el JUEZ de un torneo de código: varios coders resolvieron la MISMA tarea de forma independiente.",
89
71
  "La fase determinista ya eliminó a los que suspendieron la suite. Tu trabajo: ordenar a los finalistas",
90
72
  "por calidad de solución (diseño, legibilidad, ausencia de sobre-ingeniería), con el scoreboard a la vista.",
@@ -95,49 +77,74 @@ export async function judgeTournament({ tournamentDir, config = {}, logger = con
95
77
  'Responde SOLO con JSON: {"ranking": ["coder", ...], "reasons": {"coder": "por qué"}, "tie": false}',
96
78
  "Declara tie:true únicamente si de verdad no puedes distinguirlos.",
97
79
  ].join("\n\n");
80
+ }
98
81
 
99
- logger?.info?.(`torneo ${board.id}: juez=${judge} sobre ${finalists.length} finalistas`);
100
- const res = await agentFn({ judge, prompt, config, logger });
101
- if (!res?.ok) throw new Error(`tournament: el juez ${judge} falló: ${res?.error || "sin salida"}`);
82
+ function parseVerdict(output, judge, finalists) {
102
83
  let verdict;
103
84
  try {
104
- verdict = JSON.parse(String(res.output).replace(/^[^{]*/, "").replace(/[^}]*$/, ""));
85
+ verdict = JSON.parse(String(output).replace(/^[^{]*/, "").replace(/[^}]*$/, ""));
105
86
  } catch {
106
87
  throw new Error(`tournament: el juez ${judge} no devolvió un veredicto parseable`);
107
88
  }
108
89
  // An LLM verdict is untrusted input: the ranking must name FINALISTS and
109
90
  // nobody else — a hallucinated coder must fail loud, never get crowned.
110
- const isFinalist = (c) => finalists.includes(c);
111
- if (!Array.isArray(verdict.ranking) || verdict.ranking.length === 0 || !verdict.ranking.every(isFinalist)) {
91
+ if (!Array.isArray(verdict.ranking) || verdict.ranking.length === 0 || !verdict.ranking.every((c) => finalists.includes(c))) {
112
92
  throw new Error(`tournament: ranking del juez inválido (${JSON.stringify(verdict.ranking)}) — debe nombrar solo finalistas: ${finalists.join(", ")}`);
113
93
  }
94
+ return verdict;
95
+ }
114
96
 
115
- // Only FIRST place crowns a winner: a judge that reorders lower positions
116
- // but agrees on who wins is agreement, not a dispute for solomon.
97
+ // Only FIRST place crowns a winner: a judge that reorders lower positions
98
+ // but agrees on who wins is agreement, not a dispute for solomon.
99
+ async function crownedBy({ verdict, board, finalists, solomonFn }) {
117
100
  const judgeTop = verdict.ranking?.[0];
118
101
  const boardTop = board.ranking[0];
119
- const disagreement = judgeTop !== boardTop;
120
- const escalated = Boolean(verdict.tie || disagreement);
102
+ const escalated = Boolean(verdict.tie || judgeTop !== boardTop);
103
+ if (!escalated) return { winner: judgeTop, solomon: null, escalated };
104
+ if (!solomonFn) {
105
+ const why = verdict.tie ? "empate declarado" : `discrepancia juez(${judgeTop}) vs scoreboard(${boardTop})`;
106
+ throw new Error(`tournament: ${why} y sin árbitro configurado`);
107
+ }
108
+ const solomon = await solomonFn({
109
+ judgeRanking: verdict.ranking,
110
+ boardRanking: board.ranking,
111
+ reasons: verdict.reasons,
112
+ tie: Boolean(verdict.tie),
113
+ });
114
+ if (!finalists.includes(solomon?.winner)) {
115
+ throw new Error(`tournament: solomon devolvió un ganador inválido (${solomon?.winner}) — debe ser finalista`);
116
+ }
117
+ return { winner: solomon.winner, solomon, escalated };
118
+ }
121
119
 
122
- let winner = judgeTop;
123
- let solomon = null;
124
- if (escalated) {
125
- if (!solomonFn) {
126
- const why = verdict.tie ? "empate declarado" : `discrepancia juez(${judgeTop}) vs scoreboard(${boardTop})`;
127
- throw new Error(`tournament: ${why} y sin árbitro configurado`);
128
- }
129
- solomon = await solomonFn({
130
- judgeRanking: verdict.ranking,
131
- boardRanking: board.ranking,
132
- reasons: verdict.reasons,
133
- tie: Boolean(verdict.tie),
134
- });
135
- if (!isFinalist(solomon?.winner)) {
136
- throw new Error(`tournament: solomon devolvió un ganador inválido (${solomon?.winner}) — debe ser finalista`);
137
- }
138
- winner = solomon.winner;
120
+ /**
121
+ * Judge a scored tournament: rank finalists, escalate on tie/disagreement.
122
+ * @returns {Promise<{winner: string, judge: string|null, escalated: boolean, walkover?: boolean}>}
123
+ */
124
+ export async function judgeTournament({ tournamentDir, config = {}, logger = console, deps = {} }) {
125
+ const { judgeFn = defaultJudge, agentFn = defaultAgent, solomonFn = null } = deps;
126
+ const board = JSON.parse(fs.readFileSync(path.join(tournamentDir, "scoreboard.json"), "utf8"));
127
+ const finalists = board.rows.filter((r) => r.finalist).map((r) => r.coder);
128
+ const participants = board.rows.map((r) => r.coder);
129
+
130
+ if (finalists.length === 0) throw new Error("tournament: sin finalistas (todas las suites rojas) — no hay nada que juzgar");
131
+ if (finalists.length === 1) return walkover({ board, tournamentDir, logger, winner: finalists[0] });
132
+
133
+ const judge = await judgeFn({ config, participants });
134
+ if (!judge) {
135
+ throw new Error(
136
+ "tournament: sin juez limpio — todos los agentes disponibles compitieron o son el host. " +
137
+ "Un participante jamás juzga su propio torneo: quita un coder o añade un agente."
138
+ );
139
139
  }
140
140
 
141
+ const prompt = judgePrompt(board, finalists, tournamentDir);
142
+ logger?.info?.(`torneo ${board.id}: juez=${judge} sobre ${finalists.length} finalistas`);
143
+ const res = await agentFn({ judge, prompt, config, logger });
144
+ if (!res?.ok) throw new Error(`tournament: el juez ${judge} falló: ${res?.error || "sin salida"}`);
145
+ const verdict = parseVerdict(res.output, judge, finalists);
146
+ const { winner, solomon, escalated } = await crownedBy({ verdict, board, finalists, solomonFn });
147
+
141
148
  const judgement = {
142
149
  winner, judge, escalated,
143
150
  judge_ranking: verdict.ranking, judge_reasons: verdict.reasons || {},
@@ -0,0 +1,33 @@
1
+ /**
2
+ * KJC-TSK-1344 — where each CLI keeps its account. One person may hold two
3
+ * subscriptions per CLI (personal and work): claude switches account with
4
+ * CLAUDE_CONFIG_DIR and codex with CODEX_HOME. kj passes both to the agents
5
+ * (role-env allows CLAUDE* and CODEX*), so it must also LOOK there for the
6
+ * session and the config, never at a fixed ~/.codex or ~/.claude.json.
7
+ */
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+
11
+ const ACCOUNT_VAR = { codex: "CODEX_HOME", claude: "CLAUDE_CONFIG_DIR" };
12
+
13
+ export const codexHome = ({ home = os.homedir(), env = process.env } = {}) =>
14
+ env.CODEX_HOME || path.join(home, ".codex");
15
+
16
+ export const claudeConfigFile = ({ home = os.homedir(), env = process.env } = {}) =>
17
+ path.join(env.CLAUDE_CONFIG_DIR || home, ".claude.json");
18
+
19
+ /** Absolute files that mark a logged-in session, or null for agents without an account variable. */
20
+ export function agentAuthFiles(agent, { home = os.homedir(), env = process.env } = {}) {
21
+ if (agent === "codex") return [path.join(codexHome({ home, env }), "auth.json")];
22
+ if (agent === "claude") {
23
+ const dir = env.CLAUDE_CONFIG_DIR;
24
+ return dir ? [path.join(dir, ".claude.json"), path.join(dir, ".credentials.json")] : [path.join(home, ".claude.json"), path.join(home, ".claude")];
25
+ }
26
+ return null;
27
+ }
28
+
29
+ /** "CODEX_HOME=/path" when the variable moves the account, null on the default. */
30
+ export function accountLabel(agent, { env = process.env } = {}) {
31
+ const name = ACCOUNT_VAR[agent];
32
+ return name && env[name] ? `${name}=${env[name]}` : null;
33
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * KJC-BUG-0458 (ADR 0040): with several gh accounts and parallel sessions, another session can
3
+ * switch the active account between `gh auth switch` and the gh calls kj makes, so kj posted a
4
+ * review under the wrong account. At startup kj pins GH_TOKEN to the repo's account, so every
5
+ * gh it runs inherits that token whatever the active account is by then. The account is the
6
+ * configured one, or else the owner of the origin remote. A token already set is never touched.
7
+ */
8
+ import { execFileSync } from "node:child_process";
9
+
10
+ // The owner segment of github.com remotes: https, ssh and ssh host aliases
11
+ // (github.com-work:owner/repo.git). Any other host is not gh's to answer for.
12
+ const OWNER = /(?:^|[@/])github\.com(?:-[\w.-]+)?[:/]([^/:\s]+)\/[^/\s]+?(?:\.git)?\/?$/;
13
+
14
+ /** The owner of a github.com remote URL, or null for any other host. */
15
+ export function remoteOwner(url) {
16
+ return OWNER.exec(String(url ?? "").trim())?.[1] ?? null;
17
+ }
18
+
19
+ const quiet = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
20
+
21
+ /**
22
+ * Pin GH_TOKEN for every gh child of this process.
23
+ * @returns {{account: string|null, reason: string|null}} the pinned account, or why none was
24
+ */
25
+ export function pinGhAccount({ env = process.env, cwd = process.cwd(), account = null, exec = quiet } = {}) {
26
+ if (env.GH_TOKEN || env.GITHUB_TOKEN) return { account: null, reason: null };
27
+ let login = account;
28
+ if (!login) {
29
+ try { login = remoteOwner(exec("git", ["remote", "get-url", "origin"], cwd)); } catch { login = null; }
30
+ }
31
+ if (!login) return { account: null, reason: null };
32
+ let token;
33
+ try { token = exec("gh", ["auth", "token", "--user", login], cwd); } catch { token = ""; }
34
+ if (!token) return { account: null, reason: `gh has no logged-in account ${login}: gh calls use the active account` };
35
+ env.GH_TOKEN = token;
36
+ return { account: login, reason: null };
37
+ }
package/src/utils/git.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { runCommand } from "./process.js";
2
2
  import { getRunContext } from "./run-context.js";
3
3
  import { identityEnv } from "../git/run-identity.js";
4
- import { labelKarajanPr } from "../git/karajan-label.js";
5
4
 
6
5
  /**
7
6
  * Module-scoped runner — legacy back-compat for callers outside a
@@ -313,15 +312,3 @@ export async function hasRemote(cwd = null) {
313
312
  export async function pushBranch(branch, cwd = null) {
314
313
  await runGit(["push", "origin", branch], cwd ? { cwd } : {});
315
314
  }
316
-
317
- export async function createPullRequest({ baseBranch, branch, title, body, cwd = null }) {
318
- const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
319
- const res = await runCommand("gh", args, cwd ? { cwd } : {});
320
- if (res.exitCode !== 0) {
321
- throw new Error(`gh ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
322
- }
323
- const url = res.stdout.trim();
324
- // KJC-TSK-1123: the PR Karajan opens carries its label; a failure is logged, the PR stays.
325
- await labelKarajanPr({ url, cwd });
326
- return url;
327
- }
@@ -46,6 +46,9 @@ export async function migrateKjToKarajan({ dryRun = false, force = false } = {})
46
46
  // the developer's real HOME. Migrator's own tests pass `force: true`.
47
47
  if (process.env.VITEST && !force) return { migrated: false, reason: "vitest-skip" };
48
48
 
49
+ // KJC-BUG-0439: a KARAJAN_HOME chosen on purpose (CI, a sandbox, a canary) never receives
50
+ // the person's default ~/.kj; only an explicit KJ_HOME → KARAJAN_HOME pair is migrated.
51
+ if (process.env.KARAJAN_HOME && !process.env.KJ_HOME) return { migrated: false, reason: "explicit-home" };
49
52
  const sourceDir = process.env.KJ_HOME || path.join(os.homedir(), ".kj");
50
53
  const targetDir = process.env.KARAJAN_HOME || path.join(os.homedir(), ".karajan");
51
54
 
@@ -64,10 +67,13 @@ export async function migrateKjToKarajan({ dryRun = false, force = false } = {})
64
67
  if (dryRun) return { migrated: false, reason: "dry-run", sourceDir, targetDir };
65
68
 
66
69
  const backupDir = path.join(targetDir, "backup");
67
- await fs.mkdir(backupDir, { recursive: true });
70
+ // The backup copies the person's files: readable by them only (KJC-BUG-0439).
71
+ await fs.mkdir(backupDir, { recursive: true, mode: 0o700 });
72
+ await fs.chmod(backupDir, 0o700);
68
73
  const ts = new Date().toISOString().replaceAll(/[:.]/g, "-");
69
74
  const backupPath = path.join(backupDir, `kj-pre-migration-${ts}.tar.gz`);
70
75
  await execa("tar", ["-czf", backupPath, "-C", path.dirname(sourceDir), path.basename(sourceDir)]);
76
+ await fs.chmod(backupPath, 0o600);
71
77
 
72
78
  const counts = {};
73
79
  for (const sub of MOVE_DIRS) {