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
@@ -13,6 +13,7 @@
13
13
  import { listCached, clearCache, getCacheRoot, DEFAULT_TTL_MS } from "../skills/skills-cache.js";
14
14
  import { refreshIfStale, listAvailableSlugs, getCatalogRoot, loadSkillBySlug } from "../skills/addyosmani-catalog.js";
15
15
  import { ATRIL_COMMANDS, runAtril } from "../skills/atril-cli.js";
16
+ import { skillsSearch } from "../skills/skills-search.js";
16
17
 
17
18
  function humanAge(cachedAt) {
18
19
  const ms = Date.now() - Date.parse(cachedAt);
@@ -22,7 +23,9 @@ function humanAge(cachedAt) {
22
23
  return `${days.toFixed(1)}d ago`;
23
24
  }
24
25
 
25
- export async function skillsCommand({ action = "list", args = [], deps = { atril: runAtril } } = {}) {
26
+ export async function skillsCommand({ action = "list", args = [], deps = { atril: runAtril, skillsSearch } } = {}) {
27
+ // HU-2782: search is kj's own, so the query can be translated into English first.
28
+ if (action === "search") return deps.skillsSearch({ args });
26
29
  // KJC-TSK-1193: atril's own commands go to atril, on the .karajan store.
27
30
  if (ATRIL_COMMANDS.includes(action)) return deps.atril({ action, args });
28
31
  switch (action) {
@@ -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,37 @@ 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, stopLaneIndexers } from "../hu/lane-prune.js";
17
+ import { stopBootstrap } from "../rag/bootstrap-index.js";
15
18
  import { recordGateDecision } from "../policy/decisions.js";
19
+ import { mainCheckout } from "../utils/main-checkout.js";
20
+ import { cardsOfLane, parseLanes, prsByBranch } from "../hu/lanes.js";
21
+ import { listPlans, loadPlan } from "../plan/plan-store.js";
22
+
23
+ // The pid itself, not the marker's expiry: an indexer past MAX_RUN_MS still runs. EPERM is a process too.
24
+ const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (err) { return err.code === "EPERM"; } };
25
+
26
+ const REF_SEPARATOR =" · "; // external_ref joins a tracker card and an issue (src/commands/hu.js)
27
+
28
+ // KJC-TSK-1316: ONE gh call for every lane, open and merged PRs alike.
29
+ const listPrs = async (cwd) => JSON.parse((await execFileAsync("gh", ["pr", "list", "--state", "all", "--limit", "200", "--json", "number,url,state,headRefName"], { cwd })).stdout);
30
+
31
+ /** Each branch's PR; when gh fails the lanes go without PRs, and kj says so. */
32
+ async function lanePrs(list, cwd) {
33
+ try {
34
+ return prsByBranch(await list(cwd));
35
+ } catch (err) {
36
+ console.warn(`! kj worktree list: cannot read the PRs (${String(err.stderr || err.message).trim().split("\n")[0]}); lanes listed without them`);
37
+ return new Map();
38
+ }
39
+ }
40
+
41
+ /** Every HU of the project's plans with its references, as `{id, refs}`. */
42
+ async function planCards(projectDir) {
43
+ const plans = await Promise.all((await listPlans(projectDir)).map((m) => loadPlan(projectDir, m.planId)));
44
+ return plans.flatMap((p) => (p?.hus ?? []).map((h) => ({ id: h.id, refs: String(h.external_ref ?? "").split(REF_SEPARATOR).filter(Boolean) })));
45
+ }
16
46
 
17
47
  const execFileAsync = promisify(execFile);
18
48
  const WORKTREE_DIR = ".kj/worktrees"; // shared with the headless lanes
@@ -22,7 +52,10 @@ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
22
52
  export async function worktreeCommand({ config = null, action, args = [], flags = {}, deps = {} }) {
23
53
  const projectDir = config?.projectDir || process.cwd();
24
54
  const git = (gitArgs, cwd = projectDir) => execFileAsync("git", gitArgs, { cwd });
25
- const lanePath = (slug) => path.join(projectDir, WORKTREE_DIR, slug);
55
+ // #2566: lanes hang from the main checkout, never from the lane the command runs in.
56
+ const lanesDir = path.join(mainCheckout(projectDir), WORKTREE_DIR);
57
+ const lanePath = (slug) => path.join(lanesDir, slug);
58
+ const baseName = config?.base_branch || "main";
26
59
 
27
60
  if (action === "start") {
28
61
  const slug = String(args[0] || "").trim();
@@ -34,28 +67,48 @@ export async function worktreeCommand({ config = null, action, args = [], flags
34
67
  throw new Error(`lane "${slug}" already exists — cd ${wtPath}, or finish it with: kj worktree done ${slug}`);
35
68
  }
36
69
  const branch = `feat/${slug}`;
70
+ // HU-2761: a lane is cut from origin/<base>, never from the HEAD the command runs in (#2566).
71
+ const from = await resolveBase({ git, base: baseName });
72
+ try { await git(["rev-parse", "--verify", "--quiet", `${from}^{commit}`]); } catch {
73
+ throw new Error(`base "${baseName}" not found — set base_branch in kj config`);
74
+ }
75
+ if (from === baseName) {
76
+ console.warn(`! no origin/${baseName} ref — lane starts from local ${baseName}, which may lag (git fetch origin ${baseName} to refresh)`);
77
+ }
78
+ const leftover = await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).then(() => true, () => false);
79
+ if (leftover) {
80
+ throw new Error(`branch ${branch} already exists (kept by an unmerged done?) — delete it with git branch -D ${branch} or pick another slug`);
81
+ }
37
82
  // 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]);
83
+ await git(["worktree", "add", "--no-track", "-b", branch, wtPath, from]);
39
84
  const boot = await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null });
40
- console.log(`✓ lane ready: ${branch}`);
85
+ // KJC-TSK-1371 (#2852): identity and Sentinel in the lane; a failure names the step and the way out.
86
+ const retire = `kj worktree done ${slug} --force`;
87
+ const setup = await (deps.setupLane ?? setupLane)({ mainDir: mainCheckout(projectDir), lanePath: wtPath }).catch((err) => {
88
+ throw new Error(`${err.message} — the lane is half set; retire it with: ${retire}`, { cause: err });
89
+ });
90
+ const { stdout: touched } = await git(["status", "--porcelain", "--untracked-files=no"], wtPath);
91
+ if (touched.trim()) throw new Error(`lane setup changed tracked files (${touched.trim().split("\n").join(", ")}) — retire it with: ${retire}`);
92
+ console.log(`✓ lane ready: ${branch} (from ${from})`);
41
93
  console.log(` cd ${wtPath}`);
42
94
  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 };
95
+ return { path: wtPath, branch, from, bootstrap: boot, setup };
44
96
  }
