elanous 0.2.27 → 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 (285) 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-a9c338f5bb556f42.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-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  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-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  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-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  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-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  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-4eb96fc7241c2456.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-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  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-dab163b338e3b966.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-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  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-99c4e6c7345a9156.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-11bf9e5725a72c22.js +1 -0
  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 +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -0,0 +1,128 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { chmodSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
+ import { debug } from '../debug/log.js';
6
+
7
+ /** 특허 후보의 갈래 — 구체 후보와 기전은 비공개 원장에만 둔다. */
8
+ const PATENT_TRACKS = ['비공개', '출원', '신규'] as const;
9
+ /** 특허 후보의 상태 — 조사/명세/출원/공개 허용 중 공개 허용 상태만 공개 읽기 통로에 나온다. */
10
+ const PATENT_STATUSES = ['조사', '명세', '출원', '공개 허용'] as const;
11
+
12
+ export type PatentTrack = (typeof PATENT_TRACKS)[number];
13
+ export type PatentStatus = (typeof PATENT_STATUSES)[number];
14
+
15
+ export class PatentsInputError extends Error {
16
+ override name = 'PatentsInputError';
17
+ }
18
+
19
+ export interface PatentCandidate {
20
+ id: string;
21
+ title: string;
22
+ track: PatentTrack;
23
+ status: PatentStatus;
24
+ deadline: string | null;
25
+ created_at: string;
26
+ updated_at: string;
27
+ }
28
+ export interface PatentLedgerOptions { stateDir?: string; now?: () => Date }
29
+
30
+ function required(value: string, field: string): string {
31
+ if (typeof value !== 'string' || !value.trim()) throw new PatentsInputError(`${field} is required`);
32
+ return value.trim();
33
+ }
34
+
35
+ function trackOf(value: PatentTrack): PatentTrack {
36
+ if (!PATENT_TRACKS.includes(value)) throw new PatentsInputError(`invalid track: ${value}`);
37
+ return value;
38
+ }
39
+
40
+ function statusOf(value: PatentStatus): PatentStatus {
41
+ if (!PATENT_STATUSES.includes(value)) throw new PatentsInputError(`invalid status: ${value}`);
42
+ return value;
43
+ }
44
+
45
+ function isoDate(value: string, field: string): string {
46
+ if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\d$/.test(value)) throw new PatentsInputError(`invalid ${field}: ${value}`);
47
+ const date = new Date(`${value}T00:00:00.000Z`);
48
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) throw new PatentsInputError(`invalid ${field}: ${value}`);
49
+ return value;
50
+ }
51
+
52
+ /** 후보 식별자·갈래·상태·마감일의 비공개 원장 — 공개 허용 상태 후보만 읽기 통로로 노출한다. */
53
+ export class PatentLedger {
54
+ readonly path: string;
55
+ private readonly now: () => Date;
56
+
57
+ constructor(options: PatentLedgerOptions = {}) {
58
+ this.path = join(options.stateDir ?? elanousStateRoot(), 'patents', 'patents.sqlite');
59
+ this.now = options.now ?? (() => new Date());
60
+ }
61
+
62
+ private open(): Database {
63
+ mkdirSync(dirname(this.path), { recursive: true });
64
+ const db = new Database(this.path, { create: true, strict: true });
65
+ try {
66
+ chmodSync(this.path, 0o600);
67
+ db.exec('PRAGMA busy_timeout = 10000');
68
+ db.exec('PRAGMA journal_mode = WAL');
69
+ db.exec(`CREATE TABLE IF NOT EXISTS patent_candidates (
70
+ id TEXT PRIMARY KEY, title TEXT NOT NULL,
71
+ track TEXT NOT NULL CHECK(track IN ('비공개','출원','신규')),
72
+ status TEXT NOT NULL CHECK(status IN ('조사','명세','출원','공개 허용')),
73
+ deadline TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);`);
74
+ return db;
75
+ } catch (error) { db.close(); throw error; }
76
+ }
77
+
78
+ private using<T>(work: (db: Database) => T): T {
79
+ const db = this.open();
80
+ try { return work(db); } finally { db.close(); }
81
+ }
82
+
83
+ private transaction<T>(db: Database, work: () => T): T {
84
+ db.exec('BEGIN IMMEDIATE');
85
+ try { const result = work(); db.exec('COMMIT'); return result; }
86
+ catch (error) { db.exec('ROLLBACK'); throw error; }
87
+ }
88
+
89
+ private row(db: Database, id: string): PatentCandidate {
90
+ const row = db.query('SELECT id, title, track, status, deadline, created_at, updated_at FROM patent_candidates WHERE id = ?').get(id) as PatentCandidate | null;
91
+ if (!row) throw new PatentsInputError(`patent candidate not found: ${id}`);
92
+ return row;
93
+ }
94
+
95
+ add(input: { id: string; title: string; track: PatentTrack; status?: PatentStatus; deadline?: string }): PatentCandidate {
96
+ const id = required(input.id, 'id');
97
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) throw new PatentsInputError('invalid patent candidate id');
98
+ const title = required(input.title, 'title');
99
+ const track = trackOf(input.track);
100
+ const status = statusOf(input.status ?? '조사');
101
+ const deadline = input.deadline === undefined ? null : isoDate(input.deadline, 'deadline');
102
+ return this.using(db => this.transaction(db, () => {
103
+ if (db.query('SELECT 1 FROM patent_candidates WHERE id = ?').get(id)) throw new PatentsInputError(`patent candidate already exists: ${id}`);
104
+ const at = this.now().toISOString();
105
+ db.query('INSERT INTO patent_candidates (id, title, track, status, deadline, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
106
+ .run(id, title, track, status, deadline, at, at);
107
+ debug.log('patents.ledger', 'add', { id, status });
108
+ return this.row(db, id);
109
+ }));
110
+ }
111
+
112
+ setStatus(id: string, status: PatentStatus): PatentCandidate {
113
+ const next = statusOf(status);
114
+ return this.using(db => this.transaction(db, () => {
115
+ this.row(db, id);
116
+ const at = this.now().toISOString();
117
+ db.query('UPDATE patent_candidates SET status = ?, updated_at = ? WHERE id = ?').run(next, at, id);
118
+ debug.log('patents.ledger', 'status', { id, status: next });
119
+ return this.row(db, id);
120
+ }));
121
+ }
122
+
123
+ /** 이 원장의 유일한 공개 읽기 통로 — 공개 허용 상태 후보만 나온다(조사·명세·출원 상태 후보는 나오지 않는다). */
124
+ listPublicCandidates(): PatentCandidate[] {
125
+ return this.using(db =>
126
+ db.query(`SELECT id, title, track, status, deadline, created_at, updated_at FROM patent_candidates WHERE status = '공개 허용' ORDER BY created_at, id`).all() as PatentCandidate[]);
127
+ }
128
+ }
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { withFileLockSync } from '../../storage/file-lock.js';
4
- import { dirname, join, resolve } from 'node:path';
4
+ import { loadPluginManifestFromDir } from '../core/manifest.js';
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
6
  import { elanousStateRoot } from '../../autopilot/state-paths.js';
