elanous 0.2.8 → 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 (385) 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-d4b6df905f185055.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-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  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-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  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-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.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-7464c1987dd76e6d.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-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  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-26102fd7350f171e.js +1 -0
  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-139ba5c367bbf60a.js +1 -0
  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 +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  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 +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  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 +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -1,10 +1,11 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync, statSync, type Dirent } from 'node:fs';
2
+ import { dirname, join, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { parse as parseYaml } from 'yaml';
5
5
  import { elanousStateRoot } from '../autopilot/state-paths.js';
6
6
  import { defaultGraphsDir } from '../self-implement/graph-templates.js';
7
- import { handleGraphsGet } from '../nexus/api/graphs-api.js';
7
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
8
+ import type { ExecAttachment } from './store.js';
8
9
  import { debug } from '../debug/log.js';
9
10
 
10
11
  export interface InstalledGraph {
@@ -12,7 +13,7 @@ export interface InstalledGraph {
12
13
  title: string;
13
14
  description: string;
14
15
  path: string;
15
- /** 플러그인 그래프가 받는 입력 키(`examples/*.json` 표본에서) — COO 가 inputs 를 채울 수 있게. */
16
+ /** 코어 loop.inputs 또는 플러그인 examples/*.json 표본의 입력 키. */
16
17
  inputKeys?: string[];
17
18
  }
18
19
  export interface ExecPlanItem {
@@ -83,24 +84,41 @@ export async function installedGraphs(coreDir = defaultGraphsDir(), mineDir = jo
83
84
  }
84
85
 
85
86
  async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<InstalledGraph[]> {
86
- const catalog = handleGraphsGet('/v1/graphs', coreDir, { coreDir, mineDir });
87
- if (!catalog.ok) throw new Error('installed graph catalog unavailable');
88
- const { graphs } = await catalog.json() as { graphs: Array<{ id: string; source: 'core' | 'mine' }> };
89
- return graphs.flatMap(({ id, source }) => {
90
- const dir = source === 'core' ? coreDir : mineDir;
91
- if (!/^[a-z0-9-]+$/.test(id) || !existsSync(join(dir, 'recipes.yaml'))) return [];
92
- let names: string[];
93
- try { names = readdirSync(dir).filter(name => /\.ya?ml$/.test(name)); }
94
- catch { return []; }
95
- for (const name of names) {
96
- const path = join(dir, name);
97
- try {
98
- const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
99
- if (doc?.graph_id === id) return [{ id, title: doc.loop?.title ?? id, description: doc.loop?.description ?? '', path }];
100
- } catch { /* A malformed file is not an executable graph. */ }
87
+ const graphs: InstalledGraph[] = [];
88
+ const seen = new Set<string>();
89
+ const scan = (dir: string, core: boolean): void => {
90
+ let entries: Dirent[];
91
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
92
+ catch { return; }
93
+ if (!core || dir === coreDir || entries.some(entry => entry.isFile() && entry.name === 'recipes.yaml')) {
94
+ for (const entry of entries) {
95
+ if (!entry.isFile() || !/\.ya?ml$/.test(entry.name) || entry.name === 'recipes.yaml') continue;
96
+ const path = join(dir, entry.name);
97
+ try {
98
+ const doc = parseYaml(readFileSync(path, 'utf8')) as {
99
+ graph_id?: unknown;
100
+ loop?: { title?: string; description?: string; exec_request?: unknown; inputs?: unknown };
101
+ } | null;
102
+ const id = doc?.graph_id;
103
+ if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id) || seen.has(id)) continue;
104
+ if (core && doc?.loop?.exec_request !== true) continue;
105
+ const inputs = doc?.loop?.inputs;
106
+ const inputKeys = core && Array.isArray(inputs)
107
+ ? inputs.filter((key): key is string => typeof key === 'string')
108
+ : core && inputs && typeof inputs === 'object' ? Object.keys(inputs) : undefined;
109
+ graphs.push({ id, title: doc?.loop?.title ?? id, description: doc?.loop?.description ?? '', path,
110
+ ...(inputKeys ? { inputKeys } : {}) });
111
+ seen.add(id);
112
+ } catch { /* A malformed file is not an executable graph. */ }
113
+ }
101
114
  }
102
- return [];
103
- });
115
+ if (core) for (const entry of entries) {
116
+ if (entry.isDirectory() && !entry.name.startsWith('.')) scan(join(dir, entry.name), true);
117
+ }
118
+ };
119
+ scan(coreDir, true);
120
+ scan(mineDir, false);
121
+ return graphs;
104
122
  }
105
123
 
106
124
  export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tracks.json', import.meta.url))): string[] {
@@ -108,8 +126,32 @@ export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tra
108
126
  return data.tracks.flatMap(track => track.title ? [track.title] : []);
109
127
  }
110
128
 
111
- export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[]): Promise<unknown> {
112
- const prompt = `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄에서 알 수 있는 값만 넣는다(모르는 값은 넣지 않는다). 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 한 줄에서 얻을 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n한 줄: ${JSON.stringify(text)}`;
129
+ export function latestFieldFolder(configRoot = getElanousConfigDir()): string | null {
130
+ const root = join(configRoot, 'field');
131
+ let latest: { path: string; time: number } | null = null;
132
+ try {
133
+ for (const event of readdirSync(root, { withFileTypes: true })) {
134
+ if (!event.isDirectory() || event.name.startsWith('.')) continue;
135
+ const path = join(root, event.name);
136
+ try {
137
+ const photos = readdirSync(path, { withFileTypes: true }).filter(file => file.isFile() && /\.(?:png|jpe?g|webp|heic)$/i.test(file.name));
138
+ for (const photo of photos) {
139
+ const time = statSync(join(path, photo.name)).mtimeMs;
140
+ if (!latest || time > latest.time || (time === latest.time && path.localeCompare(latest.path) > 0)) latest = { path, time };
141
+ }
142
+ } catch { /* Unreadable event does not supply photos. */ }
143
+ }
144
+ } catch { /* No field directory. */ }
145
+ return latest?.path ?? null;
146
+ }
147
+
148
+ export function execPlanPrompt(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): string {
149
+ const supplied = attachments.map(({ name, path }) => `첨부: ${name} (${/\.(?:png|jpe?g|webp|gif|heic)$/i.test(name) ? '이미지' : '파일'}) — ${path}`).join('\n');
150
+ return `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄과 아래 알려진 값에서 얻을 수 있는 값만 넣는다(모르는 값은 넣지 않는다). image/photo/file 입력에는 알맞은 첨부 경로를 쓸 수 있다. folder 입력에는 첨부가 없거나 첨부 사진이 그 현장 폴더 안에 있을 때만 오늘 현장 폴더를 쓸 수 있다. 다른 현장 사진이나 출처를 모르는 업로드 사진이 있으면 폴더를 추측하지 마라. 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 알 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 단순히 묻는 말에는 없는 그래프를 지어내지 말고 graphId=""로 표시한다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n${fieldFolder ? `알려진 값: 오늘 현장 폴더 = ${fieldFolder}\n` : ''}${supplied ? `${supplied}\n` : ''}한 줄: ${JSON.stringify(text)}`;
151
+ }
152
+
153
+ export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): Promise<unknown> {
154
+ const prompt = execPlanPrompt(text, graphs, seats, attachments, fieldFolder);
113
155
  const { streamLLM } = await import('../llm.js');
114
156
  const { tierModel } = await import('../llm/model-defaults.js');
115
157
  const raw = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
@@ -120,13 +162,17 @@ export async function planExecRequest(text: string, deps: {
120
162
  graphs?: () => Promise<InstalledGraph[]>;
121
163
  seats?: () => string[];
122
164
  judge?: typeof judgeExecPlan;
165
+ attachments?: readonly ExecAttachment[];
166
+ fieldFolder?: () => string | null;
123
167
  } = {}): Promise<ExecPlanItem[]> {
124
168
  const graphs = await (deps.graphs ?? installedGraphs)();
125
169
  const seats = (deps.seats ?? seatTitles)();
126
- const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats);
170
+ const attachments = deps.attachments ?? [];
171
+ const fieldFolder = (deps.fieldFolder ?? latestFieldFolder)();
172
+ const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats, attachments, fieldFolder);
127
173
  if (!Array.isArray(raw) || raw.length === 0) throw new Error('COO 계획이 비었거나 배열이 아닙니다');
