elanous 0.2.11 → 0.2.12

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 (246) 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/1147-466ec15afacc1a65.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-ee3795b112907122.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-0259994816604c49.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-cefe8169e36cf62a.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/board/{page-cb95a7432c9ee744.js → page-d2e35228bc497119.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12b6d2f8d2d1aea.js → page-5c05b0da4bc1392c.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b9efe868bfd36255.js → page-e0ccdb576812b523.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-813c4a52c9d05ca8.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-a022b257950f39e6.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-a2ffc717e04b8b0a.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-b59ee4e70fc18fcf.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-d5dc7ff69a37b4cf.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/{page-2a9932ddb62d76bd.js → page-1d6a533ce5a6efcb.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-dfca9b7d2de0b135.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6dd3bfbbde2a5c2c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-e7a529e0507ac86c.js → not-found-490653e8d4cc8852.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-77447356baad75d2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-6d604227e1664b71.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-0be8d4b2ced75ba0.js → page-4048543b4b7ad5e0.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-4c3a3126abed551a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-64b0aecb1ca75e75.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-53f80edc6b8dc07c.js → page-db14d0be7f1c460c.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-9547b5cec7cf76bf.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-2cddeb9e2aeedc55.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-e297866051595d81.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-baba2e047872fb0f.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-e895da4ec9cac345.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
  54. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +14 -14
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +14 -14
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +14 -14
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +14 -14
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +14 -14
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +14 -14
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +14 -14
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +14 -14
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +14 -14
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +14 -14
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +14 -14
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +14 -14
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +14 -14
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +14 -14
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +14 -14
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +14 -14
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +14 -14
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +15 -15
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +14 -14
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +14 -14
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +14 -14
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +14 -14
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +14 -14
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +14 -14
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +14 -14
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +14 -14
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +14 -14
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +14 -14
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +14 -14
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +14 -14
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +14 -14
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +14 -14
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +14 -14
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +14 -14
  140. package/graphs/context/nightly-condense.yaml +21 -0
  141. package/graphs/context/recipes.yaml +12 -0
  142. package/graphs/demo/inside-seed.yaml +20 -0
  143. package/graphs/demo/recipes.yaml +12 -0
  144. package/package.json +1 -1
  145. package/plugins/card-followup/examples/sample-card.png +0 -0
  146. package/scripts/lib/telegram-inject-args.ts +31 -0
  147. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  148. package/scripts/release-loop/auto-approve-node.ts +1 -1
  149. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  150. package/scripts/release-loop/gate-node.ts +8 -2
  151. package/scripts/release-loop/ops-upgrade-node.ts +47 -6
  152. package/scripts/release-loop/publish-node.ts +4 -1
  153. package/scripts/release-loop/version-node.ts +13 -3
  154. package/scripts/telegram-inject.ts +14 -6
  155. package/scripts/test-diet/remeasure-list.ts +112 -0
  156. package/scripts/ux/demo-rehearsal.ts +17 -13
  157. package/scripts/ux/demo-seed.ts +108 -0
  158. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  159. package/src/agent-mission/driver.ts +138 -16
  160. package/src/agent-mission/pty-decision.ts +1 -1
  161. package/src/autopilot/pty-control-loop.ts +4 -1
  162. package/src/chat/index.ts +1 -0
  163. package/src/cli/persona-cli.ts +17 -14
  164. package/src/cli/pod-cli.ts +17 -0
  165. package/src/cli/pod-memory-advice.ts +35 -0
  166. package/src/cli/release-cli.ts +46 -6
  167. package/src/context-bus/context-now-surfaces.ts +17 -0
  168. package/src/context-bus/context-now.ts +2 -2
  169. package/src/context-bus/coord-events.ts +2 -2
  170. package/src/context-bus/nightly-condense.ts +153 -0
  171. package/src/context-bus/session-events.ts +35 -0
  172. package/src/dashboard/dashboard-status-lines.ts +2 -0
  173. package/src/dashboard/index.ts +8 -0
  174. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  175. package/src/domains/outbound-alert.ts +99 -31
  176. package/src/domains/outbound-log-probe.ts +8 -0
  177. package/src/execution-loop/launch-gate.ts +17 -8
  178. package/src/field-note/telegram.ts +142 -0
  179. package/src/graph-runner/graph-grow-llm.ts +23 -13
  180. package/src/graph-runner/graph-grow.ts +5 -2
  181. package/src/graph-runner/runner.ts +12 -8
  182. package/src/harness/harness-cli-command.ts +138 -16
  183. package/src/hooks/consult-intake.ts +153 -0
  184. package/src/hooks/receiver.ts +78 -1
  185. package/src/index.ts +33 -9
  186. package/src/intake-plane/discord-seat-work.ts +28 -2
  187. package/src/intake-plane/wish-card.ts +33 -0
  188. package/src/llm/difficulty.ts +1 -1
  189. package/src/maturity/feature-maturity.ts +2 -1
  190. package/src/nexus/api/http-server.ts +31 -8
  191. package/src/nexus/api/outbound-report.ts +40 -7
  192. package/src/nexus/api/personas.ts +95 -53
  193. package/src/nexus/api/task-cards-api.ts +19 -0
  194. package/src/nexus/api/workflow-history.ts +88 -0
  195. package/src/nexus/api/workflows.ts +52 -0
  196. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  197. package/src/nexus/outbound/router.ts +8 -3
  198. package/src/persona/presets.ts +8 -1
  199. package/src/plugins/maker/market-bundle.ts +2 -1
  200. package/src/release-loop/checklist.ts +15 -3
  201. package/src/release-loop/feature-store.ts +24 -10
  202. package/src/release-loop/landed-but-yellow.ts +48 -0
  203. package/src/seat-loop/seat-loop.ts +168 -7
  204. package/src/self-dev/draft-sweep.ts +44 -10
  205. package/src/self-dev/draft-triage-rules.ts +54 -2
  206. package/src/self-implement/budget-gate.ts +29 -12
  207. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  208. package/src/self-implement/gate-scope.ts +9 -8
  209. package/src/self-implement/headless-elanous-driver.ts +7 -2
  210. package/src/self-implement/orchestrator.ts +15 -0
  211. package/src/self-implement/test-seams.ts +1 -0
  212. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  213. package/src/task-orchestrator/surfaces/self-implement-pod.ts +8 -0
  214. package/src/telegram-commands.ts +21 -0
  215. package/src/telegram-project-command.ts +78 -4
  216. package/src/telegram.ts +32 -5
  217. package/src/user-config.ts +8 -4
  218. package/src/version/packed-revision.json +1 -1
  219. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  220. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  221. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  244. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  245. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_buildManifest.js +0 -0
  246. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
@@ -0,0 +1,108 @@
1
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ export type SeedArgs = { only: 'graph' | 'wizard' | 'all'; dryRun: boolean; json: boolean };
5
+ export type SeedEvent = { target: 'graph' | 'wizard'; phase: 'start' | 'finish' | 'failure'; command: string[]; exitCode?: number; output?: string };
6
+
7
+ export const SCENE_SIX_GUIDANCE = '⑥ PTY 인텔리전스는 이 시드로 생성하지 않습니다. 실제 격리 미션을 실행하고 read → judge → answer 이벤트를 관측하세요.';
8
+ const GRAPH = 'graphs/demo/inside-seed.yaml';
9
+ const WIZARD_REQUEST = '데모용 자료 조사 커넥터와 스킬·그래프 초안을 만들어 주세요. 설치하거나 실행하지 마세요.';
10
+ const DRAFT = { description: 'Demo research draft only; no connector credentials or outbound messages.', connectors: [],
11
+ skill: { description: 'Demo research draft', instructions: 'Research draft only. Implement and validate processing before installing or running.', requires: [] } };
12
+
13
+ export function parseArgs(argv: string[]): SeedArgs {
14
+ const result: SeedArgs = { only: 'all', dryRun: false, json: false };
15
+ const seen = new Set<string>();
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const flag = argv[i]!;
18
+ if (!['--only', '--dry-run', '--json'].includes(flag)) throw new Error(`알 수 없는 인자: ${flag}`);
19
+ if (seen.has(flag)) throw new Error(`중복 인자: ${flag}`);
20
+ seen.add(flag);
21
+ if (flag === '--only') {
22
+ const value = argv[++i];
23
+ if (value !== 'all' && value !== 'graph' && value !== 'wizard') throw new Error('--only 값은 graph|wizard|all 이어야 합니다');
24
+ result.only = value;
25
+ } else if (flag === '--dry-run') result.dryRun = true;
26
+ else result.json = true;
27
+ }
28
+ return result;
29
+ }
30
+
31
+ export type SeedDeps = {
32
+ repo: string;
33
+ run: (command: string[], cwd: string) => Promise<{ exitCode: number; output: string; stdout?: string }>;
34
+ prepareDraft: (repo: string) => { file: string; parent: string };
35
+ };
36
+
37
+ function prepareDraft(repo: string): { file: string; parent: string } {
38
+ const root = resolve(repo, '.elanous-test', 'demo-seed');
39
+ mkdirSync(root, { recursive: true });
40
+ const parent = mkdtempSync(join(root, 'wizard-'));
41
+ const file = join(parent, 'research-draft.json');
42
+ writeFileSync(file, JSON.stringify(DRAFT) + '\n');
43
+ return { file, parent };
44
+ }
45
+
46
+ const defaultDeps: SeedDeps = {
47
+ repo: resolve(import.meta.dir, '../..'),
48
+ prepareDraft,
49
+ run: async (command, cwd) => {
50
+ const child = Bun.spawn(command, { cwd, env: { ...process.env, NODE_ENV: 'test' }, stdout: 'pipe', stderr: 'pipe' });
51
+ const [output, errors, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
52
+ return { exitCode, stdout: output, output: [output, errors].filter(Boolean).join('\n').trim() };
53
+ },
54
+ };
55
+
56
+ export async function seed(args: SeedArgs, deps: SeedDeps = defaultDeps, onEvent?: (event: SeedEvent) => void): Promise<{ events: SeedEvent[]; guidance: string; ok: boolean }> {
57
+ const events: SeedEvent[] = [];
58
+ const emit = (event: SeedEvent): void => { events.push(event); onEvent?.(event); };
59
+ let ok = true;
60
+ for (const target of (args.only === 'all' ? ['graph', 'wizard'] : [args.only]) as Array<'graph' | 'wizard'>) {
61
+ const base = ['bun', 'bin/elanous.mjs', '--test'];
62
+ const graphCommand = [...base, 'graph', 'run', GRAPH, '--json'];
63
+ const wizardCommand = [...base, 'plugin', 'make', WIZARD_REQUEST, '--name', 'inside-demo-draft', '--draft-file', '<isolated-draft-file>', '--dir', '<isolated-parent>', '--json'];
64
+ const command = target === 'graph' ? graphCommand : wizardCommand;
65
+ if (args.dryRun) {
66
+ emit({ target, phase: 'start', command });
67
+ emit({ target, phase: 'finish', command, exitCode: 0, output: 'dry-run: no process started' });
68
+ continue;
69
+ }
70
+ try {
71
+ if (target === 'wizard') {
72
+ const { file, parent } = deps.prepareDraft(deps.repo);
73
+ command.splice(command.indexOf('<isolated-draft-file>'), 1, file);
74
+ command.splice(command.indexOf('<isolated-parent>'), 1, parent);
75
+ }
76
+ emit({ target, phase: 'start', command });
77
+ const result = await deps.run(command, deps.repo);
78
+ const parsed = (() => {
79
+ try { return JSON.parse(result.stdout ?? result.output) as { status?: string; dryRun?: boolean; errors?: string[]; timings?: { install?: number } }; } catch { return null; }
80
+ })();
81
+ const complete = target === 'wizard'
82
+ ? parsed?.status === 'draft' && parsed.timings?.install === 0 && parsed.errors?.length === 0
83
+ : parsed?.status === 'done' && parsed.dryRun === false;
84
+ const phase = result.exitCode === 0 && complete ? 'finish' : 'failure';
85
+ emit({ target, phase, command, exitCode: result.exitCode, output: result.output });
86
+ if (phase === 'failure') ok = false;
87
+ } catch (error) {
88
+ ok = false;
89
+ emit({ target, phase: 'failure', command, output: error instanceof Error ? error.message : String(error) });
90
+ }
91
+ }
92
+ return { events, guidance: SCENE_SIX_GUIDANCE, ok };
93
+ }
94
+
95
+ if (import.meta.main) {
96
+ try {
97
+ const args = parseArgs(process.argv.slice(2));
98
+ const report = await seed(args, defaultDeps, args.json ? undefined : (event) => {
99
+ console.log(`${event.target} ${event.phase}${event.exitCode === undefined ? '' : ` (exit ${event.exitCode})`}: ${event.command.join(' ')}${event.output ? `\n${event.output}` : ''}`);
100
+ });
101
+ if (args.json) console.log(JSON.stringify(report));
102
+ else console.log(report.guidance);
103
+ if (!report.ok) process.exitCode = 1;
104
+ } catch (error) {
105
+ console.error(error instanceof Error ? error.message : String(error));
106
+ process.exitCode = 1;
107
+ }
108
+ }
@@ -10,6 +10,8 @@ export type SceneObservation = {
10
10
  leaks: string[];
11
11
  sectionsInDom: number;
12
12
  visibleScene: number | null;
13
+ /** Empty-state phrases observed in the visible scene. */
14
+ emptyStates?: string[];
13
15
  /** Number of captured JPEG frames with no discernible scene content. */
14
16
  blankFrames?: number;
15
17
  };
@@ -36,7 +38,7 @@ export function failedApiResponse(url: string, status: number): string | null {
36
38
  } catch { return null; }
37
39
  }
38
40
 
39
- export type SceneResult = SceneObservation & { verdict: 'ok' | 'broken' | 'unverified'; reasons: string[] };
41
+ export type SceneResult = SceneObservation & { verdict: 'ok' | 'broken' | 'unverified' | 'no-data'; reasons: string[] };
40
42
 
41
43
  /** A scene the demo hides is not a pass — it was not seen. OP 10-03: ⑤ (wizard → market) is the 10-08 highlight. */
42
44
  export const UNVERIFIED_REASON = '시연 모드에서 숨긴 장면 — WIZ1 착지 뒤 실물 필요';
@@ -58,16 +60,21 @@ export function judgeScene(obs: SceneObservation): SceneResult {
58
60
  if (obs.sectionsInDom < 6) reasons.push(`장면 언마운트 (${obs.sectionsInDom}/6)`);
59
61
  if (obs.visibleScene !== obs.scene) reasons.push(`보이는 장면 불일치 (기대 ${obs.scene}, 실제 ${obs.visibleScene ?? '없음'})`);
60
62
  if (obs.leaks.length) reasons.push(`공개 화면 누설 ${obs.leaks.length}건`);
61
- return { ...obs, verdict: reasons.length ? 'broken' : 'ok', reasons };
63
+ if (reasons.length) return { ...obs, verdict: 'broken', reasons };
64
+ if (obs.emptyStates?.length) return { ...obs, verdict: 'no-data', reasons: obs.emptyStates.map((phrase) => `실데이터 없음 — ${phrase}`) };
65
+ return { ...obs, verdict: 'ok', reasons };
62
66
  }
63
67
 
64
68
  export function judgeRun(scenes: SceneResult[]) {
65
69
  const ok = scenes.filter((scene) => scene.verdict === 'ok').length;
66
70
  const broken = scenes.filter((scene) => scene.verdict === 'broken').length;
67
71
  const unverified = scenes.filter((scene) => scene.verdict === 'unverified').length;
68
- // broken wins; otherwise any unseen scene keeps the whole run from reading as a pass.
69
- const verdict = broken ? 'broken' as const : unverified ? 'unverified' as const : 'ok' as const;
72
+ const noData = scenes.filter((scene) => scene.verdict === 'no-data').length;
73
+ // Broken wins, then unseen scenes, then scenes without real data; none is a pass.
74
+ const verdict = broken ? 'broken' as const : unverified ? 'unverified' as const : noData ? 'no-data' as const : 'ok' as const;
70
75
  const banner = scenes.filter((scene) => scene.verdict === 'unverified')
71
76
  .map((scene) => `${CIRCLED[scene.scene - 1] ?? scene.scene} 미검증 — WIZ1 착지 뒤 실물 필요`);
72
- return { verdict, ok, broken, unverified, banner, scenes };
77
+ const noDataScenes = scenes.filter((scene) => scene.verdict === 'no-data').map((scene) => CIRCLED[scene.scene - 1] ?? String(scene.scene));
78
+ if (noDataScenes.length) banner.push(`${noDataScenes.join('')} 실데이터 없음 — 런·PTY·마법사가 도는 판에서 다시`);
79
+ return { verdict, ok, broken, unverified, noData, banner, scenes };
73
80
  }
@@ -22,8 +22,10 @@ import { streamLLM, type LLMMessage } from '../llm.js';
22
22
  import { debug } from '../debug/log.js';
23
23
  import { emitPtyDecision, type DecisionInput } from './pty-decision.js';
24
24
  import { observeExternalEvent } from '../context-bus/external-events.js';
25
+ import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
25
26
  import { emitDecision } from '../live/detail-switch.js';
26
27
  import { resolvePtyWebAddress } from '../cli/pty-web-address.js';
28
+ import { verdictForFinalPtyScreen } from '../cli/pty-drive-verdict.js';
27
29
  import { reemitPtyUsage } from '../budget/pty-usage-reemit.js';
28
30
  import { classifyAuthError } from '../oauth/codex.js';
29
31
  import type { FallbackStep } from '../oauth/fallback-chain.js';
@@ -451,6 +453,8 @@ export interface AgentMissionSpec {
451
453
  evidence: EvidenceMode;
452
454
  /** RFC 최대 라운드(기본 16). */
453
455
  maxRounds?: number;
456
+ /** 화면 무변화 회복 문턱(ms, 기본 45초). */
457
+ recoverAfterMs?: number;
454
458
  /** 완료 시 자동 commit(기본 true). */
455
459
  commit?: boolean;
456
460
  /** 스크린 캡처 저장 디렉토리. */
@@ -538,6 +542,12 @@ export interface AgentMissionDeps {
538
542
  checkEvidence?: typeof checkEvidence;
539
543
  commitWorktree?: typeof commitWorktree;
540
544
  dispatchOpenPullRequest?: typeof dispatchOpenPullRequest;
545
+ /** Optional context-journal sink for isolated missions. */
546
+ emitContextEvent?: (input: SessionEventInput) => void;
547
+ /** Internal continuation retains the original mission's context-event owner. */
548
+ contextEventRun?: boolean;
549
+ /** Carries a PR number back to the outer mission's context event. */
550
+ onMissionPrNumber?: (number: number) => void;
541
551
  terminateProcessTree?: typeof terminateMissionProcessTree;
542
552
  repoStatus?: (repo: string) => string | Promise<string>;
543
553
  emitDecision?: typeof emitDecision;
@@ -835,18 +845,36 @@ export interface MissionControlBrainOpts {
835
845
  /** Screen-grounded brain summary for the next backend, never the raw PTY screen. */
836
846
  readonly onSummary?: (summary: string) => void;
837
847
  readonly onDecision?: (text: string, reply?: { question: string; answer: string }) => void;
848
+ readonly recoverAfterMs?: number;
849
+ readonly initialCommand?: string;
850
+ readonly onRecover?: (blocked: string, action: string) => void;
838
851
  }
839
852
 
840
853
  /** 기존 5-action mission 판단을 canonical PTY control-loop 결정으로 축소한다. search는 이 경계에서만
841
854
  * 부작용(context 기록)을 끝내고, child에는 context를 읽고 계속하라는 단일 input만 전달한다. */
842
- export function createMissionControlBrain(opts: MissionControlBrainOpts): RunSupervisor {
855
+ export type MissionControlBrain = RunSupervisor & {
856
+ /** Report whether the last returned input was accepted by the PTY; recovery stages and events commit only on acceptance. */
857
+ readonly onInputResult: (text: string, accepted: boolean) => void;
858
+ };
859
+
860
+ export function createMissionControlBrain(opts: MissionControlBrainOpts): MissionControlBrain {
843
861
  const history: string[] = [];
844
862
  const backendName = (opts.backend ?? codexBackend).name;
845
863
  // ★ P4 dedup(제한적 재시도·리뷰) — (layer:spec)별 시도횟수. 성공 시 영구 차단, 실패는 최대 2회까지
846
864
  // 허용(일시 네트워크/프로세스 오류는 재시도, 지속 실패는 루프 차단).
847
865
  const provisionAttempts = new Map<string, number>();
848
866
  const PROVISION_MAX_ATTEMPTS = 2;
849
- return createLlmControlBrain({
867
+ const recoverAfterMs = opts.recoverAfterMs ?? 45_000;
868
+ if (!Number.isFinite(recoverAfterMs) || recoverAfterMs <= 0) throw new Error('recoverAfterMs must be positive');
869
+ let lastScreen: string | undefined;
870
+ let repeatedQuestion = 0;
871
+ let lastQuestion: string | undefined;
872
+ let recoveryStage = 0;
873
+ let pendingReplayBlocked: string | undefined;
874
+ // The recovery input returned by the last decide(); committed (stage advance + event) only when the PTY accepts it.
875
+ let pendingRecovery: { blocked: string; action: string; text: string; replay?: boolean } | undefined;
876
+ let lastCommand = opts.initialCommand ?? opts.mission;
877
+ const llmBrain = createLlmControlBrain({
850
878
  goal: opts.mission,
851
879
  stream: opts.stream,
852
880
  maxTokens: 500,
@@ -887,7 +915,10 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
887
915
  if (decision.reason && decision.action !== 'ask-human') opts.onSummary?.(decision.reason.slice(0, 2000));
888
916
  if (decision.action === 'handoff' && decision.to && decision.mission) return { action: 'handoff', to: decision.to, mission: decision.mission, ...(decision.carry ? { carry: decision.carry } : {}) };
889
917
  if (decision.action === 'ask-human') return { action: 'ask-human', reason: decision.reason, ...(decision.url ? { url: decision.url } : {}), ...(decision.code ? { code: decision.code } : {}) };
890
- if (decision.action === 'send') return { action: 'input', text: `${decision.text || 'continue'}\r` };
918
+ if (decision.action === 'send') {
919
+ lastCommand = decision.text || 'continue';
920
+ return { action: 'input', text: `${lastCommand}\r` };
921
+ }
891
922
  if (decision.action === 'verify' || decision.action === 'done') return { action: 'done', reason: decision.reason || 'agent completed' };
892
923
  if (decision.action === 'search') {
893
924
  await opts.search(decision.query || opts.mission.slice(0, 120), obs.step);
@@ -922,6 +953,57 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
922
953
  return { action: 'wait' };
923
954
  },
924
955
  });
956
+ return {
957
+ onInputResult: (text, accepted) => {
958
+ const recovery = pendingRecovery;
959
+ // Only the input this brain returned settles its recovery step (a trust answer in between does not).
960
+ if (!recovery || recovery.text !== text) return;
961
+ pendingRecovery = undefined;
962
+ if (!accepted) return;
963
+ if (recovery.replay) { pendingReplayBlocked = undefined; opts.onRecover?.(recovery.blocked, recovery.action); return; }
964
+ recoveryStage += 1;
965
+ if (recovery.action === 'Ctrl-C and replay previous command') pendingReplayBlocked = recovery.blocked;
966
+ else opts.onRecover?.(recovery.blocked, recovery.action);
967
+ },
968
+ decide: async (obs, signal) => {
969
+ pendingRecovery = undefined;
970
+ if (signal?.aborted) return { action: 'wait' };
971
+ const lines = classifierFrameLines(obs.screen);
972
+ // Only the active bottom line can be a repeated prompt; a question left in
973
+ // scrollback while new work appears is not a request for input.
974
+ const tail = lines.at(-1);
975
+ const question = tail && /\?(?:\s*\[[^\]\r\n]+\])?\s*$/.test(tail) ? tail : undefined;
976
+ const screenChanged = lastScreen !== undefined && obs.screen !== lastScreen;
977
+ if (screenChanged) recoveryStage = 0;
978
+ repeatedQuestion = question ? (!screenChanged && question === lastQuestion ? repeatedQuestion + 1 : 1) : 0;
979
+ lastQuestion = question;
980
+ lastScreen = obs.screen;
981
+ if (pendingReplayBlocked !== undefined) {
982
+ // Ctrl-C consumes its own step; replay on the next observation even when the
983
+ // interrupted child keeps redrawing a non-prompt screen. A rejected replay is retried.
984
+ const text = `${lastCommand}\r`;
985
+ pendingRecovery = { blocked: pendingReplayBlocked, action: 'Ctrl-C and replay previous command', text, replay: true };
986
+ return { action: 'input', text };
987
+ }
988
+ const blocked = repeatedQuestion >= 3 ? `same question repeated three times: ${question}`
989
+ : (obs.sameScreenMs ?? 0) >= recoverAfterMs ? `screen unchanged for ${obs.sameScreenMs}ms` : undefined;
990
+ if (blocked && opts.evidenceReady()) return { action: 'done', reason: 'Evidence ready on stalled screen' };
991
+ if (blocked && !signal?.aborted) {
992
+ const action = ['Enter', 'Esc', 'Ctrl-C and replay previous command', 'Human needed'] as const;
993
+ const next = action[Math.min(recoveryStage, 3)]!;
994
+ if (next !== 'Human needed') {
995
+ const text = next === 'Enter' ? '\r' : next === 'Esc' ? '\x1b' : '\x03';
996
+ pendingRecovery = { blocked, action: next, text };
997
+ return { action: 'input', text };
998
+ }
999
+ recoveryStage += 1;
1000
+ opts.onRecover?.(blocked, next);
1001
+ const summary = lines.slice(-3).join(' | ').slice(0, 240) || '(empty screen)';
1002
+ return { action: 'no-progress', terminal: true, reason: `사람 필요 · 마지막 화면 요약: ${summary}` };
1003
+ }
1004
+ return llmBrain.decide(obs, signal);
1005
+ },
1006
+ };
925
1007
  }
926
1008
 
927
1009
  /**
@@ -1273,6 +1355,21 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1273
1355
  } finally { signal?.removeEventListener('abort', stopWaiting); }
1274
1356
  }
1275
1357
  const startedAt = Date.now();
1358
+ const contextOwner = !deps.contextEventRun;
1359
+ const contextRunId = contextOwner ? ensureRunId() : undefined;
1360
+ let contextPrNumber: number | undefined;
1361
+ const emitContext = (kind: 'task-claimed' | 'task-done', detail: string): void => {
1362
+ if (!contextOwner) return;
1363
+ try {
1364
+ (deps.emitContextEvent ?? emitSessionEvent)({ kind, seat: 'TC', text: `Agent mission ${detail}: ${spec.mission.split(/\r?\n/, 1)[0]}`,
1365
+ ref: `runId=${contextRunId}${contextPrNumber === undefined ? '' : ` PR=#${contextPrNumber}`}` });
1366
+ } catch (error) {
1367
+ try { debug.log('agent-mission', 'context-event-failed', { kind, runId: contextRunId, error: String(error) }, { level: 'warn' }); } catch { /* fail-soft */ }
1368
+ }
1369
+ };
1370
+ emitContext('task-claimed', 'claimed');
1371
+ let result: AgentMissionResult | undefined;
1372
+ try {
1276
1373
  const initialFiles = workdir && spec.evidence.kind === 'doc' ? initialEvidenceFiles(workdir, spec.evidence) : undefined;
1277
1374
  let handle: PtyHandle | undefined;
1278
1375
  let childPid: number | undefined;
@@ -1306,10 +1403,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1306
1403
  });
1307
1404
  signal?.addEventListener('abort', abort, { once: true });
1308
1405
  if (signal?.aborted) abort();
1309
- let result: AgentMissionResult;
1310
1406
  try {
1311
1407
  if (cancelled) await aborted;
1312
- const body = runAgentMissionBody(spec, { ...deps, onControlActive: (pending) => {
1408
+ const body = runAgentMissionBody(spec, { ...deps, contextEventRun: true, onMissionPrNumber: (number) => {
1409
+ contextPrNumber = number;
1410
+ try { deps.onMissionPrNumber?.(number); } catch { /* context propagation never changes a mission result */ }
1411
+ }, onControlActive: (pending) => {
1313
1412
  activeSettled = false;
1314
1413
  activeControl = pending.then(() => { activeSettled = true; }, () => { activeSettled = true; });
1315
1414
  }, onControlPending: (pending) => {
@@ -1377,10 +1476,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1377
1476
  if (paths.length) {
1378
1477
  debug.log('agent-mission', 'outside-write', { workdir, pids: [], reason: paths.join(', ') });
1379
1478
  (deps.emitDecision ?? emitDecision)({ kind: 'ESCALATE', what: '작업 폴더 밖 쓰기', reason: paths.join(', '), purpose: '사람이 확인한다', target: 'human' });
1380
- return { ...result, ok: false, reason: 'outside-write', paths, committed: false };
1479
+ result = { ...result, ok: false, reason: 'outside-write', paths, committed: false };
1480
+ return result;
1381
1481
  }
1382
1482
  }
1383
1483
  return result;
1484
+ } finally { emitContext('task-done', result?.ok ? 'completed' : 'ended'); }
1384
1485
  }
1385
1486
 
1386
1487
  async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDeps, startedAt: number, initialFiles?: ReadonlyMap<string, string>): Promise<AgentMissionResult> {
@@ -1779,6 +1880,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1779
1880
  if (deps.isMissionAborted?.()) throw new Error('MISSION_ABORTED');
1780
1881
  // 미션 전송 — 인핸싱/기억으로 가공됐으면(멀티라인) 파일로 떨궈 read 지시(TUI 멀티라인 위험·verbatim 보존).
1781
1882
  // 원문 그대로면(단문) 타이핑.
1883
+ let submittedCommand = spec.mission;
1884
+ const submittedInputs: string[] = [];
1782
1885
  if (missionText !== spec.mission) {
1783
1886
  const pf = join(wt.path, '.mission-prompt.md');
1784
1887
  try { writeFileSync(pf, missionText); } catch { /* noop */ }
@@ -1786,9 +1889,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1786
1889
  ? '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라. [elanous 인핸싱]의 커버리지 체크리스트 모든 항목을 산출물에 빠짐없이 반영하고(요약·일반화 금지), 완료하면 MISSION-COMPLETE 라고 답하라.'
1787
1890
  : '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라(원문 그대로·[elanous 기억]은 참조 컨텍스트). 완료하면 MISSION-COMPLETE 라고 답하라.';
1788
1891
  debug.log('agent-mission', 'mission-send', { via: 'file', file: '.mission-prompt.md', chars: missionText.length, enhanced: enhanceActive });
1892
+ submittedCommand = note;
1893
+ submittedInputs.push(note);
1789
1894
  drive(note); await sleep(800); drive('\r');
1790
1895
  } else {
1791
1896
  debug.log('agent-mission', 'mission-send', { via: 'type', chars: spec.mission.length });
1897
+ submittedInputs.push(spec.mission);
1792
1898
  drive(spec.mission); await sleep(800); drive('\r');
1793
1899
  }
1794
1900
 
@@ -1803,6 +1909,9 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1803
1909
  backend,
1804
1910
  stream: deps.controlStream,
1805
1911
  handoffEnabled: true,
1912
+ recoverAfterMs: spec.recoverAfterMs,
1913
+ initialCommand: submittedCommand,
1914
+ onRecover: (blocked, action) => { decisionEvent({ step: 'recover', text: action, detail: { blocked, action } }); },
1806
1915
  onSummary: (summary) => { controllerSummary = summary; },
1807
1916
  onDecision: (text, reply) => {
1808
1917
  pendingAnswer = reply ? { text: `${reply.answer || 'continue'}\r`, ...reply } : undefined;
@@ -1824,6 +1933,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1824
1933
  // before asking the LLM — otherwise the mission text is typed into the prompt and the run stalls.
1825
1934
  const trustAwareBrain: RunSupervisor = {
1826
1935
  decide: (obs, signal) => {
1936
+ decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` });
1827
1937
  pendingAnswer = undefined;
1828
1938
  let answer: string | undefined;
1829
1939
  if (backend.handleTrust?.(obs.screen, (bytes) => { answer = bytes; }) && answer !== undefined) {
@@ -1889,7 +1999,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1889
1999
  handoffPrSkipped = true;
1890
2000
  debug.log('agent-mission', 'handoff-pr-skipped', { reason: 'origin-not-github' });
1891
2001
  } else {
1892
- (deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
2002
+ const opened = (deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
2003
+ if (opened.number !== null) deps.onMissionPrNumber?.(opened.number);
1893
2004
  }
1894
2005
  gatedHandoff = true;
1895
2006
  },
@@ -1916,7 +2027,6 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1916
2027
  renderPng: () => h.renderScreenPng(),
1917
2028
  bus,
1918
2029
  ident: { ptyId: h.id, instance: resolveInstanceName(), runId },
1919
- onPublishedFrame: (obs) => decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` }),
1920
2030
  });
1921
2031
  debug.log('agent-mission', 'takeover-wait', { id: h.id, maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS });
1922
2032
  const controlRun = runWithControlObserve(
@@ -1932,7 +2042,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1932
2042
  pendingAnswer = undefined;
1933
2043
  const accepted = inject(text);
1934
2044
  if (accepted) {
2045
+ submittedInputs.push(text);
1935
2046
  decisionEvent({ step: 'input', text: `Sent ${text.startsWith('\x1b') ? 'key' : 'text'}: ${text.startsWith('\x1b') ? 'navigation' : text}` });
2047
+ }
2048
+ // Recovery stages and recover events are committed only after the PTY accepted the input.
2049
+ brain.onInputResult(text, accepted);
2050
+ if (accepted) {
1936
2051
  if (reply) decisionEvent({ step: 'answer', text: 'Answered agent question', detail: { question: reply.question, answer: reply.answer } });
1937
2052
  }
1938
2053
  return accepted;
@@ -1948,6 +2063,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1948
2063
  } : {}),
1949
2064
  awaitOwnership: { maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS },
1950
2065
  settle: async () => {
2066
+ // A frozen screen goes quiet after 6 s, so stall recovery is still observed; work that keeps
2067
+ // drawing output keeps the original 360 s settle and round budget.
1951
2068
  const quiet = waitForQuiet(h, 6000, 360000);
1952
2069
  await (deps.awaitMission ? deps.awaitMission(quiet) : quiet);
1953
2070
  },
@@ -1986,7 +2103,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1986
2103
  throw error;
1987
2104
  }
1988
2105
  },
1989
- }, { maxSteps: maxRounds });
2106
+ }, { maxSteps: maxRounds, stuckLimit: maxRounds + 1 });
1990
2107
  deps.onControlActive?.(pending);
