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
@@ -8,15 +8,17 @@
8
8
  import { addHu, updateHuStatus } from "../plan/plan-hu-ops.js";
9
9
  import { generatePlanId } from "../plan/plan-id.js";
10
10
  import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
11
+ import { migrateLanePlans } from "../plan/lane-plans.js";
11
12
  import { detectHostAgent } from "../utils/agent-detect.js";
12
13
  import { readIdentity } from "../identity/store.js";
13
14
  import { effectiveGitEmail } from "../identity/detect.js";
14
15
  import { roleSkills } from "../skills/role-skills.js";
15
16
  import { readBoardConfig, setBoardBackend } from "../hu/board-config.js";
16
17
  import { createHu, listHus, claim, setStatus, assertNotHeld, isOffline } from "../hu/github-board.js";
17
- import { assertGhIdentity } from "../hu/gh-identity.js";
18
+ import { pinGhIdentity } from "../hu/gh-identity.js";
18
19
  import { listWithCache, queueMove, flushQueue } from "../hu/team-board-cache.js";
19
20
  import { writeTeamMirror } from "../hu/team-board-mirror.js";
21
+ import { startAction } from "./hu-start.js";
20
22
 
21
23
  // KJC-TSK-0661 (Jorge's friction): every HU records WHO created it — the
22
24
  // board must never show work nobody can trace. Host agent env → that agent;
@@ -58,11 +60,14 @@ function linesOf(v) {
58
60
  }
59
61
 
60
62
  /** KJC-TSK-1163: kj hu add on the shared board; the same no-duplicates rule, checked against the issues. */
61
- async function addToTeamBoard({ repo, title, flags, deps, emit }) {
63
+ async function addToTeamBoard({ projectDir, repo, title, flags, deps, emit }) {
62
64
  if (flags.id) throw new Error(`--id is not used with the shared board: GitHub gives each HU its id (the issue number)`);
63
65
  let hu;
66
+ let existing;
67
+ const ref = flags.ref ? String(flags.ref).trim() : null;
64
68
  try {
65
- const { identical, similar } = findTitleMatches(title, await (deps.listTeamHus ?? listHus)({ repo }));
69
+ existing = await (deps.listTeamHus ?? listHus)({ repo });
70
+ const { identical, similar } = findTitleMatches(title, existing);
66
71
  if (identical.length > 0) {
67
72
  throw new Error(`An HU with this title already exists: ${identical[0].short_id} (status: ${identical[0].status}). Work that card or move it with: kj hu move ${identical[0].short_id} <status>`);
68
73
  }
@@ -71,13 +76,15 @@ async function addToTeamBoard({ repo, title, flags, deps, emit }) {
71
76
  console.warn("⚠ possible duplicate — check before working it:\n" + candidates);
72
77
  }
73
78
  hu = await (deps.createTeamHu ?? createHu)({
74
- repo, title, acceptanceCriteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)], tests: linesOf(flags.tests), ref: flags.ref ? String(flags.ref).trim() : null,
79
+ repo, title, acceptanceCriteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)], tests: linesOf(flags.tests), ref,
75
80
  });
76
81
  } catch (err) {
77
82
  // KJC-TSK-1165: an id is only ever minted by GitHub; nothing is created offline.
78
83
  if (isOffline(err)) throw new Error(`creating a HU needs the network: GitHub gives it its id and no local one is made up (${err.message})`, { cause: err });
79
84
  throw err;
80
85
  }
86
+ // KJC-BUG-0443: the gates read the mirror; the new HU is in it before the next kj hu list.
87
+ await (deps.writeMirror ?? writeTeamMirror)(projectDir, [...existing, { ...hu, claimed_by: null, ref }], { repo });
81
88
  return emit({ ...hu, board: "github", repo }, `✓ HU created: ${hu.short_id} (pending) in the shared board, issue #${hu.number} of ${repo}`);
82
89
  }
83
90
 
@@ -176,6 +183,61 @@ export async function backlogPlan(projectDir) {
176
183
  return loadPlan(projectDir, planId);
177
184
  }
178
185
 