128
174
  const known = new Map(graphs.map(graph => [graph.id, graph]));
129
- return raw.map((item: unknown, index: number) => {
175
+ const plans: ExecPlanItem[] = raw.map((item: unknown, index: number) => {
130
176
  if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('COO 계획 항목 형식 오류');
131
177
  const row = item as Record<string, unknown>;
132
178
  if (typeof row.seat !== 'string' || !seats.includes(row.seat) || typeof row.title !== 'string' || !row.title.trim()
@@ -136,11 +182,35 @@ export async function planExecRequest(text: string, deps: {
136
182
  const after = Array.isArray(row.after)
137
183
  ? [...new Set(row.after.filter((n): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) < index))]
138
184
  : [];
185
+ const graph = known.get(row.graphId);
186
+ const inputs = { ...row.inputs as Record<string, unknown> };
187
+ let folderReason: string | undefined;
188
+ if (graph) {
189
+ for (const key of graph.inputKeys ?? []) {
190
+ if (key === 'folder') {
191
+ // A folder input must contain every attached photo — never guess it from «the latest event» (review r1).
192
+ const photos = attachments.filter(a => /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name)).map(a => resolve(a.path));
193
+ const holds = (folder: string) => photos.every(photo => photo.startsWith(`${resolve(folder)}${sep}`));
194
+ const given = typeof inputs.folder === 'string' ? inputs.folder : undefined;
195
+ if (given !== undefined) {
196
+ if (!holds(given)) { delete inputs.folder; folderReason = `${row.seat}: folder 필요 — 첨부 사진이 그 현장 폴더 안에 없습니다`; }
197
+ } else if (fieldFolder && holds(fieldFolder)) inputs.folder = fieldFolder;
198
+ else if (photos.length) folderReason = `${row.seat}: folder 필요 — 첨부 사진의 현장 폴더를 알 수 없습니다`;
199
+ continue;
200
+ }
201
+ if (inputs[key] !== undefined) continue;
202
+ if (['image', 'photo', 'file'].includes(key)) {
203
+ const attachment = attachments.find(a => key === 'file' || /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name));
204
+ if (attachment) inputs[key] = attachment.path;
205
+ }
206
+ }
207
+ }
139
208
  return {
140
- seat: row.seat, title: row.title.trim(), graphId: row.graphId,
141
- inputs: row.inputs as Record<string, unknown>,
209
+ seat: row.seat, title: row.title.trim(), graphId: row.graphId, inputs,
142
210
  ...(after.length ? { after } : {}),
143
- ...(!known.has(row.graphId) ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}),
211
+ ...(!graph ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : folderReason ? { reason: folderReason } : {}),
144
212
  };
145
213
  });
214
+ for (const plan of plans) debug.log('exec.planner', 'select', { seat: plan.seat, chosen: plan.graphId, candidates: graphs.slice(0, 10).map(graph => graph.id) });
215
+ return plans;
146
216
  }
