elanous 0.2.25 → 0.2.28

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 (337) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1454-37fd16f479f5554a.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-24453f6a302816b4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-21050928a697ed7a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +119 -7
  154. package/scripts/coord-msg-bridge.ts +209 -0
  155. package/scripts/coord-post.sh +40 -7
  156. package/scripts/coord-requests-bridge.ts +10 -5
  157. package/scripts/detect-test-interference.ts +31 -1
  158. package/scripts/expose-rubric-suggest.ts +1 -1
  159. package/scripts/measure-tsc-affected-roots.ts +10 -1
  160. package/scripts/release-loop/docs-node.ts +51 -22
  161. package/scripts/release-loop/export-check-node.ts +8 -5
  162. package/scripts/release-loop/gate-node.ts +37 -4
  163. package/scripts/release-loop/node-verdict.ts +10 -3
  164. package/scripts/release-loop/publish-node.ts +3 -0
  165. package/scripts/release-loop/tui-node-checks.ts +5 -2
  166. package/scripts/skills/public-check.ts +5 -2
  167. package/scripts/test-deterministic.ts +520 -37
  168. package/scripts/ux-sim.ts +7 -1
  169. package/scripts/verify-kpack6-live.ts +230 -0
  170. package/src/acp/codex-plugins.ts +17 -0
  171. package/src/agent/task-notification.ts +32 -2
  172. package/src/agent-mission/in-agent-plugin.ts +8 -3
  173. package/src/agent-substrate/review-observation.ts +9 -2
  174. package/src/agent-substrate/self-review-cli.ts +62 -16
  175. package/src/boot/daemon-tools/self-implement.ts +72 -4
  176. package/src/briefing/brief-items.ts +151 -35
  177. package/src/budget/llm-cost.ts +30 -3
  178. package/src/budget/usage-outcomes.ts +32 -0
  179. package/src/claims/claims-ledger.ts +23 -0
  180. package/src/claims/claims-render.ts +22 -2
  181. package/src/cli/brief-cli.ts +131 -10
  182. package/src/cli/claims-cli.ts +24 -4
  183. package/src/cli/control-cli.ts +29 -0
  184. package/src/cli/doctor-cli.ts +90 -8
  185. package/src/cli/msg-cli.ts +177 -11
  186. package/src/cli/plugin-cli.ts +11 -3
  187. package/src/cli/pr-cli.ts +133 -18
  188. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  189. package/src/cli/pr-land-refusal.ts +20 -0
  190. package/src/cli/release-cli.ts +34 -5
  191. package/src/cli/rfc-cli.ts +24 -0
  192. package/src/cli/seat-cli.ts +121 -0
  193. package/src/cli/self-utterances-cli.ts +140 -0
  194. package/src/cli/tailscale-serve.ts +5 -8
  195. package/src/cli/tasks-cli.ts +528 -31
  196. package/src/cli/usage-cli.ts +9 -4
  197. package/src/context-card/cell.ts +40 -0
  198. package/src/dashboard/boot-timing.ts +35 -0
  199. package/src/dashboard/first-screen-band.ts +63 -13
  200. package/src/dashboard/index.ts +94 -27
  201. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  202. package/src/dashboard/skill-detection-runtime.ts +24 -4
  203. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  204. package/src/debug/log.ts +1 -1
  205. package/src/domains/morning-report.ts +3 -1
  206. package/src/domains/release-tool.ts +29 -12
  207. package/src/domains/surface-events.ts +16 -0
  208. package/src/elanous-config-dir.ts +2 -2
  209. package/src/fleet/host-doctor.ts +73 -2
  210. package/src/github/pr-labels.ts +10 -4
  211. package/src/graph-runner/runner.ts +104 -15
  212. package/src/harness/harness-clean.ts +21 -3
  213. package/src/harness/harness-cli-command.ts +68 -10
  214. package/src/harness/harness-pod-dispatch.ts +11 -4
  215. package/src/harness/harness-stop.ts +11 -7
  216. package/src/hq/seats.ts +17 -3
  217. package/src/index.ts +69 -9
  218. package/src/instance/current.ts +2 -6
  219. package/src/instance/resolve.ts +5 -6
  220. package/src/intelligence-map/model-bench-scores.ts +102 -0
  221. package/src/llm/openai-sampling-fields.ts +89 -0
  222. package/src/llm/runtime-model-family.ts +61 -0
  223. package/src/llm/zai-coding-plan.ts +27 -1
  224. package/src/llm.ts +13 -89
  225. package/src/loops/orchestrator/inbox.ts +73 -0
  226. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  227. package/src/loops/orchestrator/tick.ts +73 -2
  228. package/src/machines/machine-ledger.ts +31 -3
  229. package/src/maturity/cli-maturity.ts +13 -3
  230. package/src/maturity/feature-maturity.ts +1 -1
  231. package/src/msg/msg-sse-server.ts +141 -0
  232. package/src/msg/msg-store.ts +149 -17
  233. package/src/ops/overview-collect.ts +3 -2
  234. package/src/ops/overview-owner.ts +29 -0
  235. package/src/patents/ledger.ts +128 -0
  236. package/src/plugin-hooks/shell-hook.ts +17 -2
  237. package/src/plugin-missions/evaluator-runner.ts +18 -2
  238. package/src/plugins/install/market-fetch.ts +32 -1
  239. package/src/plugins/install/plugin-install.ts +11 -1
  240. package/src/pod-lease/admission.ts +116 -41
  241. package/src/prompt-enhance/enhance.ts +11 -0
  242. package/src/release-loop/checklist.ts +52 -4
  243. package/src/release-loop/feature-store.ts +10 -9
  244. package/src/release-loop/merged-pr-checklist.ts +24 -5
  245. package/src/release-loop/rubric.ts +70 -0
  246. package/src/rfc-registry/registry.ts +88 -0
  247. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  248. package/src/seat-loop/knowhow-lint.ts +233 -0
  249. package/src/seat-loop/seat-loop.ts +5 -1
  250. package/src/seat-loop/seat-pack.ts +16 -4
  251. package/src/seat-loop/traffic-signal.ts +7 -0
  252. package/src/self-dev/ask-launch-flow.ts +48 -8
  253. package/src/self-dev/ask-path-candidates.ts +69 -14
  254. package/src/self-dev/decompose-proposal.ts +43 -1
  255. package/src/self-dev/draft-sweep.ts +23 -14
  256. package/src/self-dev/graph-journey-nodes.ts +2 -0
  257. package/src/self-dev/launch-preflight-text.ts +5 -1
  258. package/src/self-dev/launch-preflight.ts +143 -10
  259. package/src/self-dev/orchestrate.ts +6 -2
  260. package/src/self-dev/run-supervisor.ts +35 -1
  261. package/src/self-implement/gate-cli.ts +6 -2
  262. package/src/self-implement/goal-author.ts +5 -28
  263. package/src/self-implement/goal-command-execution.ts +26 -0
  264. package/src/self-implement/goal-new-paths.ts +4 -5
  265. package/src/self-implement/graph-authority.ts +17 -3
  266. package/src/self-implement/graph-yaml.ts +15 -7
  267. package/src/self-implement/headless-elanous-driver.ts +16 -1
  268. package/src/self-implement/host-regate.ts +44 -9
  269. package/src/self-implement/orchestrator.ts +5 -11
  270. package/src/self-implement/pwa-import-graph.ts +194 -7
  271. package/src/self-implement/rework-policy.ts +80 -2
  272. package/src/self-implement/self-implement-runtime.ts +126 -21
  273. package/src/self-implement/speed-claims.ts +145 -6
  274. package/src/session/index.ts +14 -4
  275. package/src/session-runtime/index.ts +55 -11
  276. package/src/skills/tools/persistent-grounding.ts +59 -4
  277. package/src/skills/tools/shell.ts +79 -1
  278. package/src/steward/rejudge-tick.ts +12 -1
  279. package/src/steward/release-day.ts +158 -0
  280. package/src/steward/resync-tick.ts +123 -0
  281. package/src/task-agent/actions.ts +1 -0
  282. package/src/task-agent/adopt-pr.ts +140 -0
  283. package/src/task-agent/board.ts +11 -1
  284. package/src/task-agent/card-evidence.ts +37 -5
  285. package/src/task-agent/cover.ts +105 -19
  286. package/src/task-agent/failure-notify.ts +61 -0
  287. package/src/task-agent/green-sweep.ts +170 -0
  288. package/src/task-agent/land-worktree.ts +52 -5
  289. package/src/task-agent/live-move-history.ts +1 -0
  290. package/src/task-agent/live-moves.ts +56 -17
  291. package/src/task-agent/parent-host.ts +1 -1
  292. package/src/task-agent/postland-card.ts +139 -0
  293. package/src/task-agent/rejudge-on-head.ts +321 -25
  294. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  295. package/src/task-agent/repair-branch-cas.ts +182 -0
  296. package/src/task-agent/repair-cycle.ts +275 -0
  297. package/src/task-agent/repair-next-move.ts +31 -0
  298. package/src/task-agent/repair-review.ts +24 -0
  299. package/src/task-agent/repair-worker.ts +87 -0
  300. package/src/task-agent/review-result-capture.ts +66 -2
  301. package/src/task-agent/shadow.ts +150 -29
  302. package/src/task-agent/task-hand.ts +133 -8
  303. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
  305. package/src/telegram.ts +30 -1
  306. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  307. package/src/tui.ts +20 -1
  308. package/src/user-config.ts +11 -60
  309. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  310. package/src/version/packed-revision.json +1 -1
  311. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
  336. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  337. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -15,21 +15,27 @@ export interface PodLeaseAdmissionOptions {
15
15
  dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean>;
16
16
  /** Recheck even when there are no local releases (another launcher may free a slot). */
17
17
  pollMs?: number;
18
+ /** Maximum time spent waiting for a free slot; defaults to 30 minutes (predecessor waits do not count). */
19
+ maxWaitMs?: number;
18
20
  }
