karajan-code 4.70.3 → 5.0.0

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