45
97
 
46
98
  if (action === "list") {
47
- const laneRoot = path.join(projectDir, WORKTREE_DIR) + path.sep;
48
99
  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
- }
100
+ const rows = parseLanes(stdout, { root: lanesDir });
101
+ if (flags.json) {
102
+ // KJC-TSK-1313: what the HU Board paints on each card.
103
+ const base = await resolveBase({ git, base: baseName });
104
+ const cards = await planCards(projectDir);
105
+ const prs = flags.prs ? await lanePrs(deps.listPrs ?? listPrs, projectDir) : null;
106
+ const out = await Promise.all(rows.map(async (r) => ({
107
+ ...r, ahead: await commitsNotIn({ git, base, ref: r.branch }).catch(() => null), cards: cardsOfLane(r, cards),
108
+ ...(prs && { pr: prs.get(r.branch) ?? null }),
109
+ })));
110
+ console.log(JSON.stringify(out));
111
+ return out;
59
112
  }
60
113
  if (rows.length === 0) console.log("no lanes — create one with: kj worktree start <slug>");
61
114
  for (const r of rows) console.log(`${r.slug.padEnd(24)} ${r.branch.padEnd(30)} ${r.path}`);
@@ -66,19 +119,46 @@ export async function worktreeCommand({ config = null, action, args = [], flags
66
119
  const slug = String(args[0] || "").trim();
67
120
  const wtPath = lanePath(slug);
68
121
  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) {
122
+ // KJC-TSK-1371: the identity and Sentinel files lane setup wrote are not the lane's work.
123
+ const { stdout: status } = await git(["status", "--porcelain", "--untracked-files=all"], wtPath);
124
+ const dirty = laneWork(status, wtPath).join("\n");
125
+ if (dirty && !flags.force) {
71
126
  throw new Error(`lane "${slug}" has uncommitted changes — commit them (through the gate) or discard with --force`);
72
127
  }
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 };
128
+ // HU-3114: the lane's background indexer must not outlive it (any age); a failure never blocks the removal.
129
+ const stopIndexer = deps.stopBootstrap ?? stopBootstrap;
130
+ const indexerStopped = await stopIndexer(wtPath, { force: true }).catch((err) => {
131
+ console.warn(`warning: could not stop the background indexer of "${slug}": ${err.message}`);
132
+ return null;
133
+ });
134
+ // The lane's real branch (headless kj-hu-* lanes share the folder); detached HEAD → null.
135
+ // symbolic-ref -q exits 1 on a detached HEAD; any other failure is a real error.
136
+ const branch = await git(["symbolic-ref", "--short", "-q", "HEAD"], wtPath).then((r) => r.stdout.trim() || null, (err) => {
137
+ if (err.code === 1) return null;
138
+ throw err;
139
+ });
140
+ const base = await resolveBase({ git, base: baseName });
141
+ // A missing base must not block removing a clean lane: the branch is kept, with its remedy.
142
+ const ahead = branch ? await commitsNotIn({ git, base, ref: branch }).catch(() => null) : 0;
143
+ // Only setup files left (checked above): git still calls them untracked, so the remove is forced.
144
+ await git(["worktree", "remove", ...(flags.force || status.trim() ? ["--force"] : []), wtPath]);
145
+ // -D after our own check: -d measures against HEAD/upstream, not origin/<base>. Never the base itself.
146
+ const branchDeleted = Boolean(branch) && branch !== baseName && (ahead === 0 || Boolean(flags.force));
147
+ if (branchDeleted) await git(["branch", "-D", branch]);
148
+ const note = !branch ? "detached HEAD, no branch"
149
+ : branchDeleted ? (ahead === 0 ? `branch ${branch} deleted (merged into ${base})` : `branch ${branch} deleted with --force (${ahead ?? "unknown"} commit(s) not in ${base} were discarded)`)
150
+ : ahead === null ? `branch ${branch} kept: base "${base}" not found, cannot tell if it is merged (git branch -D ${branch} to delete it)`
151
+ : `branch ${branch} kept: ${ahead} commit(s) not in ${base} (squash-merged or not fetched? git fetch, then git branch -D ${branch})`;
152
+ console.log(`✓ lane "${slug}" removed; ${note}`);
153
+ return { removed: true, slug, branch, branchDeleted, merged: branch ? ahead === 0 : null, indexerStopped };
76
154
  }