19
21
 
20
22
  /** FIFO admission across launches sharing this instance. Unknown status never grants a slot. */
21
23
  export class PodLeaseAdmission {
22
- private readonly queue: Array<{ resolve: (release: PodLeaseRelease) => void; reject: (error: Error) => void; signal?: AbortSignal; after?: PodLeasePredecessor; onAbort?: () => void; cancelCheck?: () => void; waitReason?: string }> = [];
24
+ private readonly queue: Array<{ resolve: (release: PodLeaseRelease) => void; reject: (error: Error) => void; signal?: AbortSignal; after?: PodLeasePredecessor; onAbort?: () => void; cancelCheck?: () => void; checkingDependency?: boolean; predecessorMerged?: boolean; waitReason?: string; waitStartedAt?: number }> = [];
23
25
  private active = 0;
24
26
  private limit = 0;
25
27
  private checking: Promise<void> | undefined;
26
28
  private recheckRequested = false;
27
29
  private timer: ReturnType<typeof setTimeout> | undefined;
30
+ private expiryTimer: ReturnType<typeof setTimeout> | undefined;
28
31
  private readonly pollMs: number;
32
+ private readonly maxWaitMs: number;
29
33
 
30
34
  constructor(private readonly options: PodLeaseAdmissionOptions) {
31
35
  this.pollMs = options.pollMs ?? 15_000;
36
+ this.maxWaitMs = options.maxWaitMs ?? 30 * 60_000;
32
37
  if (!Number.isFinite(this.pollMs) || this.pollMs <= 0) throw new Error('pod lease pollMs must be positive');
38
+ if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs <= 0) throw new Error('pod lease maxWaitMs must be positive');
33
39
  }