@@ -1,16 +1,20 @@
1
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { debug } from '../debug/log.js';
5
+ import { notifyExecRequestTransition } from '../web-push/notify-exec-request.js';
5
6
  import { effectiveInstanceRoot } from '../instance/resolve.js';
6
7
  import { lastJsonObject, runGraph, type GraphRunState } from '../graph-runner/runner.js';
7
8
  import { installedGraphs, planExecRequest, type ExecPlanItem, type InstalledGraph } from './planner.js';
8
- import { ExecRequestStore, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
9
+ import { ExecRequestStore, type ExecAttachment, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
10
+ import { answerAsSeat } from '../intake-plane/seat-answer.js';
11
+ import { redactSecrets } from '../task-cards/card-store.js';
9
12
 
10
13
  export interface ExecRunnerDeps {
11
14
  store?: ExecRequestStore;
12
15
  graphs?: () => Promise<InstalledGraph[]>;
13
- plan?: (text: string) => Promise<ExecPlanItem[]>;
16
+ plan?: (text: string, attachments?: readonly ExecAttachment[]) => Promise<ExecPlanItem[]>;
17
+ answer?: typeof answerAsSeat;
14
18
  run?: typeof runGraph;
15
19
  root?: string;
16
20
  }
@@ -38,7 +42,11 @@ function safeFile(path: string, root: string, graphId: string, runId: string): s
38
42
  ? realpathSync(artifactsDir) : null;
39
43
  const inRunDirectory = realArtifactsDir === join(base, `${runId}.json.artifacts`)
40
44
  && real.startsWith(`${realArtifactsDir}${sep}`);
41
- if ((!local && !inRunDirectory) || !statSync(real).isFile()) return null;
45
+ // EXEC2 — plugin graphs (doc-draft · geo-check) write into `<graphId>/<runId>/`; that directory is this run's own too.
46
+ const runDir = join(allowed, runId);
47
+ const realRunDir = existsSync(runDir) && statSync(runDir).isDirectory() ? realpathSync(runDir) : null;
48
+ const inOwnRunDir = realRunDir === join(base, runId) && real.startsWith(`${realRunDir}${sep}`);
49
+ if ((!local && !inRunDirectory && !inOwnRunDir) || !statSync(real).isFile()) return null;
42
50
  return real;
43
51
  } catch { return null; }
44
52
  }
@@ -56,8 +64,32 @@ function fileName(path: string, runId: string): string {
56
64
  return `${runId}--${digest}--${basename(path)}`;
57
65
  }
58
66
 
67
+ /** EXEC2 — `report` is how geo-check names its file; treat it like `file`/`path`. */
68
+ function fileCandidate(output: Record<string, unknown>): string | null {
69
+ if (typeof output.file === 'string') return output.file;
70
+ if (typeof output.path === 'string') return output.path;
71
+ if (typeof output.report === 'string' && /\.(md|txt|pdf)$/i.test(output.report)) return output.report;
72
+ return null;
73
+ }
74
+
75
+ const TEXT_RESULT_LIMIT = 200_000;
76
+ const TEXT_RESULT_NAME = 'result.md';
77
+
78
+ /** EXEC2 — a done seat whose graph only answered in text gets that text as a run artifact (redacted), so the result is viewable. */
79
+ function textResultFile(seat: ExecSeat, state: GraphRunState, root: string): string | null {
80
+ const outputs = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)).filter((o): o is Record<string, unknown> => !!o);
81
+ const text = outputs.map(o => o.markdown ?? o.text).find((v): v is string => typeof v === 'string' && v.trim().length > 0);
82
+ if (!text || !SEGMENT.test(seat.graphId) || !SEGMENT.test(seat.runId)) return null;
83
+ const dir = join(root, 'graph-runs', seat.graphId, `${seat.runId}.json.artifacts`);
84
+ const path = join(dir, TEXT_RESULT_NAME);
85
+ try {
86
+ if (!existsSync(path)) { mkdirSync(dir, { recursive: true }); writeFileSync(path, redactSecrets(text.slice(0, TEXT_RESULT_LIMIT)), { mode: 0o600 }); }
87
+ return path;
88
+ } catch { return null; }
89
+ }
90
+
59
91
  function resultOf(output: Record<string, unknown>, seat: ExecSeat, id: string, root: string): ExecResult | null {
60
- const candidate = typeof output.file === 'string' ? output.file : typeof output.path === 'string' ? output.path : null;
92
+ const candidate = fileCandidate(output);
61
93
  let url: string;
62
94
  let kind: ExecResult['kind'];
63
95
  let name: string;
@@ -95,6 +127,7 @@ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root:
95
127
  if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision && !existsSync(claim)) {
96
128
  if (!item.approvals.some(a => a.graphId === seat.graphId && a.runId === seat.runId)) {
97
129
  item.approvals.push({ graphId: seat.graphId, runId: seat.runId, message: state.pending.message });
130
+ notifyTransition(item, item.status, item.approvals.length);
98
131
  }
99
132
  } else item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