186
+ // KJC-BUG-0305 (issue #2042): the HU Board is always kj's board. An external
187
+ // tracker coexists with it: each HU points at its replica there (--ref).
188
+ const externalBoardOf = (config) => (config?.state_backend === "external" ? (config?.board?.name || "the external board") : null);
189
+
190
+ // KJC-TSK-1166 (TEAM-GH-E): the HUs created before the shared board, moved to it once.
191
+ function migrateAction({ projectDir, board, flags, deps, emit, who }) {
192
+ if (flags.to !== "github") throw new Error("usage: kj hu migrate --to github [--dry-run]");
193
+ if (board.backend !== "github") throw new Error("the shared board is off in this repo: turn it on first with kj hu backend github");
194
+ return migrateToTeamBoard({ projectDir, repo: board.repo, who, dryRun: Boolean(flags.dryRun), deps, emit });
195
+ }
196
+
197
+ async function backendAction({ projectDir, args, deps, emit }) {
198
+ if (!args[0]) throw new Error("usage: kj hu backend <github | local>");
199
+ const r = await (deps.setBackend ?? setBoardBackend)({ projectDir, backend: args[0] });
200
+ const where = r.backend === "github" ? `GitHub Issues of ${r.repo}` : "the local board";
201
+ const files = r.gitignoreChanged ? `${r.file} and .gitignore` : r.file;
202
+ return emit(r, `✓ HU Board: ${where} — commit ${files} in a PR so the whole team inherits it`);
203
+ }
204
+
205
+ // #2566: plans a lane saved under its own slug move to the main checkout's; loud, never blocking.
206
+ async function migrateLanePlansLoudly(projectDir) {
207
+ const { moved, kept } = await migrateLanePlans(projectDir)
208
+ .catch((err) => { console.warn(`⚠ plans from lanes not migrated: ${err.message}`); return { moved: [], kept: [] }; });
209
+ const keptNote = kept.length ? `, ${kept.length} kept (name taken, compare and delete): ${kept.join(", ")}` : "";
210
+ if (moved.length || kept.length) console.warn(`plans from lanes: ${moved.length} moved to the main checkout${keptNote}`);
211
+ }
212
+
213
+ async function listTeam({ projectDir, repo, flags, deps }) {
214
+ const { source, rows: hus, updatedAt } = await listWithCache({ projectDir, repo, list: deps.listTeamHus ?? listHus });
215
+ if (source === "cache") console.warn(`⚠ GitHub did not answer: this is the local copy of the board from ${updatedAt}`);
216
+ // KJC-TSK-1166: what GitHub answered is what kj board shows, as a read-only plan.
217
+ else await writeTeamMirror(projectDir, hus, { repo });
218
+ const rows = hus.map((h) => ({ ...h, id: h.short_id, plan: "github" }));
219
+ if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
220
+ for (const r of rows) {
221
+ const held = r.claimed_by ? " · held by " + r.claimed_by : "";
222
+ console.log(`${r.short_id.padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${repo}${held}]`);
223
+ }
224
+ if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
225
+ return rows;
226
+ }
227
+
228
+ async function moveTeamOrQueue({ projectDir, repo, args, flags, deps, emit, who }) {
229
+ try {
230
+ return await moveOnTeamBoard({ repo, args, flags, deps, emit, who });
231
+ } catch (err) {
232
+ // Offline: the move waits in the queue; GitHub still decides when it is applied.
233
+ if (!isOffline(err)) throw err;
234
+ const number = Number(/^(?:HU-|#)(\d+)$/i.exec(args[0])[1]);
235
+ queueMove({ projectDir, number, status: args[1], who, release: Boolean(flags.release) });
236
+ console.warn(`⚠ GitHub did not answer: HU-${number} → ${args[1]} queued, applied when GitHub answers again`);
237
+ return emit({ id: `HU-${number}`, status: args[1], queued: true }, `⏳ HU-${number} → ${args[1]} (queued)`);
238
+ }
239
+ }
240
+
179
241
  // KJC-TSK-1047 (TEAM-A): who works this clone, the one a started card is reserved to.
180
242
  const workerOf = (projectDir) => readIdentity(projectDir)?.gh_user || effectiveGitEmail(projectDir) || null;
181
243
 
@@ -183,238 +245,243 @@ export async function huCommand({ config = null, action, args = [], flags = {},
183
245
  const projectDir = config?.projectDir || process.cwd();
184
246
  const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
185
247
 
186
- // KJC-BUG-0305 (issue #2042): the HU Board is always kj's board. An external
187
- // tracker coexists with it: each HU points at its replica there (--ref).
188
- const externalBoard = config?.state_backend === "external" ? (config?.board?.name || "the external board") : null;
189
-
190
248
  // KJC-TSK-1163/1164 (ADR 0025): the repo's board; with the shared one on, add/list/move go to GitHub.
191
249
  // The repo turns its board on once; the file travels in a PR. Before reading it: it may be what is fixed.
192
- if (action === "backend") {
193
- if (!args[0]) throw new Error("usage: kj hu backend <github | local>");
194
- const r = await (deps.setBackend ?? setBoardBackend)({ projectDir, backend: args[0] });
195
- const where = r.backend === "github" ? `GitHub Issues of ${r.repo}` : "the local board";
196
- const files = r.gitignoreChanged ? `${r.file} and .gitignore` : r.file;
197
- return emit(r, `✓ HU Board: ${where} — commit ${files} in a PR so the whole team inherits it`);
198
- }
250
+ if (action === "backend") return backendAction({ projectDir, args, deps, emit });
199
251
  const board = await (deps.boardConfig ?? readBoardConfig)(projectDir);
200
- // KJC-TSK-1186: the shared board is written as the active gh account; with an identity declared
201
- // it must be that one, said with the remedy before any gh call instead of a GraphQL permission error.
202
- if (board.backend === "github") (deps.assertGhIdentity ?? assertGhIdentity)({ projectDir });
252
+ const shared = board.backend === "github";
253
+ // KJC-TSK-1186 + KJC-BUG-0406: with an identity declared, every gh call of the command goes out as
254
+ // that account (GH_TOKEN pinned before the first one), whatever another session switches to.
255
+ if (shared) (deps.pinGhIdentity ?? pinGhIdentity)({ projectDir });
203
256
  const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
204
- // KJC-TSK-1166 (TEAM-GH-E): the HUs created before the shared board, moved to it once.
205
- if (action === "migrate") {
206
- if (flags.to !== "github") throw new Error("usage: kj hu migrate --to github [--dry-run]");
207
- if (board.backend !== "github") throw new Error("the shared board is off in this repo: turn it on first with kj hu backend github");
208
- return migrateToTeamBoard({ projectDir, repo: board.repo, who, dryRun: Boolean(flags.dryRun), deps, emit });
209
- }
257
+ if (action === "migrate") return migrateAction({ projectDir, board, flags, deps, emit, who });
210
258
  // KJC-TSK-1165 (TEAM-GH-D): moves made without network go first, once GitHub answers.
211
- if (board.backend === "github") await applyQueuedMoves({ projectDir, repo: board.repo, deps });
212
- if (board.backend === "github" && action === "list") {
213
- const { source, rows: hus, updatedAt } = await listWithCache({ projectDir, repo: board.repo, list: deps.listTeamHus ?? listHus });
214
- if (source === "cache") console.warn(`⚠ GitHub did not answer: this is the local copy of the board from ${updatedAt}`);
215
- // KJC-TSK-1166: what GitHub answered is what kj board shows, as a read-only plan.
216
- else await writeTeamMirror(projectDir, hus, { repo: board.repo });
217
- const rows = hus.map((h) => ({ ...h, id: h.short_id, plan: "github" }));
218
- if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
219
- for (const r of rows) {
220
- const held = r.claimed_by ? " · held by " + r.claimed_by : "";
221
- console.log(`${r.short_id.padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${board.repo}${held}]`);
222
- }
223
- if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
224
- return rows;
225
- }
259
+ if (shared) await applyQueuedMoves({ projectDir, repo: board.repo, deps });
260
+ if (action === "list") await migrateLanePlansLoudly(projectDir);
261
+ if (shared && action === "list") return listTeam({ projectDir, repo: board.repo, flags, deps });
226
262
  // KJC-BUG-0392: a zero-padded id (HU-0104) is a local HU from before the shared board: it stays local.
227
263
  const legacyLocal = /^HU-0\d+$/i.test(String(args[0] ?? ""));
228
- if (board.backend === "github" && action === "move" && !legacyLocal) {
229
- try {
230
- return await moveOnTeamBoard({ repo: board.repo, args, flags, deps, emit, who });
231
- } catch (err) {
232
- // Offline: the move waits in the queue; GitHub still decides when it is applied.
233
- if (!isOffline(err)) throw err;
234
- const number = Number(/^(?:HU-|#)(\d+)$/i.exec(args[0])[1]);
235
- queueMove({ projectDir, number, status: args[1], who, release: Boolean(flags.release) });
236
- console.warn(`⚠ GitHub did not answer: HU-${number} → ${args[1]} queued, applied when GitHub answers again`);
237
- return emit({ id: `HU-${number}`, status: args[1], queued: true }, `⏳ HU-${number} → ${args[1]} (queued)`);
238
- }
264
+ if (shared && action === "move" && !legacyLocal) return moveTeamOrQueue({ projectDir, repo: board.repo, args, flags, deps, emit, who });
265
+
266
+ // KJC-TSK-1257: one function per action on the local board (add also routes to the shared one).
267
+ if (action === "list") return listLocal({ projectDir, flags });
268
+ if (action === "add") return addAction({ projectDir, board, args, flags, deps, emit, externalBoard: externalBoardOf(config) });
269
+ if (action === "move") return moveLocal({ projectDir, args, flags, deps, emit });
270
+ // KJC-TSK-1372 (#2853): card, lane, running, in that order.
271
+ if (action === "start") {
272
+ const findCard = deps.findCard ?? (shared ? findSharedCard(board.repo, deps) : findLocalCard(projectDir));
273
+ const move = () => huCommand({ config, action: "move", args: [args[0], "running"], flags: { json: flags.json }, deps });
274
+ return startAction({ config, args, flags, deps: { ...deps, findCard }, move, who, emit });
239
275
  }
240
276
 
241
- if (action === "list") {
242
- const plans = await listPlans(projectDir);
243
- const rows = [];
244
- for (const meta of plans) {
245
- const plan = await loadPlan(projectDir, meta.planId);
246
- for (const h of plan.hus || []) {
247
- rows.push({
248
- id: h.id, short_id: h.short_id, title: h.title, status: h.status,
249
- plan: plan.alias || plan.planId,
250
- // KJC-TSK-0661: provenance — where a card came from is part of
251
- // the card. Older HUs predate the stamp: shown as "?".
252
- created_by: h.created_by || null, created_at: h.createdAt || null,
253
- external_ref: h.external_ref || null,
254
- claimed_by: h.claimed_by || null, claimed_at: h.claimed_at || null,
255
- });
256
- }
257
- }
258
- if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
259
- for (const r of rows) {
260
- const reserved = r.status === "running" && r.claimed_by ? `reserved by ${r.claimed_by}` : null;
261
- const suffixes = [r.created_at?.slice(0, 10), r.external_ref, reserved].filter(Boolean).map((s) => ` · ${s}`).join("");
262
- const origin = `${r.plan} · by ${r.created_by || "?"}${suffixes}`;
263
- console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
277
+ throw new Error(`unknown action "${action}" — use: add | move | list | start | backend`);
278
+ }
279
+
280
+ async function listLocal({ projectDir, flags }) {
281
+ const plans = await listPlans(projectDir);
282
+ const rows = [];
283
+ for (const meta of plans) {
284
+ const plan = await loadPlan(projectDir, meta.planId);
285
+ for (const h of plan.hus || []) {
286
+ rows.push({
287
+ id: h.id, short_id: h.short_id, title: h.title, status: h.status,
288
+ plan: plan.alias || plan.planId,
289
+ // KJC-TSK-0661: provenance — where a card came from is part of
290
+ // the card. Older HUs predate the stamp: shown as "?".
291
+ created_by: h.created_by || null, created_at: h.createdAt || null,
292
+ external_ref: h.external_ref || null,
293
+ claimed_by: h.claimed_by || null, claimed_at: h.claimed_at || null,
294
+ });
264
295
  }
265
- if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
266
- else console.log("→ work one: ask your agent, or kj run --plan <plan> · discard one: kj hu move <id> skipped");
267
- return rows;
268
296
  }
297
+ if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
298
+ for (const r of rows) {
299
+ const reserved = r.status === "running" && r.claimed_by ? `reserved by ${r.claimed_by}` : null;
300
+ const suffixes = [r.created_at?.slice(0, 10), r.external_ref, reserved].filter(Boolean).map((s) => ` · ${s}`).join("");
301
+ const origin = `${r.plan} · by ${r.created_by || "?"}${suffixes}`;
302
+ console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
303
+ }
304
+ if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
305
+ else console.log("→ work one: ask your agent · discard one: kj hu move <id> skipped");
306
+ return rows;
307
+ }
269
308
 
270
- if (action === "add") {
271
- const title = args[0];
272
- if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
273
- // KJC-TSK-1163 (ADR 0025): with the shared board on, the HU is an issue and GitHub gives its id.
274
- if (board.backend === "github") return addToTeamBoard({ repo: board.repo, title, flags, deps, emit });
275
- // Cross-plan title dedup (KJC-TSK-0675) — check BEFORE creating anything.
276
- const allHus = [];
277
- for (const meta of await listPlans(projectDir)) {
278
- const p = await loadPlan(projectDir, meta.planId);
279
- for (const h of p.hus || []) allHus.push({ ...h, plan: p.alias || p.name || p.planId });
280
- }
281
- const { identical, similar } = findTitleMatches(title, allHus);
282
- if (identical.length > 0) {
283
- const ref = identical[0];
284
- throw new Error(
285
- `An HU with this title already exists: ${ref.short_id || ref.id} (status: ${ref.status}, plan: ${ref.plan}). `
286
- + `HUs are never duplicated — work that card, move it with: kj hu move ${ref.short_id || ref.id} <status>, `
287
- + `or run kj hu list before adding.`
288
- );
289
- }
290
- if (similar.length > 0) {
291
- const plural = similar.length === 1 ? "" : "s";
292
- const candidates = similar.map((h) => ` · ${h.short_id || h.id} (${h.status}, plan: ${h.plan}) "${h.title}"`).join("\n");
293
- console.warn(`⚠ possible duplicate${plural} — check before working it:\n${candidates}`);
294
- }
295
- const plan = await backlogPlan(projectDir);
296
- /** Lowest free HU-#### in this plan — ids are permanent, never reused. */
297
- const nextHuRef = (p) => {
298
- const taken = new Set((p.hus || []).map((h) => h.short_id).filter(Boolean));
299
- let n = 1;
300
- while (taken.has(`HU-${String(n).padStart(4, "0")}`)) n += 1;
301
- return `HU-${String(n).padStart(4, "0")}`;
302
- };
303
- // KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
304
- // recreate "fix" an agent improvises loses history and duplicates ids —
305
- // refuse it with the norm spelled out.
306
- if (flags.id) {
307
- const dup = (plan.hus || []).find((h) => h.short_id === flags.id);
308
- if (dup) {
309
- throw new Error(
310
- `HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
311
- + `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
312
- + `or discard it with: kj hu move ${flags.id} skipped`
313
- );
314
- }
315
- }
316
- // KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
317
- // are repeatable AND accept multiline strings; --criteria stays as the
318
- // legacy single-criterion alias.
319
- const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
320
- if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
321
- throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
322
- }
323
- const splitLines = linesOf;
324
- const hu = addHu(plan, {
325
- title,
326
- // KJC-BUG-0189: the card-first gate demands a branch that references a
327
- // card, and it is this command the gate sends the agent to. The
328
- // canonical plan id (hu_plan-<stamp>-<rand>_001) does not match
329
- // CARD_REF_RE, so without an explicit --id the card kj just created
330
- // could not satisfy kj's own gate. Sequential HU-#### does.
331
- short_id: flags.id || nextHuRef(plan),
332
- acceptance_criteria: [...splitLines(flags.ac), ...splitLines(flags.criteria)],
333
- acceptance_tests: splitLines(flags.tests),
334
- scope: flags.scope || null,
335
- task_type: flags.taskType || undefined,
336
- });
337
- hu.created_by = creatorLabel();
338
- if (flags.ref) hu.external_ref = String(flags.ref).trim();
339
- await savePlan(projectDir, plan);
340
- if (externalBoard && !hu.external_ref) {
341
- console.log(`→ replicate it in ${externalBoard} through your agent's MCP/tools and link both: kj hu add ... --ref <id in ${externalBoard}>`);
342
- }
343
- const missing = [];
344
- if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
345
- if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
346
- if (missing.length > 0) {
347
- console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
348
- }
349
- return emit({
350
- id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
351
- acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
352
- scope: hu.scope, task_type: hu.task_type, external_ref: hu.external_ref || null,
353
- }, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
309
+ /** Cross-plan title dedup (KJC-TSK-0675) — checked BEFORE creating anything. */
310
+ async function refuseDuplicateTitle(projectDir, title) {
311
+ const allHus = [];
312
+ for (const meta of await listPlans(projectDir)) {
313
+ const p = await loadPlan(projectDir, meta.planId);
314
+ for (const h of p.hus || []) allHus.push({ ...h, plan: p.alias || p.name || p.planId });
315
+ }
316
+ const { identical, similar } = findTitleMatches(title, allHus);
317
+ if (identical.length > 0) {
318
+ const ref = identical[0];
319
+ throw new Error(
320
+ `An HU with this title already exists: ${ref.short_id || ref.id} (status: ${ref.status}, plan: ${ref.plan}). `
321
+ + `HUs are never duplicated — work that card, move it with: kj hu move ${ref.short_id || ref.id} <status>, `
322
+ + `or run kj hu list before adding.`
323
+ );
324
+ }
325
+ if (similar.length > 0) {
326
+ const plural = similar.length === 1 ? "" : "s";
327
+ const candidates = similar.map((h) => ` · ${h.short_id || h.id} (${h.status}, plan: ${h.plan}) "${h.title}"`).join("\n");
328
+ console.warn(`⚠ possible duplicate${plural} — check before working it:\n${candidates}`);
354
329
  }
330
+ }
355
331
 
356
- if (action === "move") {
357
- const [ref, status] = args;
358
- if (!ref || !status) throw new Error("usage: kj hu move <id> <status>");
359
- if (!HU_STATUSES.includes(status)) {
360
- throw new Error(`invalid status "${status}" — valid: ${HU_STATUSES.join(", ")}`);
361
- }
362
- // Exact canonical id wins outright (it IS the disambiguator); only then
363
- // fall back to short_id matches — which can repeat between plans, so
364
- // silently moving the first hit could move the wrong card.
365
- const byId = [];
366
- const byShort = [];
367
- const running = [];
368
- for (const meta of await listPlans(projectDir)) {
369
- const plan = await loadPlan(projectDir, meta.planId);
370
- for (const hu of plan.hus || []) {
371
- if (hu.id === ref) byId.push({ plan, hu });
372
- else if (hu.short_id === ref) byShort.push({ plan, hu });
373
- if (hu.status === "running") running.push(hu);
374
- }
375
- }
376
- const matches = byId.length > 0 ? byId : byShort;
377
- if (matches.length === 0) throw new Error(`HU "${ref}" not found — see kj hu list`);
378
- if (matches.length > 1) {
379
- const ids = matches.map((m) => m.hu.id).join(", ");
380
- throw new Error(`"${ref}" is ambiguous (${matches.length} matches: ${ids}) — use the full id`);
381
- }
382
- const { plan, hu } = matches[0];
383
- // KJC-TSK-0688 (MG-C): the nudge fires AT the deviation — a second
384
- // concurrent task belongs in its own lane, not in the shared tree.
385
- const others = running.filter((r) => r.id !== hu.id);
386
- if (status === "running" && hu.status !== "running" && others.length > 0) {
387
- const slug = String(hu.short_id || hu.id).toLowerCase();
388
- console.warn(
389
- `⚠ ${others.map((r) => r.short_id || r.id).join(", ")} ${others.length === 1 ? "is" : "are"} already running — `
390
- + `a second concurrent task should live in its own lane: kj worktree start ${slug}`
391
- );
392
- }
393
- // KJC-TSK-1047 (TEAM-A): a running card reserved by someone else moves only with --release.
394
- const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
395
- const label = hu.short_id || hu.id;
396
- // An unidentified caller is not the one who reserved it.
397
- const takenOver = hu.status === "running" && hu.claimed_by && hu.claimed_by !== who;
398
- if (takenOver && !flags.release) {
399
- throw new Error(`${label} is reserved by ${hu.claimed_by} since ${hu.claimed_at} — releasing it is explicit: kj hu move ${ref} ${status} --release`);
400
- }
401
- const starting = status === "running" && hu.status !== "running";
402
- // A running card nobody reserved is reserved by who moves it there again, once kj knows who.
403
- const unclaimedRunning = status === "running" && !hu.claimed_by && who;
404
- updateHuStatus(plan, hu.id, status);
405
- // Starting it, or releasing it to any status, rewrites the reservation: who did it, or nobody.
406
- if (starting || takenOver || unclaimedRunning) Object.assign(hu, { claimed_by: who ?? null, claimed_at: who ? new Date().toISOString() : null });
407
- if (starting && !who) console.warn(`⚠ ${label} is not reserved: kj cannot tell who works this clone — declare it with kj identity set`);
408
- await savePlan(projectDir, plan);
409
- // KJC-TSK-1065: the host session starts the card here; it learns which skills apply and where to read them.
410
- if (starting && !flags.json) {
411
- const text = [hu.title, hu.scope, ...(Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [])].filter(Boolean).join(" ");
412
- const { skills } = await (deps.skills ?? roleSkills)({ role: "coder", task: text, projectDir });
413
- const where = (s) => (s.path ? ": read " + s.path : " (native)");
414
- if (skills.length) console.log([`skills for ${label} (read them before you start):`, ...skills.map((s) => "- " + s.name + where(s))].join("\n"));
332
+ /** Lowest free HU-#### in this plan — ids are permanent, never reused. */
333
+ function nextHuRef(plan) {
334
+ const taken = new Set((plan.hus || []).map((h) => h.short_id).filter(Boolean));
335
+ let n = 1;
336
+ while (taken.has(`HU-${String(n).padStart(4, "0")}`)) n += 1;
337
+ return `HU-${String(n).padStart(4, "0")}`;
338
+ }
339
+
340
+ // KJC-TSK-0678 (issue #1296): complete HUs in ONE call. --ac / --tests
341
+ // are repeatable AND accept multiline strings; --criteria stays as the
342
+ // legacy single-criterion alias.
343
+ const TASK_TYPES = ["sw", "infra", "doc", "add-tests", "refactor"];
344
+
345
+ function assertAddFlags(plan, flags) {
346
+ // KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
347
+ // recreate "fix" an agent improvises loses history and duplicates ids —
348
+ // refuse it with the norm spelled out.
349
+ const dup = flags.id ? (plan.hus || []).find((h) => h.short_id === flags.id) : null;
350
+ if (dup) {
351
+ throw new Error(
352
+ `HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
353
+ + `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
354
+ + `or discard it with: kj hu move ${flags.id} skipped`
355
+ );
356
+ }
357
+ if (flags.taskType && !TASK_TYPES.includes(flags.taskType)) {
358
+ throw new Error(`invalid --task-type "${flags.taskType}" — valid: ${TASK_TYPES.join(", ")}`);
359
+ }
360
+ }
361
+
362
+ async function addAction({ projectDir, board, args, flags, deps, emit, externalBoard }) {
363
+ const title = args[0];
364
+ if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
365
+ // KJC-TSK-1163 (ADR 0025): with the shared board on, the HU is an issue and GitHub gives its id.
366
+ if (board.backend === "github") return addToTeamBoard({ projectDir, repo: board.repo, title, flags, deps, emit });
367
+ await refuseDuplicateTitle(projectDir, title);
368
+ const plan = await backlogPlan(projectDir);
369
+ assertAddFlags(plan, flags);
370
+ const hu = addHu(plan, {
371
+ title,
372
+ // KJC-BUG-0189: the card-first gate demands a branch that references a
373
+ // card, and it is this command the gate sends the agent to. The
374
+ // canonical plan id (hu_plan-<stamp>-<rand>_001) does not match
375
+ // CARD_REF_RE, so without an explicit --id the card kj just created
376
+ // could not satisfy kj's own gate. Sequential HU-#### does.
377
+ short_id: flags.id || nextHuRef(plan),
378
+ acceptance_criteria: [...linesOf(flags.ac), ...linesOf(flags.criteria)],
379
+ acceptance_tests: linesOf(flags.tests),
380
+ scope: flags.scope || null,
381
+ task_type: flags.taskType || undefined,
382
+ });
383
+ hu.created_by = creatorLabel();
384
+ if (flags.ref) hu.external_ref = String(flags.ref).trim();
385
+ await savePlan(projectDir, plan);
386
+ if (externalBoard && !hu.external_ref) {
387
+ console.log(`→ replicate it in ${externalBoard} through your agent's MCP/tools and link both: kj hu add ... --ref <id in ${externalBoard}>`);
388
+ }
389
+ const missing = [];
390
+ if (hu.acceptance_criteria.length === 0) missing.push("acceptance criteria (--ac \"...\")");
391
+ if (hu.acceptance_tests.length === 0) missing.push("tests (--tests \"...\")");
392
+ if (missing.length > 0) {
393
+ console.warn(`⚠ incomplete spec — kj run's spec review will flag it. Missing: ${missing.join(", ")}`);
394
+ }
395
+ return emit({
396
+ id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by,
397
+ acceptance_criteria: hu.acceptance_criteria, acceptance_tests: hu.acceptance_tests,
398
+ scope: hu.scope, task_type: hu.task_type, external_ref: hu.external_ref || null,
399
+ }, `✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
400
+ }
401
+
402
+ /**
403
+ * Exact canonical id wins outright (it IS the disambiguator); only then
404
+ * fall back to short_id matches — which can repeat between plans, so
405
+ * silently moving the first hit could move the wrong card.
406
+ */
407
+ async function findHuToMove(projectDir, ref) {
408
+ const byId = [];
409
+ const byShort = [];
410
+ const running = [];
411
+ for (const meta of await listPlans(projectDir)) {
412
+ const plan = await loadPlan(projectDir, meta.planId);
413
+ for (const hu of plan.hus || []) {
414
+ if (hu.id === ref) byId.push({ plan, hu });
415
+ else if (hu.short_id === ref) byShort.push({ plan, hu });
416
+ if (hu.status === "running") running.push(hu);
415
417
  }
416
- return emit({ id: hu.id, status, claimed_by: hu.claimed_by ?? null }, `✓ ${label} → ${status}`);
417
418
  }
419
+ const matches = byId.length > 0 ? byId : byShort;
420
+ if (matches.length === 0) throw new Error(`HU "${ref}" not found — see kj hu list`);
421
+ if (matches.length > 1) {
422
+ const ids = matches.map((m) => m.hu.id).join(", ");
423
+ throw new Error(`"${ref}" is ambiguous (${matches.length} matches: ${ids}) — use the full id`);
424
+ }
425
+ return { ...matches[0], running };
426
+ }
427
+
428
+ // KJC-TSK-1372: a card as `kj hu start` reads it, from either board; null when there is none.
429
+ const findSharedCard = (repo, deps) => async (ref) => {
430
+ const rows = await (deps.listTeamHus ?? listHus)({ repo });
431
+ const row = rows.find((r) => r.short_id.toLowerCase() === String(ref).toLowerCase());
432
+ return row ? { id: row.short_id, title: row.title, status: row.status, claimed_by: row.claimed_by } : null;
433
+ };
434
+ const findLocalCard = (projectDir) => async (ref) => {
435
+ const found = await findHuToMove(projectDir, ref).catch((err) => {
436
+ if (/not found/.test(err.message)) return null;
437
+ throw err;
438
+ });
439
+ if (!found) return null;
440
+ const { hu } = found;
441
+ return { id: hu.short_id || hu.id, title: hu.title, status: hu.status, claimed_by: hu.status === "running" ? hu.claimed_by ?? null : null };
442
+ };
443
+
444
+ async function moveLocal({ projectDir, args, flags, deps, emit }) {
445
+ const [ref, status] = args;
446
+ if (!ref || !status) throw new Error("usage: kj hu move <id> <status>");
447
+ if (!HU_STATUSES.includes(status)) {
448
+ throw new Error(`invalid status "${status}" — valid: ${HU_STATUSES.join(", ")}`);
449
+ }
450
+ const { plan, hu, running } = await findHuToMove(projectDir, ref);
451
+ // KJC-TSK-0688 (MG-C): the nudge fires AT the deviation — a second
452
+ // concurrent task belongs in its own lane, not in the shared tree.
453
+ const others = running.filter((r) => r.id !== hu.id);
454
+ if (status === "running" && hu.status !== "running" && others.length > 0) {
455
+ const slug = String(hu.short_id || hu.id).toLowerCase();
456
+ console.warn(
457
+ `⚠ ${others.map((r) => r.short_id || r.id).join(", ")} ${others.length === 1 ? "is" : "are"} already running — `
458
+ + `a second concurrent task should live in its own lane: kj worktree start ${slug}`
459
+ );
460
+ }
461
+ // KJC-TSK-1047 (TEAM-A): a running card reserved by someone else moves only with --release.
462
+ const who = deps.who === undefined ? workerOf(projectDir) : deps.who;
463
+ const label = hu.short_id || hu.id;
464
+ // An unidentified caller is not the one who reserved it.
465
+ const takenOver = hu.status === "running" && hu.claimed_by && hu.claimed_by !== who;
466
+ if (takenOver && !flags.release) {
467
+ throw new Error(`${label} is reserved by ${hu.claimed_by} since ${hu.claimed_at} — releasing it is explicit: kj hu move ${ref} ${status} --release`);
468
+ }
469
+ const starting = status === "running" && hu.status !== "running";
470
+ // A running card nobody reserved is reserved by who moves it there again, once kj knows who.
471
+ const unclaimedRunning = status === "running" && !hu.claimed_by && who;
472
+ updateHuStatus(plan, hu.id, status);
473
+ // Starting it, or releasing it to any status, rewrites the reservation: who did it, or nobody.
474
+ if (starting || takenOver || unclaimedRunning) Object.assign(hu, { claimed_by: who ?? null, claimed_at: who ? new Date().toISOString() : null });
475
+ if (starting && !who) console.warn(`⚠ ${label} is not reserved: kj cannot tell who works this clone — declare it with kj identity set`);
476
+ await savePlan(projectDir, plan);
477
+ // KJC-TSK-1065: the host session starts the card here; it learns which skills apply and where to read them.
478
+ if (starting && !flags.json) await printStartSkills({ projectDir, hu, label, deps });
479
+ return emit({ id: hu.id, status, claimed_by: hu.claimed_by ?? null }, `✓ ${label} → ${status}`);
480
+ }
418
481
 
419
- throw new Error(`unknown action "${action}" — use: add | move | list | backend`);
482
+ async function printStartSkills({ projectDir, hu, label, deps }) {
483
+ const text = [hu.title, hu.scope, ...(Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [])].filter(Boolean).join(" ");
484
+ const { skills } = await (deps.skills ?? roleSkills)({ role: "coder", task: text, projectDir });
485
+ const where = (s) => (s.path ? ": read " + s.path : " (native)");
486
+ if (skills.length) console.log([`skills for ${label} (read them before you start):`, ...skills.map((s) => "- " + s.name + where(s))].join("\n"));
420
487
  }