karajan-code 4.70.4 → 5.0.1

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