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
@@ -1,5 +1,5 @@
1
1
  import { Database } from 'bun:sqlite';
2
- import { mkdirSync } from 'node:fs';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { elanousStateRoot } from '../autopilot/state-paths.js';
5
5
 
@@ -8,6 +8,7 @@ export interface MessageEnvelope {
8
8
  to: string;
9
9
  body: string;
10
10
  kind?: string;
11
+ replyTo?: number;
11
12
  }
12
13
 
13
14
  export interface Message extends MessageEnvelope {
@@ -20,7 +21,7 @@ export interface UnreadRecipient {
20
21
  count: number;
21
22
  }
22
23
 
23
- const SEAT_ALIASES: Readonly<Record<string, string>> = {
24
+ export const SEAT_ALIASES: Readonly<Record<string, string>> = {
24
25
  S: 'OP', OP: 'OP', COO: 'OP',
25
26
  T: 'MK', MK: 'MK', CMO: 'MK',
26
27
  O: 'TC', TC: 'TC', CTO: 'TC',
@@ -41,12 +42,21 @@ export function validateMessageEnvelope(input: unknown): MessageEnvelope {
41
42
  const record = input as Record<string, unknown>;
42
43
  const from = canonicalSeatId(record.from as string);
43
44
  const to = canonicalSeatId(record.to as string);
45
+ if (from === 'ALL') throw new Error('ALL cannot send messages');
44
46
  if (typeof record.body !== 'string' || !record.body.trim()) throw new Error('message body must be nonempty');
45
47
  if (record.kind !== undefined && (typeof record.kind !== 'string' || !/^[a-z][a-z0-9_-]{0,63}$/.test(record.kind))) {
46
48
  throw new Error('invalid message kind');
47
49
  }
48
- if (Object.keys(record).some(key => !['from', 'to', 'body', 'kind'].includes(key))) throw new Error('unknown message field');
49
- return record.kind === undefined ? { from, to, body: record.body } : { from, to, body: record.body, kind: record.kind as string };
50
+ const replyTo = record.replyTo;
51
+ if (replyTo !== undefined && (typeof replyTo !== 'number' || !Number.isSafeInteger(replyTo) || replyTo < 1)) {
52
+ throw new Error('replyTo must be a positive integer message ID');
53
+ }
54
+ if (Object.keys(record).some(key => !['from', 'to', 'body', 'kind', 'replyTo'].includes(key))) throw new Error('unknown message field');
55
+ return {
56
+ from, to, body: record.body,
57
+ ...(record.kind === undefined ? {} : { kind: record.kind as string }),
58
+ ...(replyTo === undefined ? {} : { replyTo }),
59
+ };
50
60
  }
51
61
 
52
62
  function validCursor(cursor: number): number {
@@ -54,48 +64,109 @@ function validCursor(cursor: number): number {
54
64
  return cursor;
55
65
  }
56
66
 
67
+ function unreadCountOn(db: Database, to: string): number {
68
+ return (db.query(`SELECT COUNT(*) AS count FROM msg_messages m
69
+ WHERE m.id > COALESCE((SELECT last_id FROM msg_cursors WHERE recipient = ?), 0)
70
+ AND (m.recipient = ? OR (m.recipient = 'ALL' AND m.sender <> ?))`)
71
+ .get(to, to, to) as { count: number }).count;
72
+ }
73
+
57
74
  interface MessageRow {
58
75
  id: number;
59
76
  sender: string;
60
77
  recipient: string;
61
78
  body: string;
62
79
  kind: string | null;
80
+ reply_to: number | null;
63
81
  created_at: string;
64
82
  }
65
83
 
66
84
  function fromRow(row: MessageRow): Message {
67
85
  return { id: row.id, from: row.sender, to: row.recipient, body: row.body,
68
- ...(row.kind === null ? {} : { kind: row.kind }), createdAt: row.created_at };
86
+ ...(row.kind === null ? {} : { kind: row.kind }),
87
+ ...(row.reply_to == null ? {} : { replyTo: row.reply_to }),
88
+ createdAt: row.created_at };
69
89
  }
70
90
 
71
91
  /** SQLite owns both append order and per-recipient acknowledgement progress. */
72
92
  export class MsgStore {
73
93
  readonly db: Database;
74
94
 
75
- constructor(path: string = defaultMsgStorePath()) {
76
- if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
77
- this.db = new Database(path);
95
+ // A read-only SSE connection may outlive another process's in-place migration.
96
+ private get hasReplyTo(): boolean {
97
+ return (this.db.query(`SELECT COUNT(*) AS count FROM pragma_table_info('msg_messages') WHERE name = 'reply_to'`)
98
+ .get() as { count: number }).count > 0;
99
+ }
100
+
101
+ private readWithSchema<T>(read: (hasReplyTo: boolean) => T): T {
102
+ // The schema projection and rows must share one SQLite snapshot. Otherwise a writer can
103
+ // migrate between them and a reader could advance past a reply with its link erased.
104
+ return this.db.transaction(() => read(this.hasReplyTo)).deferred();
105
+ }
106
+
107
+ constructor(path: string = defaultMsgStorePath(), options: { readOnly?: boolean } = {}) {
108
+ if (options.readOnly && (path === ':memory:' || !existsSync(path))) throw new Error('message ledger unavailable for read-only SSE service');
109
+ if (!options.readOnly && path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
110
+ this.db = options.readOnly ? new Database(path, { readonly: true }) : new Database(path);
78
111
  this.db.exec('PRAGMA busy_timeout = 5000');
79
- this.db.exec(`CREATE TABLE IF NOT EXISTS msg_messages (
112
+ if (!options.readOnly) this.db.exec(`CREATE TABLE IF NOT EXISTS msg_messages (
80
113
  id INTEGER PRIMARY KEY AUTOINCREMENT,
81
114
  sender TEXT NOT NULL,
82
115
  recipient TEXT NOT NULL,
83
116
  body TEXT NOT NULL,
84
117
  kind TEXT,
118
+ reply_to INTEGER,
85
119
  created_at TEXT NOT NULL
86
120
  );
87
121
  CREATE INDEX IF NOT EXISTS idx_msg_messages_recipient_id ON msg_messages(recipient, id);
88
122
  CREATE TABLE IF NOT EXISTS msg_cursors (
89
123
  recipient TEXT PRIMARY KEY,
90
124
  last_id INTEGER NOT NULL DEFAULT 0 CHECK(last_id >= 0)
125
+ );
126
+ CREATE TABLE IF NOT EXISTS msg_provenance (
127
+ provenance_key TEXT PRIMARY KEY,
128
+ message_id INTEGER NOT NULL REFERENCES msg_messages(id)
91
129
  );`);
130
+ // Serialize the schema check and ALTER across processes opening an old ledger at once.
131
+ if (!options.readOnly) this.db.transaction(() => {
132
+ const replyToColumn = (this.db.query(`SELECT COUNT(*) AS count FROM pragma_table_info('msg_messages') WHERE name = 'reply_to'`)
133
+ .get() as { count: number }).count;
134
+ if (replyToColumn === 0) this.db.exec('ALTER TABLE msg_messages ADD COLUMN reply_to INTEGER');
135
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_msg_messages_reply_to ON msg_messages(reply_to, id)');
136
+ }).immediate();
92
137
  }
93
138
 
94
139
  append(input: MessageEnvelope): Message {
140
+ return this.insertMessage(input);
141
+ }
142
+
143
+ /** Append once for a coordination comment or a task failure incident. */
144
+ appendWithProvenance(input: MessageEnvelope, provenanceKey: string): Message {
145
+ if (typeof provenanceKey !== 'string' || !/^(?:coord|ta-failure):[^:]+:[^:]+$/.test(provenanceKey)) {
146
+ throw new Error('invalid message provenance key');
147
+ }
95
148
  const msg = validateMessageEnvelope(input);
149
+ if (provenanceKey.split(':')[2] !== msg.to) throw new Error('provenance recipient does not match message recipient');
150
+ return this.db.transaction(() => {
151
+ const existing = this.db.query(`SELECT m.id, m.sender, m.recipient, m.body, m.kind, m.reply_to, m.created_at
152
+ FROM msg_provenance p JOIN msg_messages m ON m.id = p.message_id WHERE p.provenance_key = ?`)
153
+ .get(provenanceKey) as MessageRow | null;
154
+ if (existing) return fromRow(existing);
155
+ const message = this.insertMessage(msg);
156
+ this.db.query('INSERT INTO msg_provenance(provenance_key, message_id) VALUES (?, ?)')
157
+ .run(provenanceKey, message.id);
158
+ return message;
159
+ }).immediate();
160
+ }
161
+
162
+ private insertMessage(input: MessageEnvelope): Message {
163
+ const msg = validateMessageEnvelope(input);
164
+ if (msg.replyTo !== undefined && this.db.query('SELECT 1 FROM msg_messages WHERE id = ?').get(msg.replyTo) === null) {
165
+ throw new Error(`reply target not found: #${msg.replyTo}`);
166
+ }
96
167
  const createdAt = new Date().toISOString();
97
- const result = this.db.query(`INSERT INTO msg_messages(sender, recipient, body, kind, created_at)
98
- VALUES (?, ?, ?, ?, ?)`).run(msg.from, msg.to, msg.body, msg.kind ?? null, createdAt);
168
+ const result = this.db.query(`INSERT INTO msg_messages(sender, recipient, body, kind, reply_to, created_at)
169
+ VALUES (?, ?, ?, ?, ?, ?)`).run(msg.from, msg.to, msg.body, msg.kind ?? null, msg.replyTo ?? null, createdAt);
99
170
  return { id: Number(result.lastInsertRowid), ...msg, createdAt };
100
171
  }
101
172
 
@@ -104,24 +175,61 @@ export class MsgStore {
104
175
  /** Messages strictly after the supplied cursor, ordered by durable insertion ID. */
105
176
  listByRecipient(recipient: string, after: number = 0, limit: number = 100): Message[] {
106
177
  const to = canonicalSeatId(recipient);
178
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
107
179
  validCursor(after);
108
180
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
109
- return (this.db.query(`SELECT id, sender, recipient, body, kind, created_at FROM msg_messages
110
- WHERE recipient = ? AND id > ? ORDER BY id ASC LIMIT ?`).all(to, after, limit) as MessageRow[]).map(fromRow);
181
+ return this.readWithSchema(hasReplyTo => (this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
182
+ WHERE id > ? AND (recipient = ? OR (recipient = 'ALL' AND sender <> ?))
183
+ ORDER BY id ASC LIMIT ?`).all(after, to, to, limit) as MessageRow[]).map(fromRow));
184
+ }
185
+
186
+ /** One message by durable ID, or null when absent. */
187
+ getMessage(messageId: number): Message | null {
188
+ if (!Number.isSafeInteger(messageId) || messageId < 1) throw new Error('message ID must be a positive integer');
189
+ return this.readWithSchema(hasReplyTo => {
190
+ const row = this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
191
+ WHERE id = ?`).get(messageId) as MessageRow | null;
192
+ return row === null ? null : fromRow(row);
193
+ });
194
+ }
195
+
196
+ /** Replies pointing at the supplied message, ordered by durable insertion ID. */
197
+ listReplies(messageId: number, limit: number = 100, afterId: number = 0): Message[] {
198
+ if (!Number.isSafeInteger(messageId) || messageId < 1) throw new Error('message ID must be a positive integer');
199
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
200
+ validCursor(afterId);
201
+ return this.readWithSchema(hasReplyTo => hasReplyTo
202
+ ? (this.db.query(`SELECT id, sender, recipient, body, kind, reply_to, created_at FROM msg_messages
203
+ WHERE reply_to = ? AND id > ? ORDER BY id ASC LIMIT ?`).all(messageId, afterId, limit) as MessageRow[]).map(fromRow)
204
+ : []);
111
205
  }
112
206
 
113
207
  list(recipient: string, after: number = 0, limit: number = 100): Message[] {
114
208
  return this.listByRecipient(recipient, after, limit);
115
209
  }
116
210
 
211
+ /** One ordered scan for every SSE subscriber; recipient filtering happens in the server. */
212
+ listAfter(after: number, limit: number = 1000): Message[] {
213
+ validCursor(after);
214
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
215
+ return this.readWithSchema(hasReplyTo => (this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
216
+ WHERE id > ? ORDER BY id ASC LIMIT ?`).all(after, limit) as MessageRow[]).map(fromRow));
217
+ }
218
+
219
+ latestId(): number {
220
+ return (this.db.query('SELECT COALESCE(MAX(id), 0) AS id FROM msg_messages').get() as { id: number }).id;
221
+ }
222
+
117
223
  getCursor(recipient: string): number {
118
224
  const to = canonicalSeatId(recipient);
225
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
119
226
  return (this.db.query('SELECT last_id FROM msg_cursors WHERE recipient = ?').get(to) as { last_id: number } | null)?.last_id ?? 0;
120
227
  }
121
228
 
122
229
  /** Atomically reject future IDs and keep concurrent or stale acknowledgements from rewinding. */
123
230
  advanceCursor(recipient: string, cursor: number): number {
124
231
  const to = canonicalSeatId(recipient);
232
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
125
233
  validCursor(cursor);
126
234
  const result = this.db.query(`INSERT INTO msg_cursors(recipient, last_id)
127
235
  SELECT ?, ? WHERE ? <= (SELECT COALESCE(MAX(id), 0) FROM msg_messages)
@@ -133,11 +241,24 @@ export class MsgStore {
133
241
 
134
242
  ack(recipient: string, cursor: number): number { return this.advanceCursor(recipient, cursor); }
135
243
 
244
+ unreadCount(recipient: string): number {
245
+ const to = canonicalSeatId(recipient);
246
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
247
+ return unreadCountOn(this.db, to);
248
+ }
249
+
136
250
  unreadRecipients(): UnreadRecipient[] {
137
- return this.db.query(`SELECT m.recipient AS recipient, COUNT(*) AS count
138
- FROM msg_messages m LEFT JOIN msg_cursors c ON c.recipient = m.recipient
139
- WHERE m.id > COALESCE(c.last_id, 0)
140
- GROUP BY m.recipient ORDER BY m.recipient ASC`).all() as UnreadRecipient[];
251
+ return this.db.query(`WITH recipients(recipient) AS (
252
+ SELECT 'OP' UNION SELECT 'MK' UNION SELECT 'TC' UNION SELECT 'UX'
253
+ UNION SELECT recipient FROM msg_messages WHERE recipient <> 'ALL'
254
+ UNION SELECT recipient FROM msg_cursors WHERE recipient <> 'ALL'
255
+ )
256
+ SELECT r.recipient AS recipient, COUNT(m.id) AS count
257
+ FROM recipients r
258
+ LEFT JOIN msg_cursors c ON c.recipient = r.recipient
259
+ JOIN msg_messages m ON m.id > COALESCE(c.last_id, 0)
260
+ AND (m.recipient = r.recipient OR (m.recipient = 'ALL' AND m.sender <> r.recipient))
261
+ GROUP BY r.recipient ORDER BY r.recipient ASC`).all() as UnreadRecipient[];
141
262
  }
142
263
 
143
264
  unread(): UnreadRecipient[] { return this.unreadRecipients(); }
@@ -147,3 +268,14 @@ export class MsgStore {
147
268
 
148
269
  export function defaultMsgStorePath(): string { return join(elanousStateRoot(), 'msg', 'messages.db'); }
149
270
  export function openMsgStore(path?: string): MsgStore { return new MsgStore(path); }
271
+ export function openMsgStoreReadOnly(path?: string): MsgStore { return new MsgStore(path, { readOnly: true }); }
272
+
273
+ /** Observe one recipient without creating a store or advancing its acknowledgement. */
274
+ export function readUnreadCount(recipient: string, path: string = defaultMsgStorePath()): number | null {
275
+ const to = canonicalSeatId(recipient);
276
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
277
+ if (!existsSync(path)) return null;
278
+ const db = new Database(path, { readonly: true });
279
+ try { return unreadCountOn(db, to); }
280
+ finally { db.close(); }
281
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type OverviewSnapshot, type OwnerTaskRow, type ParentsInfo, type PodMemberRow, type ReleaseRunInfo, type ScheduleInfo,
17
17
  } from './overview.js';
18
18
  import { SEAT_ERA_SOURCE, readDispatcherBeats, readDispatcherCapacity, readSeatEraHanded, seatEraFeederDir } from './overview-seat-era.js';
19
+ import { matchesOverviewOwner, normalizeOverviewOwner } from './overview-owner.js';
19
20
 
20
21
  const GB = 1024 ** 3;
21
22
  /** 하트비트 주기 30 s × 2 (RFC §A2 «낡음»). */
@@ -77,8 +78,8 @@ export function ownerRows(cards: readonly TaskCard[], sinceIso: string, ownerFil
77
78
  const rows: Record<string, OwnerTaskRow> = {};
78
79
  for (const card of cards) {
79
80
  if (!card || typeof card.createdAt !== 'string' || card.createdAt < sinceIso) continue;
80
- const owner = card.seat ? `seat:${card.seat}` : 'unassigned';
81
- if (ownerFilter && ownerFilter !== 'all' && owner !== ownerFilter && owner !== `seat:${ownerFilter}`) continue;
81
+ const owner = normalizeOverviewOwner(card.seat);
82
+ if (ownerFilter !== undefined && !matchesOverviewOwner(owner, ownerFilter)) continue;
82
83
  const row = rows[owner] ??= { handed: 0, launched: 0, prBound: 0, failed: 0, launchFailed: 0 };
83
84
  if (card.status === 'handed') row.handed++;
84
85
  else if (card.status === 'launched') row.launched++;
@@ -0,0 +1,29 @@
1
+ /** Owner labels used by ops overview task rows and the --owner filter. */
2
+ type OverviewOwnerKind = 'seat' | 'steward' | 'orchestrator' | 'external';
3
+ type OverviewOwner = `${OverviewOwnerKind}:${string}` | 'unassigned';
4
+
5
+ const OWNER_KINDS = new Set<OverviewOwnerKind>(['seat', 'steward', 'orchestrator', 'external']);
6
+
7
+ /** Normalize a task-card owner into a stable kind-prefixed overview key. */
8
+ export function normalizeOverviewOwner(value: unknown): OverviewOwner {
9
+ if (typeof value !== 'string' || value.trim() === '') return 'unassigned';
10
+ const normalized = value.trim();
11
+ const separator = normalized.indexOf(':');
12
+ if (separator >= 0) {
13
+ const kind = normalized.slice(0, separator).toLowerCase() as OverviewOwnerKind;
14
+ const name = normalized.slice(separator + 1).trim();
15
+ if (OWNER_KINDS.has(kind) && name) return `${kind}:${name}`;
16
+ }
17
+ const kind = normalized.toLowerCase();
18
+ if (kind === 'steward' || kind === 'orchestrator' || kind === 'external') return `${kind}:default`;
19
+ return `seat:${normalized}`;
20
+ }
21
+
22
+ /** Whether a normalized owner key matches a CLI filter, accepting either the full key or its kind. */
23
+ export function matchesOverviewOwner(owner: OverviewOwner, filter?: string): boolean {
24
+ if (!filter || filter === 'all') return true;
25
+ const normalizedFilter = filter.trim();
26
+ if (!normalizedFilter) return true;
27
+ if (normalizedFilter.toLowerCase() === 'unassigned') return owner === 'unassigned';
28
+ return owner === normalizeOverviewOwner(normalizedFilter) || owner.startsWith(`${normalizedFilter.toLowerCase()}:`);
29
+ }
@@ -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
+ }
@@ -118,6 +118,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
118
118
  let stdout = '';
119
119
  let stderrBuffer = '';
120
120
  let killed = false;
121
+ let stdinError: string | undefined;
121
122
  const onAbort = () => {
122
123
  if (!killed) {
123
124
  killed = true;
@@ -132,6 +133,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
132
133
  opts.abortSignal.addEventListener('abort', onAbort, { once: true });
133
134
 
134
135
  child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
136
+ // A short-lived command can close its read end before end() finishes.
137
+ // EPIPE says only that stdin was unread; stdout and exit status still
138
+ // determine the hook result. Other pipe errors are real I/O failures.
139
+ child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
140
+ if (err.code === 'EPIPE') return;
141
+ stdinError = `stdin error: ${err.message}`;
142
+ opts.onStderr(stdinError);
143
+ });
135
144
  child.stderr?.on('data', (chunk) => {
136
145
  stderrBuffer += String(chunk);
137
146
  let idx;
@@ -145,13 +154,19 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
145
154
  clearTimeout(timer);
146
155
  if (stderrBuffer.trim()) opts.onStderr(stderrBuffer.trim());
147
156
  // killed => treat as a non-zero exit so dispatcher abort.
148
- resolve({ stdout, exitCode: killed ? 124 : (code ?? 1) });
157
+ resolve({ stdout, exitCode: killed ? 124 : stdinError ? 127 : (code ?? 1), error: stdinError });
149
158
  });
150
159
  child.on('error', (err) => {
151
160
  clearTimeout(timer);
152
161
  opts.onStderr(`spawn error: ${err.message}`);
153
162
  resolve({ stdout, exitCode: 127 });
154
163
  });
155
- try { child.stdin?.end(opts.stdin); } catch { /* pipe may have closed */ }
164
+ try { child.stdin?.end(opts.stdin); }
165
+ catch (err) {
166
+ if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
167
+ stdinError = `stdin error: ${(err as Error).message}`;
168
+ opts.onStderr(stdinError);
169
+ }
170
+ }
156
171
  });
157
172
  }
@@ -137,6 +137,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
137
137
  let stderrBuf = '';
138
138
  let killed = false;
139
139
  let timedOut = false;
140
+ let stdinError: string | undefined;
140
141
  const kill = (timeout: boolean) => {
141
142
  if (killed) return;
142
143
  killed = true;
@@ -150,6 +151,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
150
151
  opts.abortSignal.addEventListener('abort', () => kill(false), { once: true });
151
152
 
152
153
  child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
154
+ // Early exit may close stdin before end() finishes. A broken pipe must
155
+ // not replace the child's stdout/exit result; other write errors must
156
+ // surface as evaluator errors.
157
+ child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
158
+ if (err.code === 'EPIPE') return;
159
+ stdinError = `stdin error: ${err.message}`;
160
+ opts.onStderr(stdinError);
161
+ });
153
162
  child.stderr?.on('data', (chunk) => {
154
163
  stderrBuf += String(chunk);
155
164
  let idx;
@@ -164,8 +173,9 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
164
173
  if (stderrBuf.trim()) opts.onStderr(stderrBuf.trim());
165
174
  resolve({
166
175
  stdout,
167
- exitCode: timedOut ? 124 : (code ?? 1),
176
+ exitCode: timedOut ? 124 : stdinError ? 127 : (code ?? 1),
168
177
  timedOut,
178
+ error: stdinError,
169
179
  });
170
180
  });
171
181
  child.on('error', (err) => {
@@ -173,6 +183,12 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
173
183
  opts.onStderr(`spawn error: ${err.message}`);
174
184
  resolve({ stdout, exitCode: 127, timedOut: false });
175
185
  });
176
- try { child.stdin?.end(opts.stdin); } catch { /* pipe may have closed */ }
186
+ try { child.stdin?.end(opts.stdin); }
187
+ catch (err) {
188
+ if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
189
+ stdinError = `stdin error: ${(err as Error).message}`;
190
+ opts.onStderr(stdinError);
191
+ }
192
+ }
177
193
  });
178
194
  }
@@ -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);
@@ -438,6 +438,13 @@ function declaresMain(packageRoot: string): boolean {
438
438
  return !existsSync(join(packageRoot, 'plugin.json')) && !existsSync(join(packageRoot, '.codex-plugin', 'plugin.json'));
439
439
  }
440
440
 
441
+ /** [wiring] Caller: installPlugin. The PluginManifest normalized by
442
+ * loadPluginManifestFromDir is handed into the install path here — the install
443
+ * destination follows the manifest identity, never the source spec/dir name. */
444
+ export function pluginInstallPath(root: string, market: string, manifest: PluginManifest): string {
445
+ return join(root, 'plugins', market, manifest.id, manifest.version);
446
+ }
447
+
441
448
  export async function installPlugin(spec: string, opts: InstallOptions = {}): Promise<InstalledPlugin> {
442
449
  const root = resolve(opts.root ?? elanousStateRoot());
443
450
  const stageRoot = join(root, 'plugins', '.staging');
@@ -466,6 +473,10 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
466
473
  const manifest: PluginManifest = loadPluginManifestFromDir(join(stage, 'package'), { id: source.expectedName ?? basename(spec) }).manifest;
467
474
  if (!NAME.test(manifest.id) || !VERSION.test(manifest.version)) fail('io', 'invalid plugin name or version');
468
475
  if (source.expectedName && (manifest.id !== source.expectedName || manifest.version !== source.expectedVersion)) fail('conflict', 'market name or version differs from manifest');
476
+ // [wiring] installPlugin is the caller: the PluginManifest normalized above by
477
+ // loadPluginManifestFromDir (present manifest or the loader's manifest-absent
478
+ // fallback) is handed into the install path here, past the NAME/VERSION gates.
479
+ const destination = pluginInstallPath(root, source.market, manifest);
469
480
  const packageRoot = join(stage, 'package');
470
481
  const mainPath = inside(packageRoot, manifest.main);
471
482
  // A skills/graphs-only pack (official `elanous-basics`) declares no `main` — the default `./plugin.ts` is not a promise.
@@ -480,7 +491,6 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
480
491
  const path = inside(packageRoot, asset);
481
492
  if (!existsSync(path) || !realpathSync(path).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin asset not found or unsafe: ${asset}`);
482
493
  }
483
- const destination = join(root, 'plugins', source.market, name, version);
484
494
  const emit = (event: InstallEvent) => opts.onEvent?.(event);
485
495
  emit({ event: 'resolve', plugin: name, version, sha256: source.sha256 });
486
496
  let security: ReturnType<typeof inspectPluginSecurity>;