100
133
  for (const node of state.nodes ?? []) {
@@ -107,9 +140,28 @@ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root:
107
140
  if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url)) item.results.push(result);
108
141
  }
109
142
  }
143
+ if (seat.status === 'done' && !item.results.some(existing => existing.seat === seat.seat)) {
144
+ const file = textResultFile(seat, state, root);
145
+ const result = file ? resultOf({ file, title: seat.title }, seat, item.id, root) : null;
146
+ if (result) item.results.push(result);
147
+ else {
148
+ const keys = [...new Set((state.nodes ?? []).flatMap(node => Object.keys(lastJsonObject(node.output) ?? {})))];
149
+ debug.log('exec.result', 'none', { seat: seat.seat, graphId: seat.graphId, keys });
150
+ }
151
+ }
110
152
  const summary = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)?.summary).find(value => typeof value === 'string');
111
153
  if (typeof summary === 'string') item.summary = summary;
112
- if (seat.status === 'failed') seat.reason = state.nodes?.at(-1)?.error ?? `그래프 런 ${state.status}`;
154
+ if (seat.status === 'failed') seat.reason = failureReason(state);
155
+ }
156
+
157
+ /** EXEC2 — the failing node's own error (e.g. doc-draft `check` {outcome:'fail', error}) beats the terminal gate's empty one. */
158
+ function failureReason(state: GraphRunState): string {
159
+ for (const node of [...(state.nodes ?? [])].reverse()) {
160
+ if (typeof node.error === 'string' && node.error) return node.error;
161
+ const output = lastJsonObject(node.output);
162
+ if (output && typeof output.error === 'string' && output.error) return redactSecrets(output.error);
163
+ }
164
+ return `그래프 런 ${state.status}`;
113
165
  }
114
166
 
115
167
  const PRIOR_TEXT_LIMIT = 4_000;
@@ -129,8 +181,14 @@ export function withPriorResults(inputs: Record<string, unknown> | undefined, pr
129
181
  return { ...base, context: `${previous}앞 자리 결과:\n${parts.join('\n\n')}` };
130
182
  }
131
183
 