34
40
 
35
41
  /** Queue view: each waiting item says why it waits (predecessor not merged yet, or no free slot). */
@@ -42,19 +48,18 @@ export class PodLeaseAdmission {
42
48
  acquire(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease> {
43
49
  if (signal?.aborted) return Promise.reject(new Error('pod lease admission aborted'));
44
50
  return new Promise((resolve, reject) => {
45
- const waiter: (typeof this.queue)[number] = { resolve, reject, signal, after };
51
+ const waiter: (typeof this.queue)[number] = { resolve, reject, signal, after,
52
+ ...(after === undefined ? { waitStartedAt: Date.now(), waitReason: 'no-free-slot' }
53
+ : { waitReason: `predecessor-unmerged:${String(after)}` }) };
46
54
  if (signal) {
47
55
  waiter.onAbort = () => {
48
- const index = this.queue.indexOf(waiter);
49
- if (index < 0) return;
50
- this.queue.splice(index, 1);
51
- waiter.cancelCheck?.();
56
+ if (!this.removeWaiter(waiter)) return;
52
57
  reject(new Error('pod lease admission aborted'));
53
- this.schedule();
54
58
  };
55
59
  signal.addEventListener('abort', waiter.onAbort, { once: true });
56
60
  }
57
61
  this.queue.push(waiter);
62
+ this.scheduleExpiry();
58
63
  void this.refresh();
59
64
  });
60
65
  }
@@ -83,41 +88,29 @@ export class PodLeaseAdmission {
83
88
  } catch {
84
89
  this.limit = 0;
85
90
  }
86
- // No head-of-line blocking: a waiter whose predecessor is not merged yet is skipped and later waiters may admit.
91
+ // Start independent predecessor checks before admitting eligible waiters in queue order.
92
+ // A slow predecessor cannot reserve a slot or delay a follower's admission decision.
93
+ this.expireWaiters();
94
+ const checks = this.queue.filter((waiter) => waiter.after !== undefined && !waiter.checkingDependency && !waiter.predecessorMerged)
95
+ .map((waiter) => this.checkPredecessor(waiter));
96
+ if (checks.length) {
97
+ // Let ready states settle, but do not hold a free slot for an unanswered dependency check.
98
+ await Promise.race([Promise.all(checks), new Promise<void>((resolve) => { setTimeout(resolve, 0); })]);
99
+ }
100
+ this.expireWaiters();
87
101
  let index = 0;
88
- while (this.active < this.limit && index < this.queue.length) {
102
+ while (index < this.queue.length) {
89
103
  const waiter = this.queue[index]!;
90
- if (waiter.after !== undefined) {
91
- let state: PredecessorState = 'waiting';
92
- const controller = new AbortController();
93
- try {
94
- const check = this.options.dependencyMerged?.(waiter.after, controller.signal);
95
- const result = await Promise.race([
96
- Promise.resolve(check),
97
- new Promise<PredecessorState>((resolve) => { waiter.cancelCheck = () => { controller.abort(); resolve('waiting'); }; }),
98
- ]);
99
- state = result === true ? 'merged' : result === false || result === undefined ? 'waiting' : result;
100
- } catch {
101
- // An unavailable dependency state cannot grant admission.
102
- } finally {
103
- waiter.cancelCheck = undefined;
104
- }
105
- if (!this.queue.includes(waiter)) continue; // aborted while checking
106
- if (state === 'blocked') {
107
- this.queue.splice(this.queue.indexOf(waiter), 1);
108
- if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
109
- debug.log('pod-lease', 'predecessor-blocked', { after: waiter.after });
110
- waiter.reject(new Error(`pod lease predecessor blocked: ${String(waiter.after)} was closed without merging`));
111
- continue;
112
- }
113
- if (state !== 'merged') {
114
- waiter.waitReason = `predecessor-unmerged:${String(waiter.after)}`;
115
- index++;
116
- continue;
117
- }
104
+ if (waiter.after !== undefined && !waiter.predecessorMerged) {
105
+ index++;
106
+ continue;
107
+ }
108
+ if (this.active >= this.limit) {
109
+ waiter.waitReason = 'no-free-slot';
110
+ index++;
111
+ continue;
118
112
  }
119
- this.queue.splice(this.queue.indexOf(waiter), 1);
120
- if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
113
+ this.removeWaiter(waiter);
121
114
  this.active++;
122
115
  let released = false;
123
116
  waiter.resolve(() => {
@@ -127,21 +120,103 @@ export class PodLeaseAdmission {
127
120
  void this.refresh();
128
121
  });
129
122
  }
130
- for (const waiter of this.queue.slice(index)) if (!waiter.waitReason?.startsWith('predecessor-')) waiter.waitReason = 'no-free-slot';
131
123
  } while (this.recheckRequested && this.queue.length > 0);
132
124
  } finally {
133
125
  this.checking = undefined;
126
+ this.scheduleExpiry();
134
127
  this.schedule();
135
128
  }
136
129
  }
137
130
 
131
+ private async checkPredecessor(waiter: (typeof this.queue)[number]): Promise<void> {
132
+ waiter.checkingDependency = true;
133
+ const controller = new AbortController();
134
+ let state: PredecessorState = 'waiting';
135
+ try {
136
+ const check = this.options.dependencyMerged?.(waiter.after!, controller.signal);
137
+ const result = await Promise.race([
138
+ Promise.resolve(check),
139
+ new Promise<PredecessorState>((resolve) => { waiter.cancelCheck = () => { controller.abort(); resolve('waiting'); }; }),
140
+ ]);
141
+ state = result === true ? 'merged' : result === false || result === undefined ? 'waiting' : result;
142
+ } catch {
143
+ // An unavailable dependency state cannot grant admission.
144
+ } finally {
145
+ waiter.cancelCheck = undefined;
146
+ waiter.checkingDependency = false;
147
+ }
148
+ if (!this.queue.includes(waiter)) return;
149
+ if (state === 'blocked') {
150
+ this.removeWaiter(waiter);
151
+ debug.log('pod-lease', 'predecessor-blocked', { after: waiter.after });
152
+ waiter.reject(new Error(`pod lease predecessor blocked: ${String(waiter.after)} was closed without merging`));
153
+ } else if (state === 'merged') {
154
+ waiter.predecessorMerged = true;
155
+ waiter.waitStartedAt = Date.now();
156
+ waiter.waitReason = 'no-free-slot';
157
+ this.scheduleExpiry();
158
+ void this.refresh();
159
+ } else {
160
+ waiter.waitReason = `predecessor-unmerged:${String(waiter.after)}`;
161
+ waiter.waitStartedAt = undefined;
162
+ }
163
+ }
164
+
165
+ private expireWaiters(): void {
166
+ const now = Date.now();
167
+ for (let index = 0; index < this.queue.length;) {
168
+ const waiter = this.queue[index]!;
169
+ if (waiter.waitReason !== 'no-free-slot' || waiter.waitStartedAt === undefined ||
170
+ now - waiter.waitStartedAt <= this.maxWaitMs) {
171
+ index++;
172
+ continue;
173
+ }
174
+ this.removeWaiter(waiter);
175
+ const waitedMs = now - waiter.waitStartedAt;
176
+ const reason = waiter.waitReason;
177
+ debug.log('pod-lease', 'admit-starved', { waitedMs, reason, after: waiter.after });
178
+ waiter.reject(new Error(`pod lease admit-starved: waited ${Math.round(waitedMs / 60_000)}m · reason ${reason}`));
179
+ }
180
+ this.scheduleExpiry();
181
+ }
182
+
183
+ /** Every queue exit also updates both timers; an empty queue must retain no timer. */
184
+ private removeWaiter(waiter: (typeof this.queue)[number]): boolean {
185
+ const index = this.queue.indexOf(waiter);
186
+ if (index < 0) return false;
187
+ this.queue.splice(index, 1);
188
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
189
+ waiter.cancelCheck?.();
190
+ this.scheduleExpiry();
191
+ if (!this.checking || this.queue.length === 0) this.schedule();
192
+ return true;
193
+ }
194
+
195
+ private scheduleExpiry(): void {
196
+ if (this.expiryTimer) clearTimeout(this.expiryTimer);
197
+ this.expiryTimer = undefined;
198
+ const now = Date.now();
199
+ let delay = Infinity;
200
+ for (const waiter of this.queue) {
201
+ if (waiter.waitReason === 'no-free-slot' && waiter.waitStartedAt !== undefined) {
202
+ delay = Math.min(delay, Math.max(1, waiter.waitStartedAt + this.maxWaitMs - now + 1));
203
+ }
204
+ }
205
+ if (Number.isFinite(delay)) {
206
+ this.expiryTimer = setTimeout(() => {
207
+ this.expiryTimer = undefined;
208
+ this.expireWaiters();
209
+ }, delay);
210
+ }
211
+ }
212
+
138
213
  private schedule(): void {
139
214
  if (!this.queue.length) {
140
215
  if (this.timer) clearTimeout(this.timer);
141
216
  this.timer = undefined;
142
217
  return;
143
218
  }
144
- if (this.timer) return;
219
+ if (this.timer) clearTimeout(this.timer);
145
220
  this.timer = setTimeout(() => {
146
221
  this.timer = undefined;
147
222
  void this.refresh();
@@ -393,6 +393,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
393
393
  let complication: string | undefined;
394
394
  let decisionSignal: EnhanceResult['decisionSignal'];
395
395
  let enhancedBy: 'llm' | 'fallback' = 'llm';
396
+ let fallbackReason: 'llm-error' | 'empty-reply' | 'unparsed' | 'none' = 'none';
397
+ let replyChars = 0;
396
398
  const groundedFacts = opts.groundedFacts ?? [];
397
399
  const wantsScqa = groundedFacts.length > 0;
398
400
 
@@ -439,6 +441,10 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
439
441
  try { opts.onUsage?.(usage); } catch { /* usage observation must not change enhancement */ }
440
442
  },
441
443
  });
444
+ replyChars = rawOut.length;
445
+ if (rawOut.trim().length === 0) {
446
+ fallbackReason = 'empty-reply';
447
+ }
442
448
  const parsed = parseEnhanceJson(rawOut);
443
449
  if (parsed) {
444
450
  goal = parsed.goal;
@@ -449,9 +455,11 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
449
455
  complication = parsed.complication;
450
456
  decisionSignal = parsed.decisionSignal;
451
457
  } else {
458
+ fallbackReason = fallbackReason === 'empty-reply' ? 'empty-reply' : 'unparsed';
452
459
  enhancedBy = 'fallback';
453
460
  }
454
461
  } catch (e) {
462
+ fallbackReason = 'llm-error';
455
463
  debug.log('prompt-enhance', 'llm-error', { error: String((e as { message?: string })?.message ?? e).slice(0, 160) }, { level: 'error' });
456
464
  enhancedBy = 'fallback';
457
465
  }
@@ -461,6 +469,7 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
461
469
  checklist = extractChecklistFallback(original);
462
470
  checklistProvenance = checklist.map(() => 'unknown');
463
471
  enhancedBy = 'fallback';
472
+ if (fallbackReason === 'none') fallbackReason = 'unparsed';
464
473
  }
