elanous 0.2.12 → 0.2.13

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 (289) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -23,11 +23,11 @@ export interface NodeKindEntry {
23
23
  export const HARNESS_CORE_KINDS = ['agent', 'gate', 'git', 'judge', 'observe', 'hitl', 'subgraph'] as const;
24
24
 
25
25
  // Workflow validation, the palette, and executor variant labels read the same keys.
26
- export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow'] as const;
26
+ export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow', 'knowledge'] as const;
27
27
 
28
28
  const descriptions: Record<GraphKind, readonly string[]> = {
29
29
  harness: ['Run an agent', 'Check an execution gate', 'Perform git operations', 'Judge an outcome', 'Observe a result', 'Request human input', 'Run a child graph'],
30
- workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow'],
30
+ workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow', 'Search the vault for context'],
31
31
  };
32
32
 
33
33
  const kinds = new Map<GraphKind, Map<string, NodeKindEntry>>();
@@ -26,7 +26,7 @@ export function raiseApprovalCard(state: GraphRunState, pending: Pending, deps:
26
26
  const nodeId = pending.nodeId;
27
27
  const identity = { graphId: state.graphId, runId: state.runId, nodeId };
28
28
  const visit = state.path.filter(node => node === nodeId).length;
29
- if (state.dryRun || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
29
+ if (state.dryRun || state.growthPark || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
30
30
  state.pending.decision || state.path.at(-1) !== nodeId || visit < 1 ||
31
31
  existsSync(`${join(root, 'graph-runs', state.graphId, `${state.runId}.json`)}.${state.path.length}.decision.json`)) {
32
32
  observation('skipped', identity);
@@ -88,7 +88,7 @@ function applyOne(entry: DecisionEntry, identity: { graphId: string; runId: stri
88
88
  observation('skipped', identity, entry.id, entry.choice);
89
89
  return false;
90
90
  }
91
- if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.status !== 'awaiting-approval' ||
91
+ if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.growthPark || state.status !== 'awaiting-approval' ||
92
92
  !state.pending || state.pending.nodeId !== nodeId || state.pending.decision || state.path.at(-1) !== nodeId ||
93
93
  state.path.filter(node => node === nodeId).length !== visit ||
94
94
  existsSync(`${statePath}.${state.path.length}.decision.json`)) {
@@ -1,3 +1,6 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
3
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
1
4
  import { createGraphVariant } from '../self-implement/graph-variant.js';
2
5
  import type { GraphEdgeSpec, GraphNodeSpec, GraphTemplateSpec } from '../self-implement/graph-yaml.js';
3
6
  import { createLLMGrowthProposer } from './graph-grow-llm.js';
@@ -10,6 +13,8 @@ export interface GrowthProposal {
10
13
 
11
14
  export type GrowthProposer = (input: { graph: GraphTemplateSpec; nodeId: string; outcome: string; runId: string; output?: unknown }) => GrowthProposal | undefined | Promise<GrowthProposal | undefined>;
12
15
  export type GrowthRecipeEffect = 'read-only' | 'external-effect' | 'unknown';
16
+ export interface GrowthCommand { command: string; dry_run_command?: string; timeout_ms?: number }
17
+ export interface GrowthRecipeClassification { effect: GrowthRecipeEffect; command?: GrowthCommand; approval?: string }
13
18
 
14
19
  export interface GraphGrowth {
15
20
  node: GraphNodeSpec;
@@ -18,8 +23,52 @@ export interface GraphGrowth {
18
23
  undo: { removeNode: string; removeOutcome: { from: string; outcome: string }; removeEdge?: GraphEdgeSpec };
19
24
  }
20
25
 
26
+ export interface GrowthDecisionDeps {
27
+ ledger?: Pick<DecisionLedger, 'raiseOnce'>;
28
+ root?: string;
29
+ now?: () => Date;
30
+ }
31
+
32
+ function proposalGrowth(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal): GraphGrowth {
33
+ const edges: GraphEdgeSpec[] = [
34
+ { from: input.nodeId, on: 'outcome', map: { [input.outcome]: proposal.node.nodeId } },
35
+ { from: proposal.node.nodeId, to: proposal.returnTo as string },
36
+ ];
37
+ return { node: proposal.node, edges, reason: proposal.reason,
38
+ undo: { removeNode: proposal.node.nodeId, removeOutcome: { from: input.nodeId, outcome: input.outcome }, removeEdge: edges[1]! } };
39
+ }
40
+
41
+ /** The card's own ref commits to the entire growth and resolved recipe, independently of the parked run state. */
42
+ export function growthApprovalRef(identity: { graphId: string; runId: string; from: string; outcome: string },
43
+ growth: GraphGrowth, command?: GrowthCommand, approval?: string): string {
44
+ const hash = createHash('sha256').update(JSON.stringify({ identity, growth, command, approval })).digest('hex');
45
+ return `graph-growth:${hash}`;
46
+ }
47
+
48
+ export function growthDecisionRef(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal, command?: GrowthCommand, approval?: string): string {
49
+ return growthApprovalRef({ graphId: input.graph.graphId, runId: input.runId, from: input.nodeId, outcome: input.outcome },
50
+ proposalGrowth(input, proposal), command, approval);
51
+ }
52
+
53
+ export function growthDecision(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal, deps: GrowthDecisionDeps = {}, command?: GrowthCommand, approval?: string): DecisionEntry {
54
+ const ledger = deps.ledger ?? new DecisionLedger({ stateDir: deps.root ?? effectiveInstanceRoot() });
55
+ const ref = growthDecisionRef(input, proposal, command, approval);
56
+ const summary = `${proposal.node.nodeId} (${proposal.node.kind}, ${proposal.node.recipe}) → ${proposal.returnTo}`;
57
+ const commandDigest = command ? createHash('sha256').update(JSON.stringify(command)).digest('hex') : undefined;
58
+ return ledger.raiseOnce({ title: `${input.graph.graphId} / ${input.nodeId} / ${input.outcome} growth approval`, category: 'scope',
59
+ scqa: { s: `Graph ${input.graph.graphId}, node ${input.nodeId}, outcome ${input.outcome} has no mapped growth edge.`,
60
+ c: `Proposed node: ${summary}; command SHA-256: ${commandDigest ?? 'unresolved'}; reason: ${proposal.reason}`.slice(0, 240) },
61
+ options: [{ key: 'a', label: '승인', consequence: `Apply proposed node ${proposal.node.nodeId} and continue the run` },
62
+ { key: 'b', label: '거절', consequence: 'Keep the original fallback route' }],
63
+ recommendation: { skipped: true, reason: 'Side-effect recipe requires a human decision' },
64
+ ...(command || approval ? { pendingQuestion: [approval ? `Approval recipe: ${approval}` : undefined,
65
+ command ? `Proposed command (${commandDigest}):\n${command.command}` : undefined].filter(Boolean).join('\n') } : {}),
66
+ raisedBy: { agent: 'graph-runner' }, refs: [ref], dueAt: new Date((deps.now?.() ?? new Date()).getTime() + 8 * 60 * 60 * 1000).toISOString() }, ref);
67
+ }
68
+
21
69
  export async function proposeGrowth(input: Parameters<GrowthProposer>[0],
22
- proposer: GrowthProposer = createLLMGrowthProposer(), classifyRecipe?: (node: GraphNodeSpec) => GrowthRecipeEffect): Promise<{ ok: true; graph: GraphTemplateSpec; growth: GraphGrowth } | { ok: false; reason: string; park?: boolean }> {
70
+ proposer: GrowthProposer = createLLMGrowthProposer(), classifyRecipe?: (node: GraphNodeSpec) => GrowthRecipeEffect | GrowthRecipeClassification,
71
+ decisionDeps: GrowthDecisionDeps = {}): Promise<{ ok: true; graph: GraphTemplateSpec; growth: GraphGrowth } | { ok: false; reason: string; park?: boolean; growth?: GraphGrowth; decisionId?: string; decisionRef?: string; command?: GrowthCommand; approval?: string }> {
23
72
  let proposal: GrowthProposal | undefined;
24
73
  try { proposal = await proposer(input); }
25
74
  catch (error) { return { ok: false, reason: `proposer failed: ${String(error)}` }; }
@@ -35,16 +84,20 @@ export async function proposeGrowth(input: Parameters<GrowthProposer>[0],
35
84
  // Only a trusted classification of the resolved recipe may authorize execution.
36
85
  let effect: GrowthRecipeEffect = 'unknown';
37
86
  if (proposal.node.recipe !== 'none') {
38
- try { effect = classifyRecipe?.(proposal.node) ?? 'unknown'; }
39
- catch { effect = 'unknown'; }
40
- if (effect !== 'read-only') {
41
- return { ok: false, park: true, reason: '사람 확인 필요: external or unclassified side-effect recipe' };
87
+ let command: GrowthCommand | undefined;
88
+ let approval: string | undefined;
89
+ try {
90
+ const classification = classifyRecipe?.(proposal.node) ?? 'unknown';
91
+ effect = typeof classification === 'string' ? classification : classification.effect;
92
+ command = typeof classification === 'string' ? undefined : classification.command;
93
+ approval = typeof classification === 'string' ? undefined : classification.approval;
94
+ } catch { effect = 'unknown'; }
95
+ if (effect !== 'read-only' || approval) {
96
+ const decision = growthDecision(input, proposal, decisionDeps, command, approval);
97
+ return { ok: false, park: true, reason: '사람 확인 필요: external or unclassified side-effect recipe',
98
+ decisionId: decision.id, decisionRef: growthDecisionRef(input, proposal, command, approval), command, approval,
99
+ growth: proposalGrowth(input, proposal) };
42
100
  }
43
101
  }
44
- const edges: GraphEdgeSpec[] = [
45
- { from: input.nodeId, on: 'outcome', map: { [input.outcome]: proposal.node.nodeId } },
46
- { from: proposal.node.nodeId, to: proposal.returnTo as string },
47
- ];
48
- return { ok: true, graph: result.template, growth: { node: proposal.node, edges, reason: proposal.reason,
49
- undo: { removeNode: proposal.node.nodeId, removeOutcome: { from: input.nodeId, outcome: input.outcome }, removeEdge: edges[1]! } } };
102
+ return { ok: true, graph: result.template, growth: proposalGrowth(input, proposal) };
50
103
  }
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { debug } from '../debug/log.js';
5
5
  import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import { DecisionLedger } from '../decisions/decision-ledger.js';
6
7
  import { latestGraphRun, runGraph, type GraphRunOptions, type GraphRunState } from './runner.js';
7
8
 
8
9
  export interface GraphTickOptions {
@@ -35,10 +36,16 @@ export async function graphTick(path: string, options: GraphTickOptions = {}): P
35
36
  acquired = true;
36
37
  await options.deps?.notify?.();
37
38
  const latest = latestGraphRun(graphId, root);
39
+ const growthCard = latest?.growthPark
40
+ ? (options.deps?.growthDecision?.list ?? ((filters: { status: 'all' }) => new DecisionLedger({ stateDir: root }).list(filters)))({ status: 'all' })
41
+ .find(entry => entry.id === latest.growthPark?.decisionId && entry.refs?.includes(latest.growthPark.decisionRef))
42
+ : undefined;
43
+ const growthDecided = growthCard?.status === 'decided' && growthCard.decidedBy?.kind === 'human' &&
44
+ (growthCard.choice === 'a' || growthCard.choice === 'b');
38
45
  let result: GraphTickResult;
39
- if (latest?.status === 'awaiting-approval' && !latest.pending?.decision) {
46
+ if (latest?.status === 'awaiting-approval' && !latest.pending?.decision && !growthDecided) {
40
47
  result = { action: 'waiting', runId: latest.runId, status: latest.status };
41
- } else if (latest?.status === 'running' || (latest?.status === 'awaiting-approval' && latest.pending?.decision)) {
48
+ } else if (latest?.status === 'running' || (latest?.status === 'awaiting-approval' && (latest.pending?.decision || growthDecided))) {
42
49
  const state = await runGraph(path, { resumeRunId: latest.runId, deps: { ...options.deps, root } });
43
50
  result = { action: 'resumed', runId: state.runId, status: state.status };
44
51
  } else if (options.startIfIdle) {
@@ -11,7 +11,8 @@ import { listInstalledPlugins } from '../plugins/install/plugin-install.js';
11
11
  import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentials.js';
12
12
  import { effectiveInstanceRoot } from '../instance/resolve.js';
13
13
  import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
14
- import { proposeGrowth, type GraphGrowth, type GrowthProposer, type GrowthRecipeEffect } from './graph-grow.js';
14
+ import { growthApprovalRef, proposeGrowth, type GraphGrowth, type GrowthProposer, type GrowthRecipeEffect, type GrowthDecisionDeps, type GrowthCommand } from './graph-grow.js';
15
+ import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
15
16
  import { createLLMGrowthProposer } from './graph-grow-llm.js';
16
17
  import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec, type GraphTemplateSpec } from '../self-implement/graph-yaml.js';
17
18
  import { createGraphVariant, type GraphVariantPlan } from '../self-implement/graph-variant.js';
@@ -43,7 +44,7 @@ export interface GraphRunState {
43
44
  variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
44
45
  growth?: GraphGrowth[];
45
46
  growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
46
- growthPark?: { from: string; outcome: string; reason: string };
47
+ growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
47
48
  resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
48
49
  executed: number;
49
50
  dryRun: boolean;
@@ -59,7 +60,7 @@ export interface GraphRunOptions {
59
60
  resumeGraphId?: string;
60
61
  fromNodeId?: string;
61
62
  useCurrentGraph?: boolean;
62
- deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null; growthProposer?: GrowthProposer; growthLLM?: (prompt: string) => Promise<string>; classifyGrowthRecipe?: (node: GraphNodeSpec, resolved: { command?: string; approval?: string }) => GrowthRecipeEffect };
63
+ deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null; growthProposer?: GrowthProposer; growthLLM?: (prompt: string) => Promise<string>; classifyGrowthRecipe?: (node: GraphNodeSpec, resolved: { command?: string; approval?: string }) => GrowthRecipeEffect; growthDecision?: GrowthDecisionDeps & { list?: (filters: { status: 'all' }) => DecisionEntry[] } };
63
64
  }
64
65
 
65
66
  function safeSegment(value: string): string {
@@ -70,13 +71,16 @@ function safeSegment(value: string): string {
70
71
  }
71
72
 
72
73
  type CommandRecipe = { command: string; dry_run_command?: string; timeout_ms?: number };
73
- type Recipe = CommandRecipe | { approval: string };
74
+ type Recipe = CommandRecipe | ({ approval: string } & Partial<CommandRecipe>);
74
75
 
75
76
  /** 접두 없는 recipe 가 카탈로그 역할이고, 그래프 옆 recipes.yaml 에 같은 키의 `{ command }` 가 있을 때만 cmd 처럼 실행한다. */
76
77
  function roleCommand(recipe: string, recipes: Record<string, Recipe>, catalogRoles: ReadonlySet<string>): CommandRecipe | undefined {
77
78
  if (!recipe || recipe === 'none' || recipe.includes(':') || !catalogRoles.has(recipe)) return undefined;
78
79
  const entry = recipes[recipe];
79
- return entry && 'command' in entry ? entry : undefined;
80
+ // An entry that carries an approval only runs through `approval:` — never as a bare command.
81
+ return entry && !('approval' in entry) && typeof entry.command === 'string' ? { command: entry.command,
82
+ ...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
83
+ ...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } : undefined;
80
84
  }
81
85
 
82
86
  function resolveNodeRecipe(node: GraphNodeSpec, recipes: Record<string, Recipe>, catalogRoles: ReadonlySet<string>): { command?: CommandRecipe; approval?: { approval: string } } {
@@ -84,12 +88,17 @@ function resolveNodeRecipe(node: GraphNodeSpec, recipes: Record<string, Recipe>,
84
88
  if (node.recipe.startsWith('cmd:')) {
85
89
  const id = node.recipe.slice(4);
86
90
  const entry = recipes[id];
87
- return entry && 'command' in entry ? { command: entry } : {};
91
+ return entry && !('approval' in entry) && typeof entry.command === 'string' ? { command: { command: entry.command,
92
+ ...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
93
+ ...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } } : {};
88
94
  }
89
95
  if (node.recipe.startsWith('approval:')) {
90
96
  const id = node.recipe.slice(9);
91
97
  const entry = recipes[id];
92
- return entry && 'approval' in entry ? { approval: entry } : {};
98
+ return entry && 'approval' in entry ? { approval: entry,
99
+ ...('command' in entry && typeof entry.command === 'string' ? { command: { command: entry.command,
100
+ ...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
101
+ ...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } } : {}) } : {};
93
102
  }
94
103
  const command = roleCommand(node.recipe, recipes, catalogRoles);
95
104
  return command ? { command } : {};
@@ -113,8 +122,16 @@ function recipesFor(path: string, recipeSource: string): Record<string, Recipe>
113
122
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`invalid recipe: ${id}`);
114
123
  const item = raw as Record<string, unknown>;
115
124
  if (Object.hasOwn(item, 'approval')) {
116
- if (typeof item.approval !== 'string' || !item.approval.trim() || Object.keys(item).length !== 1) throw new Error(`invalid recipe: ${id}`);
117
- recipes[id] = { approval: item.approval };
125
+ if (typeof item.approval !== 'string' || !item.approval.trim() ||
126
+ (item.command !== undefined && (typeof item.command !== 'string' || !item.command.trim())) ||
127
+ (item.timeout_ms !== undefined && (!Number.isSafeInteger(item.timeout_ms) || (item.timeout_ms as number) <= 0)) ||
128
+ (item.dry_run_command !== undefined && (typeof item.dry_run_command !== 'string' || !item.dry_run_command.trim())) ||
129
+ (item.command === undefined && (item.timeout_ms !== undefined || item.dry_run_command !== undefined)) ||
130
+ Object.keys(item).some(key => !['approval', 'command', 'dry_run_command', 'timeout_ms'].includes(key))) throw new Error(`invalid recipe: ${id}`);
131
+ recipes[id] = { approval: item.approval,
132
+ ...(item.command === undefined ? {} : { command: item.command as string }),
133
+ ...(item.dry_run_command === undefined ? {} : { dry_run_command: item.dry_run_command as string }),
134
+ ...(item.timeout_ms === undefined ? {} : { timeout_ms: item.timeout_ms as number }) };
118
135
  } else {
119
136
  if (typeof item.command !== 'string' || !item.command.trim() ||
120
137
  (item.timeout_ms !== undefined && (!Number.isSafeInteger(item.timeout_ms) || (item.timeout_ms as number) <= 0)) ||
@@ -493,8 +510,51 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
493
510
  // Persist the restart boundary before executing it, so previous outputs remain durable.
494
511
  persistOwnedRun(state, saved!, true);
495
512
  }
513
+ let resumedGrowth = false;
496
514
  if (options.resumeRunId && state.growthPark) {
497
- throw new Error(`growth is parked for human confirmation: ${graphId}/${runId}`);
515
+ if (state.graphId !== graphId || state.runId !== runId || (graphMode === 'current' && state.sourceHash !== approvalSourceHash)) {
516
+ throw new Error(`growth source changed since run was paused: ${graphId}/${runId}`);
517
+ }
518
+ const parked = state.growthPark;
519
+ const decision = (options.deps?.growthDecision?.list?.({ status: 'all' }) ??
520
+ new DecisionLedger({ stateDir: root }).list({ status: 'all' }))
521
+ .find(entry => entry.id === parked.decisionId && entry.refs?.includes(parked.decisionRef));
522
+ if (!decision || decision.status !== 'decided' || decision.decidedBy?.kind !== 'human' ||
523
+ (decision.choice !== 'a' && decision.choice !== 'b')) {
524
+ throw new Error(`growth is parked for human confirmation: ${graphId}/${runId}`);
525
+ }
526
+ if (typeof parked.decisionRef !== 'string' ||
527
+ growthApprovalRef({ graphId, runId, from: parked.from, outcome: parked.outcome },
528
+ parked.growth, parked.command, parked.approval) !== parked.decisionRef) {
529
+ throw new Error(`growth approval target changed since card was raised: ${graphId}/${runId}`);
530
+ }
531
+ if (state.status !== 'awaiting-approval' || state.dryRun || state.path.at(-1) !== parked.from ||
532
+ state.path.length !== state.nodes.length || state.nodes.at(-1)?.nodeId !== parked.from ||
533
+ state.pending?.nodeId !== parked.from || state.growthRejections?.at(-1)?.from !== parked.from ||
534
+ state.growthRejections.at(-1)?.outcome !== parked.outcome ||
535
+ parked.growth.undo.removeOutcome.from !== parked.from || parked.growth.undo.removeOutcome.outcome !== parked.outcome) {
536
+ throw new Error(`invalid parked growth: ${graphId}/${runId}`);
537
+ }
538
+ if (decision.choice === 'a') {
539
+ const currentCommand = resolveNodeRecipe(parked.growth.node, recipes, catalogRoles).command;
540
+ const currentApproval = resolveNodeRecipe(parked.growth.node, recipes, catalogRoles).approval?.approval;
541
+ // The approved target is the (approval, command) pair as raised — a command appearing, vanishing or changing all fail.
542
+ if (parked.growth.node.recipe !== 'none' &&
543
+ (currentApproval !== parked.approval ||
544
+ JSON.stringify(currentCommand ?? null) !== JSON.stringify(parked.command ?? null) ||
545
+ (parked.approval === undefined && !parked.command))) {
546
+ throw new Error(`growth command changed since approval: ${graphId}/${runId}`);
547
+ }
548
+ graph = withGrowth(graph, [parked.growth]);
549
+ state.growth ??= [];
550
+ state.growth.push(parked.growth);
551
+ state.growthRejections!.pop();
552
+ }
553
+ resumedGrowth = true;
554
+ delete state.pending;
555
+ delete state.growthPark;
556
+ state.status = 'running';
557
+ persistOwnedRun(state, saved!);
498
558
  }
499
559
  if (options.resumeRunId && state.status === 'running' && state.pending) {
500
560
  throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
@@ -593,7 +653,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
593
653
  let exit: number | null = null;
594
654
  let error: string | undefined;
595
655
  let output: unknown;
596
- if (command && (!state.dryRun || command.dry_run_command) && !resumingCompleted) {
656
+ if (command && (!approval || state.dryRun || ok) && (!state.dryRun || command.dry_run_command) && !resumingCompleted) {
597
657
  const outputs: Record<string, unknown> = Object.create(null);
598
658
  // 구조 산출(마지막 JSON 줄)이 있으면 그것을, 없으면 원문을 준다 — 다음 노드가 판단을 «값»으로 받는다.
599
659
  for (const previous of state.nodes) outputs[previous.nodeId] = lastJsonObject(previous.output) ?? previous.output ?? null;
@@ -681,7 +741,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
681
741
  }
682
742
  const fromNodeId = current;
683
743
  const edge = graph.edges.find((candidate) => candidate.from === current);
684
- if (document.grow === 'on' && !state.dryRun && namedOutcome !== undefined &&
744
+ if (!resumedGrowth && document.grow === 'on' && !state.dryRun && namedOutcome !== undefined &&
685
745
  edge?.on === 'outcome' && edge.map && !Object.hasOwn(edge.map, namedOutcome) &&
686
746
  !state.growth?.some((item) => item.undo.removeOutcome.from === current && item.undo.removeOutcome.outcome === namedOutcome) &&
687
747
  !state.growthRejections?.some((item) => item.from === current && item.outcome === namedOutcome)) {
@@ -689,9 +749,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
689
749
  : await proposeGrowth({ graph, nodeId: current, outcome: namedOutcome, runId, output: state.nodes.at(-1)?.output },
690
750
  options.deps?.growthProposer ?? createLLMGrowthProposer(options.deps?.growthLLM), (candidate) => {
691
751
  const resolved = resolveNodeRecipe(candidate, recipes, catalogRoles);
692
- if (resolved.approval || (!resolved.command && candidate.recipe !== 'none')) return 'unknown';
693
- return options.deps?.classifyGrowthRecipe?.(candidate, { command: resolved.command?.command }) ?? 'unknown';
694
- });
752
+ if (!resolved.command && !resolved.approval && candidate.recipe !== 'none') return 'unknown';
753
+ return { effect: resolved.approval ? 'unknown' : options.deps?.classifyGrowthRecipe?.(candidate, { command: resolved.command?.command }) ?? 'unknown',
754
+ command: resolved.command, approval: resolved.approval?.approval };
755
+ }, { ...options.deps?.growthDecision, root });
695
756
  if (result.ok) {
696
757
  graph = result.graph;
697
758
  state.growth ??= [];
@@ -703,10 +764,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
703
764
  } else {
704
765
  state.growthRejections ??= [];
705
766
  state.growthRejections.push({ from: current, outcome: namedOutcome, reason: result.reason });
706
- if (result.park) {
767
+ if (result.park && result.growth && result.decisionId && result.decisionRef) {
707
768
  state.status = 'awaiting-approval';
708
769
  state.pending = { nodeId: current, message: result.reason, since: new Date().toISOString() };
709
- state.growthPark = { from: current, outcome: namedOutcome, reason: result.reason };
770
+ state.growthPark = { from: current, outcome: namedOutcome, reason: result.reason,
771
+ growth: result.growth, decisionId: result.decisionId, decisionRef: result.decisionRef,
772
+ ...(result.command ? { command: result.command } : {}), ...(result.approval ? { approval: result.approval } : {}) };
710
773
  persist();
711
774
  break;
712
775
  }
@@ -714,6 +777,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
714
777
  persist();
715
778
  }
716
779
  current = nextNode(graph.edges, current, state.dryRun && !command?.dry_run_command ? 'ok' : ok ? 'ok' : 'fail', namedOutcome);
780
+ resumedGrowth = false;
717
781
  if (current !== undefined) publishInsideEvent({ kind: 'edge', graphId, runId, from: fromNodeId, to: current });
718
782
  if (current === undefined) {
719
783
  state.status = 'failed';
@@ -29,6 +29,7 @@ import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
29
29
  import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
30
30
  import { getUserConfig } from '../user-config.js';
31
31
  import { installHarnessCliSinkHook } from './harness-cli-sink.js';
32
+ import { addHarnessQueue, listHarnessQueue, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps } from './harness-queue.js';
32
33
  import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
33
34
  import { runHarnessPlanRfc } from './harness-plan-rfc.js';
34
35
  import { classifyGarbage, isGarbageProcessTarget, type GarbageProcess } from './process-garbage.js';
@@ -1471,7 +1472,7 @@ export function readProcessOwnership(
1471
1472
  }
1472
1473
  }
1473
1474
 
1474
- function defaultListHarnessProcesses(): HarnessProcessListObservation {
1475
+ export function defaultListHarnessProcesses(): HarnessProcessListObservation {
1475
1476
  let out: string;
1476
1477
  try {
1477
1478
  out = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime=,command='], {
@@ -2104,6 +2105,7 @@ export interface HarnessCliCommandDeps {
2104
2105
  deliverableVerify?: InstallDeliverableVerifyCliDeps;
2105
2106
  processObservation?: HarnessProcessObservationDeps;
2106
2107
  draftSweep?: HarnessDraftSweepDeps;
2108
+ queue?: HarnessQueueDeps;
2107
2109
  ask?: HarnessAskHandler;
2108
2110
  say?: HarnessSayHandler;
2109
2111
  /** Observe-only Pod host dispatch seam. */
@@ -2156,6 +2158,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2156
2158
  installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
2157
2159
  installHarnessBudgetCommand(harnessCmd);
2158
2160
  installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
2161
+ const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
2162
+ const queueDeps = deps.queue ?? {};
2163
+ const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
2164
+ queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
2165
+ .option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
2166
+ .option('--hold', '자동 병합 금지').option('--heavy', 'Pod high 메모리')
2167
+ .action((opts: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }) => queueAction(async () => {
2168
+ const row = await addHarnessQueue(opts, queueDeps);
2169
+ console.log(`${row.id} ${row.seat} ${row.kind} queued`);
2170
+ }));
2171
+ queue.command('list').description('대기·발사 원장 조회').action(() => queueAction(async () => {
2172
+ for (const row of listHarnessQueue(queueDeps)) console.log(`${row.id} ${row.seat} ${row.status} ${row.kind} ${row.input}`);
2173
+ }));
2174
+ queue.command('remove <id>').description('대기 중 또는 종료 확인된 항목 제거').action((id: string) => queueAction(async () => {
2175
+ if (!await removeHarnessQueue(id, queueDeps)) throw new Error(`queue item active or not found: ${id}`);
2176
+ console.log(`${id} removed`);
2177
+ }));
2178
+ queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
2179
+ console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
2180
+ }));
2181
+ queue.command('tick').description('맨 앞 한 건의 자리 몫과 Pod 풀 여유를 확인하고 발사').action(() => queueAction(async () => {
2182
+ const result = await tickHarnessQueue(queueDeps);
2183
+ console.log(`${result.outcome}${result.item ? ` ${result.item.id}` : ''}: ${result.reason}`);
2184
+ }));
2159
2185
 
2160
2186
  const ask = deps.ask;
2161
2187
  if (ask) {
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { renameSync, writeFileSync } from 'node:fs';
4
+
5
+ /** Only a confirmed terminal observation allows reconciliation to release an uncertain launch. */
6
+ export function writeHarnessQueueReceipt(receiptPath: string, state: 'started' | 'finished' | 'not-started'): void {
7
+ const temp = `${receiptPath}.${randomUUID()}.tmp`;
8
+ writeFileSync(temp, JSON.stringify({ state, at: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
9
+ renameSync(temp, receiptPath);
10
+ }
11
+
12
+ export async function runHarnessQueueChild(
13
+ receiptPath: string, command: string, args: string[],
14
+ start: typeof spawn = spawn,
15
+ ): Promise<number> {
16
+ const record = (state: 'started' | 'finished' | 'not-started') => writeHarnessQueueReceipt(receiptPath, state);
17
+ let child: ReturnType<typeof spawn>;
18
+ try { child = start(process.execPath, [command, ...args], { stdio: 'inherit', env: process.env }); }
19
+ catch { record('not-started'); return 1; }
20
+ return await new Promise<number>((done) => {
21
+ let started = false;
22
+ child.once('spawn', () => {
23
+ started = true;
24
+ record('started');
25
+ child.once('exit', (code) => { record('finished'); done(code ?? 1); });
26
+ });
27
+ child.once('error', () => {
28
+ if (!started) { record('not-started'); done(1); }
29
+ });
30
+ });
31
+ }
32
+
33
+ if (import.meta.main) {
34
+ const [receiptPath, command, ...args] = process.argv.slice(2);
35
+ if (!receiptPath || !command) throw new Error('queue child requires receipt path and command');
36
+ process.exitCode = await runHarnessQueueChild(receiptPath, command, args);
37
+ }