karajan-code 4.70.4 → 5.0.0

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