77
155
 
78
156
  if (action === "prune") {
79
157
  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 });
158
+ const base = await resolveBase({ git, base: baseName });
159
+ const lanes = await classifyLanes({ git, laneRoot: lanesDir, base, processes, now: deps.now });
160
+ // HU-3092: a removable lane's own background indexer is stopped first (never in a dry run); one that survives keeps the lane.
161
+ if (!flags.dryRun) await stopLaneIndexers(lanes, { stop: deps.stopIndexer ?? ((l) => stopBootstrap(l.path, { force: true })), stillRunning: deps.indexerRunning ?? ((l) => pidAlive(l.indexer.pid)) });
82
162
  const abandoned = lanes.filter((l) => l.reasons.length === 0);
83
163
  const kept = lanes.filter((l) => l.reasons.length > 0);
84
164
  for (const l of kept) console.log(`kept ${l.slug.padEnd(24)} ${l.reasons.join("; ")}`);
@@ -87,7 +167,7 @@ export async function worktreeCommand({ config = null, action, args = [], flags
87
167
  const { empty: branches, withWork } = await looseLaneBranches({ git, base });
88
168
  for (const b of withWork) console.log(`kept branch ${b.branch.padEnd(17)} ${b.reason}`);
89
169
  if (flags.dryRun) {
90
- for (const l of abandoned) console.log(`would retire ${l.slug} (${l.branch ?? "detached"})`);
170
+ for (const l of abandoned) console.log(`would retire ${l.slug} (${l.branch ?? "detached"})${l.indexer ? ` (stopping its background index, pid ${l.indexer.pid})` : ""}`);
91
171
  for (const b of branches) console.log(`would delete branch ${b}`);
92
172
  return { wouldRemove: abandoned, kept, wouldRemoveBranches: branches, branchesKept: withWork };
93
173
  }
@@ -96,7 +176,8 @@ export async function worktreeCommand({ config = null, action, args = [], flags
96
176
  console.log(`✓ deleted branch ${b}`);
97
177
  }
98
178
  for (const l of abandoned) {
99
- await git(["worktree", "remove", l.path]);
179
+ // Judged abandoned above: what is left are setup or regenerable files, which git would refuse.
180
+ await git(["worktree", "remove", "--force", l.path]);
100
181
  if (l.branch) await git(["branch", "-D", l.branch]);
101
182
  console.log(`✓ retired ${l.slug} (${l.branch ?? "detached"})`);
102
183
  }
@@ -28,6 +28,7 @@ const DEFAULTS = {
28
28
  discover: { provider: null, model: null },
29
29
  architect: { provider: null, model: null },
30
30
  hu_reviewer: { provider: null, model: null },
31
+ translator: { provider: null, model: null },
31
32
  start: { provider: null, model: "haiku" }
32
33
  },
33
34
  pipeline: {