1991
2108
  return pending;
1992
2109
  },
@@ -2079,24 +2196,29 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2079
2196
  if (deps.isMissionAborted?.()) return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
2080
2197
  rounds: round, evidencePath: null, committed: false, usedOmniCrawl: usedOmni, detail: 'aborted' };
2081
2198
  const finalEv = verifyEvidence(wt.path, spec.evidence);
2082
- const evidencePath = finalEv.ok ? finalEv.path : null;
2199
+ const finalScreen = await h.renderScreen();
2200
+ const driveVerdict = verdictForFinalPtyScreen({ screen: finalScreen, inputHistory: submittedInputs, exitCode: h.exitCode ?? null });
2201
+ debug.log('agent-mission', 'drive-verdict', { kind: driveVerdict.kind, reason: driveVerdict.reason.slice(0, 200) });
2202
+ const recoverExhausted = control.termination.kind === 'stuck' && control.termination.reason.startsWith('사람 필요');
2203
+ const accepted = finalEv.ok && !recoverExhausted && driveVerdict.kind !== 'done-but-failed';
2204
+ const evidencePath = accepted ? finalEv.path : null;
2083
2205
  let committed = false;
2084
- if (!deps.isMissionAborted?.() && !spec.workdir && finalEv.ok && (spec.commit ?? true)) {
2206
+ if (!deps.isMissionAborted?.() && !spec.workdir && accepted && (spec.commit ?? true)) {
2085
2207
  const c = (deps.commitWorktree ?? commitWorktree)(wt.path, `chore(agent-mission): ${spec.branch} — ${backend.name}-in-elanous PTY RFC 산출`);
2086
2208
  committed = c.ok;
2087
2209
  debug.log('agent-mission', 'commit', { ok: c.ok, out: c.out.slice(0, 120) });
2088
2210
  }
2089
- debug.log('agent-mission', 'result', { ok: finalEv.ok, rounds: round, evidencePath, committed, usedOmni });
2090
- decisionEvent({ step: 'done', text: finalEv.ok ? 'Mission evidence accepted' : 'Mission ended without sufficient evidence', detail: { result: { kind: evidencePath ? 'file' : 'text', ref: evidencePath ?? (control.termination.kind === 'error' ? control.termination.message : 'evidence unavailable') } } });
2091
- stopLive(finalEv.ok);
2211
+ debug.log('agent-mission', 'result', { ok: accepted, rounds: round, evidencePath, committed, usedOmni });
2212
+ decisionEvent({ step: 'done', text: accepted ? 'Mission evidence accepted' : 'Mission ended without sufficient evidence', detail: { result: { kind: evidencePath ? 'file' : 'text', ref: evidencePath ?? (control.termination.kind === 'error' ? control.termination.message : recoverExhausted ? control.termination.reason : 'evidence unavailable') } } });
2213
+ stopLive(accepted);
2092
2214
  if (h.pid) await terminateMissionProcessTree(h.pid);
2093
2215
  try { h.kill(); } catch { /* noop */ }
2094
2216
 
2095
2217
  return {
2096
- ok: finalEv.ok, worktree: wt.path, branch: wt.branch, rounds: round,
2218
+ ok: accepted, worktree: wt.path, branch: wt.branch, rounds: round,
2097
2219
  evidencePath, committed, usedOmniCrawl: usedOmni,
2098
2220
  ptyId: h.id, webUrl: webAddress.webUrl,
2099
- detail: done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2221
+ detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2100
2222
  };
2101
2223
  } finally { stopLive(); finishContext(false); }