6
7
  import { userConfigPath } from '../../user-config.js';
7
8
  import { OFFICIAL_INDEX_KEYS } from '../../market/official-keys.js';
@@ -37,6 +38,36 @@ export interface MarketIndex {
37
38
  signature: 'ok';
38
39
  keyId: string;
39
40
  }
41
+ /** Resolve only an installed, available Codex plugin package to the local-path input
42
+ * already accepted by `plugin add`. Codex marketplace records are not Elanous signed indices. */
43
+ export function codexPluginPackagePath(response: unknown, name: string, marketplace: string): string {
44
+ if (!NAME.test(name) || !NAME.test(marketplace)) throw new MarketFetchError('io', 'invalid Codex plugin or marketplace name');
45
+ const inventory = response as { marketplaces?: unknown } | null;
46
+ if (!inventory || !Array.isArray(inventory.marketplaces)) throw new MarketFetchError('io', 'invalid Codex plugin/list response');
47
+ const matches: string[] = [];
48
+ for (const rawMarket of inventory.marketplaces) {
49
+ if (!rawMarket || typeof rawMarket !== 'object') throw new MarketFetchError('io', 'invalid Codex marketplace');
50
+ const market = rawMarket as { name?: unknown; plugins?: unknown };
51
+ if (market.name !== marketplace) continue;
52
+ if (!Array.isArray(market.plugins)) throw new MarketFetchError('io', 'invalid Codex marketplace plugins');
53
+ for (const rawPlugin of market.plugins) {
54
+ if (!rawPlugin || typeof rawPlugin !== 'object') continue;
55
+ const plugin = rawPlugin as Record<string, unknown>;
56
+ if (plugin.name !== name || plugin.installed !== true) continue;
57
+ if (typeof plugin.availability === 'string' && plugin.availability.toUpperCase() !== 'AVAILABLE') continue;
58
+ const path = plugin.installPath ?? plugin.path;
59
+ if (typeof path !== 'string' || !isAbsolute(path) || !existsSync(path) || !statSync(path).isDirectory()) {
60
+ throw new MarketFetchError('io', 'Codex installed plugin package directory unavailable');
61
+ }
62
+ const manifest = loadPluginManifestFromDir(path, { id: name }).manifest;
63
+ if (manifest.id !== name) throw new MarketFetchError('io', 'Codex package name differs from inventory');
64
+ matches.push(path);
65
+ }
66
+ }
67
+ if (matches.length !== 1) throw new MarketFetchError('io', `Codex plugin not installed or ambiguous: ${name}@${marketplace}`);
68
+ return matches[0]!;
69
+ }
70
+
40
71
  export class MarketFetchError extends Error {
41
72
  constructor(public readonly reason: 'io' | 'signature', message: string) {
42
73
  super(message);
@@ -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();
@@ -299,9 +299,20 @@ export function addItem(v: string, input: { id: string; title: string; refs?: st
299
299
  }
300
300
 
301
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 {
302
311
  return mutate(v, (data, otherItems) => {
303
312
  const item = data.items.find((i) => i.id === id);
304
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}`, '현재 칸으로 다시 측정한다');
305
316
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
306
317
  if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
307
318
  if (patch.kind !== undefined && !CHECKLIST_KINDS.includes(patch.kind)) throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen|postpub');
@@ -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');
@@ -0,0 +1,88 @@
1
+ import { chmodSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { Database } from 'bun:sqlite';
4
+ import { releaseLedgerRoot } from '../instance/resolve.js';
5
+ import { assertNotTestWritingOps } from '../instance/test-write-guard.js';
6
+
7
+ /** The RFC ledger is independent of the release checklist ledger. Callers own and close the handle. */
8
+ export function openRfcRegistry(root = releaseLedgerRoot()): Database {
9
+ const path = join(root, 'rfc', 'registry.sqlite');
10
+ assertNotTestWritingOps(path, 'open the RFC registry');
11
+ mkdirSync(dirname(path), { recursive: true });
12
+ const db = new Database(path, { create: true, strict: true });
13
+ try {
14
+ chmodSync(path, 0o600);
15
+ db.exec('PRAGMA busy_timeout = 10000; PRAGMA foreign_keys = ON');
16
+ db.exec(`CREATE TABLE IF NOT EXISTS rfc (
17
+ id TEXT PRIMARY KEY CHECK(length(id) = 8 AND id GLOB 'RFC-[0-9][0-9][0-9][0-9]'),
18
+ title TEXT NOT NULL CHECK(length(trim(title)) > 0),
19
+ status TEXT NOT NULL,
20
+ doc_path TEXT,
21
+ created_at TEXT NOT NULL
22
+ );
23
+ CREATE TABLE IF NOT EXISTS rfc_item (
24
+ id TEXT PRIMARY KEY CHECK(length(id) = 12 AND id GLOB 'RFC-[0-9][0-9][0-9][0-9].I[0-9][0-9]'),
25
+ rfc_id TEXT NOT NULL REFERENCES rfc(id),
26
+ title TEXT NOT NULL CHECK(length(trim(title)) > 0),
27
+ status TEXT NOT NULL,
28
+ created_at TEXT NOT NULL,
29
+ CHECK(substr(id, 1, 8) = rfc_id)
30
+ );
31
+ CREATE TABLE IF NOT EXISTS rfc_item_link (
32
+ item_id TEXT NOT NULL REFERENCES rfc_item(id),
33
+ target_item_id TEXT NOT NULL REFERENCES rfc_item(id),
34
+ PRIMARY KEY(item_id, target_item_id),
35
+ CHECK(item_id <> target_item_id)
36
+ );
37
+ CREATE TABLE IF NOT EXISTS rfc_event (
38
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ rfc_id TEXT NOT NULL REFERENCES rfc(id),
40
+ item_id TEXT REFERENCES rfc_item(id),
41
+ at TEXT NOT NULL,
42
+ actor TEXT NOT NULL,
43
+ event TEXT NOT NULL,
44
+ payload TEXT
45
+ );
46
+ CREATE TRIGGER IF NOT EXISTS rfc_id_immutable BEFORE UPDATE OF id ON rfc
47
+ BEGIN SELECT RAISE(ABORT, 'RFC identifier is immutable'); END;
48
+ CREATE TRIGGER IF NOT EXISTS rfc_no_delete BEFORE DELETE ON rfc
49
+ BEGIN SELECT RAISE(ABORT, 'RFC identifier is immutable'); END;
50
+ CREATE TRIGGER IF NOT EXISTS rfc_item_id_immutable BEFORE UPDATE OF id, rfc_id ON rfc_item
51
+ BEGIN SELECT RAISE(ABORT, 'RFC item identifier is immutable'); END;
52
+ CREATE TRIGGER IF NOT EXISTS rfc_item_no_delete BEFORE DELETE ON rfc_item
53
+ BEGIN SELECT RAISE(ABORT, 'RFC item identifier is immutable'); END;
54
+ CREATE TRIGGER IF NOT EXISTS rfc_event_item_parent BEFORE INSERT ON rfc_event
55
+ WHEN NEW.item_id IS NOT NULL AND NOT EXISTS
56
+ (SELECT 1 FROM rfc_item WHERE id = NEW.item_id AND rfc_id = NEW.rfc_id)
57
+ BEGIN SELECT RAISE(ABORT, 'RFC event item belongs to another RFC'); END;
58
+ CREATE TRIGGER IF NOT EXISTS rfc_event_no_update BEFORE UPDATE ON rfc_event
59
+ BEGIN SELECT RAISE(ABORT, 'RFC event history is append-only'); END;
60
+ CREATE TRIGGER IF NOT EXISTS rfc_event_no_delete BEFORE DELETE ON rfc_event
61
+ BEGIN SELECT RAISE(ABORT, 'RFC event history is append-only'); END;`);
62
+ return db;
63
+ } catch (error) { db.close(); throw error; }
64
+ }
65
+
66
+ export interface RfcRow { id: string; title: string; status: string; doc_path: string | null; created_at: string }
67
+ export interface RfcItemRow { id: string; rfc_id: string; title: string; status: string; created_at: string }
68
+ export interface RfcItemLinkRow { item_id: string; target_item_id: string }
69
+ export interface RfcDetail extends RfcRow { items: Array<RfcItemRow & { links: RfcItemLinkRow[] }> }
70
+
71
+ export function listRfcs(root?: string): RfcRow[] {
72
+ const db = openRfcRegistry(root);
73
+ try { return db.query('SELECT id, title, status, doc_path, created_at FROM rfc ORDER BY id').all() as RfcRow[]; }
74
+ finally { db.close(); }
75
+ }
76
+
77
+ export function showRfc(id: string, root?: string): RfcDetail | null {
78
+ const db = openRfcRegistry(root);
79
+ try {
80
+ const read = db.transaction(() => {
81
+ const row = db.query('SELECT id, title, status, doc_path, created_at FROM rfc WHERE id = ?').get(id) as RfcRow | null;
82
+ if (!row) return null;
83
+ const items = db.query('SELECT id, rfc_id, title, status, created_at FROM rfc_item WHERE rfc_id = ? ORDER BY id').all(id) as RfcItemRow[];
84
+ return { ...row, items: items.map((item) => ({ ...item, links: db.query('SELECT item_id, target_item_id FROM rfc_item_link WHERE item_id = ? ORDER BY target_item_id').all(item.id) as RfcItemLinkRow[] })) };
85
+ });
86
+ return read();
87
+ } finally { db.close(); }
88
+ }