184
+ function notifyTransition(item: ExecRequest, from: ExecRequest['status'], pendingApprovals: number): void {
185
+ void notifyExecRequestTransition({ id: item.id, text: item.text, from, to: item.status,
186
+ summary: item.summary, pendingApprovals }).catch(() => { /* Push never interrupts execution. */ });
187
+ }
188
+
132
189
  function aggregate(item: ExecRequest): void {
133
190
  if (item.status === 'planning' || item.seats.length === 0) return;
191
+ const previous = item.status;
134
192
  if (item.seats.some(seat => seat.status === 'waiting' || seat.status === 'running')) item.status = 'running';
135
193
  else item.status = item.seats.some(seat => seat.status === 'failed') ? 'failed' : 'done';
136
194
  // 실패면 요약은 «실패 사유»가 이긴다 — 먼저 끝난 자리의 산출 설명이 남아 있어도 덮는다(혼합 결과에서 성공처럼 보이지 않게).
@@ -138,6 +196,7 @@ function aggregate(item: ExecRequest): void {
138
196
  const reasons = item.seats.filter(seat => seat.status === 'failed').map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`);
139
197
  if (reasons.length > 0) item.summary = reasons.join('; ');
140
198
  }
199
+ if (previous !== item.status && (item.status === 'done' || item.status === 'failed')) notifyTransition(item, previous, item.approvals.length);
141
200
  }
142
201
 
143
202
  export class ExecRequestRunner {
@@ -156,10 +215,12 @@ export class ExecRequestRunner {
156
215
  if (item.status === 'planning') {
157
216
  item.status = 'failed';
158
217
  item.summary = '데몬 재시작으로 COO 계획이 중단됐습니다';
218
+ notifyTransition(item, 'planning', item.approvals.length);
159
219
  } else {
160
220
  if (item.seats.length === 0) {
161
221
  item.status = 'failed';
162
222
  item.summary = '데몬 재시작으로 자리 배정이 중단됐습니다';
223
+ notifyTransition(item, 'running', item.approvals.length);
163
224
  }
164
225
  for (const seat of item.seats) {
165
226
  if (seat.status === 'done' || seat.status === 'failed') continue;
@@ -192,14 +253,16 @@ export class ExecRequestRunner {
192
253
  }
193
254
  }
194
255
 
195
- submit(text: string): ExecRequest {
196
- const item = this.store.create(text);
256
+ submit(text: string, attachments?: ExecAttachment[]): ExecRequest {
257
+ const item = this.store.create(text, attachments);
197
258
  void this.execute(item.id).catch(error => {
198
259
  const current = this.store.get(item.id);
199
260
  if (!current) return;
261
+ const previous = current.status;
200
262
  current.status = 'failed';
201
263
  current.summary = error instanceof Error ? error.message : String(error);
202
264
  this.store.save(current);
265
+ if (previous !== current.status) notifyTransition(current, previous, current.approvals.length);
203
266
  debug.log('exec-requests', 'failed', { id: item.id, reason: current.summary });
204
267
  });
205
268
  return item;
@@ -209,8 +272,40 @@ export class ExecRequestRunner {
209
272
  const item = this.store.get(id);
210
273
  if (!item) return;
211
274
  const graphs = await (this.deps.graphs ?? installedGraphs)();
212
- const plans = await (this.deps.plan ?? ((text: string) => planExecRequest(text, { graphs: async () => graphs })))(item.text);
275
+ const plans = await (this.deps.plan ?? ((text: string, attachments?: readonly ExecAttachment[]) => planExecRequest(text, { graphs: async () => graphs, attachments })))(item.text, item.attachments);
213
276
  const byId = new Map(graphs.map(graph => [graph.id, graph]));
277
+ // Every clause must be a question — «상황 알려줘? 카드 만들어줘» carries a work item (review r1).
278
+ const clauses = [...item.text.trim().matchAll(/([^??.!。\n]+)([??]?)/g)].map(m => ({ body: m[1]!.trim(), asked: m[2] !== '' })).filter(c => c.body);
279
+ const isQuestion = clauses.length > 0 && clauses.every(c => c.asked
280
+ || /(?:알려\s*줘|알려\s*주세요|보여\s*줘|보여\s*주세요|어떻게|무엇|뭐|어떤|언제|누구|질문|인가요|나요|습니까)$/.test(c.body));
281
+ if (isQuestion && plans.length > 0 && plans.every(plan => !plan.graphId
282
+ && !/\s—\s.+\s필요\s*$/.test(plan.title) && !/\s—\s.+\s필요\s*$/.test(plan.reason ?? '')
283
+ && !/(?:초안|작성|제작|생성|게시|분석|전략|메일|보고서)/.test(plan.title)) && !(item.attachments?.length)) {
284
+ for (const plan of plans) {
285
+ const answered = await (this.deps.answer ?? answerAsSeat)(plan.seat, item.text);
286
+ if (!answered) {
287
+ item.seats.push({ seat: plan.seat, title: plan.title, status: 'failed', graphId: '', runId: '',
288
+ reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` });
289
+ continue;
290
+ }
291
+ const seat: ExecSeat = { seat: plan.seat, title: plan.title, status: 'done', graphId: 'seat-answer', runId: randomUUID() };
292
+ const dir = join(this.root, 'graph-runs', seat.graphId);
293
+ const statePath = join(dir, `${seat.runId}.json`);
294
+ const artifactDir = join(dir, `${seat.runId}.json.artifacts`);
295
+ mkdirSync(artifactDir, { recursive: true });
296
+ const file = join(artifactDir, TEXT_RESULT_NAME);
297
+ writeFileSync(file, redactSecrets(answered.text.slice(0, TEXT_RESULT_LIMIT)), { mode: 0o600 });
298
+ const state: GraphRunState = { graphId: seat.graphId, runId: seat.runId, status: 'done', path: [], executed: 0, dryRun: false, statePath, nodes: [] };
299
+ writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
300
+ item.seats.push(seat);
301
+ const result = resultOf({ file, title: answered.title }, seat, id, this.root);
302
+ if (result) item.results.push(result);
303
+ }
304
+ item.status = 'running';
305
+ aggregate(item);
306
+ this.store.save(item);
307
+ return;
308
+ }
214
309
  item.seats = plans.map(plan => ({ seat: plan.seat, title: plan.title, status: plan.reason || !byId.has(plan.graphId) ? 'failed' : 'waiting', graphId: plan.graphId,
215
310
  runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs, ...(plan.after?.length ? { after: plan.after } : {}),
216
311
  ...(plan.reason || !byId.has(plan.graphId) ? { reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}) }));
@@ -366,12 +461,14 @@ export class ExecRequestRunner {
366
461
  for (const entry of Array.isArray(output.artifacts) ? output.artifacts : [output]) {
367
462
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
368
463
  const artifact = entry as Record<string, unknown>;
369
- const candidate = typeof artifact.file === 'string' ? artifact.file : artifact.path;
464
+ const candidate = fileCandidate(artifact);
370
465
  if (typeof candidate !== 'string') continue;
371
466
  const path = safeFile(candidate, this.root, seat.graphId, seat.runId);
372
467
  if (path && fileName(path, seat.runId) === name) return path;
373
468
  }
374
469
  }
