karajan-code 4.70.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +34 -3
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -2,7 +2,7 @@
2
2
  // Wires the read-only sweep (0568) → deterministic assessment (0569) → haiku
3
3
  // decider (0569), reports the recommended next step, and — interactively only —
4
4
  // confirms and dispatches the chosen intent to its saved command
5
- // (harden/rag/run/plan). --json / --yes / no-TTY stay read-only: emit the
5
+ // (harden/rag). --json / --yes / no-TTY stay read-only: emit the
6
6
  // assessment + intent and apply nothing. Collaborators are injected for testing.
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
@@ -17,16 +17,15 @@ const NEXT_STEP = {
17
17
  ASSESS_ONLY: "Everything looks healthy — nothing to do.",
18
18
  RECOMMEND_HARDEN: "Suggested: `kj harden --interactive`",
19
19
  RECOMMEND_INDEX: "Suggested: `kj rag index`",
20
- START_TASK: 'Suggested: `kj run "<task>"`',
21
- PROPOSE_PLAN: 'Suggested: `kj plan "<goal>"`',
20
+ // KJC-TSK-1322: kj run and kj plan generate are retired in 5.0; your agent does the work.
21
+ START_TASK: 'Suggested: ask your agent for it, and put it on the board first with `kj hu add "<task>"`',
22
+ PROPOSE_PLAN: 'Suggested: have your agent plan it with `kj brief planner`, then `kj hu add` each piece',
22
23
  };
23
24
 
24
25
  // Each writing intent maps to the saved command + args `kj start` dispatches.
25
26
  const DISPATCH = {
26
27
  RECOMMEND_HARDEN: () => ["harden", "--interactive"],
27
28
  RECOMMEND_INDEX: () => ["rag", "index"],
28
- START_TASK: (task) => ["run", task],
29
- PROPOSE_PLAN: (task) => ["plan", task],
30
29
  };
31
30
 
32
31
  const ASK_FALLBACK = "What would you like to do with this project?";
@@ -19,6 +19,7 @@ import { loadPreviousAudit } from "../audit/basal-cost.js";
19
19
  import { collectOsvFindings } from "../audit/osv-findings.js";
20
20
  import { recordGateDecision } from "../policy/decisions.js";
21
21
  import { syncProposedWork } from "../steward/proposed-work.js";
22
+ import { sweepPhantoms } from "../steward/phantom-sweep.js";
22
23
 
23
24
  const stewardDir = (projectDir) => path.join(projectDir, ".karajan", "steward");
24
25
 
