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
@@ -681,6 +681,87 @@ export function injectGoalDocumentClarificationOtherAnswer(document: string, que
681
681
  * 「답이 한 번도 안 들어가서 효과를 못 쟀다」***일 수 있다 — 두 무리 다 «답이 없는» 무리였다.
682
682
  * ⛔ 무인 계약은 유지한다 — 대화형이 아니면 종전대로 지나가되 «그 사실을 값으로» 낸다.
683
683
  */
684
+ export type GoalClarificationSelfAnswerPath = 'say-first' | 'ask' | 'rework';
685
+ export type GoalClarificationSelfResolver = NonNullable<GoalAuthorClarificationResolutionDeps['selfResolve']>;
686
+
687
+ /** Attempt only unanswered document questions; retain every unanswered line if evidence is insufficient. */
688
+ export async function selfAnswerGoalDocumentClarifications(
689
+ document: string,
690
+ options: {
691
+ path: GoalClarificationSelfAnswerPath;
692
+ ask?: string;
693
+ evidence?: readonly string[];
694
+ resolver?: GoalClarificationSelfResolver;
695
+ },
696
+ ): Promise<{ document: string; asked: number; answered: number; unanswered: number; newlyAnswered: number }> {
697
+ const clarifications = parseGoalDocumentClarifications(document);
698
+ const pending = clarifications.filter((item) => !item.answered);
699
+ // The first say pass may already have self-answered some questions while authoring: count those,
700
+ // never re-ask them, and still attempt every question that is left unanswered.
701
+ const alreadySelfAnswered = options.path === 'say-first'
702
+ ? clarifications.filter((item) => item.answered && item.provenanceSource === 'self-authored').length
703
+ : 0;
704
+ let updated = document;
705
+ let answered = 0;
706
+ const evidence = options.evidence ?? document.split(/\r?\n/)
707
+ .filter((line) => /^(?:\s*- (?:\[code:|\[document:|TRACED PATHS|evidence:|Evidence:)|\s*-?\s*`?(?:src|test|scripts)\/[^\s`]+:\d+)/i.test(line))
708
+ .slice(0, 40);
709
+ for (const item of pending) {
710
+ if (parseGoalDocumentClarifications(updated).filter((entry) => entry.questionId === item.questionId).length !== 1) continue;
711
+ let response: Awaited<ReturnType<typeof resolveGoalAuthorClarification>>;
712
+ try { response = await resolveGoalAuthorClarification({
713
+ questionId: item.questionId,
714
+ kind: 'scope',
715
+ header: item.header,
716
+ question: item.question,
717
+ options: item.options.map((option) => ({ label: option.label, description: option.description })),
718
+ blocking: false,
719
+ }, {
720
+ selfResolve: options.resolver ?? defaultGoalAuthorSelfResolve,
721
+ evidence,
722
+ ask: options.ask,
723
+ selfResolutionSelected: true,
724
+ }); } catch {
725
+ // A resolver failure leaves this question unanswered; the remaining questions are still attempted.
726
+ continue;
727
+ }
728
+ if (response.answer === null || response.provenance?.source !== 'self-authored'
729
+ || response.answer.startsWith(DEFERRED_ANSWER_PREFIX)) continue;
730
+ try {
731
+ if (!response.answer.trim() || /[\r\n]/.test(response.answer)
732
+ || (!item.includeOther && !item.options.some((option) => option.label === response.answer))) continue;
733
+ const current = findUnansweredGoalDocumentClarification(updated, item.questionId);
734
+ const lines = updated.split(/\r?\n/);
735
+ const provenanceLines = current.provenanceLines ?? [];
736
+ for (const line of [...provenanceLines].sort((left, right) => right - left)) lines.splice(line, 1);
737
+ const answerLine = current.answerLine - provenanceLines.filter((line) => line < current.answerLine).length;
738
+ lines[answerLine] = ` - answer: ${response.answer}`;
739
+ lines.splice(answerLine + 1, 0,
740
+ ' - provenance.source: self-authored',
741
+ ...response.provenance.evidence!.map((entry) => ` - evidence: ${entry.replace(/[\r\n]/g, ' ')}`));
742
+ updateWhatToBuildRenderAnswers(lines, item.questionId, response.answer);
743
+ if (hasWhatToBuildUnansweredRenderAnswer(lines, item.questionId)) continue;
744
+ const candidate = lines.join(document.includes('\r\n') ? '\r\n' : '\n');
745
+ if (!parseGoalDocumentClarifications(candidate).some((entry) => entry.questionId === item.questionId
746
+ && entry.answered && entry.answer === response.answer && entry.provenanceSource === 'self-authored')) continue;
747
+ updated = candidate;
748
+ answered += 1;
749
+ } catch {
750
+ // Leave malformed or disallowed answers deferred.
751
+ }
752
+ }
753
+ const asked = pending.length + alreadySelfAnswered;
754
+ const unanswered = pending.length - answered;
755
+ const newlyAnswered = answered;
756
+ answered += alreadySelfAnswered;
757
+ debug.log('self-implement.clarification-escalation', 'self-answer', {
758
+ path: options.path, asked, answered, unanswered,
759
+ reason: unanswered === 0 ? 'answered' : 'insufficient-evidence',
760
+ });
761
+ return { document: updated, asked, answered, unanswered, newlyAnswered };
762
+ }
763
+
764
+
684
765
  export type ClarificationIntakeMode = 'none' | 'deferred-noninteractive' | 'ask';
685
766
 
686
767
  export interface ClarificationIntakePlan {
@@ -1,9 +1,11 @@
1
1
  import { readFileSync, realpathSync } from 'node:fs';
2
+ import { writeGoalDocumentAtomic } from './goal-document-write.js';
2
3
  import { isAbsolute, relative, resolve } from 'node:path';
3
4
  import { debug } from '../debug/log.js';
5
+ import { getUserConfig } from '../user-config.js';
4
6
  import { parseAskProseTitle } from '../self-dev/launch-preflight.js';
5
7
  import { createGoalAuthorDecomposeSteps, readRecentStepCountsFailOpen, type RecentStepCountReader } from './goal-author-decompose.js';
6
- import { defaultGoalAuthorSelfResolve, parseGoalDocumentClarifications, seedGoalAuthorFromAnsweredClarification, seedGoalAuthorFromClarification } from './goal-author-clarification.js';
8
+ import { defaultGoalAuthorSelfResolve, parseGoalDocumentClarifications, seedGoalAuthorFromAnsweredClarification, seedGoalAuthorFromClarification, selfAnswerGoalDocumentClarifications } from './goal-author-clarification.js';
7
9
  import { classifyGoalInterviewRound, type GoalInterviewRoundStatus } from './goal-author-closure.js';
8
10
  import { recordGoalAsk } from './goal-ask-store.js';
9
11
  import { pressDecisionSignal } from './decision-signal-press.js';
@@ -15,6 +17,8 @@ import {
15
17
  verbatimOriginalAsk,
16
18
  writeAuthoredGoal,
17
19
  type GoalAuthorDeps,
20
+ type GoalAuthorGradeSelection,
21
+ resolveGoalAuthorGrade,
18
22
  type GoalType,
19
23
  type GoalFileLintFinding,
20
24
  } from './goal-author.js';
@@ -77,6 +81,7 @@ interface GoalAuthorCliOptions {
77
81
  supersedes?: string;
78
82
  rootIntent?: string;
79
83
  goalType?: string;
84
+ authorGrade?: GoalAuthorGradeSelection;
80
85
  selfResolveClarifications?: boolean;
81
86
  adversarialReview?: boolean;
82
87
  disableAdversarialReview?: boolean;
@@ -97,6 +102,7 @@ export interface GoalAuthorCliDeps {
97
102
  recentStepCountReader?: RecentStepCountReader;
98
103
  /** Resolver supplied only by the explicit CLI self-resolution opt-in. */
99
104
  selfResolveClarification?: NonNullable<GoalAuthorDeps['selfResolveClarification']>;
105
+ writeFile?: (path: string, document: string) => void;
100
106
  recordAsk?: typeof recordGoalAsk;
101
107
  }
102
108
 
@@ -268,8 +274,9 @@ export async function runGoalAuthorCli(
268
274
  : opts.reauthorFromAnsweredClarification
269
275
  ? { origin: 'answered-clarification-reauthor' }
270
276
  : { origin: 'direct-request' };
271
- const recentStepCounts = readRecentStepCountsFailOpen(deps.recentStepCountReader);
272
- const decomposeSteps = deps.decomposeSteps ?? (deps.createDecomposeSteps ?? createGoalAuthorDecomposeSteps)(undefined, {
277
+ const authorGrade = opts.authorGrade ?? resolveGoalAuthorGrade(undefined, getUserConfig().harness?.authorGrade);
278
+ const recentStepCounts = authorGrade.grade === 'lite' ? undefined : readRecentStepCountsFailOpen(deps.recentStepCountReader);
279
+ const decomposeSteps = authorGrade.grade === 'lite' ? undefined : deps.decomposeSteps ?? (deps.createDecomposeSteps ?? createGoalAuthorDecomposeSteps)(undefined, {
273
280
  ...(adversarialReview !== undefined ? { adversarialReview, adversarialReviewSource: 'cli' as const } : {}),
274
281
  ...(recentStepCounts !== undefined ? { recentStepCounts } : {}),
275
282
  });
@@ -283,13 +290,34 @@ export async function runGoalAuthorCli(
283
290
  debug.log('goal-author', 'goal-title-forwarded', proseTitle === undefined
284
291
  ? { passed: false }
285
292
  : { passed: true, titleChars: proseTitle.length });
293
+ const gradeDeps = authorGrade.grade === 'lite' || opts.authorGrade ? { authorGrade } : {};
286
294
  const authorDeps: Partial<GoalAuthorDeps> = parentGoalFile && parentQuestionId
287
- ? { parent: { goalFile: parentGoalFile, questionId: parentQuestionId }, parentDocument, clarificationAnswers, decomposeSteps, ...selfResolutionDeps, ...progressDeps, ...launchPreflightDeps, ...(opts.rootIntent !== undefined && { rootIntent: opts.rootIntent }), ...(goalType !== undefined && { goalType }), ...goalTitleDeps }
288
- : { ...(clarificationAnswers && { clarificationAnswers }), decomposeSteps, ...selfResolutionDeps, ...progressDeps, ...launchPreflightDeps, ...(opts.rootIntent !== undefined && { rootIntent: opts.rootIntent }), ...(goalType !== undefined && { goalType }), ...goalTitleDeps };
295
+ ? { parent: { goalFile: parentGoalFile, questionId: parentQuestionId }, parentDocument, clarificationAnswers, ...(decomposeSteps ? { decomposeSteps } : {}), ...gradeDeps, ...selfResolutionDeps, ...progressDeps, ...launchPreflightDeps, ...(opts.rootIntent !== undefined && { rootIntent: opts.rootIntent }), ...(goalType !== undefined && { goalType }), ...goalTitleDeps }
296
+ : { ...(clarificationAnswers && { clarificationAnswers }), ...(decomposeSteps ? { decomposeSteps } : {}), ...gradeDeps, ...selfResolutionDeps, ...progressDeps, ...launchPreflightDeps, ...(opts.rootIntent !== undefined && { rootIntent: opts.rootIntent }), ...(goalType !== undefined && { goalType }), ...goalTitleDeps };
289
297
  const write = deps.write ?? writeAuthoredGoal;
290
298
  const result = opts.groundingCwd === undefined
291
299
  ? await write(ask, opts.cwd, authorDeps, superseded ? { supersedes: superseded } : undefined)
292
300
  : await write(ask, opts.cwd, authorDeps, superseded ? { supersedes: superseded } : undefined, opts.groundingCwd);
301
+ if (opts.selfResolveClarifications) {
302
+ const rework = Boolean(supersededDocument || opts.reauthorFromAnsweredClarification);
303
+ {
304
+ try {
305
+ const resolved = await selfAnswerGoalDocumentClarifications(result.authored.document, {
306
+ path: rework ? 'rework' : 'say-first', ask, resolver: deps.selfResolveClarification ?? defaultGoalAuthorSelfResolve,
307
+ evidence: result.authored.facts
308
+ ? [...(result.authored.facts.persistentEvidence ?? []), ...result.authored.facts.codeFacts, ...result.authored.facts.documentFacts]
309
+ : undefined,
310
+ });
311
+ if (resolved.newlyAnswered > 0) {
312
+ (deps.writeFile ?? writeGoalDocumentAtomic)(result.path, resolved.document);
313
+ result.authored.document = resolved.document;
314
+ }
315
+ } catch (error) {
316
+ // The previous goal file is left intact (atomic replace); implementation proceeds from it with the questions deferred.
317
+ debug.log('goal-author.clarify', 'self-answer-deferred', { reason: error instanceof Error ? error.message : String(error) });
318
+ }
319
+ }
320
+ }
293
321
  try {
294
322
  const recorded = (deps.recordAsk ?? recordGoalAsk)({
295
323
  authorRunId: result.authored.authorRunId,
@@ -18,6 +18,17 @@ function observeGoalAuthor(
18
18
  debug.log(category === 'goal-author' ? 'harness.author' : 'harness.author.clarify', event, data);
19
19
  }
20
20
  import { getUserConfig } from '../user-config.js';
21
+
22
+ export type GoalAuthorGrade = 'full' | 'lite';
23
+ export type GoalAuthorGradeSource = 'flag' | 'config' | 'default';
24
+ export interface GoalAuthorGradeSelection { grade: GoalAuthorGrade; source: GoalAuthorGradeSource }
25
+
26
+ /** The flag is authoritative; malformed or absent configuration keeps the full pipeline. */
27
+ export function resolveGoalAuthorGrade(flag?: GoalAuthorGrade, configured?: unknown): GoalAuthorGradeSelection {
28
+ if (flag !== undefined) return { grade: flag, source: 'flag' };
29
+ if (configured === 'lite' || configured === 'full') return { grade: configured, source: 'config' };
30
+ return { grade: 'full', source: 'default' };
31
+ }
21
32
  import type { ObserveOnlySource } from './observe-only.js';
22
33
  import type { Question } from '../ask-user-question/types.js';
23
34
  import { requestTestScenario as defaultRequestTestScenario, type TestScenarioRequestInput, type TestScenarioRequestResult } from './test-scenario-request.js';
@@ -45,6 +56,7 @@ type GoalEnhancement = Omit<Pick<EnhanceResult, 'original' | 'checklist' | 'verb
45
56
  export interface GoalAuthorDeps {
46
57
  ground: (ask: string, groundingDeps?: GoalAuthorGroundingDeps) => Promise<CodebaseGrounding>;
47
58
  persistentGrounding?: GoalAuthorGroundingDeps;
59
+ authorGrade?: GoalAuthorGradeSelection;
48
60
  enhance: (ask: string, opts?: EnhanceOpts) => Promise<GoalEnhancement>;
49
61
  /** Additive enhancer options supplied by the file-writing entrypoint. */
50
62
  enhanceOpts?: EnhanceOpts;
@@ -4925,6 +4937,17 @@ async function authorGoalWithSupersededRootIntent(
4925
4937
  }
4926
4938
  // ⭐ 이 저작 «한 번»의 id. goalId 보다 «먼저» 있어야 ground·enhance 의 소요를 그 골에 붙일 수 있다.
4927
4939
  const authorRunId = generateGoalId();
4940
+ const authorGrade = deps.authorGrade ?? resolveGoalAuthorGrade(undefined, getUserConfig().harness?.authorGrade);
4941
+ const lite = authorGrade.grade === 'lite';
4942
+ // lite 는 판정선을 남기는 대신 단계를 덜 한다 — 요청에 반증 줄이 하나도 없으면 판정선 없는 골이 나가므로 시작 전에 멈춘다.
4943
+ if (lite && !ask.split(/\r?\n/u).some((line) => /판정선|판정 신호|반증|decision signal/iu.test(line))) {
4944
+ try { observeGoalAuthor('goal-author', 'author-grade-refused', { authorRunId, reason: 'lite-without-falsifiable-signal' }); }
4945
+ catch { /* observation is fail-soft */ }
4946
+ throw new Error('lite authoring needs a falsifiable signal line (판정선/반증) in the ask — add one or use --author-grade full');
4947
+ }
4948
+ const skippedPhases = lite ? ['adversarial-review', 'goal-steps-decomposed', 'persistent-grounding'] : [];
4949
+ try { observeGoalAuthor('goal-author', 'author-grade', { authorRunId, grade: authorGrade.grade, source: authorGrade.source, skippedPhases }); }
4950
+ catch { /* observation is fail-soft */ }
4928
4951
  const authorStartedAt = Date.now();
4929
4952
  const enhanceTokens: GoalAuthorTokens = { inputTokens: null, outputTokens: null };
4930
4953
  const onEnhanceUsage = (usage: LLMUsage): void => {
@@ -4934,18 +4957,20 @@ async function authorGoalWithSupersededRootIntent(
4934
4957
  };
4935
4958
  let facts: CodebaseGrounding | null = null;
4936
4959
  let groundingError = false;
4937
- const persistentGrounding = resolveGoalAuthorPersistentGrounding('persistentGrounding' in deps ? deps.persistentGrounding : undefined);
4938
- const groundStartedAt = startGoalAuthorPhase('ground', authorRunId, deps.onProgress);
4939
- try {
4940
- facts = await deps.ground(ask, persistentGrounding.deps);
4941
- } catch {
4942
- groundingError = true;
4943
- } finally {
4944
- endGoalAuthorPhase('ground', groundStartedAt, authorRunId, deps.onProgress);
4945
- observeGoalAuthorPersistentGroundingResult(
4946
- persistentGrounding.decision,
4947
- goalAuthorPersistentGroundingMetrics(facts, groundingError, authorRunId),
4948
- );
4960
+ if (!lite) {
4961
+ const persistentGrounding = resolveGoalAuthorPersistentGrounding('persistentGrounding' in deps ? deps.persistentGrounding : undefined);
4962
+ const groundStartedAt = startGoalAuthorPhase('ground', authorRunId, deps.onProgress);
4963
+ try {
4964
+ facts = await deps.ground(ask, persistentGrounding.deps);
4965
+ } catch {
4966
+ groundingError = true;
4967
+ } finally {
4968
+ endGoalAuthorPhase('ground', groundStartedAt, authorRunId, deps.onProgress);
4969
+ observeGoalAuthorPersistentGroundingResult(
4970
+ persistentGrounding.decision,
4971
+ goalAuthorPersistentGroundingMetrics(facts, groundingError, authorRunId),
4972
+ );
4973
+ }
4949
4974
  }
4950
4975
 
4951
4976
  const enhanceStartedAt = startGoalAuthorPhase('enhance', authorRunId, deps.onProgress);
@@ -4989,6 +5014,57 @@ async function authorGoalWithSupersededRootIntent(
4989
5014
  }
4990
5015
  enhancement = { ...enhancement, original: ask };
4991
5016
 
5017
+ if (lite) {
5018
+ const goalType = deps.goalType ?? 'implement';
5019
+ const goalId = deps.goalId ?? generateGoalId();
5020
+ const signal = decisionSignalSection(null, ask, enhancement.decisionSignal);
5021
+ const falsifiableSignal = signal.filled > 0 ? signal.lines : [
5022
+ ...signal.lines,
5023
+ ...ask.split(/\r?\n/u).filter((line) => /판정선|판정 신호|반증|decision signal/iu.test(line) && !signal.lines.some((rendered) => rendered.includes(line.trim()))),
5024
+ ];
5025
+ const targetPaths = [...new Set(declaredTargetPaths(ask))];
5026
+ const checklistIds = [...new Set(Array.from(ask.matchAll(/\b[A-Z][A-Z0-9]+-[A-Z0-9]+\b/g), (match) => match[0]))];
5027
+ const askFile = deps.askFile === undefined ? undefined : validateAskFile(deps.askFile);
5028
+ const inheritedRootIntent = deps.parentDocument === undefined ? null : parseRootIntent(deps.parentDocument);
5029
+ const rootIntent = validateRootIntent(deps.supersessionRootIntent ?? inheritedRootIntent ?? deps.rootIntent ?? goalSummary(ask));
5030
+ const document = [
5031
+ deps.goalTitle ?? goalSummary(ask),
5032
+ `- GoalId: ${goalId}`,
5033
+ `- RootIntent: ${rootIntent}`,
5034
+ `- GoalType: ${goalType}`,
5035
+ ...(askFile === undefined ? [] : [`- AskFile: ${askFile}`]),
5036
+ '',
5037
+ '## PROBLEM',
5038
+ `- 왜: ${enhancement.complication ?? goalSummary(ask)}`,
5039
+ '',
5040
+ '## WHAT TO BUILD',
5041
+ `- 무엇: ${enhancement.situation ?? goalSummary(ask)}`,
5042
+ ORIGINAL_ASK_MARKER,
5043
+ fenceFor(ask), ask, fenceFor(ask),
5044
+ '',
5045
+ '## ACCEPTANCE CRITERIA',
5046
+ ...enhancement.checklist.map(requestedCriterionLine),
5047
+ '',
5048
+ '## REQUIRED EVIDENCE',
5049
+ '- [requested] Verify the decision signal and the verbatim ask.',
5050
+ '',
5051
+ ...(goalType === 'implement' ? ['## TRACED PATHS', ...(targetPaths.length ? targetPaths.map((path) => `- ${path}`) : ['- Target paths: not declared in the ask.']), ''] : []),
5052
+ '## SCOPE BOUNDARY',
5053
+ `- 대상 경로: ${targetPaths.length ? targetPaths.join(' · ') : 'not declared'}`,
5054
+ `- 칸 id: ${checklistIds.length ? checklistIds.join(' · ') : 'not declared'}`,
5055
+ '',
5056
+ '## 답하지 못하는 것',
5057
+ '- No persistent grounding was performed in lite authoring.',
5058
+ '',
5059
+ '## 불변식',
5060
+ '- No persistent grounding was performed in lite authoring.',
5061
+ '',
5062
+ '## 판정 신호',
5063
+ ...falsifiableSignal,
5064
+ ].join('\n');
5065
+ return { document, facts: null, grounded: false, authorRunId };
5066
+ }
5067
+
4992
5068
  observeGoalAuthor('goal-author', 'constant-instruction-placement', {
4993
5069
  authorRunId,
4994
5070
  lines: CONSTANT_DOCUMENT_INSTRUCTION_POLICY.length,
@@ -11,9 +11,13 @@ import {
11
11
  writePendingQuestion,
12
12
  } from '../ask-user-question/pending-questions.js';
13
13
  import { debug } from '../debug/log.js';
14
+ import { writeGoalDocumentAtomic } from './goal-document-write.js';
14
15
  import {
15
16
  applyClarificationReply,
16
17
  parseGoalDocumentClarifications,
18
+ selfAnswerGoalDocumentClarifications,
19
+ type GoalClarificationSelfResolver,
20
+ type GoalClarificationSelfAnswerPath,
17
21
  } from './goal-author-clarification.js';
18
22
  import type { HitlDelivery } from '../hitl/types.js';
19
23
 
@@ -49,6 +53,8 @@ export interface GoalClarificationEscalationInput {
49
53
  resolveOriginDelivery?: (sessionId: string) => HitlDelivery | undefined;
50
54
  /** Non-blocking terminal fallback for a non-TUI delivery with no installed resolver. */
51
55
  fallback?: (message: string) => void;
56
+ selfResolveClarification?: GoalClarificationSelfResolver;
57
+ selfAnswerPath?: GoalClarificationSelfAnswerPath;
52
58
  /** Fail-soft pending persistence for terminal fallbacks; injectable for focused tests. */
53
59
  pendingQuestionPersistence?: {
54
60
  create: typeof createPendingQuestion;
@@ -404,6 +410,7 @@ export async function escalateGoalDocumentClarifications(
404
410
  const dispatchDelivery = delivery ?? 'modal';
405
411
  const recordedDelivery = input.resolvedDelivery ?? dispatchDelivery;
406
412
  let answersWritten = 0;
413
+ let selfAnswered = 0;
407
414
  let questionIds: string[] = [];
408
415
  const observeEscalation = (event: GoalClarificationObservationEvent, data: Record<string, unknown>): void => {
409
416
  writeEscalationObservation(event, { ...data, deliverySource, answersWritten, questionIds });
@@ -430,6 +437,35 @@ export async function escalateGoalDocumentClarifications(
430
437
  return result;
431
438
  }
432
439
 
440
+ if (input.selfAnswerPath) {
441
+ try {
442
+ const resolution = await selfAnswerGoalDocumentClarifications(document, {
443
+ path: input.selfAnswerPath,
444
+ ask: document,
445
+ resolver: input.selfResolveClarification,
446
+ });
447
+ if (resolution.newlyAnswered > 0) {
448
+ writeGoalDocumentAtomic(input.goalFile, resolution.document);
449
+ answersWritten += resolution.newlyAnswered;
450
+ selfAnswered += resolution.newlyAnswered;
451
+ document = resolution.document;
452
+ clarifications = parseGoalDocumentClarifications(document);
453
+ unanswered = clarifications.filter((clarification) => !clarification.answered);
454
+ deliveryIds = mapClarificationDeliveryIds(clarifications);
455
+ questionIds = [...new Set(unanswered.map((clarification) => clarification.questionId))];
456
+ }
457
+ if (unanswered.length === 0) {
458
+ const result = { unanswered: 0, escalated: 0, delivery: recordedDelivery, outcome: 'skipped' as const, answeredBy: 'agent' as const };
459
+ observeEscalation(result.outcome, { goalFile: input.goalFile, ...result, reason: 'self-answered' });
460
+ return result;
461
+ }
462
+ } catch (error) {
463
+ debug.log(GOAL_CLARIFICATION_ESCALATION_CATEGORY, 'self-answer-failed', {
464
+ path: input.selfAnswerPath, reason: error instanceof Error ? error.message : String(error),
465
+ });
466
+ }
467
+ }
468
+
433
469
  const dispatch = input.dispatch ?? dispatchAskUserQuestion;
434
470
  const pendingQuestionPersistence = input.pendingQuestionPersistence ?? {
435
471
  create: createPendingQuestion,
@@ -444,7 +480,7 @@ export async function escalateGoalDocumentClarifications(
444
480
  // ⛔⭐⭐⭐ 여기서부터의 조기 반환·폴백은 이 «누적값»을 쓴다 — 'none' 을 하드코딩하면
445
481
  // 앞 배치가 답했는데 뒤 배치가 취소·실패했을 때 「아무도 안 답했다」는 «거짓»이 된다
446
482
  // (무인 리뷰 R2 must-fix · 실측 형태: 관측이 거짓말한다).
447
- const seenProvenance = { explicit: new Set<'human' | 'agent'>(), unmarkedAnswer: false };
483
+ const seenProvenance = { explicit: new Set<'human' | 'agent'>(selfAnswered > 0 ? ['agent' as const] : []), unmarkedAnswer: false };
448
484
  const aggregate = (): GoalClarificationAnsweredBy => {
449
485
  const { explicit, unmarkedAnswer } = seenProvenance;
450
486
  // ⭐ 표시 없는 답도 «한 출처»로 센다 — legacy 경로는 사람이다(레포 안 리졸버는 이제 전부 명시한다).
@@ -0,0 +1,28 @@
1
+ /** Atomic goal-document replacement for self-answered clarifications (POD-SELF-ANSWER). */
2
+ import { chmodSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+
4
+ export interface GoalDocumentWriteIo {
5
+ writeFileSync: (path: string, data: string, options?: { mode?: number }) => void;
6
+ renameSync: (from: string, to: string) => void;
7
+ rmSync: (path: string, options: { force: boolean }) => void;
8
+ statSync: (path: string) => { mode: number };
9
+ chmodSync: (path: string, mode: number) => void;
10
+ }
11
+
12
+ const realIo: GoalDocumentWriteIo = { writeFileSync, renameSync, rmSync, statSync, chmodSync };
13
+
14
+ /** Replace a goal document atomically (temp file next to it + rename) so a failed write never leaves a torn goal.
15
+ * The existing file's permission bits are kept, so a 0600 goal stays 0600 after replacement. */
16
+ export function writeGoalDocumentAtomic(path: string, document: string, io: GoalDocumentWriteIo = realIo): void {
17
+ let mode: number | undefined;
18
+ try { mode = io.statSync(path).mode & 0o777; } catch { mode = undefined; }
19
+ const temporary = `${path}.self-answer-${process.pid}-${Date.now()}.tmp`;
20
+ try {
21
+ io.writeFileSync(temporary, document, mode === undefined ? undefined : { mode });
22
+ if (mode !== undefined) io.chmodSync(temporary, mode);
23
+ io.renameSync(temporary, path);
24
+ } catch (error) {
25
+ try { io.rmSync(temporary, { force: true }); } catch { /* best effort */ }
26
+ throw error;
27
+ }
28
+ }
@@ -68,6 +68,10 @@ function liveHealRuns(root: string): Set<string> {
68
68
  /** A test process must never start the real heal loop — it spawns external grounding (10-05 load 150 incident). */
69
69
  export async function startHealLoop(file: string, options: HealLoopOptions, env: NodeJS.ProcessEnv = process.env,
70
70
  run: StartHealLoop = async (graph, args) => (await import('../graph-runner/runner.js')).runGraph(graph, args)): Promise<unknown> {
71
+ if (env.ELANOUS_POD_NAME) {
72
+ debug.log('heal.intake', 'skipped-in-pod', { ref: options.input.failureEvent.ref });
73
+ return undefined;
74
+ }
71
75
  if (env.NODE_ENV === 'test' || env.ELANOUS_TEST_HOME) {
72
76
  debug.log('heal.intake', 'loop-start-skipped-test', { runId: options.runId });
73
77
  return undefined;
@@ -30,6 +30,8 @@ export type HostRegateDeps = {
30
30
  acquire?: (repoRoot: string) => Promise<() => void>;
31
31
  log?: (event: 'passed' | 'failed' | 'unmeasured' | 'base-raced', data: Record<string, unknown>) => void;
32
32
  freezeRoot?: string;
33
+ /** Test seam for the operational freeze authority; production always uses the host root. */
34
+ prodFreezeRoot?: string;
33
35
  runExposeGate?: typeof runExposeGate;
34
36
  syncMergedChecklist?: typeof import('../release-loop/merged-pr-checklist.js').syncMergedPrChecklist;
35
37
  };
@@ -241,7 +243,7 @@ export async function runHostRegate(input: HostRegateInput, deps: HostRegateDeps
241
243
  // --match-head-commit pins the head. gh has no base pin, so the base was re-read just
242
244
  // above; the seconds between that read and the merge are checked after the fact below.
243
245
  if (input.verifyOnly) return result('passed');
244
- const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot, ...(input.goalFile ? { goalFile: input.goalFile } : {}) }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit });
246
+ const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot, ...(input.goalFile ? { goalFile: input.goalFile } : {}) }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit }, { prodFreezeRoot: deps.prodFreezeRoot });
245
247
  if (landing.kind !== 'merge') {
246
248
  debug.log('harness.merge', 'frozen', { pr: input.prNumber, ...(landing.kind === 'held' ? { reason: landing.freeze.reason, until: landing.freeze.until } : { resumedElsewhere: true }) });
247
249
  return { passed: true, failures: [], os: process.platform, status: 'frozen' };
@@ -34,6 +34,7 @@ import { judgePredictionAccuracy } from './judge-prediction-accuracy.js';
34
34
  import { measureReviewFindingRecurrence, type ReviewFindingRecurrence } from './review-finding-recurrence.js';
35
35
  import { createHash } from 'node:crypto';
36
36
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
37
+ import { writeGoalDocumentAtomic } from './goal-document-write.js';
37
38
  import { hostname, loadavg } from 'node:os';
38
39
  import { resolveHostId } from '../platform/host-id.js';
39
40
  import { resolveInstanceName } from '../instance-identity.js';
@@ -242,7 +243,7 @@ export type { QuotaAccountAvailabilityEvidence };
242
243
  import { decideReworkSalvage, type ReworkSalvageEvidence } from './rework-salvage.js';
243
244
  import { type SupervisorGoalFillEvidence, fillUnverifiableGoalSlots, type SupervisorGoalFillResult } from './goal-supervisor-fill.js';
244
245
  import { escalateGoalDocumentClarifications, recordSupervisorPlanRevision, type GoalClarificationEscalationResult, type SupervisorPlanRevisionRelaxation, type SupervisorPlanRevisionResult } from './goal-clarification-escalation.js';
245
- import { parseGoalDocumentClarifications } from './goal-author-clarification.js';
246
+ import { parseGoalDocumentClarifications, selfAnswerGoalDocumentClarifications, type GoalClarificationSelfResolver } from './goal-author-clarification.js';
246
247
  import {
247
248
  createFileAskUserQuestionResolver,
248
249
  type AskUserQuestionDispatchContext,
@@ -1154,6 +1155,8 @@ export interface SelfImplementSeams {
1154
1155
  /** ⭐ 「이 표면에 답할 사람이 있나」. 기본은 실제 stdin 의 TTY 여부다 —
1155
1156
  * ⛔ 실물에 매이면 테스트가 «환경»을 검사하게 되므로 주입 가능하게 둔다. */
1156
1157
  stdinIsInteractive?: () => boolean;
1158
+ /** Optional author self-answer seam for non-TTY Pods; never installs a terminal HITL resolver. */
1159
+ selfResolveClarification?: GoalClarificationSelfResolver;
1157
1160
  }
1158
1161
 
1159
1162
  /** onProgress seam 이 통지하는 단계(관측·UX 라벨). */
@@ -5364,6 +5367,7 @@ async function runSelfImplementInner(
5364
5367
  ? { resolvedDelivery: installDecision.delivery }
5365
5368
  : {}),
5366
5369
  ...(s.escalateGoalClarifications ? { dispatch: s.escalateGoalClarifications } : {}),
5370
+ ...(!stdinIsInteractive && !clarificationResolverLease.installed ? { selfAnswerPath: 'ask' as const, selfResolveClarification: s.selfResolveClarification } : {}),
5367
5371
  fallback: (message) => process.stdout.write(`${message}\n`),
5368
5372
  });
5369
5373
  } finally {
@@ -6175,6 +6179,20 @@ async function runSelfImplementInner(
6175
6179
  if (relaxation) pendingPlanRevision = { ...pendingPlanRevision!, relaxation };
6176
6180
  if (opts.goalFile) {
6177
6181
  try {
6182
+ if (!(s.stdinIsInteractive ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()) {
6183
+ try {
6184
+ const source = readFileSync(opts.goalFile, 'utf8');
6185
+ // Always run the shared self-answer so every rework pass leaves one observation, even with nothing pending.
6186
+ const resolved = await selfAnswerGoalDocumentClarifications(source, {
6187
+ path: 'rework', ask: source, resolver: s.selfResolveClarification,
6188
+ });
6189
+ if (resolved.newlyAnswered > 0) writeGoalDocumentAtomic(opts.goalFile, resolved.document);
6190
+ } catch (error) {
6191
+ debug.log('self-implement.clarification-escalation', 'self-answer-failed', {
6192
+ path: 'rework', reason: error instanceof Error ? error.message : String(error),
6193
+ });
6194
+ }
6195
+ }
6178
6196
  const recorded: SupervisorPlanRevisionResult = recordSupervisorPlanRevision(opts.goalFile, { reason: parsedDecision.reason, round, ...(relaxation ? { relaxation } : {}) });
6179
6197
  pendingPlanRevision = { ...pendingPlanRevision!, ...(relaxation ? { relaxation } : {}), application: {
6180
6198
  status: recorded.status === 'applied' || recorded.status === 'already-applied' ? recorded.status : 'failed',
@@ -0,0 +1,118 @@
1
+ export type NextActionKind = 'review' | 'land' | 'retry' | 'green' | 'decision';
2
+ export interface NextAction {
3
+ kind: NextActionKind;
4
+ taskId: string;
5
+ rationale: string;
6
+ pr: number;
7
+ runId: string;
8
+ original: string;
9
+ checklistId: string;
10
+ history?: string[];
11
+ mustFix?: string[];
12
+ cwd?: string;
13
+ risk?: 'money' | 'security' | 'irreversible-publication';
14
+ }
15
+ export interface ActionDeps {
16
+ command(args: string[]): Promise<{ status: number; stdout: string; stderr?: string }>;
17
+ mode?: string;
18
+ landingsToday?: number;
19
+ landingDay?: string;
20
+ saveState?(): void;
21
+ dailyLimit?: number;
22
+ failures?: number;
23
+ failureCounts?: Record<string, number>;
24
+ /** 하루 착지 칸을 잠금 안에서 먼저 확보한다 — 다른 프로세스와 같은 잔여 칸을 두 번 쓰지 않게. 없으면 메모리 셈. */
25
+ reserveLanding?(day: string, limit: number): boolean;
26
+ /** 병합이 확인되지 않으면 확보한 칸을 돌려준다. */
27
+ releaseLanding?(day: string): void;
28
+ /** 실패 한 번을 잠금 안에서 디스크에 더한다(다른 프로세스의 실패를 덮지 않게) · 더한 뒤의 값을 돌려준다. */
29
+ incrementFailure?(key: string): number;
30
+ observe(event: 'task-agent.action', data: { taskId: string; kind: NextActionKind; result: string; reason: string }): void;
31
+ }
32
+
33
+ export async function executeNextAction(action: NextAction, deps: ActionDeps): Promise<string> {
34
+ const record = (result: string, reason: string) => {
35
+ if (result === 'failed' && (deps.failureCounts || deps.incrementFailure)) {
36
+ const key = JSON.stringify([action.taskId, action.kind]);
37
+ const next = deps.incrementFailure ? deps.incrementFailure(key) : (deps.failureCounts![key] ?? 0) + 1;
38
+ if (deps.failureCounts) deps.failureCounts[key] = next;
39
+ if (!deps.incrementFailure) deps.saveState?.();
40
+ }
41
+ deps.observe('task-agent.action', { taskId: action.taskId, kind: action.kind, result, reason });
42
+ return result;
43
+ };
44
+ if (deps.mode !== 'live') return record('shadow', `would ${action.kind}: ${action.rationale}`);
45
+ const card = async (reason: string) => {
46
+ const evidence = JSON.stringify({ runId: action.runId, pr: action.pr, mustFix: action.mustFix ?? [], history: action.history ?? [], selected: action.kind, reason });
47
+ const result = await deps.command(['decisions', 'raise', '--title', `${action.taskId}: ${reason}`, '--category', action.risk === 'money' ? 'money' : action.risk === 'security' ? 'security' : action.risk === 'irreversible-publication' ? 'publish' : 'other', '--s', `Task ${action.taskId} needs a decision.`, '--c', reason, '--ref', evidence, '--q', 'Which action should be taken?', '--option', 'h=Keep on hold:no automatic execution', '--option', 'p=Approve action:execute after approval', '--skip-recommend', 'requires human decision', '--no-xcheck', 'automated escalation']);
48
+ return record(result.status === 0 ? 'decision' : 'failed', reason);
49
+ };
50
+ if (action.risk) return card(`risk: ${action.risk}`);
51
+ if ((deps.failureCounts?.[JSON.stringify([action.taskId, action.kind])] ?? deps.failures ?? 0) >= 2) return card('same task/action failed twice');
52
+ if (action.kind === 'land') {
53
+ if (!action.cwd) return card('landing worktree unknown');
54
+ const view = await deps.command(['gh', 'pr', 'view', String(action.pr), '--json', 'labels']);
55
+ if (view.status !== 0) return card('PR labels unavailable');
56
+ let labels: unknown;
57
+ try { labels = (JSON.parse(view.stdout) as { labels?: unknown }).labels; }
58
+ catch { return card('PR labels unavailable'); }
59
+ if (!Array.isArray(labels) || !labels.every(label => label && typeof label === 'object' && typeof label.name === 'string')) return card('PR labels unavailable');
60
+ if (labels.some(label => label.name === 'elanous:release-path')) return card('release-path');
61
+ const day = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
62
+ if (deps.landingDay && deps.landingDay !== day) deps.landingsToday = 0;
63
+ deps.landingDay = day;
64
+ const limit = deps.dailyLimit ?? 5;
65
+ if (deps.reserveLanding ? !deps.reserveLanding(day, limit) : (deps.landingsToday ?? 0) >= limit) return card('daily landing limit');
66
+ }
67
+ if (action.kind === 'decision') return card(action.rationale);
68
+ if (action.kind === 'green') return card('green requires a verified landing in this execution');
69
+ if (action.kind === 'retry' && !action.mustFix?.length) return card('must-fix missing; cannot narrow retry');
70
+ if (action.kind === 'review') {
71
+ const review = await deps.command(['self', 'review', String(action.pr), '--json']);
72
+ if (review.status !== 0) return record('failed', 'review command failed');
73
+ let parsed: { verdict?: string; mustFix?: string[]; reviewed?: boolean };
74
+ try {
75
+ const payload: unknown = JSON.parse(review.stdout);
76
+ const item = Array.isArray(payload) ? payload.find((entry: unknown) => entry && typeof entry === 'object' && 'pr' in entry && Number(entry.pr) === action.pr) : payload;
77
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return record('failed', 'invalid review JSON');
78
+ parsed = item as typeof parsed;
79
+ } catch { return record('failed', 'invalid review JSON'); }
80
+ if (parsed.reviewed !== true) return record('failed', 'review was not performed');
81
+ if (parsed.verdict === 'pass') { record('done', 'review pass'); return executeNextAction({ ...action, kind: 'land', history: [...(action.history ?? []), 'review pass'] }, deps); }
82
+ if (parsed.verdict === 'fail' && Array.isArray(parsed.mustFix) && parsed.mustFix.length > 0 && parsed.mustFix.every(fix => typeof fix === 'string' && fix.trim())) { record('done', 'review fail'); return executeNextAction({ ...action, kind: 'retry', mustFix: parsed.mustFix, history: [...(action.history ?? []), 'review fail'] }, deps); }
83
+ return card('review verdict or must-fix unavailable');
84
+ }
85
+ // 여기 닿는 종류는 land · retry 둘뿐이다(green·decision·review 는 위에서 끝난다).
86
+ const args = action.kind === 'land' ? ['pr', 'land', '--cwd', action.cwd!]
87
+ : ['harness', 'say', `${action.original}\n\nMust-fix:\n${(action.mustFix ?? []).join('\n')}\n\nPrevious run: ${action.runId}`];
88
+ const day = deps.landingDay ?? new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
89
+ const outcome = await deps.command(args);
90
+ if (outcome.status !== 0) {
91
+ if (action.kind === 'land') deps.releaseLanding?.(day);
92
+ return record('failed', `${action.kind} command failed: ${outcome.stderr ?? ''}`);
93
+ }
94
+ if (action.kind === 'land') {
95
+ const merged = await deps.command(['gh', 'pr', 'view', String(action.pr), '--json', 'state,mergeCommit']);
96
+ let sha: string | undefined;
97
+ let notMerged = false;
98
+ try {
99
+ const value = JSON.parse(merged.stdout) as { state?: string; mergeCommit?: { oid?: string } };
100
+ if (merged.status === 0 && value.state === 'MERGED' && /^[a-f0-9]{40}$/i.test(value.mergeCommit?.oid ?? '')) sha = value.mergeCommit?.oid;
101
+ else if (merged.status === 0 && typeof value.state === 'string' && value.state !== 'MERGED') notMerged = true;
102
+ } catch { /* unknown merge state is never green */ }
103
+ if (!sha) {
104
+ // «열림»으로 확인됐을 때만 칸을 돌려준다. 상태를 못 읽었으면 병합됐을 수 있으니 칸은 쓴 것으로 둔다(상한을 넘지 않게).
105
+ if (notMerged) deps.releaseLanding?.(day);
106
+ return card('landing commit unknown; checklist remains unchanged');
107
+ }
108
+ record('done', action.rationale);
109
+ if (!deps.reserveLanding) deps.landingsToday = (deps.landingsToday ?? 0) + 1;
110
+ deps.saveState?.();
111
+ const green = await deps.command(['release', 'checklist', 'set', action.checklistId, '--status', 'green', '--evidence', `${action.pr}·${sha}`]);
112
+ // 체크리스트 실패는 이 과제·land 의 실패로 센다(두 번이면 다음엔 카드).
113
+ if (green.status !== 0) return record('failed', `green command failed: ${green.stderr ?? ''}`);
114
+ deps.observe('task-agent.action', { taskId: action.taskId, kind: 'green', result: 'done', reason: action.rationale });
115
+ return 'done';
116
+ }
117
+ return record('done', action.rationale);
118
+ }