elanous 0.2.9 → 0.2.10

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 (361) 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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f972fc1c00a62e88.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0df30e0c7e50148a.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +14 -14
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +14 -14
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +14 -14
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +14 -14
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +14 -14
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +14 -14
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +14 -14
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +14 -14
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +14 -14
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +14 -14
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +14 -14
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +14 -14
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +14 -14
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +14 -14
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +14 -14
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +14 -14
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +14 -14
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +14 -14
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +14 -14
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/package.json +1 -1
  146. package/plugins/card-followup/graphs/run-step.ts +48 -13
  147. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  148. package/plugins/doc-draft/graphs/run-step.ts +13 -6
  149. package/scripts/ci-pwa-build-gate.ts +160 -0
  150. package/scripts/coord-channel-watch.sh +42 -8
  151. package/scripts/coord-digest.ts +199 -0
  152. package/scripts/coord-post.sh +60 -3
  153. package/scripts/install.ps1 +68 -9
  154. package/scripts/install.sh +39 -12
  155. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  156. package/scripts/lib/tui-regress-checks.ts +146 -0
  157. package/scripts/memory-lifecycle-cycle.ts +2 -2
  158. package/scripts/ops-now-probe.ts +327 -0
  159. package/scripts/publish-internal-dist.ts +11 -1
  160. package/scripts/release-loop/cutoff.ts +19 -8
  161. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  162. package/scripts/release-loop/release-readiness.ts +9 -0
  163. package/scripts/release-loop/version-node.ts +58 -10
  164. package/scripts/repo-watch-cycle.ts +1 -1
  165. package/scripts/retro-cycle.ts +2 -2
  166. package/scripts/schedule-health-report.ts +1 -1
  167. package/scripts/storyboard/storyboard.ts +5 -161
  168. package/scripts/test-diet/lib.ts +30 -1
  169. package/scripts/test-diet/node.ts +82 -7
  170. package/scripts/test-timeout-audit.ts +114 -0
  171. package/scripts/tree-sync-alert.ts +1 -1
  172. package/scripts/tui-regress.ts +71 -0
  173. package/scripts/waitlist-daily-digest.ts +1 -1
  174. package/skills/explainer-video/SKILL.md +4 -0
  175. package/skills/explainer-video/engine/reel.mjs +18 -13
  176. package/skills/explainer-video/engine/reel.sh +24 -5
  177. package/skills/explainer-video/engine/stitch.mjs +145 -0
  178. package/skills/pitch-deck/SKILL.md +36 -0
  179. package/skills/pitch-deck/template.html +80 -0
  180. package/src/acp/codex-approval-adapter.ts +9 -3
  181. package/src/acp/core-turn-bridge.ts +117 -18
  182. package/src/acp/fact-question.ts +11 -0
  183. package/src/acp/server.ts +48 -7
  184. package/src/acp/tool-approval.ts +126 -0
  185. package/src/acp/turn-runner.ts +36 -0
  186. package/src/agent/monad-agent-turn.ts +8 -0
  187. package/src/ask-user-question/pending-questions.ts +25 -10
  188. package/src/ask-user-question/types.ts +9 -0
  189. package/src/auth/setup-link-tokens.ts +16 -5
  190. package/src/autopilot/build/integrity-gate.ts +12 -0
  191. package/src/boot/daemon-prompt-turn.ts +3 -0
  192. package/src/boot/daemon-tools/index.ts +8 -0
  193. package/src/boot/daemon-tools/types.ts +2 -0
  194. package/src/boot/daemon-tools/web-search.ts +1 -0
  195. package/src/chat/ctrl-c-in-input.ts +14 -0
  196. package/src/chat/index.ts +52 -0
  197. package/src/chat/pickers/state.ts +6 -2
  198. package/src/cli/agent-cli.ts +7 -0
  199. package/src/cli/decisions-cli.ts +25 -3
  200. package/src/cli/doctor-bundle-upload.ts +68 -0
  201. package/src/cli/doctor-cli.ts +38 -4
  202. package/src/cli/ops-cli.ts +20 -0
  203. package/src/cli/pod-cli.ts +49 -0
  204. package/src/cli/pr-cli.ts +31 -2
  205. package/src/cli/pwa-stop.ts +163 -13
  206. package/src/cli/pwa-test.ts +3 -0
  207. package/src/cli/release-cli.ts +63 -20
  208. package/src/cli/self-update.ts +14 -0
  209. package/src/cli/storyboard-cli.ts +37 -0
  210. package/src/cli/test-state-dir-flag.ts +11 -4
  211. package/src/connectors/linear.ts +146 -0
  212. package/src/context-bus/coord-events.ts +115 -0
  213. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  214. package/src/dashboard/index.ts +58 -21
  215. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  217. package/src/decisions/decision-cards.ts +49 -1
  218. package/src/decisions/decision-ledger.ts +75 -4
  219. package/src/decisions/decision-linear-projection.ts +157 -0
  220. package/src/decisions/web-push-decision-cards.ts +50 -0
  221. package/src/directive/interpretation-card.ts +36 -0
  222. package/src/discord.ts +2 -1
  223. package/src/domains/coo-admin-tool.ts +62 -0
  224. package/src/domains/core-tools.ts +11 -3
  225. package/src/domains/elanous-card.ts +29 -0
  226. package/src/domains/ops-facts-tool.ts +76 -0
  227. package/src/domains/outbound-alert.ts +41 -13
  228. package/src/domains/release-tool.ts +168 -0
  229. package/src/domains/telegram-kind-route.ts +19 -1
  230. package/src/exec-requests/planner.ts +98 -28
  231. package/src/exec-requests/runner.ts +107 -10
  232. package/src/exec-requests/store.ts +8 -2
  233. package/src/field/field-media.ts +10 -7
  234. package/src/field/field-reel.ts +14 -4
  235. package/src/git-fs/worktree.ts +45 -6
  236. package/src/graph-runner/graph-approval-card.ts +113 -0
  237. package/src/graph-runner/graph-cli.ts +129 -4
  238. package/src/graph-runner/graph-notify.ts +25 -3
  239. package/src/graph-runner/runner.ts +142 -16
  240. package/src/harness/browser-verify.ts +25 -2
  241. package/src/harness/harness-cli-command.ts +231 -35
  242. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  243. package/src/harness/harness-incidents.ts +129 -0
  244. package/src/harness/harness-stop.ts +53 -3
  245. package/src/hitl/clarification-policy.ts +3 -1
  246. package/src/index.ts +156 -37
  247. package/src/intake-plane/author-goals.ts +37 -4
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/discord-seat-work.ts +47 -2
  250. package/src/intake-plane/seat-doc-route.ts +170 -6
  251. package/src/intake-plane/telegram-seat-work.ts +44 -2
  252. package/src/intake-plane/telegram-work.ts +1 -0
  253. package/src/maturity/cli-maturity.ts +34 -0
  254. package/src/maturity/feature-maturity.ts +98 -1
  255. package/src/maturity/tui-slash-maturity.ts +31 -0
  256. package/src/memory.ts +32 -4
  257. package/src/nexus/api/exec-requests.ts +33 -3
  258. package/src/nexus/api/http-server.ts +53 -1
  259. package/src/nexus/api/meta-api.ts +11 -0
  260. package/src/nexus/api/ops-api.ts +10 -1
  261. package/src/nexus/api/ops-seats-sources.ts +37 -7
  262. package/src/nexus/api/ops-seats.ts +16 -6
  263. package/src/nexus/api/seat-requests.ts +88 -8
  264. package/src/nexus/api/sessions-store.ts +63 -0
  265. package/src/nexus/api/setup-codex-login.ts +160 -0
  266. package/src/nexus/api/usage-api.ts +102 -0
  267. package/src/nexus/index.ts +45 -4
  268. package/src/onboarding/first-run.ts +89 -0
  269. package/src/onboarding/lan-address.ts +45 -0
  270. package/src/onboarding/web-first.ts +119 -24
  271. package/src/onboarding.ts +4 -0
  272. package/src/ops-now/ops-now-refresh.ts +100 -0
  273. package/src/project/project-store.ts +115 -0
  274. package/src/release-loop/checklist.ts +78 -4
  275. package/src/release-loop/feature-store.ts +154 -21
  276. package/src/release-loop/release-schedule.ts +53 -0
  277. package/src/repl/index.ts +7 -0
  278. package/src/seat-dispatch/ceo-commands.ts +41 -8
  279. package/src/seat-dispatch/ceo-intent.ts +38 -0
  280. package/src/seat-loop/seat-loop.ts +231 -0
  281. package/src/seat-loop/seat-report.ts +64 -0
  282. package/src/self-dev/decompose.ts +66 -11
  283. package/src/self-dev/run-store.ts +10 -1
  284. package/src/self-implement/execution-clarification.ts +115 -0
  285. package/src/self-implement/gate-cli.ts +6 -3
  286. package/src/self-implement/orchestrator.ts +37 -3
  287. package/src/self-implement/pipeline-shape.ts +1 -1
  288. package/src/self-implement/run-status-mapping.ts +5 -3
  289. package/src/session/chat.ts +2 -0
  290. package/src/session/index.ts +9 -0
  291. package/src/shell-env-bootstrap.ts +17 -0
  292. package/src/steward/directive.ts +51 -2
  293. package/src/steward/launch.ts +250 -0
  294. package/src/steward/outcome-notify.ts +115 -0
  295. package/src/steward/steward-cards.ts +8 -2
  296. package/src/steward/triage-plan.ts +32 -4
  297. package/src/steward/triage.ts +93 -9
  298. package/src/steward/working-backwards.ts +64 -0
  299. package/src/storyboard/storyboard.ts +141 -0
  300. package/src/task-cards/card-store.ts +2 -2
  301. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  302. package/src/telegram-card-followup.ts +233 -0
  303. package/src/telegram-report.ts +15 -9
  304. package/src/telegram-run.ts +7 -0
  305. package/src/telegram.ts +29 -8
  306. package/src/tool-runtime/types.ts +4 -0
  307. package/src/tui-client/dashboard-session.ts +2 -0
  308. package/src/user-config.ts +118 -11
  309. package/src/version/packed-revision.json +1 -1
  310. package/src/web-push/notify-exec-request.ts +32 -0
  311. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  359. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
  360. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  361. /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -1,16 +1,40 @@
