elanous 0.2.16 → 0.2.17

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 (228) 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/{1307-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +13 -13
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +13 -13
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +13 -13
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +13 -13
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +13 -13
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +13 -13
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +13 -13
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +13 -13
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +13 -13
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +13 -13
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +13 -13
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +13 -13
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +13 -13
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +13 -13
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +13 -13
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +14 -14
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +13 -13
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +13 -13
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +13 -13
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +13 -13
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +13 -13
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +13 -13
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +13 -13
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +13 -13
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +13 -13
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +13 -13
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +13 -13
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +13 -13
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +13 -13
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +13 -13
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +13 -13
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +13 -13
  143. package/graphs/intake/intake-daily.yaml +9 -2
  144. package/graphs/intake/news-sources.yaml +4 -0
  145. package/graphs/intake/recipes.yaml +6 -1
  146. package/package.json +1 -1
  147. package/scripts/__fixtures__/intake-news.xml +7 -0
  148. package/scripts/intake-news.ts +169 -0
  149. package/scripts/release-loop/docs-land-node.ts +69 -10
  150. package/scripts/release-loop/publish-node.ts +2 -2
  151. package/scripts/release-loop/release-docs.ts +134 -0
  152. package/scripts/release-loop/version-node.ts +7 -4
  153. package/scripts/release-story/draft.ts +42 -14
  154. package/src/agent-mission/driver.ts +53 -10
  155. package/src/autopilot/track-agent.ts +5 -3
  156. package/src/cli/autopilot-cli.ts +66 -0
  157. package/src/cli/decisions-cli.ts +4 -1
  158. package/src/cli/intake-cli.ts +1 -1
  159. package/src/cli/log-event-names.ts +1 -0
  160. package/src/cli/pr-cli.ts +12 -1
  161. package/src/cli/provider-cli.ts +14 -0
  162. package/src/cli/self-update-canary.ts +46 -0
  163. package/src/cli/self-update.ts +200 -1
  164. package/src/context-card/index.ts +45 -0
  165. package/src/context-card/task.ts +76 -0
  166. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  167. package/src/decisions/decision-ledger.ts +10 -1
  168. package/src/harness/harness-cli-command.ts +142 -10
  169. package/src/harness/harness-pod-dispatch.ts +11 -0
  170. package/src/harness/harness-queue.ts +86 -7
  171. package/src/harness/nested-elanous-policy.ts +23 -4
  172. package/src/hq/fence-wrapper.ts +123 -0
  173. package/src/hq/hq.ts +8 -0
  174. package/src/index.ts +57 -8
  175. package/src/intake-plane/digest.ts +39 -4
  176. package/src/nexus/api/grid.ts +61 -0
  177. package/src/nexus/api/http-server.ts +19 -1
  178. package/src/nexus/api/task-cards-api.ts +19 -0
  179. package/src/oauth/codex.ts +82 -2
  180. package/src/release-loop/landing-freeze.ts +18 -10
  181. package/src/seat-loop/seat-loop.ts +95 -31
  182. package/src/self-dev/ask-launch-flow.ts +26 -7
  183. package/src/self-dev/run-store.ts +5 -1
  184. package/src/self-dev/run-supervisor.ts +7 -1
  185. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  186. package/src/self-implement/frozen-merges.ts +25 -20
  187. package/src/self-implement/goal-author-clarification.ts +81 -0
  188. package/src/self-implement/goal-author-cli.ts +33 -5
  189. package/src/self-implement/goal-author.ts +88 -12
  190. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  191. package/src/self-implement/goal-document-write.ts +28 -0
  192. package/src/self-implement/heal-intake.ts +4 -0
  193. package/src/self-implement/host-regate.ts +3 -1
  194. package/src/self-implement/orchestrator.ts +19 -1
  195. package/src/task-agent/actions.ts +118 -0
  196. package/src/task-agent/judge.ts +96 -0
  197. package/src/task-cards/card-store.ts +4 -1
  198. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  199. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  200. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  201. package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
  202. package/src/telegram-commands.ts +1 -1
  203. package/src/telegram-loops-command.ts +21 -14
  204. package/src/user-config.ts +25 -3
  205. package/src/version/packed-revision.json +1 -1
  206. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  207. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  208. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  209. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  210. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  211. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
  212. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  213. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
  214. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  215. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  216. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  217. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  218. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  219. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  220. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  221. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
  227. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  228. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
- import { effectiveInstanceRoot } from '../instance/resolve.js';
4
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
5
5
 
6
6
  export interface LandingFreeze {
7
7
  reason: string;
@@ -60,15 +60,23 @@ function mergesDir(root: string): string { return join(root, 'landing-merges');
60
60
 
61
61
  export interface LandingMergeGuard { frozen: LandingFreeze | null; end: () => void }
62
62
 
63
- export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date()): LandingMergeGuard {
64
- const dir = mergesDir(root);
65
- mkdirSync(dir, { recursive: true });
66
- const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
67
- writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
68
- const end = () => { rmSync(marker, { force: true }); };
63
+ /** Mark both authorities before reading either freeze: the host and the child each drain their own in-flight merges. */
64
+ export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date(), prodFreezeRoot = prodInstanceRoot(), forceReason?: string): LandingMergeGuard {
65
+ const markers: string[] = [];
66
+ const end = () => { for (const marker of markers) rmSync(marker, { force: true }); };
67
+ try {
68
+ for (const authority of new Set([prodFreezeRoot, root])) {
69
+ const dir = mergesDir(authority);
70
+ mkdirSync(dir, { recursive: true });
71
+ const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
72
+ markers.push(marker);
73
+ writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
74
+ }
75
+ } catch (error) { end(); throw error; }
69
76
  let frozen: LandingFreeze | null;
70
- try { frozen = readLandingFreeze(root, now); } catch (error) { end(); throw error; }
71
- if (frozen) { end(); return { frozen, end: () => {} }; }
77
+ try { frozen = readLandingFreeze(prodFreezeRoot, now) ?? (root === prodFreezeRoot ? null : readLandingFreeze(root, now)); }
78
+ catch (error) { end(); throw error; }
79
+ if (frozen && !forceReason?.trim()) { end(); return { frozen, end: () => {} }; }
72
80
  return { frozen: null, end };
73
81
  }
74
82
 
@@ -10,7 +10,7 @@ import { surfaceEventsDbPath } from '../domains/surface-events.js';
10
10
  import { judgeNeighbor } from '../loops/neighbors.js';
11
11
  import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
12
12
  import { isRescueAllowedAction } from '../steward/rescue.js';
13
- import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
13
+ import { DecisionLedger, type DecisionEntry, type DecisionOption, type Recommendation } from '../decisions/decision-ledger.js';
14
14
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
15
15
  import { loopEvent } from '../loops/observe.js';
16
16
  import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
@@ -43,7 +43,7 @@ export type TcCandidate =
43
43
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
44
44
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
45
45
  export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
46
- export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
46
+ export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'resolved' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
47
47
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
48
48
 
49
49
  export function seatLoopTickLine(result: SeatLoopResult): string {
@@ -375,6 +375,23 @@ function decisionSummary(text: string): string {
375
375
  return Array.from(sentences).slice(0, 240).join('').trim();
376
376
  }
377
377
 
378
+ function decisionCardMaterial(input: {
379
+ what: string; soFar: string; current: string; question: string; answer: string;
380
+ options: DecisionOption[]; recommendation: Recommendation; ifUntouched: string; evidence: string; confidence: string;
381
+ }): { scqa: { s: string; c: string; q: string; a: string }; pendingQuestion: string } {
382
+ const short = (value: string) => decisionSummary(value) || '관측되지 않음';
383
+ return {
384
+ scqa: { s: short(input.what), c: short(input.current), q: short(input.question), a: short(input.answer) },
385
+ pendingQuestion: [
386
+ `무엇: ${input.what}`, `지금까지: ${input.soFar}`, `지금 상태 재측: ${input.current}`,
387
+ ...input.options.map((option) => `선택지 ${option.key} (${option.label}) 결과: ${option.consequence}`),
388
+ 'skipped' in input.recommendation ? `권고 비움: ${input.recommendation.reason}` : `권고: ${input.recommendation.option} — ${input.recommendation.why}`,
389
+ `확신: ${input.confidence}`, `그냥 두면: ${input.ifUntouched}`, `근거: ${input.evidence}`,
390
+ `Q: ${input.question}`, `A: ${input.answer}`,
391
+ ].join('\n'),
392
+ };
393
+ }
394
+
378
395
  function decisionSourceRef(seat: string, item: SeatItem): string {
379
396
  return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
380
397
  }
@@ -388,7 +405,7 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
388
405
  function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
389
406
  const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
390
407
  return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
391
- || entry.status === 'resolved-by-neighbor'
408
+ || entry.status === 'resolved-by-neighbor' || entry.status === 'resolved'
392
409
  // RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
393
410
  || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
394
411
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
@@ -853,7 +870,7 @@ const SEAT_ACTION_RULES: ReadonlyArray<{ kind: 'wait' | 'harness' | 'decision';
853
870
  { kind: 'wait', signal: /실측\s*대기|(?:사람|실제)\s*기기(?!\s*(?:(?:실측|측정|확인)\s*)?완료)(?:\s*(?:실측|측정|확인|대기))?|실물\s*측정(?!\s*완료)/iu, source: 'content', resolvedBy: /(?:실측|측정|(?:사람|실제)\s*기기|실물\s*측정)\s*완료/iu },
854
871
  { kind: 'wait', signal: /\u{1F451}\s*확인\s*대기/iu, source: 'content', resolvedBy: /\u{1F451}\s*확인\s*완료/iu },
855
872
  { kind: 'decision', signal: TRACK_AGENT_FORBIDDEN_ACTION_REGEX, source: 'task' },
856
- { kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'content' },
873
+ { kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'task' },
857
874
  { kind: 'harness', signal: /(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록|운영\s*반영)(?:를?\s*위한)?\s*(?:준비|초안|원고|자료|구현|점검|테스트)|(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록)(?:용|를?\s*위한)\s*(?:초안|원고|자료|코드|기능)/iu, source: 'task' },
858
875
  ];
859
876
 
@@ -861,9 +878,9 @@ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' |
861
878
  const task = `${item.id}\n${item.title}\n${item.text}`;
862
879
  const content = `${task}\n${item.evidence ?? ''}`;
863
880
  const preparation = SEAT_ACTION_RULES.find(({ kind }) => kind === 'harness')!;
864
- const prepared = [...content.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
881
+ const prepared = [...item.text.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
865
882
  const rule = SEAT_ACTION_RULES.map(({ kind, signal, source, resolvedBy }) => {
866
- const text = source === 'content' ? content : task;
883
+ const text = kind === 'decision' ? item.text : source === 'content' ? content : task;
867
884
  if (resolvedBy?.test(item.evidence ?? '')) return null;
868
885
  const hits = [...text.matchAll(new RegExp(signal.source, `${signal.flags.replace('g', '')}g`))];
869
886
  const hit = kind === 'decision' ? hits.find((match) => !prepared.some((prep) =>
@@ -936,13 +953,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
936
953
  if (judgment.state === 'absent') {
937
954
  if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
938
955
  const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
956
+ const options = [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
957
+ { key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }];
958
+ const recommendation = { option: 'b', why: '새 사건을 확인하기 전에는 이웃 업무를 대행하지 않는다' };
939
959
  cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
940
- scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
941
- c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
942
- q: '결측 이웃의 업무를 어떻게 처리할까?' },
943
- options: [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
944
- { key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }],
945
- recommendation: { skipped: true, reason: '결측만으로 대행이나 보류를 자동 결정하지 않는다' },
960
+ ...decisionCardMaterial({ what: `${seat} 이웃 ${neighbor.id} 결측`, soFar: '마지막 맥락 버스 사건 이후 새 ACK 없음',
961
+ current: `현재 시각 ${now.toISOString()}; 마지막 사건 ${lastSeenAt}; heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 결측`,
962
+ question: '결측 이웃의 업무를 어떻게 처리할까?', answer: '새 ACK까지 현상 유지하고 이웃 업무는 대행하지 않는다',
963
+ options, recommendation, confidence: '중간 — 사건의 부재만 관측됨',
964
+ ifUntouched: '결측 상태를 계속 관측하며 대행은 하지 않는다', evidence: `맥락 버스 마지막 사건 ${lastSeenAt}; ${ref}` }),
965
+ options, recommendation,
946
966
  raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
947
967
  crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
948
968
  action = 'decision-card';
@@ -1012,10 +1032,17 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1012
1032
  const question = proposal.inquiry?.question ?? proposal.item!.title;
1013
1033
  const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
1014
1034
  if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
1035
+ const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
1036
+ const recommendation = { option: 'b', why: `기한 ${waited}분 뒤에도 ${cell?.status ?? '질문 미해결'}이므로 착지 확인 전에는 진행하지 않는다` };
1037
+ const material = decisionCardMaterial({ what: `${seat} 질문 · ${proposal.item!.title}`, soFar: `이웃 ${neighbor} 해결 제안: ${proposal.xcheckNote ?? '기록 없음'}`,
1038
+ current: `기한 ${waited}분 경과 · 칸 상태 ${cell?.status ?? '질문 미해결'}`, question, answer: '재측상 미해결이므로 보류를 권고한다',
1039
+ options, recommendation, confidence: '중간 — 기한 경과와 미해결을 재측함', ifUntouched: '질문/칸은 미해결로 남는다',
1040
+ evidence: `${proposal.item!.evidence ?? '원천 근거 미기재'} · ${note} · ${decisionRef(seat, proposal.item!)}` });
1015
1041
  await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
1016
- '--s', `${seat} 질문 · ${proposal.item!.title}`, '--c', '이웃 자리 해결 제안이 기한 안에 해소되지 않았다',
1042
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1043
+ '--pending-question', material.pendingQuestion,
1017
1044
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
1018
- '--skip-recommend', '근거가 부족하다', '--xcheck', `${neighbor}:${note}`,
1045
+ '--recommend', recommendation.option, '--why', recommendation.why, '--xcheck', `${neighbor}:${note}`,
1019
1046
  '--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
1020
1047
  }
1021
1048
  const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
@@ -1313,10 +1340,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1313
1340
  const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
1314
1341
  ...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
1315
1342
  : ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
1343
+ const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
1344
+ const recommendation = { option: 'b', why: '교차 확인에서 해소 근거가 확보되지 않았다' };
1345
+ const material = decisionCardMaterial({ what: `${seat} 질문 · ${item.title}`, soFar: `이웃 ${requestedNeighbor}에 교차 확인 요청`,
1346
+ current: answer ? `이웃 응답: ${answer.note}` : `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`,
1347
+ question: ask.question, answer: '해소 근거 확인 전까지 보류를 권고한다', options, recommendation,
1348
+ confidence: answer ? '중간 — 이웃 응답을 확인함' : '낮음 — 기한 내 이웃 응답 없음',
1349
+ ifUntouched: '사람 판단 전 집행하지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${decisionRef(seat, item)}` });
1316
1350
  await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
1317
- '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
1351
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1352
+ '--pending-question', material.pendingQuestion,
1318
1353
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
1319
- '--skip-recommend', '근거가 부족하다', ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1354
+ '--recommend', recommendation.option, '--why', recommendation.why, ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1320
1355
  if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
1321
1356
  entry.status = 'hitl';
1322
1357
  xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
@@ -1442,11 +1477,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1442
1477
  if (action.kind === 'decision') {
1443
1478
  const title = `${seat}: ${decisionSummary(action.text)}`;
1444
1479
  const source = decisionSourceRef(seat, item);
1480
+ const options = [{ key: 'a', label: '사람 승인', consequence: '승인 후 별도로 집행' }, { key: 'b', label: '보류', consequence: '집행하지 않음' }];
1481
+ const recommendation = { option: 'b', why: `요청에서 걸린 문면 «${action.reason}»은 자동 집행 대상이 아니므로 보류한다` };
1482
+ let currentStatus: string | undefined;
1483
+ try {
1484
+ currentStatus = item.source === 'checklist' && item.version
1485
+ ? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
1486
+ .find((cell) => cell.id === item.id && cell.owner === seat)?.status
1487
+ : item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
1488
+ : undefined;
1489
+ } catch (error) { observe('decision-source-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
1490
+ const material = decisionCardMaterial({ what: `${seat} 배정 항목: ${item.title} 역할: ${inputs.role || '역할 근거 없음'}`, soFar: `요청 내용: ${item.text}`,
1491
+ current: `밑바탕 재측: ${currentStatus ?? '상태 미확인'}; 요청에서 금지 문면 «${action.reason}» 발견 — 실행 전 사람 승인 필요`,
1492
+ question: `금지 문면 «${action.reason}»이 걸린 요청을 승인할까?`, answer: '승인 전에는 집행하지 않고 보류한다',
1493
+ options, recommendation, confidence: '높음 — 요청에 금지 문면 직접 일치',
1494
+ ifUntouched: '자동 집행되지 않고 해당 칸은 보류 상태로 남는다',
1495
+ evidence: `${item.evidence?.trim() || '추가 근거 미기재'} · 요청 ${source} · 걸린 낱말 ${action.reason}` });
1445
1496
  const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
1446
- '--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
1447
- '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1497
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1498
+ '--pending-question', material.pendingQuestion,
1448
1499
  '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
1449
- '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1500
+ '--recommend', recommendation.option, '--why', recommendation.why, '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1450
1501
  '--agent', 'seat-loop', '--ref', source, '--json']);
1451
1502
  const decision: unknown = JSON.parse(raised.trim());
1452
1503
  if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
@@ -1461,25 +1512,38 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1461
1512
  const limit = repeatStopLimit(config);
1462
1513
  const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
1463
1514
  if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
1515
+ // Re-read the underlying item at the decision boundary, not the picked snapshot.
1516
+ const current = item.source === 'checklist' && item.version
1517
+ ? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
1518
+ .find((cell) => cell.id === item.id && cell.owner === seat)?.status
1519
+ : item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
1520
+ : undefined;
1521
+ if (current === 'green' || current === 'done') {
1522
+ entry.status = 'resolved';
1523
+ entry.reason = `밑바탕 재측: ${current} — 반복 정지 카드 불필요`;
1524
+ append(path, entry);
1525
+ observe('repeat-recovered', { seat, item: item.id, status: current });
1526
+ return entry;
1527
+ }
1464
1528
  const reason = repeatStopReason(attempts);
1465
1529
  entry.status = 'held';
1466
1530
  entry.reason = reason;
1467
1531
  const ref = decisionRef(seat, item);
1468
1532
  const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
1469
1533
  if (!prior) {
1534
+ const options = [
1535
+ { key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
1536
+ { key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
1537
+ ];
1538
+ const recommendation = { option: 'b', why: `착지 없는 ${attempts}회 재시도보다 현재 칸을 멈추는 편이 중복 실행을 피한다` };
1470
1539
  cardLedger(stateRoot, deps, now).raiseOnce({
1471
- title: `${seat}: ${item.id} 반복 착지 0`,
1472
- category: 'other',
1473
- scqa: {
1474
- s: `${seat} 칸 ${item.id} 가 착지 없이 ${attempts}회 끝났다.`,
1475
- c: '같은 칸을 다시 발사하지 않고 사람 판단을 기다린다.',
1476
- q: '이 칸을 계속할까, 멈출까?',
1477
- },
1478
- options: [
1479
- { key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
1480
- { key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
1481
- ],
1482
- recommendation: { skipped: true, reason: '착지 없는 반복만으로 계속할지 자동 결정하지 않는다' },
1540
+ title: `${seat}: ${item.id} 반복 착지 0`, category: 'other',
1541
+ ...decisionCardMaterial({ what: `${seat} 칸 ${item.id} · ${item.title}`, soFar: `${attempts}회 착지 없이 종료`,
1542
+ current: `발행 직전 밑바탕 재측: ${current ?? '상태 미확인'}; 같은 칸 착지 0`,
1543
+ question: '이 칸을 계속할까, 멈출까?', answer: '중복 발사를 멈추고 사람의 재개 결정을 기다린다',
1544
+ options, recommendation, confidence: current ? '높음 — 밑바탕 상태 재측됨' : '낮음 — 밑바탕 상태 미확인',
1545
+ ifUntouched: '같은 칸은 다시 발사되지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${ref} · 완료되지 않은 시도 ${attempts}회` }),
1546
+ options, recommendation,
1483
1547
  raisedBy: { agent: 'seat-loop' },
1484
1548
  refs: [ref],
1485
1549
  raisedAt: now.toISOString(),
@@ -35,6 +35,8 @@ import { dirname, isAbsolute as isAbsolutePath, relative as relativePath, resolv
35
35
  import {
36
36
  applyClarificationReply,
37
37
  planClarificationIntake,
38
+ selfAnswerGoalDocumentClarifications,
39
+ type GoalClarificationSelfResolver,
38
40
  } from '../self-implement/goal-author-clarification.js';
39
41
  import { classifyReauthoredAsk, countMissingAuthoredConstraintMarkers, askGoalTypeDeclaration, declaredGoalType, parseGoalId } from '../self-implement/goal-author.js';
40
42
  import { observeFrontNodeEntry } from './graph-front-nodes.js';
@@ -143,6 +145,9 @@ export interface AskLaunchFlowDeps {
143
145
  /** Optional structured clarification adapter. Omitted callers retain the
144
146
  * existing CLI line-input contract exactly. */
145
147
  readClarification?(clarification: GoalDocumentClarification): Promise<string>;
148
+ selfResolveClarification?: GoalClarificationSelfResolver;
149
+ /** Replaces the goal document after a self-answer; production entrances pass an atomic writer. */
150
+ writeGoalDocument?(path: string, document: string): void;
146
151
  readFile(path: string): string;
147
152
  writeFile(path: string, data: string): void;
148
153
  cwd(): string;
@@ -188,6 +193,7 @@ export interface AskLaunchFlowInput {
188
193
  readonly askFile?: string;
189
194
  /** Optional repository root used only for code grounding during goal authoring. */
190
195
  readonly groundingCwd?: string;
196
+ readonly authorGrade?: import('../self-implement/goal-author.js').GoalAuthorGradeSelection;
191
197
  readonly liveRunWindowMinutes: number;
192
198
  readonly recentChangeWindowDays: number;
193
199
  readonly forceRequested: boolean;
@@ -724,16 +730,27 @@ async function intakeClarifications(
724
730
  return effectiveGoalFile;
725
731
  }
726
732
  if (mode === 'deferred-noninteractive') {
727
- // ⛔⭐ 종전엔 「N건 있다」만 말하고 «무엇을 묻는지»도 «어떻게 답하는지»도 안 줬다.
728
- // ⇒ 비대화형 표면(무인 런 · TUI 슬래시)에서 그 물음은 사실상 «사라졌다».
729
- // ⛔ 여기에 새 인터뷰 UI 를 만들지 «않는다» — 답변 창구는 `elanous self clarify answer` 로 «이미» 있다.
730
- // 이 자리는 그것을 «가리키기»만 한다(2026-08-11 72차: 오늘만 `F12` 를 여섯 번 셌다).
731
- deps.print(`[ask] ⑴b ⚠️ 미답 되묻기 ${pending.length}건 — 저작은 그대로 간다 · 구현 전에 LLM 릴레이가 먼저 답하고, 못 하면 미답으로 진행한다 (사람을 기다리지 않는다)`);
732
- for (const item of pending) {
733
+ // The resolver comes in through deps (I/O seam): production entrances pass the author
734
+ // self-resolver; a caller that injects none keeps the previous deferred behaviour.
735
+ if (selfResolveClarifications.value && deps.selfResolveClarification) try {
736
+ const resolution = await selfAnswerGoalDocumentClarifications(document, {
737
+ path: 'ask', ask: input.askText, resolver: deps.selfResolveClarification,
738
+ });
739
+ if (resolution.newlyAnswered > 0) {
740
+ (deps.writeGoalDocument ?? deps.writeFile)(effectiveGoalFile, resolution.document);
741
+ document = resolution.document;
742
+ }
743
+ } catch (error) {
744
+ deps.log('ask-clarify', { goalFile: effectiveGoalFile, reason: 'self-answer-failed', error: error instanceof Error ? error.message : String(error) }, 'warn');
745
+ }
746
+ const remaining = planClarificationIntake(document, false).pending;
747
+ // 미답으로 남은 문항만 기존 답변 창구를 안내한다.
748
+ deps.print(`[ask] ⑴b ⚠️ 미답 되묻기 ${remaining.length}건 — 저작은 그대로 간다 · 구현 전에 LLM 릴레이가 먼저 답하고, 못 하면 미답으로 진행한다 (사람을 기다리지 않는다)`);
749
+ for (const item of remaining) {
733
750
  deps.print(`[ask] ❓ [${item.questionId}] ${item.question}`);
734
751
  item.options.forEach((option, index) => deps.print(`[ask] ${index}) ${option.label}`));
735
752
  }
736
- if (pending.length > 0) {
753
+ if (remaining.length > 0) {
737
754
  const goalArg = deps.relativeToCwd(effectiveGoalFile);
738
755
  deps.print(`[ask] ▶ 답하려면: elanous self clarify answer ${goalArg} <questionId> <옵션번호> (자유 답은 --other "<문장>")`);
739
756
  deps.print(`[ask] ▶ 답한 뒤 재저작: elanous self author --supersedes ${goalArg}`);
@@ -771,6 +788,7 @@ async function intakeClarifications(
771
788
  const reauthored = await deps.authorGoal([], {
772
789
  cwd: deps.cwd(),
773
790
  ...(input.groundingCwd === undefined ? {} : { groundingCwd: input.groundingCwd }),
791
+ ...(input.authorGrade === undefined ? {} : { authorGrade: input.authorGrade }),
774
792
  fromClarification: `${deps.relativeToCwd(effectiveGoalFile)}#${seedQuestionId}`,
775
793
  reauthorFromAnsweredClarification: true,
776
794
  selfResolveClarifications: selfResolveClarifications.value,
@@ -853,6 +871,7 @@ export async function runAskLaunchFlow(
853
871
  const authored = await deps.authorGoal([input.askText], {
854
872
  cwd: deps.cwd(),
855
873
  ...(input.groundingCwd === undefined ? {} : { groundingCwd: input.groundingCwd }),
874
+ ...(input.authorGrade === undefined ? {} : { authorGrade: input.authorGrade }),
856
875
  ...(goalType === null ? {} : { goalType }),
857
876
  // ⛔ 인라인 ask(`harness say`)는 askFile 이 없다 — 그 칸을 넘기면 저작기가 빈 줄을 쓴다.
858
877
  ...(() => {
@@ -28,6 +28,7 @@ import type { RunIdSource } from '../harness/harness-space.js';
28
28
  import { withFileLockSync } from '../storage/file-lock.js';
29
29
  import type { SelfDevJobResult, SelfDevGoal } from './orchestrate.js';
30
30
  import type { SupervisorStopReason } from './run-supervisor.js';
31
+ import type { SupervisorNext } from './supervisor-verdict-edges.js';
31
32
  import { loadMirroredRunRecord, mirrorRunRecord } from './run-record-mirror.js';
32
33
 
33
34
  export interface SelfDevRunParticipant {
@@ -92,6 +93,8 @@ export interface SelfDevRunState {
92
93
  parkedResolution?: SelfDevRunResolution;
93
94
  /** The closed supervisor termination reason, persisted independently from per-goal results. */
94
95
  supervisorStopReason?: SupervisorStopReason;
96
+ /** Named next step at supervisor termination; legacy checkpoints omit it. */
97
+ next?: SupervisorNext;
95
98
  /** ★ C(2026-07-21·HITL 정황 품질) — 이 run 을 구동하는 오케스트레이터 프로세스 pid. 살아있으면
96
99
  * non-terminal goal 은 **실행중(running)** 이지 막힌(interrupted) 게 아니다. 죽었으면(오케스트레이터
97
100
  * killed) 진짜 interrupted. isPidAlive 로 구분해 라이브 잡을 parked/repair-signals 에서 제외. */
@@ -311,13 +314,14 @@ export function recordSelfDevRunSupervisorStop(
311
314
  runId: string,
312
315
  supervisorStopReason: SupervisorStopReason,
313
316
  dir = selfDevRunsDir(),
317
+ next?: SupervisorNext,
314
318
  ): SupervisorStopPersistenceOutcome {
315
319
  try {
316
320
  if (!existsSync(dir)) return { outcome: 'missing-directory' };
317
321
  return withFileLockSync(join(dir, `${runId}.lock`), () => {
318
322
  const state = loadSelfDevRunFromPath(runPath(runId, dir));
319
323
  if (!state) return { outcome: 'missing-record' };
320
- writeSelfDevRun({ ...state, updatedAt: Date.now(), supervisorStopReason }, dir);
324
+ writeSelfDevRun({ ...state, updatedAt: Date.now(), supervisorStopReason, ...(next === undefined ? {} : { next }) }, dir);
321
325
  return { outcome: 'persisted' };
322
326
  });
323
327
  } catch {
@@ -6,6 +6,7 @@ import type { SelfDevJobResult } from './orchestrate.js';
6
6
  import { recordSelfDevRunSupervisorStop, selfDevRunsDir } from './run-store.js';
7
7
  import { debug } from '../debug/log.js';
8
8
  import { sweepFrozenMerges } from '../self-implement/frozen-merges.js';
9
+ import { routeSupervisorVerdict, type SupervisorNext } from './supervisor-verdict-edges.js';
9
10
 
10
11
  export interface SupervisorRound {
11
12
  round: number;
@@ -186,6 +187,7 @@ function formatGoalPlanRevisionObservation(results: readonly SelfDevJobResult[])
186
187
  export type SupervisorJobResult = SelfDevJobResult & {
187
188
  reviewReason?: string;
188
189
  salvage?: 'launched' | 'parked';
190
+ next?: SupervisorNext;
189
191
  };
190
192
 
191
193
  export function decideNextRun(input: {
@@ -592,9 +594,13 @@ export async function superviseRun(opts: SuperviseRunOptions): Promise<Superviso
592
594
  } catch { /* fail-open */ }
593
595
 
594
596
  if (decision.action === 'stop') {
597
+ const reason = decision.stopReason ?? 'unregistered-stop';
598
+ const next = routeSupervisorVerdict(reason, results).next;
599
+ try { debug.log('self-dev.supervisor', 'verdict-edge', { runId, reason, next }); } catch { /* observation must not interrupt supervision */ }
600
+ results = results.map((result) => ({ ...result, next }));
595
601
  if (runId && decision.stopReason) {
596
602
  const storage = resolveSupervisorStopStorage(opts.runStore);
597
- const persistence = recordSelfDevRunSupervisorStop(runId, decision.stopReason, storage.path);
603
+ const persistence = recordSelfDevRunSupervisorStop(runId, decision.stopReason, storage.path, next);
598
604
  const storageObservation = {
599
605
  storagePath: storage.path,
600
606
  storageSource: storage.source,
@@ -0,0 +1,46 @@
1
+ import type { SelfDevJobResult } from './orchestrate.js';
2
+ import type { SupervisorStopReason } from './run-supervisor.js';
3
+
4
+ export type SupervisorNext = 'harvest' | 'scope-decompose' | 'self-review' | 'human-gate' | 'proposal';
5
+ export type SupervisorVerdictEdge = { readonly meaning: string; readonly next: SupervisorNext };
6
+
7
+ // 목표 규칙: 사람 관문은 결제·공개·자격·비가역 결정에만. 지금 max-rounds·provider-exhausted·step-timeout 이 human-gate 인 것은
8
+ // 그 규칙의 예외로 «그대로» 둔 값이다(시험에 고정 — 바꾸면 보인다).
9
+ export const SUPERVISOR_VERDICT_EDGES = {
10
+ converged: { meaning: 'run converged', next: 'harvest' },
11
+ 'human-stopped': { meaning: 'human stopped the run', next: 'human-gate' },
12
+ 'parent-signals-red': { meaning: 'parent decision signals are red', next: 'human-gate' },
13
+ 'needs-human': { meaning: 'unresolved human or harvest decision', next: 'human-gate' },
14
+ 'no-actionable-work': { meaning: 'no actionable work remains', next: 'self-review' },
15
+ 'harvestable-awaiting-human': { meaning: 'implementation awaits harvest', next: 'harvest' },
16
+ 'handed-off-to-salvage': { meaning: 'salvage already owns the work', next: 'harvest' },
17
+ 'max-rounds': { meaning: 'round budget exhausted', next: 'human-gate' },
18
+ 'no-progress': { meaning: 'run stalled', next: 'scope-decompose' },
19
+ 'provider-exhausted': { meaning: 'provider quota exhausted', next: 'human-gate' },
20
+ 'step-timeout': { meaning: 'step budget exhausted', next: 'human-gate' },
21
+ 'decomposable-no-progress': { meaning: 'stalled with a decomposition proposal', next: 'scope-decompose' },
22
+ 'review-unobserved': { meaning: 'review was not observed', next: 'self-review' },
23
+ 'deliverable-unobserved': { meaning: 'deliverable was not observed', next: 'self-review' },
24
+ 'deliverable-merged': { meaning: 'deliverable is merged', next: 'harvest' },
25
+ } as const satisfies Record<SupervisorStopReason, SupervisorVerdictEdge>;
26
+
27
+ const HUMAN_GATE_MERGE_REASONS: ReadonlySet<string> = new Set(['decision-signal-red', 'signal-incomplete', 'parent-unlanded', 'human-gate']);
28
+
29
+ /** Unknown stop vocabularies are proposals, never silently treated as approved edges. */
30
+ export function routeSupervisorVerdict(
31
+ reason: string,
32
+ results: readonly Partial<Pick<SelfDevJobResult, 'status' | 'stage' | 'merged' | 'prNumber' | 'prUrl' | 'blockReason' | 'mergeReason'>>[] = [],
33
+ ): SupervisorVerdictEdge {
34
+ const edge = Object.hasOwn(SUPERVISOR_VERDICT_EDGES, reason)
35
+ ? SUPERVISOR_VERDICT_EDGES[reason as SupervisorStopReason]
36
+ : undefined;
37
+ if (!edge) return { meaning: 'unregistered supervisor stop reason', next: 'proposal' };
38
+ if (reason !== 'needs-human') return edge;
39
+ if (results.some((result) => result.blockReason === 'parent-unlanded')) return edge;
40
+ // A human-gate reason on ANY result in the run keeps the whole run at the human gate.
41
+ if (results.some((result) => HUMAN_GATE_MERGE_REASONS.has(String(result.mergeReason)))) return edge;
42
+ const openPrs = results.filter((result) => result.status === 'done' && result.stage === 'pr-opened'
43
+ && result.merged !== true && (result.prNumber !== undefined || Boolean(result.prUrl)));
44
+ if (openPrs.length === 0) return edge;
45
+ return { meaning: 'open PR ready for harvest', next: 'harvest' };
46
+ }
@@ -2,9 +2,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync,
2
2
  import { dlopen, FFIType } from 'bun:ffi';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { dirname, join, resolve } from 'node:path';
5
- import { effectiveInstanceRoot } from '../instance/resolve.js';
5
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
6
6
  import { debug } from '../debug/log.js';
7
- import { beginLandingMerge, readLandingFreeze, type LandingFreeze } from '../release-loop/landing-freeze.js';
7
+ import { beginLandingMerge, type LandingFreeze } from '../release-loop/landing-freeze.js';
8
8
 
9
9
  /** `manual`: the owning repository could not be determined — a sweep never resumes it; a person does. */
10
10
  export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true; /** Host goal document, so a resumed merge can still apply its «이 칸 완료» declaration (CL-AUTO). */ goalFile?: string }
@@ -182,8 +182,10 @@ export function admitLandingMerge(
182
182
  root = effectiveInstanceRoot(),
183
183
  hooks: { afterQueue?: () => void } = {},
184
184
  claimKey?: { prNumber: number; repoRoot: string; headCommit?: string },
185
+ opts: { prodFreezeRoot?: string; forceReason?: string } = {},
185
186
  ): LandingAdmission {
186
187
  let entry: FrozenMerge | null | undefined;
188
+ const prodFreezeRoot = opts.prodFreezeRoot ?? prodInstanceRoot();
187
189
  // A held PR is merged by exactly one runner: the claim (repository + PR) is held from admission until the lander's
188
190
  // merge has ended. Its queue entry stays until that merge is confirmed — `end(true)` removes this lander's own head;
189
191
  // a failed or interrupted merge leaves it for the next resume sweep.
@@ -201,7 +203,7 @@ export function admitLandingMerge(
201
203
  } finally { landing.end(); held?.release(); }
202
204
  };
203
205
  for (;;) {
204
- const landing = beginLandingMerge(root);
206
+ const landing = beginLandingMerge(root, new Date(), prodFreezeRoot, opts.forceReason);
205
207
  if (!landing.frozen) {
206
208
  if (!claim && claimKey) {
207
209
  const key = { prNumber: claimKey.prNumber, repoRoot: repoIdentity(claimKey.repoRoot), headCommit: claimKey.headCommit };
@@ -222,17 +224,19 @@ export function admitLandingMerge(
222
224
  if (!entry) return { kind: 'held', freeze: landing.frozen };
223
225
  queueFrozenMerge(entry, root);
224
226
  hooks.afterQueue?.();
225
- if (readLandingFreeze(root)) return { kind: 'held', freeze: landing.frozen };
226
- const mine = { ...entry, repoRoot: repoIdentity(entry.repoRoot) };
227
- claim = claimFrozenMerge(mine, root);
228
- if (!claim) return { kind: 'taken' };
229
- // A resume sweep already merged and dequeued it between our queueing and our claim: nothing left to merge.
230
- let stillHeld = false;
231
- try { stillHeld = readQueue(root).some((item) => sameEntry(item, mine)); }
232
- catch (error) { claim.release(); throw error; }
233
- if (!stillHeld) { claim.release(); return { kind: 'taken' }; }
234
- ownHead = mine;
235
- debug.log('harness.merge', 'freeze-lifted-while-queueing', { pr: entry.prNumber });
227
+ const afterQueue = beginLandingMerge(root, new Date(), prodFreezeRoot, opts.forceReason);
228
+ if (afterQueue.frozen) return { kind: 'held', freeze: afterQueue.frozen };
229
+ try {
230
+ const mine = { ...entry, repoRoot: repoIdentity(entry.repoRoot) };
231
+ claim = claimFrozenMerge(mine, root);
232
+ if (!claim) return { kind: 'taken' };
233
+ // A resume sweep already merged and dequeued it between our queueing and our claim: nothing left to merge.
234
+ const stillHeld = readQueue(root).some((item) => sameEntry(item, mine));
235
+ if (!stillHeld) { claim.release(); return { kind: 'taken' }; }
236
+ ownHead = mine;
237
+ debug.log('harness.merge', 'freeze-lifted-while-queueing', { pr: entry.prNumber });
238
+ } catch (error) { claim?.release(); throw error; }
239
+ finally { afterQueue.end(); }
236
240
  }
237
241
  }
238
242
 
@@ -259,16 +263,17 @@ export async function sweepFrozenMerges(
259
263
  },
260
264
  root = effectiveInstanceRoot(),
261
265
  alreadyMerged: (entry: FrozenMerge) => boolean | Promise<boolean> = githubMergedAt,
266
+ prodFreezeRoot = prodInstanceRoot(),
262
267
  ): Promise<{ pending: number; merged: number }> {
263
- const frozen = readLandingFreeze(root);
264
- if (frozen) return { pending: readQueue(root).length, merged: 0 };
265
268
  const pending = readQueue(root);
266
269
  let merged = 0;
267
270
  for (const entry of pending) {
268
- if (readLandingFreeze(root)) break;
269
- const claim = claimFrozenMerge(entry, root);
270
- if (!claim) continue; // another sweep or the original lander is on it
271
+ const guard = beginLandingMerge(root, new Date(), prodFreezeRoot);
272
+ if (guard.frozen) break;
273
+ let claim: { release: () => void } | null = null;
271
274
  try {
275
+ claim = claimFrozenMerge(entry, root);
276
+ if (!claim) continue; // another sweep or the original lander is on it
272
277
  if (!readQueue(root).some((item) => sameEntry(item, entry))) continue; // taken back by its lander
273
278
  if (entry.manual) { debug.log('harness.merge', 'resume-manual', { pr: entry.prNumber, repoRoot: entry.repoRoot }); continue; }
274
279
  if (!(await alreadyMerged(entry))) {
@@ -289,7 +294,7 @@ export async function sweepFrozenMerges(
289
294
  });
290
295
  } catch (error) {
291
296
  debug.log('harness.merge', 'resume-failed', { pr: entry.prNumber, error: String(error) });
292
- } finally { claim.release(); }
297
+ } finally { guard.end(); claim?.release(); }
293
298
  }
294
299
  return { pending: readQueue(root).length, merged };
295
300
  }