465
474
  if (!constraints.length) {
466
475
  constraints = [
@@ -515,6 +524,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
515
524
  enhancedChars: enhanced.length,
516
525
  checklist: checklist.length,
517
526
  enhancedBy,
527
+ fallbackReason,
528
+ replyChars,
518
529
  model,
519
530
  verbatimPreserved,
520
531
  // ⭐ 관측 — 「요약을 요구했나 / 받았나 / 옮겨 적어 버렸나」를 «다른 값»으로 남긴다.
@@ -10,6 +10,8 @@ export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
10
10
  /** `screen` = a five-surface feature cell — its evidence carries a `짝:` line (MANUAL-five-surface-parity §A). `postpub` = measured after publication, outside the pre-publication gate. */
11
11
  export type ChecklistKind = 'screen' | 'postpub';
12
12
  const CHECKLIST_KINDS: readonly ChecklistKind[] = ['screen', 'postpub'];
13
+ export type ChecklistDispatchKind = 'code' | 'ops' | 'measure' | 'decision' | 'umbrella';
14
+ const CHECKLIST_DISPATCH_KINDS: readonly ChecklistDispatchKind[] = ['code', 'ops', 'measure', 'decision', 'umbrella'];
13
15
  export interface ChecklistItem {
14
16
  id: string;
15
17
  title: string;
@@ -27,6 +29,8 @@ export interface ChecklistItem {
27
29
  evidence?: string;
28
30
  disposition?: ChecklistDisposition;
29
31
  kind?: ChecklistKind;
32
+ /** Whether the seat dispatcher should launch a coding task. Missing in old rows means code. */
33
+ dispatchKind?: ChecklistDispatchKind;
30
34
  updatedAt: string;
31
35
  updatedBy: string;
32
36
  }
@@ -122,6 +126,37 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
122
126
 
123
127
  const REPO_ROOT = join(import.meta.dir, '..', '..');
124
128
 
129
+ const PATH_TOKEN = /(?:^|[^A-Za-z0-9_.\/-])((?:(?:[A-Za-z0-9_.-]+\/)+)?[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|yaml|yml|sh|py|css|html|toml|sql|rs|swift))(?![A-Za-z0-9_.\/-])/g;
130
+ function mentionedPaths(text: string): string[] {
131
+ return [...text.matchAll(PATH_TOKEN)].map((match) => match[1]!).filter((path) => !path.split('/').includes('..'));
132
+ }
133
+
134
+ /** Shadow check at checklist authorship. Callers supply the origin/main file probe and merged-PR file reader. */
135
+ export function inspectChecklistTargetPaths(input: {
136
+ title: string; evidence?: string; refs?: readonly string[]; dispatchKind?: ChecklistDispatchKind;
137
+ }, deps: { existsAtMain: (path: string) => boolean; mergedPrFiles?: (number: number) => readonly string[] }): {
138
+ targets: string[]; warnings: string[]; candidates: string[];
139
+ } {
140
+ const lines = input.title.split(/\r?\n/);
141
+ const first = /^대상 경로:\s*(.*)$/.exec(lines[0] ?? '');
142
+ const targets = first ? [...new Set(mentionedPaths(first[1]!))] : [];
143
+ const newFileDeclared = /^신설:\s*/.test(lines[1] ?? '');
144
+ const warnings: string[] = [];
145
+ if ((input.dispatchKind ?? 'code') === 'code') {
146
+ if (!first || !first[1]!.trim() || targets.length === 0) warnings.push('첫 줄 대상 경로가 없다');
147
+ for (const path of targets) if (!deps.existsAtMain(path) && !newFileDeclared) warnings.push(`${path}: origin/main에 없는 파일 · 신설: 둘째 줄 선언 필요`);
148
+ }
149
+ if (!warnings.length) return { targets, warnings, candidates: [] };
150
+ const sources = [input.title, input.evidence ?? '', ...(input.refs ?? [])].join('\n');
151
+ const candidates = [...new Set(mentionedPaths(sources).filter(deps.existsAtMain))];
152
+ for (const match of sources.matchAll(/(?:^|[^\d])#(\d{2,8})\b/g)) {
153
+ for (const path of deps.mergedPrFiles?.(Number(match[1])) ?? []) {
154
+ if (deps.existsAtMain(path) && !candidates.includes(path)) candidates.push(path);
155
+ }
156
+ }
157
+ return { targets, warnings, candidates };
158
+ }
159
+
125
160
  /** `--ref` 문서를 찾는 뿌리들 — 이 코드의 트리 ⊕ 지금 작업 디렉토리의 git 뿌리.
126
161
  * ⭐ 설치본(`~/.local/share/elanous/current`)에는 `docs/` 가 없다 — 저장소 안에서 부른 설치본도 저장소 문서를 인용할 수 있게. */
127
162
  export function refRoots(cwd = process.cwd()): string[] {
@@ -233,10 +268,11 @@ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }
233
268
  if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
234
269
  }
235
270
 
236
- export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
271
+ export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
237
272
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
238
273
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
239
274
  if (input.kind !== undefined && !CHECKLIST_KINDS.includes(input.kind)) throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen|postpub');
275
+ if (input.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(input.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${input.dispatchKind}`, 'code|ops|measure|decision|umbrella');
240
276
  if (input.owner !== undefined) parseOwner(input.owner);
241
277
  if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
242
278
  if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
@@ -255,20 +291,32 @@ export function addItem(v: string, input: { id: string; title: string; refs?: st
255
291
  assertPredecessorsExist(input.predecessors, data, otherItems);
256
292
  const by = process.env.ELANOUS_TRACK || 'cli';
257
293
  const at = new Date().toISOString();
258
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
294
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.dispatchKind !== undefined ? { dispatchKind: input.dispatchKind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
259
295
  data.items.push(item);
260
296
  change(data, item.id, 'add', null, item, by, at);
261
297
  return true;
262
298
  });
263
299
  }
264
300
 
265
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
301
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
302
+ return setItemGuarded(v, id, patch, by);
303
+ }
304
+
305
+ /** Apply a measured verdict only while the release cell is still the snapshot that was judged. */
306
+ export function setItemIfSnapshot(v: string, id: string, patch: Parameters<typeof setItem>[2], by: string, expected: Pick<ChecklistItem, 'title' | 'status' | 'evidence'>): Checklist {
307
+ return setItemGuarded(v, id, patch, by, expected);
308
+ }
309
+
310
+ function setItemGuarded(v: string, id: string, patch: Parameters<typeof setItem>[2], by: string, expected?: Pick<ChecklistItem, 'title' | 'status' | 'evidence'>): Checklist {
266
311
  return mutate(v, (data, otherItems) => {
267
312
  const item = data.items.find((i) => i.id === id);
268
313
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
314
+ if (expected && (item.title !== expected.title || item.status !== expected.status || item.evidence !== expected.evidence))
315
+ throw new CliUserError(`판정 중 칸 변경: ${id}`, '현재 칸으로 다시 측정한다');
269
316
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
270
317
  if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
271
318
  if (patch.kind !== undefined && !CHECKLIST_KINDS.includes(patch.kind)) throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen|postpub');
319
+ if (patch.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(patch.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${patch.dispatchKind}`, 'code|ops|measure|decision|umbrella');
272
320
  if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
273
321
  if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
274
322
  assertPredecessorsExist(patch.predecessors, data, otherItems);
@@ -283,7 +331,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
283
331
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
284
332
  }
285
333
  }
286
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
334
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'dispatchKind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
287
335
  if (fields.length === 0) return false;
288
336
  const at = new Date().toISOString();
289
337
  for (const field of fields) {
@@ -36,7 +36,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
36
36
  const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
37
37
  imported_versions: ['json_hash', 'imported_at'],
38
38
  events: ['reason', 'generation'],
39
- assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
39
+ assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs', 'dispatch_kind'],
40
40
  release_schedules: ['freeze_from', 'freeze_until', 'publish_at'],
41
41
  };
42
42
  function schemaCurrent(db: Database): boolean {
@@ -81,7 +81,7 @@ function open(root = releaseLedgerRoot()): Database {
81
81
  if (!eventColumns.some((column) => column.name === 'generation')) db.exec('ALTER TABLE events ADD COLUMN generation INTEGER');
82
82
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
83
83
  const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
84
- for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT']] as const) {
84
+ for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT'], ['dispatch_kind', 'TEXT']] as const) {
85
85
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
86
86
  }
87
87
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
@@ -178,12 +178,13 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
178
178
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
179
179
  }
180
180
  function items(db: Database, version: string): ChecklistItem[] {
181
- const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
181
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.dispatch_kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
182
182
  FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
183
183
  return rows.map((r) => {
184
184
  const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
185
185
  return { id: r.id as string, title: r.title as string, status: r.status as ChecklistItem['status'],
186
186
  ...(r.owner !== null ? { owner: r.owner as string } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
187
+ ...(r.dispatch_kind !== null ? { dispatchKind: r.dispatch_kind as ChecklistItem['dispatchKind'] } : {}),
187
188
  ...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
188
189
  ...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
189
190
  ...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
@@ -202,9 +203,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
202
203
  if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
203
204
  // features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
204
205
  else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
205
- db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
206
- ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
207
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
206
+ db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, dispatch_kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
207
+ ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, dispatch_kind=excluded.dispatch_kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
208
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.dispatchKind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
208
209
  }
209
210
  function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
210
211
 
@@ -245,8 +246,8 @@ function importOnDb(db: Database, version: string): boolean {
245
246
  // Keep a legacy per-version title until an explicit retitle unifies the feature identity.
246
247
  if (!existing) putItem(db, version, item);
247
248
  else {
248
- db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
249
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
249
+ db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, dispatch_kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
250
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.dispatchKind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
250
251
  }
251
252
  }
252
253
  for (const entry of data.history) record(db, version, entry);
@@ -273,7 +274,7 @@ function importOnDb(db: Database, version: string): boolean {
273
274
  record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
274
275
  continue;
275
276
  }
276
- const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
277
+ const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'dispatchKind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
277
278
  const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
278
279
  const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
279
280
  const differs = fields.some((field) => field === 'evidence'
@@ -7,21 +7,25 @@ import { listChecklist, setItem } from './checklist.js';
7
7
  import { judgeLandedCell } from './landed-to-green.js';
8
8
  import { evidencePlan, landedButYellow, type MergedChecklistPr } from './landed-but-yellow.js';
9
9
  import { assignedVersions, evidenceAdd } from './feature-store.js';
10
+ import { boundMergedTaskCells, candidateMergedTaskCells, closeMergedTaskCard } from '../task-agent/postland-card.js';
10
11
 
11
12
  export interface MergedPrChecklistDeps {
12
- readPr?: (number: number, cwd: string) => { state: string; baseRefName: string; title: string; body: string | null; mergedAt: string };
13
+ readPr?: (number: number, cwd: string) => { state: string; baseRefName: string; title: string; body: string | null; mergedAt: string; url?: string; headRefOid?: string; files?: Array<{ path: string }> };
13
14
  versions?: () => string[];
14
15
  checklist?: typeof listChecklist;
15
16
  addEvidence?: typeof evidenceAdd;
16
17
  setStatus?: typeof setItem;
17
18
  judgeLanded?: typeof judgeLandedCell;
19
+ closeTaskCard?: typeof closeMergedTaskCard;
20
+ boundTaskCells?: typeof boundMergedTaskCells;
21
+ candidateTaskCells?: typeof candidateMergedTaskCells;
18
22
  log?: typeof debug.log;
19
23
  }
20
24
 
21
25
  /** A confirmed main merge is the only entry. The existing checklist matcher owns PR-to-cell decisions. */
22
26
  export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: string, deps: MergedPrChecklistDeps = {}): void {
23
27
  const log = deps.log ?? debug.log.bind(debug);
24
- const pr = (deps.readPr ?? ((n, dir) => JSON.parse(execFileSync('gh', ['pr', 'view', String(n), '--json', 'state,baseRefName,title,body,mergedAt'], { cwd: dir, encoding: 'utf8', timeout: 30_000 })) as ReturnType<NonNullable<MergedPrChecklistDeps['readPr']>>))(number, cwd);
28
+ const pr = (deps.readPr ?? ((n, dir) => JSON.parse(execFileSync('gh', ['pr', 'view', String(n), '--json', 'state,baseRefName,title,body,mergedAt,url,headRefOid,files'], { cwd: dir, encoding: 'utf8', timeout: 30_000 })) as ReturnType<NonNullable<MergedPrChecklistDeps['readPr']>>))(number, cwd);
25
29
  if (pr.state !== 'MERGED' || pr.baseRefName !== 'main') return;
26
30
  const merged: MergedChecklistPr = { number, title: pr.title, body: pr.body, mergedAt: pr.mergedAt };
27
31
  const versions = deps.versions?.() ?? (() => {
@@ -43,6 +47,21 @@ export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: st
43
47
  const fromTitle = titleCells(pr.title);
44
48
  const fromGoal = goal ? goalFileCells(goal) : new Set<string>();
45
49
  const designated = [...new Set([...fromBody, ...fromTitle, ...fromGoal])];
50
+ // A TA card's bound checklist criterion owns its postland verdict. A prose completion
51
+ // declaration cannot turn that cell green before the measured TA judgment runs.
52
+ let taClosedId: string | null = null;
53
+ let taBoundIds = new Set<string>();
54
+ let taBindingUnreadable = false;
55
+ try {
56
+ taBoundIds = new Set((deps.candidateTaskCells ?? candidateMergedTaskCells)(number));
57
+ if (pr.url && pr.headRefOid) for (const id of (deps.boundTaskCells ?? boundMergedTaskCells)({ number, url: pr.url })) taBoundIds.add(id);
58
+ } catch (error) { taBindingUnreadable = true; log('release.checklist', 'postland-binding-unreadable', { pr: number, error: String(error) }); }
59
+ if (pr.url && pr.headRefOid && !taBindingUnreadable) {
60
+ try {
61
+ taClosedId = (deps.closeTaskCard ?? closeMergedTaskCard)({ ...pr, number, url: pr.url, headRefOid: pr.headRefOid }, cwd, { versions: [...new Set(versions)] });
62
+ if (taClosedId) taBoundIds.add(taClosedId);
63
+ } catch (error) { log('release.checklist', 'postland-card-error', { pr: number, ids: [...taBoundIds], error: String(error) }); }
64
+ }
46
65
  for (const id of designated) {
47
66
  log('release.checklist', 'merged-pr-cell-source', {
48
67
  pr: number,
@@ -67,19 +86,19 @@ export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: st
67
86
  }
68
87
  for (const { version, snapshot, row } of matches) {
69
88
  if (counts.get(row.id) !== 1) continue;
70
- for (const { id, ref } of evidencePlan([row])) {
89
+ for (const { id, ref } of taClosedId === row.id ? [] : evidencePlan([row])) {
71
90
  (deps.addEvidence ?? evidenceAdd)(id, version, ref, 'harness', snapshot.released, snapshot.dev);
72
91
  log('release.checklist', 'merged-pr-evidence-added', { version, id, pr: number, ref });
73
92
  }
74
93
  const cell = snapshot.items.find((item) => item.id === row.id)!;
75
- if (cell.status !== 'green' && cell.status !== 'done') {
94
+ if (!taBindingUnreadable && !taBoundIds.has(row.id) && cell.status !== 'green' && cell.status !== 'done') {
76
95
  try {
77
96
  (deps.judgeLanded ?? judgeLandedCell)({ version, id: row.id, title: cell.title, pr: number }, { cwd });
78
97
  } catch (error) {
79
98
  try { log('release.checklist', 'landed-green-error', { version, id: row.id, pr: number, error: String(error) }); } catch { /* Judgment remains fail-soft. */ }
80
99
  }
81
100
  }
82
- if (goal && declaresCompletion(goal)) {
101
+ if (!taBindingUnreadable && !taBoundIds.has(row.id) && goal && declaresCompletion(goal)) {
83
102
  if (!goalCells(goal).has(row.id)) {
84
103
  log('release.checklist', 'merged-pr-status-skipped', { version, id: row.id, pr: number, reason: 'goal-does-not-name-cell' });
85
104
  continue;
@@ -99,6 +99,76 @@ export function rubricPriority(text: string): ReturnType<typeof rubricGrade> {
99
99
  return rubric ? rubricGrade(rubricScore(rubric)) : 'P2';
100
100
  }
101
101
 
102
+ // Rubric v2 — first slice: the formula only (line parsing and v1/v2 output are later slots).
103
+
104
+ /** v2 value axes (L U F O R T M, each 0..3); Z divides the total, X subtracts from it. */
105
+ export const RUBRIC_V2_VALUE_AXES = ['L', 'U', 'F', 'O', 'R', 'T', 'M'] as const;
106
+ export const RUBRIC_V2_Z_CHOICES: readonly number[] = [1, 2, 4, 8];
107
+ const V2_AXIS_KEYS = new Set<string>([...RUBRIC_V2_VALUE_AXES, 'Z', 'X']);
108
+
109
+ /** Rubric v2 values: L U F O R T M each 0..3, Z ∈ {1,2,4,8}, X 0..3. */
110
+ export interface RubricV2 {
111
+ L: number;
112
+ U: number;
113
+ F: number;
114
+ O: number;
115
+ R: number;
116
+ T: number;
117
+ M: number;
118
+ Z: number;
119
+ X: number;
120
+ }
121
+
122
+ /** The F weight is 2 through 2026-10-28 and 1 from 2026-10-29 on; asOf must retain its original date text. */
123
+ export const RUBRIC_V2_F_CUTOVER_DAY = '2026-10-29';
124
+
125
+ export function rubricV2FWeight(asOf: string): 2 | 1 {
126
+ const invalid = () => new Error(`루브릭 v2 기준 날짜가 잘못되었다: ${String(asOf)}`);
127
+ // A Date may already have normalized an impossible calendar day before this call.
128
+ if (typeof asOf !== 'string') throw invalid();
129
+ const parts = /^(\d{4}|[1-9]\d{4,5}|[+-]\d{6})-(\d{2})-(\d{2})(?:$|T)/.exec(asOf);
130
+ if (!parts) throw invalid();
131
+ const year = Number(parts[1]);
132
+ const month = Number(parts[2]);
133
+ const day = Number(parts[3]);
134
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
135
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
136
+ if (!daysInMonth || day < 1 || day > daysInMonth) throw invalid();
137
+ const date = new Date(asOf);
138
+ if (Number.isNaN(date.getTime())) throw invalid();
139
+ return date.getTime() >= Date.parse(`${RUBRIC_V2_F_CUTOVER_DAY}T00:00:00.000Z`) ? 1 : 2;
140
+ }
141
+
142
+ /** V = 2L + 2U + wF + 1.5O + 1.5R + 1.5T + M (w = rubricV2FWeight), 점수 = V / Z − 0.5X. */
143
+ export function rubricScoreV2(values: RubricV2, asOf: string): number {
144
+ if (values === null || typeof values !== 'object' || Array.isArray(values)) throw new Error('루브릭 v2 값이 축 객체가 아니다');
145
+ const given = values as unknown as Record<string, unknown>;
146
+ for (const key of Object.keys(given)) {
147
+ if (!V2_AXIS_KEYS.has(key)) throw new Error(`루브릭 v2 알 수 없는 축: ${key}`);
148
+ }
149
+ const readAxis = (axis: (typeof RUBRIC_V2_VALUE_AXES)[number]): number => {
150
+ const value = given[axis];
151
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`루브릭 v2 축 ${axis} 값이 없다`);
152
+ if (value < 0 || value > 3) throw new Error(`루브릭 v2 축 ${axis} 값이 0~3 밖이다: ${value}`);
153
+ return value;
154
+ };
155
+ const L = readAxis('L');
156
+ const U = readAxis('U');
157
+ const F = readAxis('F');
158
+ const O = readAxis('O');
159
+ const R = readAxis('R');
160
+ const T = readAxis('T');
161
+ const M = readAxis('M');
162
+ const Z = given.Z;
163
+ if (typeof Z !== 'number' || !Number.isFinite(Z)) throw new Error('루브릭 v2 크기 Z 값이 없다');
164
+ if (!RUBRIC_V2_Z_CHOICES.includes(Z)) throw new Error(`루브릭 v2 크기 Z 값이 1·2·4·8 밖이다: ${Z}`);
165
+ const X = given.X;
166
+ if (typeof X !== 'number' || !Number.isFinite(X)) throw new Error('루브릭 v2 위험 X 값이 없다');
167
+ if (X < 0 || X > 3) throw new Error(`루브릭 v2 위험 X 값이 0~3 밖이다: ${X}`);
168
+ const w = rubricV2FWeight(asOf);
169
+ return (2 * L + 2 * U + w * F + 1.5 * O + 1.5 * R + 1.5 * T + M) / Z - 0.5 * X;
170
+ }
171
+
102
172
  /** Inspect an existing checklist without initializing, importing, or migrating its ledger. */
103
173
  export function readRubricItems(version: string, ledgerRoot: string): Pick<ChecklistItem, 'id' | 'title' | 'evidence' | 'priority'>[] {
104
174
  const dbPath = join(ledgerRoot, 'release', 'features.sqlite');