@@ -30,39 +31,63 @@ const ghRuns = (projectDir, baseBranch) => () => {
30
31
 
31
32
  const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
32
33
 
34
+ const MARK = { [VERDICTS.OK]: "✓", [VERDICTS.BROKEN]: "✗", [VERDICTS.UNKNOWN]: "?", [VERDICTS.NOT_OBSERVABLE]: "∅" };
35
+
36
+ /**
37
+ * STW-E (KJC-TSK-0793): --if-stale <days> — resuming work with a fresh
38
+ * report does not re-sweep; the report in the repo stays the ONE source.
39
+ * @returns {object|null} the fresh previous report, or null when a sweep is due
40
+ */
41
+ function freshReport(projectDir, flags, probes) {
42
+ if (flags.ifStale === undefined) return null;
43
+ const prev = readJson(path.join(stewardDir(projectDir), "report.json"));
44
+ const ageMs = prev?.sweptAt ? (probes.nowMs ?? Date.now()) - Date.parse(prev.sweptAt) : Infinity;
45
+ return ageMs < Number(flags.ifStale) * 86_400_000 ? prev : null;
46
+ }
47
+
48
+ /**
49
+ * Live osv probe (best-effort): unavailable is null → the invariant answers
50
+ * unknown with its remedy, never a clean bill.
51
+ */
52
+ async function liveVulns({ projectDir, logger, probes }) {
53
+ if (probes.vulns != null) return probes.vulns;
54
+ try {
55
+ const osv = await (probes.osvFn ?? collectOsvFindings)(projectDir, logger);
56
+ if (osv?.available) return (osv.vulnerabilities || []).map((v) => ({ id: v.id, severity: v.severity, publishedAt: v.publishedAt ?? null }));
57
+ } catch { /* stays null — unknown */ }
58
+ return null;
59
+ }
60
+
61
+ /** The dead code the last audit counted: knip's exports + files, or the older deadExports list. */
62
+ async function deadCodeNow(projectDir) {
63
+ const snapshot = await loadPreviousAudit(projectDir).catch(() => null);
64
+ if (typeof snapshot?.knipDeadExports?.exports === "number") {
65
+ return { deadExports: snapshot.knipDeadExports.exports + (snapshot.knipDeadExports.files || 0) };
66
+ }
67
+ return Array.isArray(snapshot?.deadExports) ? { deadExports: snapshot.deadExports.length } : null;
68
+ }
69
+
70
+ /** The phantom detectors over the project's test↔source pairs; a failure is said, never clean. */
71
+ function phantomSweep(projectDir, probes) {
72
+ if (probes.phantoms) return probes.phantoms;
73
+ try { return sweepPhantoms({ projectDir }); } catch (err) { return { reason: `the phantom sweep failed (${err.message})` }; }
74
+ }
75
+
33
76
  export async function stewardSweepCommand({ flags = {}, config = {}, logger = console, probes = {} } = {}) {
34
77
  const projectDir = config.projectDir || process.cwd();
35
78
  const baseBranch = config.base_branch || "main";
36
- // STW-E (KJC-TSK-0793): --if-stale <days> — resuming work with a fresh
37
- // report does not re-sweep; the report in the repo stays the ONE source.
38
- if (flags.ifStale !== undefined) {
39
- const prev = readJson(path.join(stewardDir(projectDir), "report.json"));
40
- const ageMs = prev?.sweptAt ? (probes.nowMs ?? Date.now()) - Date.parse(prev.sweptAt) : Infinity;
41
- if (ageMs < Number(flags.ifStale) * 86_400_000) {
42
- logger.info?.(`steward: report is fresh (swept ${prev.sweptAt}) — not re-sweeping`);
43
- return 0;
44
- }
79
+ const fresh = freshReport(projectDir, flags, probes);
80
+ if (fresh) {
81
+ logger.info?.(`steward: report is fresh (swept ${fresh.sweptAt}) — not re-sweeping`);
82
+ return 0;
45
83
  }
46
84
  const freshness = resolveFreshness(config);
47
85
  const nowMs = probes.nowMs ?? Date.now();
48
86
  const runsFn = probes.runsFn ?? ghRuns(projectDir, baseBranch);
49
-
50
- // Live osv probe (best-effort): unavailable is null → the invariant answers
51
- // unknown with its remedy, never a clean bill.
52
- let vulns = probes.vulns ?? null;
53
- if (vulns === null) {
54
- try {
55
- const osv = await (probes.osvFn ?? collectOsvFindings)(projectDir, logger);
56
- if (osv?.available) vulns = (osv.vulnerabilities || []).map((v) => ({ id: v.id, severity: v.severity, publishedAt: v.publishedAt ?? null }));
57
- } catch { /* stays null — unknown */ }
58
- }
59
-
87
+ const vulns = await liveVulns({ projectDir, logger, probes });
60
88
  // Dead-code baseline: the PREVIOUS report in the repo — shared memory.
61
89
  const prevReport = readJson(path.join(stewardDir(projectDir), "report.json"));
62
- const snapshot = await loadPreviousAudit(projectDir).catch(() => null);
63
- const deadNow = typeof snapshot?.knipDeadExports?.exports === "number"
64
- ? { deadExports: snapshot.knipDeadExports.exports + (snapshot.knipDeadExports.files || 0) }
65
- : Array.isArray(snapshot?.deadExports) ? { deadExports: snapshot.deadExports.length } : null;
90
+ const deadNow = await deadCodeNow(projectDir);
66
91
 
67
92
  const invariants = [
68
93
  { id: "main-ci", renew: "push to the base branch (or fix the red run)", evaluate: () => evaluateMainCi({ projectDir, baseBranch, freshness: freshness.values, runsFn, nowMs }) },
@@ -70,7 +95,7 @@ export async function stewardSweepCommand({ flags = {}, config = {}, logger = co
70
95
  { id: "vulnerable-deps", renew: "kj audit (osv-scanner)", evaluate: () => evaluateVulnAging({ vulns, freshness: freshness.values, nowMs }) },
71
96
  { id: "dead-code-trend", renew: "kj audit (knip inventory)", evaluate: () => evaluateDeadCodeTrend({ current: deadNow, previous: prevReport?.deadCodeBaseline ?? null }) },
72
97
  { id: "coverage-config", renew: "configure a coverage threshold", evaluate: () => evaluateCoverageConfig({ projectDir }) },
73
- { id: "phantom-coverage", renew: "KJC-TSK-0800 ships the detectors", evaluate: () => evaluatePhantomCoverage() },
98
+ { id: "phantom-coverage", renew: "kj steward sweep (runs the phantom detectors)", evaluate: () => evaluatePhantomCoverage(phantomSweep(projectDir, probes)) },
74
99
  ];
75
100
  const results = runInvariants(invariants, {}).map((r, i) => ({ ...r, renew: invariants[i].renew }));
76
101
  const broken = results.filter((r) => r.verdict === VERDICTS.BROKEN);
@@ -82,57 +107,81 @@ export async function stewardSweepCommand({ flags = {}, config = {}, logger = co
82
107
  invariants: results,
83
108
  deadCodeBaseline: deadNow ? { ...deadNow, timestamp: new Date(nowMs).toISOString() } : (prevReport?.deadCodeBaseline ?? null),
84
109
  };
85
- const mark = { [VERDICTS.OK]: "✓", [VERDICTS.BROKEN]: "✗", [VERDICTS.UNKNOWN]: "?", [VERDICTS.NOT_OBSERVABLE]: "∅" };
110
+ writeReport(projectDir, report, freshness);
111
+ sealSweep({ projectDir, results, broken, logger });
112
+ await cardBrokenInvariants({ projectDir, config, results, sweptAt: report.sweptAt, logger });
113
+ warnIfReportIgnored(projectDir, logger);
114
+ printSweep({ flags, report, results, broken, logger });
115
+ process.exitCode = broken.length ? 1 : 0;
116
+ return broken.length ? 1 : 0;
117
+ }
118
+
119
+ function writeReport(projectDir, report, freshness) {
120
+ const freshnessValues = Object.entries(freshness.values).map(([k, v]) => `${k}=${v}`).join(", ");
86
121
  const md = [
87
122
  "# Steward report",
88
123
  "", `Last swept: ${report.sweptAt}`, "",
89
- `Freshness: ${freshness.declared ? "declared by the project" : "calibrated defaults (the project declared none)"} — ${Object.entries(freshness.values).map(([k, v]) => `${k}=${v}`).join(", ")}`, "",
90
- ...results.map((r) => [
91
- `## ${mark[r.verdict] || "?"} ${r.id} — ${r.verdict}`,
124
+ `Freshness: ${freshness.declared ? "declared by the project" : "calibrated defaults (the project declared none)"} — ${freshnessValues}`, "",
125
+ ...report.invariants.flatMap((r) => [
126
+ `## ${MARK[r.verdict] || "?"} ${r.id} — ${r.verdict}`,
92
127
  `- evidence: ${r.evidence || "(none)"}`,
93
128
  ...(r.remedy ? [`- remedy: ${r.remedy}`] : []),
94
129
  `- renews with: ${r.renew}`, "",
95
- ]).flat(),
130
+ ]),
96
131
  ].join("\n");
97
-
98
132
  fs.mkdirSync(stewardDir(projectDir), { recursive: true });
99
133
  fs.writeFileSync(path.join(stewardDir(projectDir), "report.md"), md);
100
134
  fs.writeFileSync(path.join(stewardDir(projectDir), "report.json"), `${JSON.stringify(report, null, 2)}\n`);
135
+ }
101
136
 
102
- // Every sweep is SEALED in the decision chain — the same criterion policy
103
- // decisions already meet: recorded and verifiable, not just notified.
137
+ /**
138
+ * Every sweep is SEALED in the decision chain — the same criterion policy
139
+ * decisions already meet: recorded and verifiable, not just notified.
140
+ */
141
+ function sealSweep({ projectDir, results, broken, logger }) {
104
142
  try {
105
143
  const counts = {};
106
144
  for (const r of results) counts[r.verdict] = (counts[r.verdict] || 0) + 1;
107
145
  recordGateDecision(projectDir, { kind: "steward-sweep", verdicts: { ok: counts[VERDICTS.OK] || 0, broken: broken.length, unknown: counts[VERDICTS.UNKNOWN] || 0, "not-observable": counts[VERDICTS.NOT_OBSERVABLE] || 0 }, broken_ids: broken.map((b) => b.id) });
108
146
  } catch (err) { logger.warn?.(`⚠ steward: the sweep could not be sealed in the decision chain (${err.message})`); }
147
+ }
109
148
 
110
- // STW-D (KJC-TSK-0792): every break is PROPOSED work on the board the brain
111
- // already consumes. AFTER the report and the seal: a board failure never
112
- // costs either, and it is said loudly — never swallowed.
149
+ /**
150
+ * STW-D (KJC-TSK-0792): every break is PROPOSED work on the board the brain
151
+ * already consumes. AFTER the report and the seal: a board failure never
152
+ * costs either, and it is said loudly — never swallowed.
153
+ */
154
+ async function cardBrokenInvariants({ projectDir, config, results, sweptAt, logger }) {
113
155
  try {
114
- const sync = await syncProposedWork({ projectDir, config, results, sweptAt: report.sweptAt });
115
- if (!sync.synced) logger.warn?.(`⚠ steward: broken invariants not carded — ${sync.reason}`);
116
- else if (sync.created || sync.updated || sync.resolved) {
117
- logger.info?.(`steward board: ${sync.created} card(s) proposed, ${sync.updated} updated, ${sync.resolved} resolved`);
118
- if (sync.replicateIn) logger.info?.(`→ replicate them in ${sync.replicateIn} through your agent's board tools (kj hu list shows them)`);
156
+ const sync = await syncProposedWork({ projectDir, config, results, sweptAt });
157
+ if (!sync.synced) {
158
+ logger.warn?.(`⚠ steward: broken invariants not carded — ${sync.reason}`);
159
+ return;
119
160
  }
161
+ if (!(sync.created || sync.updated || sync.resolved)) return;
162
+ logger.info?.(`steward board: ${sync.created} card(s) proposed, ${sync.updated} updated, ${sync.resolved} resolved`);
163
+ if (sync.replicateIn) logger.info?.(`→ replicate them in ${sync.replicateIn} through your agent's board tools (kj hu list shows them)`);
120
164
  } catch (err) { logger.warn?.(`⚠ steward: carding the broken invariants failed (${err.message}) — the report and the seal stand`); }
165
+ }
121
166
 
122
- // A report nobody can see is not shared state — say it, do not fix their tree.
167
+ /** A report nobody can see is not shared state — say it, do not fix their tree. */
168
+ function warnIfReportIgnored(projectDir, logger) {
123
169
  try {
124
170
  execFileSync("git", ["check-ignore", "-q", path.join(".karajan", "steward", "report.md")], { cwd: projectDir });
125
171
  logger.warn?.("⚠ steward: the report path is gitignored — untrack .karajan/steward from .gitignore so the state is shared");
126
172
  } catch { /* not ignored — good */ }
173
+ }
127
174
 
175
+ function printSweep({ flags, report, results, broken, logger }) {
128
176
  if (flags.json) {
129
177
  process.stdout.write(`${JSON.stringify(report)}\n`);
130
- } else {
131
- for (const r of results) logger.info?.(`${mark[r.verdict]} ${r.id}: ${r.verdict}${r.verdict === VERDICTS.OK ? "" : ` — ${r.remedy || r.evidence}`}`);
132
- logger.info?.(broken.length ? `steward: ${broken.length} invariant(s) BROKEN — the report names them (.karajan/steward/report.md)` : "steward: nothing broken — the full state is in .karajan/steward/report.md");
178
+ return;
133
179
  }
134
- process.exitCode = broken.length ? 1 : 0;
135
- return broken.length ? 1 : 0;
180
+ for (const r of results) {
181
+ const why = r.verdict === VERDICTS.OK ? "" : ` — ${r.remedy || r.evidence}`;
182
+ logger.info?.(`${MARK[r.verdict]} ${r.id}: ${r.verdict}${why}`);
183
+ }
184
+ logger.info?.(broken.length ? `steward: ${broken.length} invariant(s) BROKEN — the report names them (.karajan/steward/report.md)` : "steward: nothing broken — the full state is in .karajan/steward/report.md");
136
185
  }
137
186
 
138
187
  /**
@@ -20,47 +20,53 @@ function tournamentDirFor(config, rawId) {
20
20
  return path.join(config?.projectDir || process.cwd(), ".kj", "tournaments", id);
21
21
  }
22
22
 
23
+ // TOR-C: `--judge <id>` ranks the finalists.
24
+ async function judgeAction({ config, logger, flags, judgeTournamentFn }) {
25
+ const judgement = await judgeTournamentFn({ tournamentDir: tournamentDirFor(config, flags.judge), config, logger });
26
+ if (flags.json) logger?.info?.(JSON.stringify(judgement));
27
+ else logger?.info?.(` siguiente: kj tournament --crown ${flags.judge}${judgement.escalated ? " (ganador vía solomon)" : ""}`);
28
+ return judgement;
29
+ }
30
+
31
+ // TOR-C: `--crown <id>` commits the winner through the real gate in its lane.
32
+ async function crownAction({ config, logger, flags, crownWinnerFn }) {
33
+ const crown = await crownWinnerFn({
34
+ tournamentDir: tournamentDirFor(config, flags.crown), config, logger, cardRef: flags.card || null,
35
+ });
36
+ if (flags.json) logger?.info?.(JSON.stringify(crown));
37
+ return crown;
38
+ }
39
+
40
+ function scoreboardRow(r) {
41
+ if (!r.finalist) return ` ${r.coder.padEnd(10)} eliminado (${r.eliminated_by})`;
42
+ const mut = typeof r.mutation?.score === "number" ? `${r.mutation.score}%` : "n/a";
43
+ return ` ${r.coder.padEnd(10)} FINALISTA loc:${String(r.loc.net).padStart(5)} tests:${r.loc.testsTouched} mutation:${mut}`;
44
+ }
45
+
46
+ // TOR-B: `--score <id>` scores an EXISTING tournament from its artifacts.
47
+ async function scoreAction({ config, logger, flags, scoreTournamentFn }) {
48
+ const board = await scoreTournamentFn({ tournamentDir: tournamentDirFor(config, flags.score) });
49
+ if (flags.json) logger?.info?.(JSON.stringify(board));
50
+ else printScoreboard(board, logger);
51
+ return board;
52
+ }
53
+
54
+ function printScoreboard(board, logger) {
55
+ logger?.info?.(`scoreboard ${board.id} — regla: ${board.rule}`);
56
+ for (const r of board.rows) logger?.info?.(scoreboardRow(r));
57
+ logger?.info?.(` ranking: ${board.ranking.join(" > ") || "(sin finalistas)"}`);
58
+ }
59
+
60
+ // The first of --judge, --crown, --score that is set wins, in that order.
61
+ const ID_ACTIONS = [["judge", judgeAction], ["crown", crownAction], ["score", scoreAction]];
62
+
23
63
  export async function tournamentCommand({
24
64
  task, config, logger = console, flags = {},
25
65
  runTournamentFn = runTournament, scoreTournamentFn = scoreTournament,
26
66
  judgeTournamentFn = judgeTournament, crownWinnerFn = crownWinner,
27
67
  }) {
28
- // TOR-C: `--judge <id>` ranks the finalists; `--crown <id>` commits the
29
- // winner through the real gate in its lane.
30
- if (flags.judge) {
31
- const judgement = await judgeTournamentFn({ tournamentDir: tournamentDirFor(config, flags.judge), config, logger });
32
- if (flags.json) logger?.info?.(JSON.stringify(judgement));
33
- else logger?.info?.(` siguiente: kj tournament --crown ${flags.judge}${judgement.escalated ? " (ganador vía solomon)" : ""}`);
34
- return judgement;
35
- }
36
- if (flags.crown) {
37
- const crown = await crownWinnerFn({
38
- tournamentDir: tournamentDirFor(config, flags.crown), config, logger, cardRef: flags.card || null,
39
- });
40
- if (flags.json) logger?.info?.(JSON.stringify(crown));
41
- return crown;
42
- }
43
-
44
- // TOR-B: `--score <id>` scores an EXISTING tournament from its artifacts.
45
- if (flags.score) {
46
- const tournamentDir = tournamentDirFor(config, flags.score);
47
- const board = await scoreTournamentFn({ tournamentDir });
48
- if (flags.json) {
49
- logger?.info?.(JSON.stringify(board));
50
- return board;
51
- }
52
- logger?.info?.(`scoreboard ${board.id} — regla: ${board.rule}`);
53
- for (const r of board.rows) {
54
- if (!r.finalist) {
55
- logger?.info?.(` ${r.coder.padEnd(10)} eliminado (${r.eliminated_by})`);
56
- continue;
57
- }
58
- const mut = typeof r.mutation?.score === "number" ? `${r.mutation.score}%` : "n/a";
59
- logger?.info?.(` ${r.coder.padEnd(10)} FINALISTA loc:${String(r.loc.net).padStart(5)} tests:${r.loc.testsTouched} mutation:${mut}`);
60
- }
61
- logger?.info?.(` ranking: ${board.ranking.join(" > ") || "(sin finalistas)"}`);
62
- return board;
63
- }
68
+ const idAction = ID_ACTIONS.find(([flag]) => flags[flag]);
69
+ if (idAction) return idAction[1]({ config, logger, flags, scoreTournamentFn, judgeTournamentFn, crownWinnerFn });
64
70
 
65
71
  if (!task || !String(task).trim()) {
66
72
  throw new Error("tournament: falta la tarea — kj tournament \"<tarea>\" --coders claude,codex");
@@ -74,12 +80,15 @@ export async function tournamentCommand({
74
80
  }
75
81
 
76
82
  const res = await runTournamentFn({ task, coders, config, logger });
83
+ printRunSummary(res, logger);
84
+ return res;
85
+ }
77
86
 
87
+ function printRunSummary(res, logger) {
78
88
  const completed = res.lanes.filter((l) => l.status === "completed");
79
89
  const green = completed.filter((l) => l.suite?.ok);
80
90
  logger?.info?.("");
81
91
  logger?.info?.(`torneo ${res.id}: ${completed.length}/${res.lanes.length} carriles completados, ${green.length} con suite verde`);
82
92
  logger?.info?.(` evidencia: ${res.dir}`);
83
93
  logger?.info?.(" siguiente: el scoreboard determinista (TOR-B) puntuará los carriles; de momento compara los diff.patch a mano.");
84
- return res;
85
94
  }
@@ -3,7 +3,8 @@
3
3
  * host agent. Field case: the playbook said "one worktree per task" and an
4
4
  * agent NARRATED the isolation while editing the project root. Ordering a
5
5
  * command beats ordering a practice: start = worktree + feat/ branch +
6
- * dependency bootstrap + the exact cd printed; done = remove after merge.
6
+ * dependency bootstrap + the exact cd printed; done = remove after merge
7
+ * and delete the branch once it is merged into origin/<base>.
7
8
  * Same lane dir (.kj/worktrees) and bootstrap the headless pipeline uses.
8
9
  */
9
10
  import fs from "node:fs";
@@ -11,8 +12,33 @@ import path from "node:path";
11
12
  import { execFile } from "node:child_process";
12
13
  import { promisify } from "node:util";
13
14
  import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
14
- import { classifyLanes, liveProcesses, looseLaneBranches, resolveBase } from "../hu/lane-prune.js";
15
+ import { laneWork, setupLane } from "../hu/lane-setup.js";
16
+ import { classifyLanes, commitsNotIn, liveProcesses, looseLaneBranches, resolveBase } from "../hu/lane-prune.js";
15
17
  import { recordGateDecision } from "../policy/decisions.js";
18
+ import { mainCheckout } from "../utils/main-checkout.js";
19
+ import { cardsOfLane, parseLanes, prsByBranch } from "../hu/lanes.js";
20
+ import { listPlans, loadPlan } from "../plan/plan-store.js";
21
+
22
+ const REF_SEPARATOR = " · "; // external_ref joins a tracker card and an issue (src/commands/hu.js)
23
+
24
+ // KJC-TSK-1316: ONE gh call for every lane, open and merged PRs alike.
25
+ const listPrs = async (cwd) => JSON.parse((await execFileAsync("gh", ["pr", "list", "--state", "all", "--limit", "200", "--json", "number,url,state,headRefName"], { cwd })).stdout);
26
+
27
+ /** Each branch's PR; when gh fails the lanes go without PRs, and kj says so. */
28
+ async function lanePrs(list, cwd) {
29
+ try {
30
+ return prsByBranch(await list(cwd));
31
+ } catch (err) {
32
+ console.warn(`! kj worktree list: cannot read the PRs (${String(err.stderr || err.message).trim().split("\n")[0]}); lanes listed without them`);
33
+ return new Map();
34
+ }
35
+ }
36
+
37
+ /** Every HU of the project's plans with its references, as `{id, refs}`. */
38
+ async function planCards(projectDir) {
39
+ const plans = await Promise.all((await listPlans(projectDir)).map((m) => loadPlan(projectDir, m.planId)));
40
+ return plans.flatMap((p) => (p?.hus ?? []).map((h) => ({ id: h.id, refs: String(h.external_ref ?? "").split(REF_SEPARATOR).filter(Boolean) })));
41
+ }
16
42
 
17
43
  const execFileAsync = promisify(execFile);
18
44
  const WORKTREE_DIR = ".kj/worktrees"; // shared with the headless lanes
@@ -22,7 +48,10 @@ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
22
48
  export async function worktreeCommand({ config = null, action, args = [], flags = {}, deps = {} }) {
23
49
  const projectDir = config?.projectDir || process.cwd();
24
50
  const git = (gitArgs, cwd = projectDir) => execFileAsync("git", gitArgs, { cwd });
25
- const lanePath = (slug) => path.join(projectDir, WORKTREE_DIR, slug);
51
+ // #2566: lanes hang from the main checkout, never from the lane the command runs in.
52
+ const lanesDir = path.join(mainCheckout(projectDir), WORKTREE_DIR);
53
+ const lanePath = (slug) => path.join(lanesDir, slug);
54
+ const baseName = config?.base_branch || "main";
26
55
 
27
56
  if (action === "start") {
28
57
  const slug = String(args[0] || "").trim();
@@ -34,28 +63,48 @@ export async function worktreeCommand({ config = null, action, args = [], flags
34
63
  throw new Error(`lane "${slug}" already exists — cd ${wtPath}, or finish it with: kj worktree done ${slug}`);
35
64
  }
36
65
  const branch = `feat/${slug}`;
66
+ // HU-2761: a lane is cut from origin/<base>, never from the HEAD the command runs in (#2566).
67
+ const from = await resolveBase({ git, base: baseName });
68
+ try { await git(["rev-parse", "--verify", "--quiet", `${from}^{commit}`]); } catch {
69
+ throw new Error(`base "${baseName}" not found — set base_branch in kj config`);
70
+ }
71
+ if (from === baseName) {
72
+ console.warn(`! no origin/${baseName} ref — lane starts from local ${baseName}, which may lag (git fetch origin ${baseName} to refresh)`);
73
+ }
74
+ const leftover = await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).then(() => true, () => false);
75
+ if (leftover) {
76
+ throw new Error(`branch ${branch} already exists (kept by an unmerged done?) — delete it with git branch -D ${branch} or pick another slug`);
77
+ }
37
78
  // KJC-TSK-1093: --no-track, so no upstream lands in .git/config (read-only in the host sandbox).
38
- await git(["worktree", "add", "--no-track", "-b", branch, wtPath]);
79
+ await git(["worktree", "add", "--no-track", "-b", branch, wtPath, from]);
39
80
  const boot = await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null });
40
- console.log(`✓ lane ready: ${branch}`);
81
+ // KJC-TSK-1371 (#2852): identity and Sentinel in the lane; a failure names the step and the way out.
82
+ const retire = `kj worktree done ${slug} --force`;
83
+ const setup = await (deps.setupLane ?? setupLane)({ mainDir: mainCheckout(projectDir), lanePath: wtPath }).catch((err) => {
84
+ throw new Error(`${err.message} — the lane is half set; retire it with: ${retire}`, { cause: err });
85
+ });
86
+ const { stdout: touched } = await git(["status", "--porcelain", "--untracked-files=no"], wtPath);
87
+ if (touched.trim()) throw new Error(`lane setup changed tracked files (${touched.trim().split("\n").join(", ")}) — retire it with: ${retire}`);
88
+ console.log(`✓ lane ready: ${branch} (from ${from})`);
41
89
  console.log(` cd ${wtPath}`);
42
90
  console.log(" The gates travel with the repo — kj review --staged works there. When merged: kj worktree done " + slug);
43
- return { path: wtPath, branch, bootstrap: boot };
91
+ return { path: wtPath, branch, from, bootstrap: boot, setup };
44
92
  }
45
93
 
46
94
  if (action === "list") {
47
- const laneRoot = path.join(projectDir, WORKTREE_DIR) + path.sep;
48
95
  const { stdout } = await git(["worktree", "list", "--porcelain"]);
49
- const rows = [];
50
- let current = null;
51
- for (const line of stdout.split("\n")) {
52
- if (line.startsWith("worktree ")) current = { path: line.slice(9).trim() };
53
- else if (line.startsWith("branch ") && current) {
54
- current.branch = line.slice(7).trim().replace("refs/heads/", "");
55
- if (current.path.startsWith(laneRoot)) {
56
- rows.push({ slug: path.basename(current.path), branch: current.branch, path: current.path });
57
- }
58
- }
96
+ const rows = parseLanes(stdout, { root: lanesDir });
97
+ if (flags.json) {
98
+ // KJC-TSK-1313: what the HU Board paints on each card.
99
+ const base = await resolveBase({ git, base: baseName });
100
+ const cards = await planCards(projectDir);
101
+ const prs = flags.prs ? await lanePrs(deps.listPrs ?? listPrs, projectDir) : null;
102
+ const out = await Promise.all(rows.map(async (r) => ({
103
+ ...r, ahead: await commitsNotIn({ git, base, ref: r.branch }).catch(() => null), cards: cardsOfLane(r, cards),
104
+ ...(prs && { pr: prs.get(r.branch) ?? null }),
105
+ })));
106
+ console.log(JSON.stringify(out));
107
+ return out;
59
108
  }
60
109
  if (rows.length === 0) console.log("no lanes — create one with: kj worktree start <slug>");
61
110
  for (const r of rows) console.log(`${r.slug.padEnd(24)} ${r.branch.padEnd(30)} ${r.path}`);
@@ -66,19 +115,38 @@ export async function worktreeCommand({ config = null, action, args = [], flags
66
115
  const slug = String(args[0] || "").trim();
67
116
  const wtPath = lanePath(slug);
68
117
  if (!fs.existsSync(wtPath)) throw new Error(`lane "${slug}" not found — see kj worktree list`);
69
- const { stdout: dirty } = await git(["status", "--porcelain"], wtPath);
70
- if (dirty.trim() && !flags.force) {
118
+ // KJC-TSK-1371: the identity and Sentinel files lane setup wrote are not the lane's work.
119
+ const { stdout: status } = await git(["status", "--porcelain", "--untracked-files=all"], wtPath);
120
+ const dirty = laneWork(status, wtPath).join("\n");
121
+ if (dirty && !flags.force) {
71
122
  throw new Error(`lane "${slug}" has uncommitted changes — commit them (through the gate) or discard with --force`);
72
123
  }
73
- await git(["worktree", "remove", ...(flags.force ? ["--force"] : []), wtPath]);
74
- console.log(`✓ lane "${slug}" removed — its branch survives; delete it after merge if you want`);
75
- return { removed: true, slug };
124
+ // The lane's real branch (headless kj-hu-* lanes share the folder); detached HEAD → null.
125
+ // symbolic-ref -q exits 1 on a detached HEAD; any other failure is a real error.
126
+ const branch = await git(["symbolic-ref", "--short", "-q", "HEAD"], wtPath).then((r) => r.stdout.trim() || null, (err) => {
127
+ if (err.code === 1) return null;
128
+ throw err;
129
+ });
130
+ const base = await resolveBase({ git, base: baseName });
131
+ // A missing base must not block removing a clean lane: the branch is kept, with its remedy.
132
+ const ahead = branch ? await commitsNotIn({ git, base, ref: branch }).catch(() => null) : 0;
133
+ // Only setup files left (checked above): git still calls them untracked, so the remove is forced.
134
+ await git(["worktree", "remove", ...(flags.force || status.trim() ? ["--force"] : []), wtPath]);
135
+ // -D after our own check: -d measures against HEAD/upstream, not origin/<base>. Never the base itself.
136
+ const branchDeleted = Boolean(branch) && branch !== baseName && (ahead === 0 || Boolean(flags.force));
137
+ if (branchDeleted) await git(["branch", "-D", branch]);
138
+ const note = !branch ? "detached HEAD, no branch"
139
+ : branchDeleted ? (ahead === 0 ? `branch ${branch} deleted (merged into ${base})` : `branch ${branch} deleted with --force (${ahead ?? "unknown"} commit(s) not in ${base} were discarded)`)
140
+ : ahead === null ? `branch ${branch} kept: base "${base}" not found, cannot tell if it is merged (git branch -D ${branch} to delete it)`
141
+ : `branch ${branch} kept: ${ahead} commit(s) not in ${base} (squash-merged or not fetched? git fetch, then git branch -D ${branch})`;
142
+ console.log(`✓ lane "${slug}" removed; ${note}`);
143
+ return { removed: true, slug, branch, branchDeleted, merged: branch ? ahead === 0 : null };
76
144
  }
77
145
 
78
146
  if (action === "prune") {
79
147
  const { processes = liveProcesses() } = deps;
80
- const base = await resolveBase({ git, base: config?.base_branch || "main" });
81
- const lanes = await classifyLanes({ git, laneRoot: path.join(projectDir, WORKTREE_DIR), base, processes, now: deps.now });
148
+ const base = await resolveBase({ git, base: baseName });
149
+ const lanes = await classifyLanes({ git, laneRoot: lanesDir, base, processes, now: deps.now });
82
150
  const abandoned = lanes.filter((l) => l.reasons.length === 0);
83
151
  const kept = lanes.filter((l) => l.reasons.length > 0);
84
152
  for (const l of kept) console.log(`kept ${l.slug.padEnd(24)} ${l.reasons.join("; ")}`);
@@ -96,7 +164,8 @@ export async function worktreeCommand({ config = null, action, args = [], flags
96
164
  console.log(`✓ deleted branch ${b}`);
97
165
  }
98
166
  for (const l of abandoned) {
99
- await git(["worktree", "remove", l.path]);
167
+ // Judged abandoned above: what is left are setup or regenerable files, which git would refuse.
168
+ await git(["worktree", "remove", "--force", l.path]);
100
169
  if (l.branch) await git(["branch", "-D", l.branch]);
101
170
  console.log(`✓ retired ${l.slug} (${l.branch ?? "detached"})`);
102
171
  }
@@ -27,6 +27,7 @@ import { getKarajanHome } from "../utils/paths.js";
27
27
  import { safeParseConfig, formatConfigIssues } from "./schema.js";
28
28
  import { DEFAULTS } from "./defaults.js";
29
29
  import { resolveTestHarness } from "./test-harness.js";
30
+ import { SECRET_PATHS } from "./secrets.js";
30
31
 
31
32
  /**
32
33
  * Parse a YAML config file with an actionable error message.
@@ -74,13 +75,18 @@ export function getProjectConfigPath(projectDir = process.cwd()) {
74
75
  return path.join(projectDir, ".karajan", "kj.config.yml");
75
76
  }
76
77
 
78
+ /** ADR 0032: a member's own preferences, ignored by git, on top of the team's project config. */
79
+ export function getLocalConfigPath(projectDir = process.cwd()) {
80
+ return path.join(projectDir, ".karajan", "kj.config.local.yml");
81
+ }
82
+
83
+ async function readYamlIfPresent(filePath) {
84
+ if (!(await exists(filePath))) return null;
85
+ return parseYamlOrThrow(await fs.readFile(filePath, "utf8"), filePath);
86
+ }
87
+
77
88
  export async function loadProjectConfig(projectDir = process.cwd()) {
78
- const projectConfigPath = getProjectConfigPath(projectDir);
79
- if (!(await exists(projectConfigPath))) {
80
- return null;
81
- }
82
- const raw = await fs.readFile(projectConfigPath, "utf8");
83
- return parseYamlOrThrow(raw, projectConfigPath);
89
+ return readYamlIfPresent(getProjectConfigPath(projectDir));
84
90
  }
85
91
 
86
92
  async function loadProjectPricingOverrides(projectDir = process.cwd()) {
@@ -113,6 +119,7 @@ export async function loadConfig(projectDir) {
113
119
 
114
120
  // Load project config (.karajan/kj.config.yml)
115
121
  const projectConfig = await loadProjectConfig(projectDir);
122
+ const localConfig = await readYamlIfPresent(getLocalConfigPath(projectDir));
116
123
 
117
124
  // KJC-TSK-0395: enforce the local-overrides-global hierarchy. A project
118
125
  // config with no global counterpart is almost always user error (copied
@@ -120,19 +127,24 @@ export async function loadConfig(projectDir) {
120
127
  // would silently behave like a global config without the defaults
121
128
  // people expect. Refuse with an actionable message.
122
129
  // KJC-BUG-0428 (issue #2694): a CI runner never has a global config; there the
123
- // team's committed project config sits on the defaults instead.
124
- if (projectConfig && !globalExists && !process.env.CI) {
130
+ // team's committed project config sits on the defaults instead. A member's local
131
+ // config is never committed, so without a global it is still refused, in CI too.
132
+ if (!globalExists && (localConfig || (projectConfig && !process.env.CI))) {
133
+ const found = projectConfig ? getProjectConfigPath(projectDir) : getLocalConfigPath(projectDir);
125
134
  throw new Error(
126
- `Local config found at ${getProjectConfigPath(projectDir)} but no global config exists.\n` +
135
+ `Local config found at ${found} but no global config exists.\n` +
127
136
  "Local configs are merged ON TOP of the global one — run `kj init --global` first to create the base."
128
137
  );
129
138
  }
130
139
 
131
- // Merge: DEFAULTS < global < project
140
+ // Merge: DEFAULTS < global < project (the team's) < local (the member's, ADR 0032)
132
141
  let merged = mergeDeep(DEFAULTS, globalConfig);
133
142
  if (projectConfig) {
134
143
  merged = mergeDeep(merged, projectConfig);
135
144
  }
145
+ if (localConfig) {
146
+ merged = mergeDeep(merged, localConfig);
147
+ }
136
148
 
137
149
  if (projectPricing) {
138
150
  merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
@@ -199,19 +211,24 @@ function stripRuntimeOnlyKeys(config) {
199
211
  // KJC-BUG-0253: a project's .karajan/kj.config.yml is versioned with the repo,
200
212
  // so the SonarQube credentials never go there; they live in the global config
201
213
  // (~/.karajan), where the token bootstrap already saves them.
202
- const SECRET_KEYS = ["token", "admin_password"];
203
214
  const isProjectConfig = (configPath) =>
204
215
  path.basename(configPath) === "kj.config.yml"
205
216
  && path.basename(path.dirname(configPath)) === ".karajan"
206
217
  && path.resolve(configPath) !== path.resolve(getConfigPath());
207
218
 
219
+ // KJC-TSK-1234 (ADR 0032): the same secret paths the pre-commit check refuses (SonarCloud's token
220
+ // included), one list for both, so writing never keeps what committing would reject.
208
221
  function stripSecrets(config) {
209
- if (!config?.sonarqube || typeof config.sonarqube !== "object") return { out: config, stripped: [] };
210
- const stripped = SECRET_KEYS.filter((k) => config.sonarqube[k] != null);
211
- if (!stripped.length) return { out: config, stripped };
212
- const sonarqube = { ...config.sonarqube };
213
- for (const k of stripped) delete sonarqube[k];
214
- return { out: { ...config, sonarqube }, stripped: stripped.map((k) => `sonarqube.${k}`) };
222
+ let out = config;
223
+ const stripped = [];
224
+ for (const [section, key] of SECRET_PATHS) {
225
+ const block = out?.[section];
226
+ if (!block || typeof block !== "object" || block[key] == null) continue;
227
+ const { [key]: _secret, ...kept } = block;
228
+ out = { ...out, [section]: kept };
229
+ stripped.push(`${section}.${key}`);
230
+ }
231
+ return { out, stripped };
215
232
  }
216
233
 
217
234
  /** @returns {Promise<{strippedSecrets: string[]}>} the secret keys kept out of a project config */
@@ -102,7 +102,8 @@ function applyRoleOverrides(out, flags) {
102
102
  if (flags.reviewerModel) out.reviewer_options.model = String(flags.reviewerModel);
103
103
 
104
104
  // KJC-TSK-0699: a profile fills only the models nothing above has set.
105
- const profile = flags.profile ?? out.model_profile;
105
+ // KJC-BUG-0448: its own key — `--profile` belongs to harden/check (the harness profile).
106
+ const profile = flags.modelProfile ?? out.model_profile;
106
107
  if (profile) {
107
108
  applyModelProfile(out, String(profile));
108
109
  out.model_profile = String(profile);