470
+ const text = safeFile(join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json.artifacts`, TEXT_RESULT_NAME), this.root, seat.graphId, seat.runId);
471
+ if (text && fileName(text, seat.runId) === name) return text;
375
472
  } catch { return null; }
376
473
  return null;
377
474
  }
@@ -22,9 +22,15 @@ export interface ExecResult {
22
22
  url: string;
23
23
  sources?: Array<{ title: string; url: string }>;
24
24
  }
25
+ export interface ExecAttachment {
26
+ name: string;
27
+ path: string;
28
+ }
29
+
25
30
  export interface ExecRequest {
26
31
  id: string;
27
32
  text: string;
33
+ attachments?: ExecAttachment[];
28
34
  createdAt: string;
29
35
  status: 'planning' | 'running' | 'done' | 'failed';
30
36
  summary: string;
@@ -39,8 +45,8 @@ export class ExecRequestStore {
39
45
  readonly dir: string;
40
46
  constructor(configDir = getElanousConfigDir()) { this.dir = join(configDir, 'exec-requests'); }
41
47
 
42
- create(text: string): ExecRequest {
43
- const item: ExecRequest = { id: randomUUID(), text, createdAt: new Date().toISOString(), status: 'planning', summary: '', seats: [], results: [], approvals: [] };
48
+ create(text: string, attachments?: ExecAttachment[]): ExecRequest {
49
+ const item: ExecRequest = { id: randomUUID(), text, ...(attachments !== undefined ? { attachments } : {}), createdAt: new Date().toISOString(), status: 'planning', summary: '', seats: [], results: [], approvals: [] };
44
50
  this.save(item);
45
51
  return item;
46
52
  }
@@ -219,19 +219,22 @@ function uniqueName(dir: string, name: string, size: number, taken: Set<string>)
219
219
 
220
220
  export const FIELD_CAPTION_TAG = '#현장';
221
221
 
222
- /** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event? }`, 없으면 null.
223
- * `#현장 <slug>` — 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
224
- export function parseFieldCaption(caption: string | undefined | null): { event?: string; text?: string } | null {
222
+ /** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event?, mode?, text? }`, 없으면 null.
223
+ * `#현장 <slug>` — 모드 토큰을 건너뛴 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
224
+ export function parseFieldCaption(caption: string | undefined | null): { event?: string; mode?: 'instant'; text?: string } | null {
225
225
  if (!caption) return null;
226
226
  const tokens = caption.trim().split(/\s+/);
227
227
  const at = tokens.indexOf(FIELD_CAPTION_TAG);
228
228
  if (at < 0) return null;
229
- const next = tokens[at + 1]?.toLowerCase();
229
+ const isInstant = (token: string) => token === '즉석' || token.toLowerCase() === 'instant';
230
+ const mode = tokens.some(isInstant) ? 'instant' : undefined;
231
+ const eventAt = at + 1 + (tokens[at + 1] && isInstant(tokens[at + 1]) ? 1 : 0);
232
+ const next = tokens[eventAt]?.toLowerCase();
230
233
  const hasEvent = !!next && isFieldSlug(next);
231
- // 태그·행사 이름을 뺀 나머지가 «글»(자막) — 태그 앞에 쓴 글도 같이.
232
- const rest = tokens.filter((_, i) => i !== at && !(hasEvent && i === at + 1)).join(' ');
234
+ // 태그·행사 이름·모드를 뺀 나머지가 «글»(자막) — 태그 앞에 쓴 글도 같이.
235
+ const rest = tokens.filter((token, i) => i !== at && !(hasEvent && i === eventAt) && !isInstant(token)).join(' ');
233
236
  const text = normalizeFieldCaption(rest);
234
- return { ...(hasEvent ? { event: next } : {}), ...(text ? { text } : {}) };
237
+ return { ...(hasEvent ? { event: next } : {}), ...(mode ? { mode } : {}), ...(text ? { text } : {}) };
235
238
  }
236
239
 
237
240
  export const FIELD_CAPTIONS_FILE = 'captions.txt';
@@ -4,11 +4,12 @@ import { join, resolve } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { spawn, execFileSync } from 'node:child_process';
6
6
  import { debug } from '../debug/log.js';
7
+ import { mergeCapturedPath } from '../shell-env-bootstrap.js';
7
8
  import { listFieldMedia } from './field-media.js';
8
9
  import { startFieldFeed, type FieldFeedStart } from './field-feed-auto.js';
9
10
 
10
11
  export interface FieldReelResult { ok: boolean; file?: string; seconds: number; error?: string; feed?: FieldFeedStart }
11
- export type FieldReelRunner = (dir: string, opts: { title: string; sub: string }) => Promise<FieldReelResult>;
12
+ export type FieldReelRunner = (dir: string, opts: { title: string; sub: string; mode?: 'standard' | 'instant' }) => Promise<FieldReelResult>;
12
13
  export interface FieldReelStatus {
13
14
  state: 'waiting' | 'rendering' | 'done' | 'failed';
14
15
  items: number;
@@ -23,6 +24,7 @@ export interface FieldReelOptions {
23
24
  quietMs?: number;
24
25
  title?: string;
25
26
  sub?: string;
27
+ mode?: 'standard' | 'instant';
26
28
  runner?: FieldReelRunner;
27
29
  onDone?: (result: FieldReelResult) => void | Promise<void>;
28
30
  /** 같은 수신자가 앨범 항목마다 예약해도 한 렌더당 한 번만 통지한다. */
@@ -96,10 +98,12 @@ export function readFieldReelStatus(dir: string): FieldReelStatus | null {
96
98
  }
97
99
  }
98
100
 
99
- export const defaultFieldReelRunner: FieldReelRunner = (dir, { title, sub }) => new Promise((finish) => {
101
+ export const defaultFieldReelRunner = (dir: string, { title, sub, mode = 'standard' }: { title: string; sub: string; mode?: 'standard' | 'instant' }, spawnReel: typeof spawn = spawn): Promise<FieldReelResult> => new Promise((finish) => {
100
102
  const script = resolve(import.meta.dir, '../../skills/explainer-video/engine/reel.sh');
101
103
  const started = Date.now();
102
- const child = spawn('zsh', [script, dir, '--title', title, '--sub', sub], { stdio: 'ignore' });
104
+ const env = { ...process.env };
105
+ mergeCapturedPath(env, { capture: true });
106
+ const child = spawnReel('zsh', [script, dir, '--title', title, '--sub', sub, ...(mode === 'instant' ? ['--instant'] : [])], { stdio: 'ignore', env });
103
107
  let settled = false;
104
108
  const done = (error?: string) => {
105
109
  if (settled) return;
@@ -147,6 +151,7 @@ async function render(dir: string, job: Job): Promise<void> {
147
151
  result = await (renderOpts.runner ?? defaultFieldReelRunner)(dir, {
148
152
  title: titleLines[0]?.trim() || renderOpts.title || event,
149
153
  sub: titleLines[1]?.trim() || renderOpts.sub || sub,
154
+ ...(renderOpts.mode ? { mode: renderOpts.mode } : {}),
150
155
  });
151
156
  } catch (err) {
152
157
  result = { ok: false, seconds: 0, error: err instanceof Error ? err.message : String(err) };
@@ -160,10 +165,15 @@ async function render(dir: string, job: Job): Promise<void> {
160
165
  debug.log('field.reel', result.ok ? 'done' : 'failed', { event, items, seconds: result.seconds });
161
166
  if (result.ok) {
162
167
  // EV10b: 엔진이 timeline.json 에 남긴 비전 호출 수·성공·«현장 N» 강등·걸린 ms — codex 잔량과 겹치므로 관측한다(OP 10-01).
168
+ let cardsCache: 'hit' | 'miss' | 'n/a' = 'n/a';
163
169
  try {
164
- const t = JSON.parse(readFileSync(join(dir, 'reel', 'timeline.json'), 'utf8')) as { vision?: Record<string, unknown>; music?: unknown };
170
+ const t = JSON.parse(readFileSync(join(dir, 'reel', 'timeline.json'), 'utf8')) as { vision?: Record<string, unknown>; music?: unknown; cardsCache?: 'hit' | 'miss' };
165
171
  if (t.vision) debug.log('field.reel', 'vision', { event, ...t.vision, music: t.music === true });
172
+ if (renderOpts.mode === 'instant' && (t.cardsCache === 'hit' || t.cardsCache === 'miss')) cardsCache = t.cardsCache;
166
173
  } catch { debug.log('field.reel', 'vision-unread', { event }); }
174
+ // A custom runner without an explicit mode may render any way — say «unknown» instead of guessing the default.
175
+ const mode = renderOpts.mode ?? (renderOpts.runner ? 'unknown' : 'standard');
176
+ debug.log('field.reel', 'rendered', { mode, seconds: result.seconds, cardsCache });
167
177
  }
168
178
  // 뒤이은 업로드로 다시 렌더할 예정이면(dirty) 피드는 그 마지막 렌더 뒤에 한 번만 띄운다.
169
179
  if (result.ok && !job.dirty) result = { ...result, feed: (renderOpts.startFeed ?? ((d: string) => startFieldFeed(d, { enabled: renderOpts.autoFeed })))(dir) };
@@ -9,7 +9,7 @@
9
9
  // the pattern; we trim the hook-based fallback (sandboxed envs)
10
10
  // because elanous's primary surface runs native git.
11
11
 
12
- import { spawnSync, execFileSync } from 'node:child_process';
12
+ import { spawnSync, execFileSync, type SpawnSyncReturns } from 'node:child_process';
13
13
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync, rmSync, symlinkSync, lstatSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { basename, dirname, join, resolve } from 'node:path';
@@ -17,8 +17,33 @@ import { findGitDir } from './locate.js';
17
17
  import { isTransientGitError } from './retry.js';
18
18
  import { runGitCommand } from './runner.js';
19
19
  import { debug } from '../debug/log.js';
20
+ import { getUserConfig } from '../user-config.js';
20
21
 
21
22
  const GIT_TIMEOUT_MS = 30_000;
23
+ const WORKTREE_ADD_TIMEOUT_MS = 5 * 60_000;
24
+
25
+ export interface WorktreeAddDeps {
26
+ spawn?: (command: string, args: string[], options: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer: number }) => SpawnSyncReturns<string>;
27
+ }
28
+
29
+ function worktreeAddTimeoutMs(): number {
30
+ const seconds = getUserConfig().harness?.worktreeAddTimeoutSec;
31
+ return typeof seconds === 'number' && Number.isSafeInteger(seconds) && seconds > 0 && seconds <= 2_147_483
32
+ ? seconds * 1000 : WORKTREE_ADD_TIMEOUT_MS;
33
+ }
34
+
35
+ function worktreeAddFailureLines(stderr: string): { lastProgress: string | null; details: string[] } {
36
+ let lastProgress: string | null = null;
37
+ const details: string[] = [];
38
+ for (const line of stderr.split(/[\r\n]+/).map((value) => value.trim()).filter(Boolean)) {
39
+ if (/^(?:Updating files|Checking out files|Receiving objects|Resolving deltas):\s*\d+%/.test(line)) {
40
+ lastProgress = line;
41
+ } else if (!/^Preparing worktree\b/.test(line)) {
42
+ details.push(line);
43
+ }
44
+ }
45
+ return { lastProgress, details: details.slice(-3) };
46
+ }
22
47
 
23
48
  /** Git metadata left by an interrupted operation in one checkout. */
24
49
  /** ⚠️ 공개인 이유(무인 리뷰 질의) — `GitResidueObservation.residues` 의 **원소 타입**이라,
@@ -618,7 +643,7 @@ function worktreeHoldingBranch(repoRoot: string, branch: string): string | null
618
643
  return null;
619
644
  }
620
645
 
621
- export function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult {
646
+ export function createWorktree(opts: CreateWorktreeOpts, deps: WorktreeAddDeps = {}): CreateWorktreeResult {
622
647
  const { repoRoot, branch, worktreeRoot } = opts;
623
648
  validateBranchName(branch);
624
649
  const branchHolder = worktreeHoldingBranch(repoRoot, branch);
@@ -684,25 +709,39 @@ export function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult {
684
709
  // ★ 병렬 안전 재시도(2026-07-21) — 동시 worktree add 가 git 락에서 경쟁하면 즉시 실패 → 락/일시적
685
710
  // 에러면 백오프 재시도(패자가 승자의 락 해제를 기다렸다 성공). non-transient 는 재시도 없이 throw.
686
711
  const MAX_ATTEMPTS = 6;
687
- let res!: ReturnType<typeof spawnSync>;
712
+ const addTimeoutMs = worktreeAddTimeoutMs();
713
+ const addSpawn = deps.spawn ?? spawnSync;
688
714
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
689
- res = spawnSync(
715
+ const res = addSpawn(
690
716
  'git',
691
717
  ['worktree', 'add', branchFlag, branch, wtPath, baseCheckout],
692
- { cwd: repoRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 },
718
+ { cwd: repoRoot, encoding: 'utf8', timeout: addTimeoutMs, maxBuffer: 4 * 1024 * 1024 },
693
719
  );
694
720
  if (res.status === 0) break;
695
721
  const err = (res.stderr || res.stdout || '').toString();
722
+ const { lastProgress, details } = worktreeAddFailureLines(err);
723
+ const timedOut = (res.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
724
+ if (timedOut) {
725
+ debug.log('git-fs.worktree', 'add.failed', { reason: 'timeout', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
726
+ throw new Error(`git worktree add timed out after ${addTimeoutMs / 1000}s (${res.signal ?? 'none'}) — ${[...details, ...(lastProgress ? [`checkout was at «${lastProgress}»`] : [])].join(' | ') || `git exited ${res.status}`}`);
727
+ }
728
+ if (res.signal) {
729
+ debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal, lastProgress }, { level: 'warn' });
730
+ throw new Error(`git worktree add failed (${res.signal}) — ${details.join(' | ') || lastProgress || `git exited ${res.status}`}`);
731
+ }
696
732
  const blockingPath = blockingWorktreePath(err);
697
733
  if (blockingPath) {
734
+ debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
698
735
  debug.log('git-fs.worktree', 'add.branch-held', { attempt, branch, blockingPath }, { level: 'warn' });
699
736
  throw new Error(`git worktree add failed — branch ${branch} is already used by worktree at ${blockingPath}`);
700
737
  }
701
738
  if (isUnbornHeadError(err)) {
739
+ debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
702
740
  throw new Error('git worktree add failed — this repository has no commits yet; create the first commit with: git commit --allow-empty -m "Initial commit"');
703
741
  }
704
742
  if (!isTransientGitError(err) || attempt === MAX_ATTEMPTS) {
705
- throw new Error(`git worktree add failed — ${err.trim() || `git exited ${res.status}`}`);
743
+ debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
744
+ throw new Error(`git worktree add failed — ${details.join(' | ') || lastProgress || `git exited ${res.status}`}`);
706
745
  }
707
746
  debug.log('git-fs.worktree', 'add.retry', { attempt, branch, err: err.trim().slice(0, 120) });
708
747
  sleepSyncMs(80 * attempt); // 선형 백오프(80·160·240…ms)