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