2102
2224
  }
@@ -41,7 +41,7 @@ function safe(value: string): string {
41
41
  }
42
42
 
43
43
  /** The only emission point for the inside-PTY decision stream. Never log unsanitized input. */
44
- export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = debug.log): PtyDecision {
44
+ export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = (category, event, data) => debug.log(category, event, data)): PtyDecision {
45
45
  let stream = missionStreams.get(input.missionId);
46
46
  if (!stream) {
47
47
  stream = { alias: `mission-${missionStreams.size + 1}`, seq: -1 };
@@ -88,7 +88,7 @@ export type ControlDecision =
88
88
  | { readonly action: 'handoff'; readonly to: 'codex' | 'claude' | 'elanous'; readonly mission: string; readonly carry?: 'diff' | 'summary' }
89
89
  | { readonly action: 'ask-human'; readonly reason: string; readonly url?: string; readonly code?: string }
90
90
  /** `no-progress` reports observed lack of progress without claiming completion. */
91
- | { readonly action: 'no-progress'; readonly reason: string };
91
+ | { readonly action: 'no-progress'; readonly reason: string; readonly terminal?: boolean };
92
92
 
93
93
  export interface RunSupervisor {
94
94
  /** run supervisor의 관측 → 다음 행동. 동기/비동기 모두 허용(LLM 은 async). `signal` 이 abort 되면(사람 takeover
@@ -664,6 +664,9 @@ export async function runPtyControlLoop(
664
664
  previousIntervention = { ...intervention, sameScreenCount: 1 };
665
665
  }
666
666
  } else if (decision.action === 'wait' || decision.action === 'no-progress') {
667
+ if (decision.action === 'no-progress' && decision.terminal) {
668
+ return finish({ kind: 'stuck', iteration: step, reason: decision.reason }, step, intervention, true);
669
+ }
667
670
  // wait/no-progress — 종료된 자식은 새 출력을 낼 수 없으므로 대기 없이 즉시 수렴한다.
668
671
  if (deps.isAlive?.() === false) {
669
672
  debug.log('autopilot.control', 'child-exited', { step });
package/src/chat/index.ts CHANGED
@@ -438,6 +438,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
438
438
  { name: 'clear', aliases: ['cls'], description: '대화 기록 지우기' },
439
439
  { name: 'status', aliases: ['st'], description: '지금 상태 — 모델 · 연결 · 계정' },
440
440
  { name: 'now', aliases: [], description: '현재 맥락과 출처 표 — /now [주제]' },
441
+ { name: 'wish', aliases: [], description: '소원을 카드로 남기기 — /wish <소원 한 줄>' },
441
442
  { name: 'remaining', aliases: [], description: '계정별 남은 사용량' },
442
443
  { name: 'setup', aliases: [], description: '상세 설정 — /setup · /setup reset(다음 실행 때 마법사 다시)', subcommands: ['help', 'reset'] },
443
444
  { name: 'quit', aliases: ['q', 'exit'], description: '나가기' },
@@ -7,7 +7,7 @@ import { parse as parseYaml } from 'yaml';
7
7
  import { debug } from '../debug/log.js';
8
8
  import { parsePersonaYaml } from '../persona/loader.js';
9
9
  import { resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
10
- import { choosePersonaId, clonePreset, describePreset, editPersona, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
10
+ import { UnknownPersonaSchemaKeyError, choosePersonaId, clonePreset, describePreset, editPersona, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
11
11
  import { addTeamSet, listTeamSets, removeTeamSet } from '../persona/team-set.js';
12
12
 
13
13
  interface StoredPersona { personaId: string; displayName: string; description?: string; preset?: string; title?: string; path: string }
@@ -43,11 +43,11 @@ function mapping(value: unknown): value is Record<string, unknown> {
43
43
 
44
44
  function setEdit(edits: Record<string, unknown>, assignment: string): void {
45
45
  const equals = assignment.indexOf('=');
46
- if (equals < 1) throw new Error(`--set requires key=value: ${assignment}`);
46
+ if (equals < 0) throw new Error(`--set requires key=value: ${assignment}`);
47
47
  const key = assignment.slice(0, equals);
48
48
  const parts = key.split('.');
49
49
  if (parts.some((part) => !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
50
- throw new Error(`invalid --set key: ${key}`);
50
+ throw new UnknownPersonaSchemaKeyError(key);
51
51
  }
52
52
  const value: unknown = parseYaml(assignment.slice(equals + 1));
53
53
  let target = edits;
@@ -94,7 +94,7 @@ export function registerPersonaCommands(program: Command): void {
94
94
  if (opts.json) { console.log(JSON.stringify({ presets: presets.map((p) => ({ id: p.personaId, displayName: p.displayName, role: p.role, oneLine: p.oneLine })), mine }, null, 2)); return; }
95
95
  if (!presets.length) console.log('페르소나 프리셋을 찾지 못했습니다(persona-presets/ 가 이 설치본에 없습니다).');
96
96
  else {
97
- console.log('프리셋 — 골라서 내 페르소나로 만들기: elanous persona add <프리셋> [--name <이름>]');
97
+ console.log('프리셋 — 골라서 내 페르소나로 만들기: elanous persona add <이름> [--from <프리셋>]');
98
98
  const width = Math.max(...presets.map((p) => p.personaId.length));
99
99
  for (const p of presets) console.log(` ${p.personaId.padEnd(width)} ${p.displayName} — ${p.role} · ${p.oneLine}`);
100
100
  }
@@ -105,24 +105,27 @@ export function registerPersonaCommands(program: Command): void {
105
105
  }
106
106
  });
107
107
 
108
- persona.command('add <preset>')
109
- .description('Make your own persona from a preset (id, name or role) — the preset file is not changed')
108
+ persona.command('add <name>')
109
+ .description('Make your own persona from a preset — the preset file is not changed')
110
+ .option('--from <preset>', 'Preset to clone (defaults to the named preset)')
110
111
  .option('--name <name>', 'What to call it (default: the preset’s first name)')
111
112
  .option('--as <name>', 'Clone this preset under a new, unique name')
112
113
  .option('--title <title>', 'Optional title, e.g. CMO')
113
114
  .option('--json', 'JSON output')
114
- .action((query: string, opts: { name?: string; as?: string; title?: string; json?: boolean }) => {
115
+ .action((query: string, opts: { from?: string; name?: string; as?: string; title?: string; json?: boolean }) => {
115
116
  const presets = loadPresets();
116
- const preset = findPreset(presets, query);
117
+ const presetQuery = opts.from ?? query;
118
+ const preset = findPreset(presets, presetQuery);
117
119
  if (!preset) {
118
- console.error(`프리셋 «${query}» 을 찾지 못했습니다. 있는 것: ${presets.map((p) => p.personaId).join(', ')}`);
120
+ console.error(`프리셋 «${presetQuery}» 을 찾지 못했습니다. 있는 것: ${presets.map((p) => p.personaId).join(', ')}`);
119
121
  process.exitCode = 1;
120
122
  return;
121
123
  }
122
- if (opts.as !== undefined) {
124
+ if (opts.as !== undefined || opts.from !== undefined) {
123
125
  try {
124
- if (opts.name !== undefined || opts.title !== undefined) throw new Error('--as cannot be combined with --name or --title');
125
- const result = clonePreset(preset, opts.as);
126
+ if (opts.from !== undefined && (opts.as !== undefined || opts.name !== undefined || opts.title !== undefined)) throw new Error('--from cannot be combined with --as, --name or --title');
127
+ if (opts.as !== undefined && (opts.name !== undefined || opts.title !== undefined)) throw new Error('--as cannot be combined with --name or --title');
128
+ const result = clonePreset(preset, opts.from !== undefined ? query : opts.as!);
126
129
  debug.log('persona.cli', 'cloned', { preset: preset.personaId, personaId: result.personaId });
127
130
  if (opts.json) console.log(JSON.stringify({ personaId: result.personaId, displayName: result.displayName, preset: preset.personaId, path: result.path }, null, 2));
128
131
  else {
@@ -170,8 +173,8 @@ export function registerPersonaCommands(program: Command): void {
170
173
  console.log(` 파일: ${result.path}`);
171
174
  }
172
175
  } catch (err) {
173
- console.error(err instanceof Error ? err.message : String(err));
174
- process.exitCode = 1;
176
+ console.error(err instanceof Error ? err.message.replace(/\s+/g, ' ').trim() : String(err));
177
+ process.exitCode = err instanceof UnknownPersonaSchemaKeyError ? 2 : 1;
175
178
  }
176
179
  });
177
180
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { Command } from 'commander';
5
5
  import { debug } from '../debug/log.js';
6
+ import { readPodMemoryAdvice } from './pod-memory-advice.js';
6
7
  import { getUserConfig } from '../user-config.js';
7
8
  import { listCodexAccountsInStore } from '../oauth/codex-account-store.js';
8
9
  import { parsePodPool, resolvePodPoolSpec, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
@@ -24,6 +25,7 @@ export interface PodCliDeps {
24
25
  perAccount?: () => number;
25
26
  poolSpec?: (explicit?: string) => string | null;
26
27
  harnessPool?: () => string | undefined;
28
+ memoryAdvice?: (logsDb?: string) => ReturnType<typeof readPodMemoryAdvice>;
27
29
  }
28
30
 
29
31
  const HELP = `elanous pod run [--pool <스펙>] [--skill <이름,…>] [--llm grok] [--clone] [--deadline <초>] -- <명령…>
@@ -38,6 +40,21 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
38
40
  };
39
41
  const run = deps.run ?? ((options: RunPodCommandOptions) => runPodCommand(options));
40
42
  const pod = program.command('pod').description('Pod 명령 Job — 하니스 없이 명령 하나를 돌리고 산출을 돌려받는다');
43
+ pod.command('memory').description('실측 기반 Pod 메모리 권고 (읽기 전용)')
44
+ .command('advise').description('골 종류별 standard 16Gi / high 32Gi 권고와 근거')
45
+ .option('--json', '권고와 측정 근거를 JSON 으로')
46
+ .option('--logs-db <path>', '기존 logs.db 경로')
47
+ .action((opts: { json?: boolean; logsDb?: string }) => {
48
+ try {
49
+ const advice = (deps.memoryAdvice ?? readPodMemoryAdvice)(opts.logsDb);
50
+ if (opts.json) io.log(JSON.stringify(advice));
51
+ else for (const row of advice.byGoalType) io.log(`${row.goalType}: ${row.recommended} ${row.limit} · ${row.reason}`);
52
+ io.exit(0);
53
+ } catch (err) {
54
+ io.error(err instanceof Error ? err.message : String(err));
55
+ io.exit(1);
56
+ }
57
+ });
41
58
  pod.command('lease').description('Pod 풀 동시 실행 권장 (DNS 점검용 Pod 를 잠깐 만들었다 지움)')
42
59
  .command('status').description('지금 추가로 쏠 수 있는 Pod 골과 제한 근거')
43
60
  .option('--pool <spec>', '풀 스펙 — 컨텍스트[@ssh호스트][:상한][#레지스트리:포트] 쉼표')