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
@@ -0,0 +1,267 @@
1
+ // KJC-TSK-0541: the project endpoints of /api, composed by routes/api.js.
2
+ import { Router } from 'express';
3
+ import fsp from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ getProjects,
8
+ deleteProject,
9
+ getKjHome,
10
+ getHuBoardPlansDirs,
11
+ listPlanIdsForProject,
12
+ setProjectIsTest,
13
+ getProjectCost,
14
+ addTombstone,
15
+ getDb,
16
+ } from '../../db.js';
17
+ import { markPlanReady, renameProject } from '../../plan-mutations.js';
18
+ import { runPreflight } from '../../preflight.js';
19
+ import { projectPlans } from '../../plans-service.js';
20
+
21
+ const router = Router();
22
+
23
+ /**
24
+ * GET /api/projects - List all projects with story counts.
25
+ */
26
+ router.get('/projects', (_req, res) => {
27
+ try {
28
+ const projects = getProjects();
29
+ res.json(projects);
30
+ } catch (err) {
31
+ res.status(500).json({ error: err.message });
32
+ }
33
+ });
34
+
35
+ /**
36
+ * GET /api/projects/:id - Project detail.
37
+ */
38
+ router.get('/projects/:id', (req, res) => {
39
+ try {
40
+ const projects = getProjects();
41
+ const project = projects.find((p) => p.id === req.params.id);
42
+ if (!project) return res.status(404).json({ error: 'Project not found' });
43
+ res.json(project);
44
+ } catch (err) {
45
+ res.status(500).json({ error: err.message });
46
+ }
47
+ });
48
+
49
+ /**
50
+ * PUT /api/projects/:id/name — PR-G: rename a project.
51
+ *
52
+ * Updates plan.name on every plan JSON belonging to the project AND
53
+ * the projects.name column directly so the UI reflects the new name
54
+ * on the next render. Body: { name: "Linux Assistant Orchestrator" }.
55
+ */
56
+ router.put('/projects/:id/name', (req, res) => {
57
+ try {
58
+ const result = renameProject({ projectId: req.params.id, name: req.body?.name });
59
+ if (!result.ok) {
60
+ const code = /no encontrado/i.test(result.error) ? 404 : 400;
61
+ return res.status(code).json({ error: result.error });
62
+ }
63
+ res.json(result);
64
+ } catch (err) {
65
+ res.status(500).json({ error: err.message });
66
+ }
67
+ });
68
+
69
+ /**
70
+ * PATCH /api/projects/:id/is-test — KJC-TSK-0371 (board polish #3).
71
+ *
72
+ * Sets/clears the per-project ephemeral-cleanup override. The
73
+ * heuristic (id under tmp_/test_/demo_/kj-test- AND last_activity
74
+ * older than 24h → auto-delete on next board start) is the default.
75
+ * `is_test` lets the user override per-project:
76
+ * - body { is_test: 1 } → always treat as ephemeral
77
+ * - body { is_test: 0 } → NEVER auto-clean (keep this one)
78
+ * - body { is_test: null } → revert to the default heuristic
79
+ *
80
+ * Sending anything else is a 400 — explicit ternary, no truthy/
81
+ * falsy coercion, because that exact distinction is what the
82
+ * cleaner relies on.
83
+ */
84
+ router.patch('/projects/:id/is-test', (req, res) => {
85
+ const raw = req.body?.is_test;
86
+ // Accept the three legal sentinel values explicitly — undefined
87
+ // (key missing) is rejected because this endpoint only exists to
88
+ // set or clear, not to "leave as-is".
89
+ const allowed = raw === 0 || raw === 1 || raw === null;
90
+ if (!allowed) {
91
+ return res.status(400).json({ error: 'is_test must be 0, 1, or null' });
92
+ }
93
+ try {
94
+ const ok = setProjectIsTest(req.params.id, raw);
95
+ if (!ok) return res.status(404).json({ error: 'project not found' });
96
+ res.json({ ok: true, project_id: req.params.id, is_test: raw });
97
+ } catch (err) {
98
+ res.status(500).json({ error: err.message });
99
+ }
100
+ });
101
+
102
+ /**
103
+ * GET /api/projects/:id/preflight — Project readiness checks.
104
+ *
105
+ * Returns the same shape regardless of the project's state. Each
106
+ * check carries a status (ok|warn|fail|info), a plain-Spanish
107
+ * detail and (when relevant) a `consequence` describing what the
108
+ * user loses if it stays unfixed. Powers both:
109
+ * - the panel at the top of the board (always visible)
110
+ * - the safety modal that appears before ▶ Run plan / ▶ Run HU
111
+ * when there are blockers or warnings
112
+ *
113
+ * The route resolves projectDir by reading the first plan it can
114
+ * find for this project (plans carry projectDir since v2.7.4). If
115
+ * the project has no plans on disk we still return the env-only
116
+ * checks (Node, Karajan, agents, config) so the user gets feedback
117
+ * even before they generate their first plan.
118
+ */
119
+ router.get('/projects/:id/preflight', async (req, res) => {
120
+ try {
121
+ const projectId = req.params.id;
122
+ let projectDir = null;
123
+ // KJC-BUG-0059 (v2.19.3): scan plans in BOTH the canonical
124
+ // ~/.karajan/plans/ AND the legacy ~/.kj/plans/ — the legacy
125
+ // fallback covers users who started the board before running any
126
+ // `kj` command on a fresh install (so the auto-migrator from
127
+ // v2.19.0 has not fired yet). Pre-fix, this endpoint hard-coded
128
+ // ~/.kj/plans/ as the default, missing every plan post-2.19.0
129
+ // → board card showed "directorio del proyecto: no detectado"
130
+ // even though the plan on disk had a valid projectDir.
131
+ try {
132
+ for await (const plan of projectPlans(projectId)) {
133
+ if (plan?.projectDir) { projectDir = plan.projectDir; break; }
134
+ }
135
+ } catch { /* projectDir stays null → preflight will report it */ }
136
+ const result = await runPreflight({ projectId, projectDir });
137
+ res.json(result);
138
+ } catch (err) {
139
+ res.status(500).json({ error: err.message });
140
+ }
141
+ });
142
+
143
+ /**
144
+ * GET /api/projects/:id/plans-outcome — Plan-level rollups (PR3).
145
+ *
146
+ * Reads every plan JSON for the project and returns the lightweight
147
+ * shape the board needs to render the "Plan finalizado · 8 done · 2
148
+ * failed · 15 min" banner. Plans that haven't finished yet have a
149
+ * null outcome — the front handles that by hiding the banner.
150
+ */
151
+ router.get('/projects/:id/plans-outcome', async (req, res) => {
152
+ try {
153
+ const projectId = req.params.id;
154
+ // KJC-BUG-0059 (v2.19.3): scan canonical + legacy roots so users
155
+ // who have not yet run the auto-migrator still see their plans.
156
+ const out = [];
157
+ for await (const plan of projectPlans(projectId)) {
158
+ if (plan?.version !== 2) continue;
159
+ out.push({
160
+ planId: plan.planId,
161
+ name: plan.name || (plan.task ? plan.task.slice(0, 80) : plan.planId),
162
+ status: plan.status || 'draft',
163
+ updatedAt: plan.updatedAt || null,
164
+ outcome: plan.outcome || null,
165
+ huCount: Array.isArray(plan.hus) ? plan.hus.length : 0,
166
+ // KJC-TSK-1197: a plan mirroring the shared board says so, with its repo.
167
+ readonly: plan.readonly || null,
168
+ repo: plan.readonly ? plan.repo || null : null,
169
+ });
170
+ }
171
+ res.json({ projectId, plans: out });
172
+ } catch (err) {
173
+ res.status(500).json({ error: err.message });
174
+ }
175
+ });
176
+
177
+ /**
178
+ * GET /api/projects/:id/cost - Aggregated $ cost for a project (KJC-TSK-0516).
179
+ * Sums stories.cost_usd, groups by plan_id, returns 404 if project missing.
180
+ */
181
+ router.get('/projects/:id/cost', (req, res) => {
182
+ try {
183
+ const cost = getProjectCost(req.params.id);
184
+ if (!cost) return res.status(404).json({ error: 'Project not found' });
185
+ res.json(cost);
186
+ } catch (err) {
187
+ res.status(500).json({ error: err.message });
188
+ }
189
+ });
190
+
191
+ /**
192
+ * DELETE /api/projects/:id - Cascade delete project + its stories + sessions.
193
+ * Also removes its session and plan directories from disk so the next sync
194
+ * does not re-import them.
195
+ */
196
+ router.delete('/projects/:id', async (req, res) => {
197
+ try {
198
+ const id = req.params.id;
199
+ // Gather filesystem paths to remove BEFORE deleting DB rows so we
200
+ // can stop the next sync from resurrecting anything.
201
+ const db = getDb();
202
+ const storyIds = db.prepare('SELECT id FROM stories WHERE project_id = ?').all(id).map((r) => r.id);
203
+ const sessionIds = db.prepare('SELECT id FROM sessions WHERE project_id = ?').all(id).map((r) => r.id);
204
+ // KJC-BUG-0055: honour KJ_PLANS_DIR (the env override the sync /
205
+ // cleanup-zombies layers already respect). Previously hardcoded to
206
+ // ~/.kj/plans relative to KJ_HOME's parent, which broke when the
207
+ // two homes were configured separately and left orphan plan dirs
208
+ // on disk after a 🗑️ delete.
209
+ // KJC-BUG-0059 (v2.19.3): when removing a deleted project's
210
+ // residual plan dirs, sweep BOTH the canonical and legacy roots
211
+ // so the cleanup is consistent for users mid-migration.
212
+ const fsPaths = [
213
+ ...sessionIds.map((sid) => path.join(getKjHome(), 'sessions', sid)),
214
+ ...getHuBoardPlansDirs().map((root) => path.join(root, id)),
215
+ ];
216
+
217
+ const existed = deleteProject(id);
218
+ if (!existed) return res.status(404).json({ error: 'Project not found' });
219
+
220
+ // Tombstone the project AND its children so chokidar / fullScan
221
+ // can't bring them back via syncSessionFile / syncPlanFile.
222
+ addTombstone('project', id, { source: 'api', fsPaths });
223
+ for (const sid of storyIds) addTombstone('story', sid, { source: 'api' });
224
+ for (const sid of sessionIds) addTombstone('session', sid, { source: 'api' });
225
+
226
+ // Best-effort fs cleanup; failure does NOT undo the tombstones.
227
+ let pathsRemoved = 0;
228
+ for (const p of fsPaths) {
229
+ try { await fsp.rm(p, { recursive: true, force: true }); pathsRemoved++; } catch { /* */ }
230
+ }
231
+ res.json({ deleted: true, pathsRemoved, tombstoned: 1 + storyIds.length + sessionIds.length });
232
+ } catch (err) {
233
+ res.status(500).json({ error: err.message });
234
+ }
235
+ });
236
+
237
+ /**
238
+ * POST /api/projects/:id/ready - Sugar: mark every plan of a project ready
239
+ * in one call. Lets the board expose a single "Mark all certified" button
240
+ * per project view without the UI having to enumerate plan ids first.
241
+ */
242
+ router.post('/projects/:id/ready', (req, res) => {
243
+ try {
244
+ const planIds = listPlanIdsForProject(req.params.id);
245
+ if (planIds.length === 0) {
246
+ return res.status(404).json({ error: 'No plan-backed stories for this project' });
247
+ }
248
+ const results = [];
249
+ for (const planId of planIds) {
250
+ const r = markPlanReady({ planId, projectId: req.params.id });
251
+ results.push({ planId, ...r });
252
+ }
253
+ const totalCertified = results
254
+ .filter((r) => r.ok)
255
+ .reduce((acc, r) => acc + r.count, 0);
256
+ res.json({
257
+ ready: true,
258
+ projectId: req.params.id,
259
+ plans: results,
260
+ totalCertified,
261
+ });
262
+ } catch (err) {
263
+ res.status(500).json({ error: err.message });
264
+ }
265
+ });
266
+
267
+ export default router;
@@ -0,0 +1,135 @@
1
+ // KJC-TSK-0541: the prompt endpoints of /api (questions the runner leaves for the board),
2
+ // composed by routes/api.js.
3
+ import { Router } from 'express';
4
+ import fsp from 'node:fs/promises';
5
+ import path from 'node:path';
6
+
7
+ import { getKjHome, addTombstone, isTombstoned as isTombstonedFn } from '../../db.js';
8
+ import { publish as publishEvent } from '../../event-bus.js';
9
+ import { pathExists } from '../../plans-service.js';
10
+
11
+ const router = Router();
12
+
13
+ /**
14
+ * DELETE /api/prompts/:promptId - Discard a pending prompt the runner left
15
+ * behind (KJC-TSK-0380). Removes the RPC files from disk, tombstones the
16
+ * prompt id so a stale chokidar event can't recreate it, and emits a
17
+ * `prompt:resolved` event so all open boards close the modal.
18
+ */
19
+ router.delete('/prompts/:promptId', async (req, res) => {
20
+ try {
21
+ const promptId = req.params.promptId;
22
+ const dir = promptsDir();
23
+ const main = path.join(dir, `${promptId}.json`);
24
+ const answer = path.join(dir, `${promptId}.answer.json`);
25
+ let removed = 0;
26
+ for (const p of [main, answer]) {
27
+ try { await fsp.unlink(p); removed++; } catch { /* missing is fine */ }
28
+ }
29
+ addTombstone('prompt', promptId, { source: 'api', fsPaths: [main, answer] });
30
+ // Re-use the same event the runner emits when answered, so any open
31
+ // modal in any connected board client closes itself.
32
+ publishEvent({ type: 'prompt-resolved', promptId });
33
+ res.json({ deleted: true, filesRemoved: removed });
34
+ } catch (err) {
35
+ res.status(500).json({ error: err.message });
36
+ }
37
+ });
38
+
39
+ /**
40
+ * Resolve the prompts directory the runner writes RPC packets to.
41
+ * Same convention as src/utils/board-prompt-bridge.js on the runner.
42
+ */
43
+ function promptsDir() {
44
+ return path.join(getKjHome(), 'prompts');
45
+ }
46
+
47
+ /**
48
+ * GET /api/prompts - List currently pending prompts. The runner is
49
+ * blocked on each one waiting for an answer; the board reads this on
50
+ * boot to surface modals for prompts that were already pending when
51
+ * the page loaded (the SSE event fires only on new arrivals).
52
+ */
53
+ // KJC-BUG-0038: prompts whose runner crashed leave the .json behind in
54
+ // ~/.kj/prompts/. Without a TTL, every board reload re-shows the zombie
55
+ // modal "Karajan needs an answer" with no runner behind it — the user
56
+ // can answer but nothing happens. 30 minutes covers the longest legitimate
57
+ // Solomon escalation; anything older is a crashed runner.
58
+ const PROMPT_ZOMBIE_TTL_MS = 30 * 60 * 1000;
59
+
60
+ router.get('/prompts', async (_req, res) => {
61
+ try {
62
+ const dir = promptsDir();
63
+ if (!(await pathExists(dir))) return res.json([]);
64
+ const entries = await fsp.readdir(dir);
65
+ const result = [];
66
+ const now = Date.now();
67
+ for (const name of entries) {
68
+ if (!name.endsWith('.json') || name.endsWith('.answer.json')) continue;
69
+ const promptId = name.replace(/\.json$/, '');
70
+ const mainPath = path.join(dir, name);
71
+ const answerPath = path.join(dir, `${promptId}.answer.json`);
72
+ // KJC-TSK-0380 — skip tombstoned prompts and clean their files.
73
+ if (isTombstonedFn('prompt', promptId)) {
74
+ try { await fsp.unlink(mainPath); } catch { /* */ }
75
+ try { await fsp.unlink(answerPath); } catch { /* */ }
76
+ continue;
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(await fsp.readFile(mainPath, 'utf-8'));
81
+ } catch { continue; /* malformed — skip */ }
82
+ // KJC-BUG-0038: zombie detection. Prefer parsed.createdAt; fall back
83
+ // to mtime if the runner skipped the timestamp (older formats).
84
+ const createdMs = Date.parse(parsed?.createdAt || '');
85
+ const ageRef = Number.isFinite(createdMs) ? createdMs : await safeMtime(mainPath);
86
+ if (ageRef && (now - ageRef) > PROMPT_ZOMBIE_TTL_MS) {
87
+ try { await fsp.unlink(mainPath); } catch { /* */ }
88
+ try { await fsp.unlink(answerPath); } catch { /* */ }
89
+ addTombstone('prompt', promptId, { source: 'zombie-ttl', fsPaths: [mainPath, answerPath] });
90
+ continue;
91
+ }
92
+ result.push(parsed);
93
+ }
94
+ result.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
95
+ res.json(result);
96
+ } catch (err) {
97
+ res.status(500).json({ error: err.message });
98
+ }
99
+ });
100
+
101
+ async function safeMtime(p) {
102
+ try { return (await fsp.stat(p)).mtimeMs; } catch { return 0; }
103
+ }
104
+
105
+ /**
106
+ * POST /api/prompts/:promptId/answer - Body { answer }. Writes a
107
+ * sibling .answer.json the runner is polling for. The runner reads
108
+ * it, deletes both files, and resolves askQuestion. An answer of
109
+ * "stop" tells the runner to abort the session.
110
+ */
111
+ router.post('/prompts/:promptId/answer', async (req, res) => {
112
+ try {
113
+ const { answer } = req.body || {};
114
+ if (typeof answer !== 'string') {
115
+ return res.status(400).json({ error: 'Body must contain an "answer" string' });
116
+ }
117
+ const dir = promptsDir();
118
+ await fsp.mkdir(dir, { recursive: true });
119
+ const promptPath = path.join(dir, `${req.params.promptId}.json`);
120
+ if (!(await pathExists(promptPath))) {
121
+ return res.status(404).json({ error: `Prompt not found: ${req.params.promptId}` });
122
+ }
123
+ const answerPath = path.join(dir, `${req.params.promptId}.answer.json`);
124
+ await fsp.writeFile(
125
+ answerPath,
126
+ JSON.stringify({ answer, answeredAt: new Date().toISOString() }, null, 2),
127
+ 'utf-8'
128
+ );
129
+ res.json({ ok: true });
130
+ } catch (err) {
131
+ res.status(500).json({ error: err.message });
132
+ }
133
+ });
134
+
135
+ export default router;
@@ -0,0 +1,95 @@
1
+ // KJC-TSK-0541: the session endpoints of /api, composed by routes/api.js.
2
+ import { Router } from 'express';
3
+ import fsp from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ getProjects,
8
+ getSessionsByProject,
9
+ getSessionDetail,
10
+ deleteSession,
11
+ getKjHome,
12
+ addTombstone,
13
+ } from '../../db.js';
14
+
15
+ const router = Router();
16
+
17
+ /**
18
+ * GET /api/projects/:id/sessions - Sessions for a project.
19
+ */
20
+ router.get('/projects/:id/sessions', (req, res) => {
21
+ try {
22
+ const sessions = getSessionsByProject(req.params.id);
23
+ res.json(sessions);
24
+ } catch (err) {
25
+ res.status(500).json({ error: err.message });
26
+ }
27
+ });
28
+
29
+ /**
30
+ * GET /api/sessions/:id - Session detail with stages, commits, duration.
31
+ */
32
+ router.get('/sessions/:id', (req, res) => {
33
+ try {
34
+ const session = getSessionDetail(req.params.id);
35
+ if (!session) return res.status(404).json({ error: 'Session not found' });
36
+ // Parse JSON fields for the response
37
+ const parsed = { ...session };
38
+ for (const field of ['checkpoints', 'llm_calls', 'config_snapshot', 'budget', 'commits', 'stages_completed']) {
39
+ if (parsed[field] && typeof parsed[field] === 'string') {
40
+ try { parsed[field] = JSON.parse(parsed[field]); } catch { /* keep as string */ }
41
+ }
42
+ }
43
+ res.json(parsed);
44
+ } catch (err) {
45
+ res.status(500).json({ error: err.message });
46
+ }
47
+ });
48
+
49
+ /**
50
+ * GET /api/sessions - All sessions across all projects.
51
+ */
52
+ router.get('/sessions', (_req, res) => {
53
+ try {
54
+ const allProjects = getProjects();
55
+ const sessions = [];
56
+ for (const p of allProjects) {
57
+ sessions.push(...getSessionsByProject(p.id));
58
+ }
59
+ // Also get default project sessions
60
+ sessions.push(...getSessionsByProject('default'));
61
+ // Deduplicate
62
+ const seen = new Set();
63
+ const unique = sessions.filter((s) => {
64
+ if (seen.has(s.id)) return false;
65
+ seen.add(s.id);
66
+ return true;
67
+ });
68
+ unique.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
69
+ res.json(unique);
70
+ } catch (err) {
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ /**
76
+ * DELETE /api/sessions/:id - Delete a single session.
77
+ *
78
+ * Tombstones + removes `~/.karajan/sessions/<id>/` (KJC-TSK-0380).
79
+ */
80
+ router.delete('/sessions/:id', async (req, res) => {
81
+ try {
82
+ const id = req.params.id;
83
+ const dir = path.join(getKjHome(), 'sessions', id);
84
+ const ok = deleteSession(id);
85
+ if (!ok) return res.status(404).json({ error: 'Session not found' });
86
+ addTombstone('session', id, { source: 'api', fsPaths: [dir] });
87
+ let dirRemoved = false;
88
+ try { await fsp.rm(dir, { recursive: true, force: true }); dirRemoved = true; } catch { /* */ }
89
+ res.json({ deleted: true, dirRemoved });
90
+ } catch (err) {
91
+ res.status(500).json({ error: err.message });
92
+ }
93
+ });
94
+
95
+ export default router;
@@ -0,0 +1,183 @@
1
+ // KJC-TSK-0541: the story (HU) endpoints of /api, composed by routes/api.js.
2
+ import { Router } from 'express';
3
+
4
+ import { getStoriesByProject, getStoryDetail, getStoryRow } from '../../db.js';
5
+ import { setHuStatus, setHuFields } from '../../plan-mutations.js';
6
+
7
+ const router = Router();
8
+
9
+ /**
10
+ * GET /api/projects/:id/stories - Stories for a project.
11
+ */
12
+ router.get('/projects/:id/stories', (req, res) => {
13
+ try {
14
+ const stories = getStoriesByProject(req.params.id);
15
+ res.json(stories);
16
+ } catch (err) {
17
+ res.status(500).json({ error: err.message });
18
+ }
19
+ });
20
+
21
+ /**
22
+ * GET /api/stories/:id - Story detail with quality scores and context requests.
23
+ */
24
+ router.get('/stories/:id', (req, res) => {
25
+ try {
26
+ const story = getStoryDetail(req.params.id);
27
+ if (!story) return res.status(404).json({ error: 'Story not found' });
28
+ res.json(story);
29
+ } catch (err) {
30
+ res.status(500).json({ error: err.message });
31
+ }
32
+ });
33
+
34
+ /**
35
+ * DELETE /api/stories/:id - Refused: stories are never deleted (KJC-TSK-0671).
36
+ */
37
+ router.delete('/stories/:id', (req, res) => {
38
+ // KJC-TSK-0671 (absolute product rule): stories are NEVER deleted — the
39
+ // delete-and-recreate "fix" agents improvise loses history and produced
40
+ // the field saga this closes. Discard goes through the lifecycle instead.
41
+ res.status(405).json({
42
+ error: 'Stories are never deleted. Discard one with: kj hu move <id> skipped '
43
+ + '(archived, history kept). Dashboard warnings are fixed with the command '
44
+ + 'they name — see kj brief board.',
45
+ });
46
+ });
47
+
48
+ /**
49
+ * PATCH /api/stories/:id - Edit a single HU.
50
+ *
51
+ * Accepts two shapes in the same call (both optional, either or both
52
+ * can be present):
53
+ *
54
+ * { status: "pending" | "certified" | "done" | "needs_context" }
55
+ * → changes the lifecycle state (original PATCH semantics).
56
+ *
57
+ * { title?, scope?, task_type?, acceptance_criteria?,
58
+ * acceptance_tests?, blocked_by? }
59
+ * → edits the user-facing fields in place. Whitelist mirrors
60
+ * plan-hu-ops::updateHu so CLI and board stay aligned.
61
+ *
62
+ * When both are present we apply status last — the status change
63
+ * transitions the plan lifecycle and can rewrite plan.status, whereas
64
+ * field edits are local to the HU and should never fight the status
65
+ * auto-promotion.
66
+ *
67
+ * The row id on the board is `${projectId}::${huId}` — split here to
68
+ * feed the plan-mutation helpers. The source-of-truth plan JSON is
69
+ * rewritten before we ack, then re-synced into SQLite, so a reload
70
+ * renders the committed state.
71
+ */
72
+ // KJC-TSK-0394 PR6 (AC 6): el PATCH endpoint sólo acepta el modelo
73
+ // canónico {pending, running, done}. Status legacy (`certified`,
74
+ // `coding`, `reviewing`, `failed`, `blocked`, `needs_context`) siguen
75
+ // vivos en plans existentes y los traduce la UI con canonicalStatus(),
76
+ // pero NO pueden ser seteados por escrituras nuevas — devolvemos 400
77
+ // con `suggestion` apuntando al mapping correcto. `running` queda fuera
78
+ // del dropdown del modal (lifecycle del orquestador), pero el endpoint
79
+ // lo acepta por si un programa externo lo necesita.
80
+ const ALLOWED_STORY_STATUSES = new Set(['pending', 'running', 'done']);
81
+ // Mapping canónico para la sugerencia del 400 cuando el caller manda
82
+ // un valor legacy. Mantenemos la regla viva en src/plan/plan-schema.js
83
+ // (mapLegacyStatusToResult) — esta tabla es sólo el subset accionable
84
+ // desde el endpoint (status + result).
85
+ const LEGACY_STATUS_SUGGESTION = {
86
+ certified: { status: 'done', result: 'pass' },
87
+ failed: { status: 'pending', result: 'fail' },
88
+ coding: { status: 'running', result: null },
89
+ reviewing: { status: 'running', result: null },
90
+ blocked: { status: 'pending', result: null },
91
+ needs_context: { status: 'pending', result: null },
92
+ };
93
+ // KJC-TSK-0406: coder_model y reviewer_model son editables desde el
94
+ // modal del board. Independientes — el usuario puede subir el reviewer
95
+ // y bajar el coder sin afectar al resto del plan.
96
+ // KJC-PRP-0002 PR6: `assignee` is editable from the board modal when the
97
+ // project is team-shared. The whitelist itself is project-agnostic — the
98
+ // modal hides the input on non-shared projects, but accepting the field
99
+ // here doesn't hurt (sync just stores it).
100
+ const EDITABLE_HU_FIELDS = ['title', 'scope', 'task_type', 'acceptance_criteria', 'acceptance_tests', 'blocked_by', 'coder_model', 'reviewer_model', 'coder_provider', 'reviewer_provider', 'assignee'];
101
+
102
+ router.patch('/stories/:id', (req, res) => {
103
+ try {
104
+ const body = req.body || {};
105
+ const hasStatus = Object.prototype.hasOwnProperty.call(body, 'status');
106
+ const fieldPatch = {};
107
+ for (const k of EDITABLE_HU_FIELDS) {
108
+ if (Object.prototype.hasOwnProperty.call(body, k)) fieldPatch[k] = body[k];
109
+ }
110
+ const hasFieldEdit = Object.keys(fieldPatch).length > 0;
111
+
112
+ if (!hasStatus && !hasFieldEdit) {
113
+ return res.status(400).json({
114
+ error: `Body must include status or one of: ${EDITABLE_HU_FIELDS.join(', ')}`,
115
+ });
116
+ }
117
+ if (hasStatus && !ALLOWED_STORY_STATUSES.has(body.status)) {
118
+ // KJC-TSK-0394 PR6 (AC 6): si el caller manda un status legacy,
119
+ // devolvemos un 400 con `suggestion` apuntando al mapping canónico.
120
+ // Una herramienta externa puede leer el body, aplicar el mapeo y
121
+ // re-intentar — sin tener que descubrir la tabla por ensayo y error.
122
+ const suggestion = LEGACY_STATUS_SUGGESTION[body.status] || null;
123
+ return res.status(400).json({
124
+ error: `status must be one of: ${[...ALLOWED_STORY_STATUSES].join(', ')}`,
125
+ suggestion,
126
+ });
127
+ }
128
+
129
+ const row = getStoryRow(req.params.id);
130
+ if (!row) return res.status(404).json({ error: 'Story not found' });
131
+ if (!row.plan_id) {
132
+ return res.status(409).json({
133
+ error: 'Story is not backed by a plan file (legacy row). Re-run `kj plan` to re-import it.',
134
+ });
135
+ }
136
+ const huId = req.params.id.includes('::')
137
+ ? req.params.id.split('::').slice(1).join('::')
138
+ : req.params.id;
139
+
140
+ let updatedHu;
141
+ if (hasFieldEdit) {
142
+ const fieldResult = setHuFields({
143
+ planId: row.plan_id,
144
+ huId,
145
+ patch: fieldPatch,
146
+ projectId: row.project_id,
147
+ });
148
+ if (!fieldResult.ok) {
149
+ // KJC-TSK-0401: validación de blocked_by devuelve 400 con cycle?
150
+ // info para que el board destaque la cadena en la UI. Resto de
151
+ // fallos siguen devolviendo 404 (plan / hu no encontrados).
152
+ if (fieldResult.code === 'INVALID_BLOCKED_BY') {
153
+ return res.status(400).json({ error: fieldResult.error, cycle: fieldResult.cycle });
154
+ }
155
+ return res.status(404).json({ error: fieldResult.error });
156
+ }
157
+ updatedHu = fieldResult.hu;
158
+ }
159
+
160
+ let statusResult;
161
+ if (hasStatus) {
162
+ statusResult = setHuStatus({
163
+ planId: row.plan_id,
164
+ huId,
165
+ status: body.status,
166
+ projectId: row.project_id,
167
+ });
168
+ if (!statusResult.ok) return res.status(404).json({ error: statusResult.error });
169
+ }
170
+
171
+ res.json({
172
+ updated: true,
173
+ id: req.params.id,
174
+ status: statusResult ? statusResult.status : undefined,
175
+ planStatus: statusResult ? statusResult.planStatus : undefined,
176
+ hu: updatedHu,
177
+ });
178
+ } catch (err) {
179
+ res.status(500).json({ error: err.message });
180
+ }
181
+ });
182
+
183
+ export default router;