1
1
  import type { Command } from 'commander';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { parse as parseYaml } from 'yaml';
2
5
  import { writeStdoutJson } from '../cli/stdout-json.js';
3
- import { decideGraphApproval, latestGraphRun, runGraph } from './runner.js';
6
+ import { debug } from '../debug/log.js';
7
+ import { decideGraphApproval, graphRunAlive, lastJsonObject, latestGraphRun, listGraphRuns, runGraph, type GraphRunState } from './runner.js';
4
8
 
5
- export function registerGraphCommands(program: Command): void {
9
+ function findGraphFile(graphId: string): string | undefined {
10
+ const roots = [resolve('graphs'), resolve(import.meta.dir, '../../graphs')];
11
+ for (const root of roots) {
12
+ for (const dir of [root, ...['release', 'ops'].map((part) => join(root, part))]) {
13
+ if (!existsSync(dir)) continue;
14
+ for (const name of readdirSync(dir)) {
15
+ if (!name.endsWith('.yaml') || name === 'recipes.yaml') continue;
16
+ const file = join(dir, name);
17
+ if (!existsSync(join(dir, 'recipes.yaml'))) continue;
18
+ try {
19
+ const parsed: unknown = parseYaml(readFileSync(file, 'utf8'));
20
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && (parsed as Record<string, unknown>).graph_id === graphId) return file;
21
+ } catch { continue; }
22
+ }
23
+ }
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function registerGraphCommands(program: Command, runsDeps: { root?: string; processStartMs?: (pid: number) => number | null } = {}): void {
6
29
  const graph = program.command('graph').description('Run a declared command graph or inspect its latest run');
7
30
  graph.command('run <file>')
8
31
  .option('--dry-run', 'Walk the success path without executing commands')
9
32
  .option('--json', 'Print run state as JSON')
10
33
  .option('--resume <run_id>', 'Resume a persisted run awaiting approval or restart a failed path with --from')
11
34
  .option('--from <node_id>', 'Restart a failed run at a node on its saved path (requires --resume)')
35
+ .option('--use-current-graph', 'Use the installed graph rather than the run snapshot')
12
36
  .option('--input <json>', 'JSON object passed to the graph run')
13
- .action(async (file: string, opts: { dryRun?: boolean; json?: boolean; resume?: string; from?: string; input?: string }) => {
37
+ .action(async (file: string, opts: { dryRun?: boolean; json?: boolean; resume?: string; from?: string; input?: string; useCurrentGraph?: boolean }) => {
14
38
  try {
15
39
  await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
16
40
  if (opts.resume && opts.dryRun) throw new Error('--resume cannot be combined with --dry-run');
@@ -23,7 +47,7 @@ export function registerGraphCommands(program: Command): void {
23
47
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('--input must be a JSON object');
24
48
  input = parsed as Record<string, unknown>;
25
49
  }
26
- const state = await runGraph(file, { ...(opts.resume ? { resumeRunId: opts.resume } : { dryRun: opts.dryRun }), ...(opts.from ? { fromNodeId: opts.from } : {}), ...(input === undefined ? {} : { input }) });
50
+ const state = await runGraph(file, { ...(opts.resume ? { resumeRunId: opts.resume } : { dryRun: opts.dryRun }), ...(opts.from ? { fromNodeId: opts.from } : {}), useCurrentGraph: opts.useCurrentGraph, ...(input === undefined ? {} : { input }) });
27
51
  if (opts.json) await writeStdoutJson(JSON.stringify(state) + '\n');
28
52
  else console.log(`${state.graphId} ${state.runId}: ${state.status} (${state.path.join(' → ')}, executed: ${state.executed})${state.pending ? ` — ${state.pending.message}` : ''}`);
29
53
  if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
@@ -87,6 +111,107 @@ export function registerGraphCommands(program: Command): void {
87
111
  process.exitCode = 1;
88
112
  }
89
113
  });
114
+ const runs = graph.command('runs').description('List and inspect persisted graph runs');
115
+ const alive = (state: GraphRunState) => graphRunAlive(state, runsDeps.processStartMs);
116
+ const row = (state: GraphRunState) => ({
117
+ runId: state.runId, graphId: state.graphId, status: state.status,
118
+ lastNode: state.path.at(-1) ?? null, startedAt: state.startedAt ?? null,
119
+ finishedAt: state.finishedAt ?? null, alive: alive(state),
120
+ });
121
+ const warning = (unreadable: number) => { if (unreadable) console.error(`⚠ 못 읽은 원장 ${unreadable}`); };
122
+ runs.command('list')
123
+ .option('--graph <id>', 'Filter by graph id')
124
+ .option('--state <status>', 'Filter by run status')
125
+ .option('--limit <N>', 'Maximum number of runs')
126
+ .option('--json', 'Print runs as JSON')
127
+ .action(async (opts: { graph?: string; state?: string; limit?: string; json?: boolean }) => {
128
+ try {
129
+ if (opts.limit !== undefined && (!/^[1-9]\d*$/.test(opts.limit) || !Number.isSafeInteger(Number(opts.limit)))) throw new Error('--limit must be a positive integer');
130
+ const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
131
+ const selected = all.filter((state) => (!opts.graph || state.graphId === opts.graph) && (!opts.state || state.status === opts.state))
132
+ .slice(0, opts.limit === undefined ? undefined : Number(opts.limit)).map(row);
133
+ debug.log('graph.runs', 'list', { count: selected.length, unreadable, alive: selected.filter((state) => state.alive === true).length });
134
+ if (opts.json) await writeStdoutJson(JSON.stringify(selected) + '\n');
135
+ else for (const state of selected) console.log(`${state.runId} · ${state.graphId} · ${state.status} · ${state.lastNode ?? '-'} · ${state.startedAt ?? '-'} · ${state.finishedAt ?? '-'} · alive=${state.alive}`);
136
+ warning(unreadable);
137
+ } catch (error) {
138
+ console.error(`graph runs list: ${error instanceof Error ? error.message : String(error)}`);
139
+ process.exitCode = 1;
140
+ }
141
+ });
142
+ runs.command('resume <run_id>')
143
+ .option('--from <node_id>', 'Restart at this node on the saved path')
144
+ .option('--use-current-graph', 'Use the installed graph and enforce the original hash')
145
+ .option('--file <path>', 'Graph file for a legacy run without a recorded graph path')
146
+ .option('--json', 'Print run state as JSON')
147
+ .action(async (runId: string, opts: { from?: string; useCurrentGraph?: boolean; file?: string; json?: boolean }) => {
148
+ try {
149
+ await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
150
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') throw new Error(`invalid run id: ${runId}`);
151
+ const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
152
+ const exact = all.filter((state) => state.runId === runId);
153
+ const matches = exact.length ? exact : all.filter((state) => state.runId.startsWith(runId));
154
+ if (matches.length !== 1) {
155
+ if (matches.length) console.error(`ambiguous run id: ${runId}\n${matches.map((state) => `${state.graphId}/${state.runId}`).join('\n')}`);
156
+ else console.error(`no run: ${runId}`);
157
+ warning(unreadable);
158
+ process.exitCode = 1;
159
+ return;
160
+ }
161
+ const saved = matches[0]!;
162
+ if (saved.graphSnapshot && opts.file && (!saved.graphPath || resolve(opts.file) !== resolve(saved.graphPath))) {
163
+ throw new Error('--file cannot change the graph path of a snapshot run (이 런은 시작 때 그래프 사본이 있다 — --file 대신 --use-current-graph)');
164
+ }
165
+ const from = opts.from ?? (saved.status === 'failed' ? [...saved.nodes].reverse().find((node) => !node.ok)?.nodeId : undefined);
166
+ const file = saved.graphSnapshot
167
+ ? saved.graphPath
168
+ : opts.file ?? (saved.graphPath && existsSync(saved.graphPath) ? saved.graphPath : undefined) ?? findGraphFile(saved.graphId);
169
+ if (!file) throw new Error(`graph file not found for ${saved.graphId}/${saved.runId}; provide --file`);
170
+ const state = await runGraph(file, { resumeRunId: saved.runId, resumeGraphId: saved.graphId, fromNodeId: from,
171
+ useCurrentGraph: opts.useCurrentGraph, deps: { root: runsDeps.root } });
172
+ if (opts.json) await writeStdoutJson(JSON.stringify(state) + '\n');
173
+ else console.log(`${state.graphId} ${state.runId}: ${state.status} (${state.path.join(' → ')}, executed: ${state.executed})`);
174
+ if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
175
+ warning(unreadable);
176
+ } catch (error) {
177
+ console.error(`graph runs resume: ${error instanceof Error ? error.message : String(error)}`);
178
+ process.exitCode = 1;
179
+ }
180
+ });
181
+ runs.command('status <run_id>')
182
+ .option('--json', 'Print run detail as JSON')
183
+ .action(async (runId: string, opts: { json?: boolean }) => {
184
+ try {
185
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') throw new Error(`invalid run id: ${runId}`);
186
+ const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
187
+ const exact = all.filter((state) => state.runId === runId);
188
+ const matches = exact.length ? exact : all.filter((state) => state.runId.startsWith(runId));
189
+ debug.log('graph.runs', 'status', { count: matches.length, unreadable, alive: matches.filter((state) => alive(state) === true).length });
190
+ if (matches.length !== 1) {
191
+ if (matches.length) console.error(`ambiguous run id: ${runId}\n${matches.map((state) => `${state.graphId}/${state.runId}`).join('\n')}`);
192
+ else console.error(`no run: ${runId}`);
193
+ warning(unreadable);
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+ const state = matches[0]!;
198
+ const detail = { ...row(state), nodes: state.nodes.map((node) => {
199
+ const summary = lastJsonObject(node.output)?.summary;
200
+ return { nodeId: node.nodeId, ok: node.ok, summary: typeof summary === 'string' ? summary.slice(0, 300) : null };
201
+ }), pending: state.pending ?? null, resume: state.resume ?? null };
202
+ if (opts.json) await writeStdoutJson(JSON.stringify(detail) + '\n');
203
+ else {
204
+ console.log(`${detail.runId} · ${detail.graphId} · ${detail.status} · alive=${detail.alive}`);
205
+ for (const node of detail.nodes) console.log(` ${node.nodeId} · ok=${node.ok} · ${node.summary ?? '-'}`);
206
+ console.log(`pending: ${JSON.stringify(detail.pending)}`);
207
+ console.log(`resume: ${JSON.stringify(detail.resume)}`);
208
+ }
209
+ warning(unreadable);
210
+ } catch (error) {
211
+ console.error(`graph runs status: ${error instanceof Error ? error.message : String(error)}`);
212
+ process.exitCode = 1;
213
+ }
214
+ });
90
215
  graph.command('status <graph_id>')
91
216
  .option('--json', 'Print latest run state as JSON')
92
217
  .action(async (graphId: string, opts: { json?: boolean }) => {
@@ -2,6 +2,8 @@ import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, readdirSy
2
2
  import { join } from 'node:path';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { effectiveInstanceRoot } from '../instance/resolve.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { applyDecidedApprovals, raiseApprovalCard, type GraphApprovalCardDeps } from './graph-approval-card.js';
5
7
  import type { GraphRunState } from './runner.js';
6
8
 
7
9
  export interface GraphNotificationEvent {
@@ -20,6 +22,7 @@ export interface GraphNotifyOptions {
20
22
  graphsDir?: string;
21
23
  dryRun?: boolean;
22
24
  send?: (message: string, event: GraphNotificationEvent) => Promise<void> | void;
25
+ approvalCard?: Pick<GraphApprovalCardDeps, 'ledger'>;
23
26
  }
24
27
 
25
28
  type NotificationSetting = boolean | string | { on?: string | string[]; enabled?: boolean };
@@ -177,16 +180,35 @@ export async function notifyGraphEvents(options: GraphNotifyOptions = {}): Promi
177
180
  return collectGraphEvents({ root, graphsDir: options.graphsDir }).filter((event) => !sent.has(event.id));
178
181
  };
179
182
  if (options.dryRun) return collectFresh();
183
+ const syncApprovals = () => {
184
+ const deps = { root, ...options.approvalCard };
185
+ try { applyDecidedApprovals(deps); }
186
+ catch (error) {
187
+ debug.log('graph.approval-card', 'skipped', { graphId: undefined, runId: undefined, nodeId: undefined,
188
+ decisionId: undefined, outcome: undefined, reason: error instanceof Error ? error.message : String(error) });
189
+ }
190
+ for (const event of collectGraphEvents({ root, graphsDir: options.graphsDir })) {
191
+ if (event.kind !== 'approval') continue;
192
+ try {
193
+ const state = JSON.parse(readFileSync(join(root, 'graph-runs', event.graphId, `${event.runId}.json`), 'utf8')) as GraphRunState;
194
+ if (state.pending?.nodeId === event.nodeId) raiseApprovalCard(state, state.pending, deps);
195
+ } catch (error) {
196
+ debug.log('graph.approval-card', 'skipped', { graphId: event.graphId, runId: event.runId, nodeId: event.nodeId,
197
+ decisionId: undefined, outcome: undefined, reason: error instanceof Error ? error.message : String(error) });
198
+ }
199
+ }
200
+ };
180
201
  // The former .lock path was a directory; use a separate, stable inode instead of trying to reclaim it.
181
202
  const lock = `${ledgerPath}.flock`;
182
203
  mkdirSync(join(root, 'graph-runs'), { recursive: true });
183
204
  const flock = await loadFlock();
184
- if (!flock) return notifyWithExclusiveFile(`${ledgerPath}.owner`, collectFresh, options.send, ledgerPath);
205
+ if (!flock) return notifyWithExclusiveFile(`${ledgerPath}.owner`, collectFresh, options.send, ledgerPath, syncApprovals);
185
206
  const fd = openSync(lock, 'a');
186
207
  let acquired = false;
187
208
  try {
188
209
  if (flock(fd, LOCK_EX_NB) !== 0) throw new Error('graph notification is already running');
189
210
  acquired = true;
211
+ syncApprovals();
190
212
  return await sendFresh(collectFresh(), options.send, ledgerPath);
191
213
  } finally {
192
214
  try {
@@ -209,12 +231,12 @@ async function sendFresh(fresh: GraphNotificationEvent[], sendOption: GraphNotif
209
231
  }
210
232
 
211
233
  /** Platforms without flock (Windows, musl): an exclusive owner file. A crash leaves it behind, and the next run names it instead of double-sending. */
212
- async function notifyWithExclusiveFile(ownerPath: string, collectFresh: () => GraphNotificationEvent[], send: GraphNotifyOptions['send'], ledgerPath: string): Promise<GraphNotificationEvent[]> {
234
+ async function notifyWithExclusiveFile(ownerPath: string, collectFresh: () => GraphNotificationEvent[], send: GraphNotifyOptions['send'], ledgerPath: string, syncApprovals: () => void): Promise<GraphNotificationEvent[]> {
213
235
  try { writeFileSync(ownerPath, String(process.pid), { flag: 'wx' }); }
214
236
  catch (error) {
215
237
  if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`graph notification is already running (or a crashed run left ${ownerPath} — remove it if no notifier is running)`);
216
238
  throw error;
217
239
  }
218
- try { return await sendFresh(collectFresh(), send, ledgerPath); }
240
+ try { syncApprovals(); return await sendFresh(collectFresh(), send, ledgerPath); }
219
241
  finally { rmSync(ownerPath, { force: true }); }
220
242
  }
@@ -11,6 +11,7 @@ import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentia
11
11
  import { effectiveInstanceRoot } from '../instance/resolve.js';
12
12
  import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
13
13
  import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec } from '../self-implement/graph-yaml.js';
14
+ import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js';
14
15
  import { executeBashNode } from '../workflow-runtime/nodes/bash.js';
15
16
  import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtime/types.js';
16
17
 
@@ -21,6 +22,8 @@ export interface GraphRunState {
21
22
  runId: string;
22
23
  startedAt?: string;
23
24
  finishedAt?: string;
25
+ pid?: number;
26
+ pidStartedAt?: string;
24
27
  status: 'running' | 'done' | 'failed' | 'budget-exceeded' | 'awaiting-approval';
25
28
  path: string[];
26
29
  nodes: Array<{ nodeId: string; ok: boolean; exit: number | null; executed: boolean; output?: unknown; error?: string; decidedBy?: string; decidedAt?: string }>;
@@ -29,7 +32,9 @@ export interface GraphRunState {
29
32
  approvalSourceHash?: string;
30
33
  /** Hash of the graph and recipes used for this run; required to safely restart a failed path. */
31
34
  sourceHash?: string;
32
- resume?: { from: string; at: string; previousStatus: 'failed' };
35
+ graphSnapshot?: { graphSha: string; recipesSha: string };
36
+ graphPath?: string;
37
+ resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
33
38
  executed: number;
34
39
  dryRun: boolean;
35
40
  statePath: string;
@@ -40,8 +45,10 @@ export interface GraphRunOptions {
40
45
  dryRun?: boolean;
41
46
  runId?: string;
42
47
  resumeRunId?: string;
48
+ resumeGraphId?: string;
43
49
  fromNodeId?: string;
44
- deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void };
50
+ useCurrentGraph?: boolean;
51
+ deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null };
45
52
  }
46
53
 
47
54
  function safeSegment(value: string): string {
@@ -157,13 +164,19 @@ function persistGraphRun(state: GraphRunState): void {
157
164
  renameSync(temporary, state.statePath);
158
165
  }
159
166
 
160
- export function decideGraphApproval(graphId: string, runId: string, decision: 'approved' | 'rejected', by?: string, root = effectiveInstanceRoot()): GraphRunState {
167
+ export interface ExpectedGraphApprovalVisit { nodeId: string; visit: number }
168
+
169
+ export function decideGraphApproval(graphId: string, runId: string, decision: 'approved' | 'rejected', by?: string, root = effectiveInstanceRoot(), expected?: ExpectedGraphApprovalVisit): GraphRunState {
161
170
  const state = readGraphRun(graphId, runId, root);
162
- if (state.status !== 'awaiting-approval' || !state.pending || state.pending.decision) {
171
+ if (state.status !== 'awaiting-approval' || !state.pending || state.pending.decision ||
172
+ (expected && (state.dryRun || state.pending.nodeId !== expected.nodeId || state.path.at(-1) !== expected.nodeId ||
173
+ !Number.isSafeInteger(expected.visit) || expected.visit < 1 ||
174
+ state.path.filter(node => node === expected.nodeId).length !== expected.visit))) {
163
175
  throw new Error(`run is not awaiting an undecided approval: ${graphId}/${runId}`);
164
176
  }
165
177
  if (by !== undefined && !by.trim()) throw new Error('approver name must not be empty');
166
178
  const recorded = { nodeId: state.pending.nodeId, decision, ...(by === undefined ? {} : { decidedBy: by }), decidedAt: new Date().toISOString() };
179
+ // A card can claim only the visit it names; never derive a later visit's claim path from a fresh read.
167
180
  const decisionPath = approvalDecisionPath(state);
168
181
  const temporary = `${decisionPath}.${randomUUID()}.tmp`;
169
182
  writeFileSync(temporary, JSON.stringify(recorded) + '\n', { flag: 'wx' });
@@ -206,7 +219,26 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
206
219
  }
207
220
 
208
221
  export async function runGraph(path: string, options: GraphRunOptions = {}): Promise<GraphRunState> {
209
- const source = readFileSync(path, 'utf8');
222
+ if (options.resumeRunId && options.runId) throw new Error('runId and resumeRunId cannot be combined');
223
+ if (options.fromNodeId && !options.resumeRunId) throw new Error('--from requires --resume');
224
+ if (options.useCurrentGraph && !options.resumeRunId) throw new Error('--use-current-graph requires --resume');
225
+ const root = options.deps?.root ?? effectiveInstanceRoot();
226
+ const matchingRuns = options.resumeRunId && !options.resumeGraphId
227
+ ? listGraphRuns(root).runs.filter((run) => run.runId === options.resumeRunId && run.graphPath === resolve(path)) : [];
228
+ if (matchingRuns.length > 1) throw new Error(`ambiguous run id: ${options.resumeRunId}`);
229
+ const currentSource = options.resumeRunId && (options.resumeGraphId || matchingRuns.length) && !options.useCurrentGraph
230
+ ? undefined : readFileSync(path, 'utf8');
231
+ const currentHeader: unknown = options.resumeRunId && !options.resumeGraphId && !matchingRuns.length ? parseYaml(currentSource!) : undefined;
232
+ const resumeGraphId = options.resumeGraphId ?? matchingRuns[0]?.graphId ?? (currentHeader && typeof currentHeader === 'object' && !Array.isArray(currentHeader)
233
+ ? (currentHeader as Record<string, unknown>).graph_id : undefined);
234
+ const saved = options.resumeRunId && typeof resumeGraphId === 'string'
235
+ ? readGraphRun(safeSegment(resumeGraphId), safeSegment(options.resumeRunId), root) : undefined;
236
+ if (saved?.graphSnapshot && (!saved.graphPath || resolve(path) !== resolve(saved.graphPath))) {
237
+ throw new Error('--file cannot change the graph path of a snapshot run (이 런은 시작 때 그래프 사본이 있다 — --file 대신 --use-current-graph)');
238
+ }
239
+ const snapshotDir = saved ? `${saved.statePath}.graph` : undefined;
240
+ const graphMode: 'snapshot' | 'current' = snapshotDir && saved?.graphSnapshot && !options.useCurrentGraph ? 'snapshot' : 'current';
241
+ const source = graphMode === 'snapshot' ? readFileSync(join(snapshotDir!, 'graph.yaml'), 'utf8') : currentSource ?? readFileSync(path, 'utf8');
210
242
  // The shared YAML parser requires a recipe string. A command-less node in
211
243
  // this runner is represented internally as `none`, without changing that parser.
212
244
  const raw: unknown = parseYaml(source);
@@ -220,7 +252,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
220
252
  if (!parsed.template || parsed.errors.length) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
221
253
  const graph = parsed.template;
222
254
  const pluginRoot = elanousStateRoot();
223
- const graphFile = realpathSync(path);
255
+ const graphFile = graphMode === 'snapshot' && !existsSync(path) ? resolve(path) : realpathSync(path);
224
256
  const installed = listInstalledPlugins(pluginRoot);
225
257
  const owner = installed.find(item => {
226
258
  const graphsDir = join(item.path, 'graphs');
@@ -235,7 +267,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
235
267
  throw new Error(`unsupported terminal node: ${terminal} (expected done or failed)`);
236
268
  }
237
269
  }
238
- const recipeSource = readFileSync(join(dirname(path), 'recipes.yaml'), 'utf8');
270
+ const recipeSource = readFileSync(graphMode === 'snapshot' ? join(snapshotDir!, 'recipes.yaml') : join(dirname(path), 'recipes.yaml'), 'utf8');
271
+ if (graphMode === 'snapshot' && saved?.graphSnapshot &&
272
+ (createHash('sha256').update(source).digest('hex') !== saved.graphSnapshot.graphSha ||
273
+ createHash('sha256').update(recipeSource).digest('hex') !== saved.graphSnapshot.recipesSha)) {
274
+ throw new Error('graph snapshot changed since run started');
275
+ }
239
276
  const recipes = recipesFor(path, recipeSource);
240
277
  const catalogRoles = new Set(loadNodeCatalog().roles.keys());
241
278
  // Bind a human decision to the exact graph and recipe contents, not just its id or approval prompt.
@@ -247,10 +284,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
247
284
  if (!resolved.command && !resolved.approval) throw new Error(`unknown command recipe for ${node.nodeId}: ${node.recipe}`);
248
285
  }
249
286
  const graphId = safeSegment(graph.graphId);
250
- if (options.resumeRunId && options.runId) throw new Error('runId and resumeRunId cannot be combined');
251
- if (options.fromNodeId && !options.resumeRunId) throw new Error('--from requires --resume');
287
+ if (saved && graphId !== saved.graphId) {
288
+ if (options.fromNodeId && graphMode === 'current') throw new Error('graph or recipes changed since failed run');
289
+ throw new Error(`run identity mismatch: ${saved.graphId}/${saved.runId}`);
290
+ }
252
291
  const runId = safeSegment(options.resumeRunId ?? options.runId ?? randomUUID());
253
- const statePath = graphRunPath(graphId, runId, options.deps?.root ?? effectiveInstanceRoot());
292
+ const statePath = graphRunPath(graphId, runId, root);
254
293
  // A resume owns the run from the first state read through its last write and command.
255
294
  // Fail closed on a concurrent (or interrupted) owner rather than replaying a command.
256
295
  const resumeLock = `${statePath}.resume.lock`;
@@ -263,12 +302,20 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
263
302
  }
264
303
  }
265
304
  try {
305
+ const startMs = (options.deps?.processStartMs ?? psProcessStartMs)(process.pid);
306
+ const runnerOwner = { pid: process.pid, pidStartedAt: new Date(startMs ?? Date.now() - process.uptime() * 1_000).toISOString() };
266
307
  const state: GraphRunState = options.resumeRunId
267
- ? readGraphRun(graphId, runId, options.deps?.root ?? effectiveInstanceRoot())
268
- : { graphId, runId, startedAt: new Date().toISOString(), status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash, ...(options.input === undefined ? {} : { input: options.input }), executed: 0, dryRun: options.dryRun === true, statePath };
308
+ ? readGraphRun(graphId, runId, root)
309
+ : { graphId, runId, startedAt: new Date().toISOString(), ...runnerOwner, status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash,
310
+ graphSnapshot: { graphSha: createHash('sha256').update(source).digest('hex'), recipesSha: createHash('sha256').update(recipeSource).digest('hex') },
311
+ graphPath: resolve(path), ...(options.input === undefined ? {} : { input: options.input }), executed: 0, dryRun: options.dryRun === true, statePath };
312
+ if (options.resumeRunId && graphMode === 'snapshot' && state.sourceHash && state.sourceHash !== approvalSourceHash) {
313
+ throw new Error('graph snapshot changed since run started');
314
+ }
315
+ if (options.resumeRunId && options.dryRun !== undefined && options.dryRun !== state.dryRun) throw new Error('cannot change dryRun when resuming');
269
316
  if (options.fromNodeId) {
270
317
  if (state.status !== 'failed' || state.pending || state.dryRun) throw new Error('--from requires a failed, non-dry run without pending approval');
271
- if (!state.sourceHash || state.sourceHash !== approvalSourceHash) throw new Error('graph or recipes changed since failed run');
318
+ if (graphMode === 'current' && (!state.sourceHash || state.sourceHash !== approvalSourceHash)) throw new Error('graph or recipes changed since failed run');
272
319
  const from = state.path.indexOf(options.fromNodeId);
273
320
  if (from < 0 || state.path.lastIndexOf(options.fromNodeId) !== from || graph.terminalNodes.includes(options.fromNodeId)) throw new Error(`--from node is not unique on the saved executable path: ${options.fromNodeId}`);
274
321
  if (state.path.length !== state.nodes.length || state.nodes.some((record, i) => record.nodeId !== state.path[i]) ||
@@ -306,10 +353,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
306
353
  state.nodes = state.nodes.slice(0, from);
307
354
  state.executed = state.nodes.filter((node) => node.executed).length;
308
355
  state.status = 'running';
309
- state.resume = { from: options.fromNodeId, at: new Date().toISOString(), previousStatus: 'failed' };
356
+ Object.assign(state, runnerOwner);
357
+ state.resume = { from: options.fromNodeId, at: new Date().toISOString(), previousStatus: 'failed', graph: graphMode };
310
358
  // Persist the restart boundary before executing it, so previous outputs remain durable.
311
359
  persistGraphRun(state);
312
- debug.log('graph.run', 'resume', { graphId, runId, from: options.fromNodeId });
313
360
  }
314
361
  if (options.resumeRunId && state.status === 'running' && state.pending) {
315
362
  throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
@@ -323,10 +370,16 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
323
370
  if (options.resumeRunId && (state.graphId !== graphId || state.runId !== runId)) {
324
371
  throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
325
372
  }
326
- if (options.resumeRunId && options.dryRun !== undefined && options.dryRun !== state.dryRun) throw new Error('cannot change dryRun when resuming');
327
373
  if (options.resumeRunId && state.status === 'awaiting-approval' && state.approvalSourceHash !== approvalSourceHash) {
328
374
  throw new Error(`approval source changed since run was paused: ${graphId}/${runId}`);
329
375
  }
376
+ if (options.resumeRunId && !options.fromNodeId) {
377
+ const previousStatus = state.status;
378
+ Object.assign(state, runnerOwner);
379
+ state.resume = { from: state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode, at: new Date().toISOString(), previousStatus, graph: graphMode };
380
+ persistGraphRun(state);
381
+ }
382
+ if (options.resumeRunId) debug.log('graph.runs', 'resume', { graphId, runId, from: state.resume?.from, graph: graphMode });
330
383
  const persist = () => {
331
384
  if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded') state.finishedAt ??= new Date().toISOString();
332
385
  persistGraphRun(state);
@@ -345,6 +398,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
345
398
  if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`run id already exists: ${graphId}/${runId}`);
346
399
  throw error;
347
400
  }
401
+ mkdirSync(`${statePath}.graph`);
402
+ writeFileSync(join(`${statePath}.graph`, 'graph.yaml'), source, { flag: 'wx' });
403
+ writeFileSync(join(`${statePath}.graph`, 'recipes.yaml'), recipeSource, { flag: 'wx' });
348
404
  }
349
405
  while (current !== undefined) {
350
406
  const node = graph.nodes.find((n) => n.nodeId === current);
@@ -474,6 +530,76 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
474
530
  }
475
531
  }
476
532
 
533
+ export type GraphRunAlive = boolean | 'unknown';
534
+
535
+ export function graphRunAlive(state: GraphRunState, processStartMs: (pid: number) => number | null = psProcessStartMs): GraphRunAlive {
536
+ if (state.pid === undefined) return 'unknown';
537
+ if (!Number.isSafeInteger(state.pid) || state.pid <= 0 || typeof state.pidStartedAt !== 'string') return false;
538
+ const recorded = Date.parse(state.pidStartedAt);
539
+ if (!Number.isFinite(recorded)) return false;
540
+ try {
541
+ const actual = processStartMs(state.pid);
542
+ return actual !== null && Number.isFinite(actual) && Math.abs(actual - recorded) <= START_TOLERANCE_MS;
543
+ } catch { return false; }
544
+ }
545
+
546
+ function isDecisionClaim(raw: unknown): boolean {
547
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return false;
548
+ const value = raw as Record<string, unknown>;
549
+ return typeof value.nodeId === 'string' && (value.decision === 'approved' || value.decision === 'rejected')
550
+ && typeof value.decidedAt === 'string' && (value.decidedBy === undefined || typeof value.decidedBy === 'string')
551
+ && !('graphId' in value) && !('runId' in value);
552
+ }
553
+
554
+ export function listGraphRuns(root = effectiveInstanceRoot()): { runs: GraphRunState[]; unreadable: number } {
555
+ const base = join(root, 'graph-runs');
556
+ let graphs: string[];
557
+ try { graphs = readdirSync(base); }
558
+ catch (error) {
559
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { runs: [], unreadable: 0 };
560
+ throw error;
561
+ }
562
+ const runs: GraphRunState[] = [];
563
+ let unreadable = 0;
564
+ for (const graphId of graphs) {
565
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(graphId) || graphId === '.' || graphId === '..') continue;
566
+ const dir = join(base, graphId);
567
+ let files: string[];
568
+ try {
569
+ if (!statSync(dir).isDirectory()) continue;
570
+ files = readdirSync(dir);
571
+ } catch { unreadable++; continue; }
572
+ for (const file of files) {
573
+ if (!file.endsWith('.json')) continue;
574
+ const runId = file.slice(0, -5);
575
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') continue;
576
+ // Decision claims are named <runId>.json.<visit>.decision.json; a run
577
+ // may itself end in .decision, so check the ledger identity before skipping.
578
+ const decisionParent = file.match(/^(.*\.json)\.\d+\.decision\.json$/)?.[1];
579
+ try {
580
+ const raw: unknown = JSON.parse(readFileSync(join(dir, file), 'utf8'));
581
+ // Skip only a VALID decision claim (decideGraphApproval writes { nodeId, decision, decidedBy?, decidedAt });
582
+ // anything else under a claim-like name is judged as a ledger and counts as unreadable when broken.
583
+ if (decisionParent && isDecisionClaim(raw)) continue;
584
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
585
+ (raw as Partial<GraphRunState>).graphId !== graphId || (raw as Partial<GraphRunState>).runId !== runId) {
586
+ unreadable++;
587
+ continue;
588
+ }
589
+ const state = readGraphRun(graphId, runId, root);
590
+ if (typeof state.startedAt !== 'string' || !Number.isFinite(Date.parse(state.startedAt)) ||
591
+ !Array.isArray(state.path) || !state.path.every((node) => typeof node === 'string') ||
592
+ !Array.isArray(state.nodes) || !state.nodes.every((node) => node && typeof node.nodeId === 'string' && typeof node.ok === 'boolean')) {
593
+ throw new Error('invalid run state');
594
+ }
595
+ runs.push(state);
596
+ } catch { unreadable++; }
597
+ }
598
+ }
599
+ runs.sort((a, b) => Date.parse(b.startedAt!) - Date.parse(a.startedAt!) || a.graphId.localeCompare(b.graphId) || a.runId.localeCompare(b.runId));
600
+ return { runs, unreadable };
601
+ }
602
+
477
603
  export function latestGraphRun(graphId: string, root = effectiveInstanceRoot()): GraphRunState | null {
478
604
  const dir = join(root, 'graph-runs', safeSegment(graphId));
479
605
  let files: string[];
@@ -22,6 +22,8 @@ import { IMAGE_LOAD_STATE_SOURCE } from './image-load-state.js';
22
22
  const execFileAsync = promisify(execFile);
23
23
  // Longer than aside's own ~10 s daemon auth-challenge timeout, so «Aside isn't running» reaches stderr before we kill it (10-01 live).
24
24
  const DEFAULT_ASIDE_TIMEOUT_MS = 30_000;
25
+ const ASIDE_SHOT_MAX_BYTES = 8 * 1024 * 1024;
26
+ const ASIDE_SHOT_MARKER = 'ELANOUS_SHOT_BASE64:';
25
27
  // Observed load events and complete page content arrive within this window; timeout remains non-blocking.
26
28
  const CDP_LOAD_WAIT_TIMEOUT_MS = 3_000;
27
29
 
@@ -188,6 +190,7 @@ function buildAsideScript(url: string): string {
188
190
  ` const state = await page.evaluate(() => { const imageCounts = ${IMAGE_COUNTS_EXPRESSION}; return { bodyLength: document.body?.innerText.length ?? 0, unloadedImageCount: imageCounts.broken, pendingLazyImages: imageCounts.pendingLazy }; });`,
189
191
  ' const screenshot = await page.screenshot({ fullPage: true });',
190
192
  ' console.log(JSON.stringify({ title, ...state, screenshotBytes: screenshot?.length ?? 0 }));',
193
+ ` if (screenshot?.length > 0 && screenshot.length <= ${ASIDE_SHOT_MAX_BYTES}) console.log('${ASIDE_SHOT_MARKER}' + Buffer.from(screenshot).toString('base64'));`,
191
194
  ' } finally { try { await closeTab(page); } catch {} }',
192
195
  '})();',
193
196
  ].join('\n');
@@ -223,6 +226,18 @@ function parseAsideObservation(stdout: string): { title: string; bodyLength: num
223
226
  throw new Error('aside observation output missing JSON');
224
227
  }
225
228
 
229
+ function decodeAsideScreenshot(stdout: string, screenshotBytes: number): Buffer | undefined {
230
+ if (screenshotBytes === 0 || screenshotBytes > ASIDE_SHOT_MAX_BYTES) return undefined;
231
+ const lines = stripScreenAnsi(stdout).split(/\r?\n/);
232
+ const shots = lines.filter((line) => line.startsWith(ASIDE_SHOT_MARKER));
233
+ if (shots.length !== 1) return undefined;
234
+ const encoded = shots[0]!.slice(ASIDE_SHOT_MARKER.length);
235
+ if (encoded.length > Math.ceil(ASIDE_SHOT_MAX_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return undefined;
236
+ const buf = Buffer.from(encoded, 'base64');
237
+ if (buf.length !== screenshotBytes || buf.toString('base64') !== encoded) return undefined;
238
+ return buf;
239
+ }
240
+
226
241
  function asideErrorResult(url: string, error: unknown, kind?: AsideRunStatus['kind']): DeployVerifyResult {
227
242
  const message = error instanceof Error ? error.message : String(error);
228
243
  const errorFinding = finding('aside-error', `aside 검증 오류: ${message.slice(0, 120)}${kind ? ` (${kind})` : ''}`, 'confirmed');
@@ -240,7 +255,7 @@ async function verifyAsidePage(url: string, deps: DeployVerifyDeps): Promise<Dep
240
255
  const script = await readFile(scriptPath, 'utf8');
241
256
  const runAside = deps.runAside ?? (async (command, args, options) => {
242
257
  try {
243
- const result = await execFileAsync(command, args, { ...options, encoding: 'utf8' });
258
+ const result = await execFileAsync(command, args, { ...options, encoding: 'utf8', maxBuffer: Math.ceil(ASIDE_SHOT_MAX_BYTES / 3) * 4 + 1024 * 1024 });
244
259
  return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
245
260
  } catch (error) {
246
261
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') throw error;
@@ -273,12 +288,20 @@ async function verifyAsidePage(url: string, deps: DeployVerifyDeps): Promise<Dep
273
288
  if (status.kind === 'repl-error') return asideErrorResult(url, new Error('aside repl reported error'), status.kind);
274
289
  if (status.kind === 'no-marker') return asideErrorResult(url, new Error('aside observation output missing completion marker'), status.kind);
275
290
  const state = parseAsideObservation(observation.stdout);
291
+ const screenshot = decodeAsideScreenshot(observation.stdout, state.screenshotBytes);
292
+ let shotReason = state.screenshotBytes === 0 ? '스크린샷 0바이트' : state.screenshotBytes > ASIDE_SHOT_MAX_BYTES ? '8MB 상한 초과' : screenshot ? undefined : '스크린샷 데이터 없음 또는 불일치';
293
+ if (screenshot && deps.onScreenshot) {
294
+ try { deps.onScreenshot(screenshot); }
295
+ catch (error) { shotReason = (error as NodeJS.ErrnoException)?.code ?? '저장 콜백 오류'; }
296
+ }
297
+ observe('aside-shot', { bytes: state.screenshotBytes, written: !!screenshot && !shotReason && !!deps.onScreenshot, reason: shotReason ?? (deps.onScreenshot ? '없음' : '저장 요청 없음') });
276
298
  const structuredFindings: DeployVerifyFinding[] = [];
299
+ if (state.screenshotBytes === 0) structuredFindings.push(finding('aside-error', '스크린샷 0바이트 — 렌더 증거 없음', 'confirmed'));
277
300
  if (state.bodyLength < minBody) structuredFindings.push(finding('empty-body', `페이지 본문이 비어있음(len=${state.bodyLength}·렌더 실패 의심)`, 'suspected'));
278
301
  if (state.unloadedImageCount) structuredFindings.push(finding('unloaded-image', `로드되지 못한 그림 ${state.unloadedImageCount}개`, 'confirmed'));
279
302
  if (!state.title.trim()) structuredFindings.push(finding('empty-title', '문서 제목이 비어있음', 'confirmed'));
280
303
  const findings = structuredFindings.map(({ message }) => message);
281
- const result: DeployVerifyResult = { ok: findings.length === 0, url, title: state.title, bodyLength: state.bodyLength, screenshotBytes: state.screenshotBytes, findings, unmeasured: ['javascript-errors'], ...(state.pendingLazyImages ? { pendingLazyImages: state.pendingLazyImages } : {}), ...(structuredFindings.length ? { structuredFindings } : {}) };
304
+ const result: DeployVerifyResult = { ok: findings.length === 0, url, title: state.title, bodyLength: state.bodyLength, screenshotBytes: state.screenshotBytes, findings, unmeasured: shotReason && (state.screenshotBytes === 0 || state.screenshotBytes > ASIDE_SHOT_MAX_BYTES || !!deps.onScreenshot) ? ['javascript-errors', 'screenshot'] : ['javascript-errors'], ...(state.pendingLazyImages ? { pendingLazyImages: state.pendingLazyImages } : {}), ...(structuredFindings.length ? { structuredFindings } : {}) };
282
305
  observe('verified-aside', { url: url.slice(0, 80), ok: result.ok, bodyLength: state.bodyLength, bytes: state.screenshotBytes, findings: findings.length, unmeasured: result.unmeasured });
283
306
  return result;
284
307
  } catch (error) {