elanous 0.2.21 → 0.2.22

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 (315) 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-08b716476f1d50af.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
  58. package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
  59. package/apps/pwa/out/approvals/index.html +1 -1
  60. package/apps/pwa/out/approvals/index.txt +14 -14
  61. package/apps/pwa/out/autopilot/index.html +1 -1
  62. package/apps/pwa/out/autopilot/index.txt +14 -14
  63. package/apps/pwa/out/board/index.html +1 -1
  64. package/apps/pwa/out/board/index.txt +14 -14
  65. package/apps/pwa/out/botlab/index.html +1 -1
  66. package/apps/pwa/out/botlab/index.txt +14 -14
  67. package/apps/pwa/out/bots/index.html +1 -1
  68. package/apps/pwa/out/bots/index.txt +14 -14
  69. package/apps/pwa/out/ceo/index.html +1 -1
  70. package/apps/pwa/out/ceo/index.txt +14 -14
  71. package/apps/pwa/out/chat/index.html +1 -1
  72. package/apps/pwa/out/chat/index.txt +14 -14
  73. package/apps/pwa/out/consult/index.html +1 -1
  74. package/apps/pwa/out/consult/index.txt +14 -14
  75. package/apps/pwa/out/control/index.html +1 -1
  76. package/apps/pwa/out/control/index.txt +14 -14
  77. package/apps/pwa/out/decisions/index.html +1 -1
  78. package/apps/pwa/out/decisions/index.txt +14 -14
  79. package/apps/pwa/out/design-check/index.html +1 -1
  80. package/apps/pwa/out/design-check/index.txt +14 -14
  81. package/apps/pwa/out/editor/index.html +1 -1
  82. package/apps/pwa/out/editor/index.txt +14 -14
  83. package/apps/pwa/out/exec/index.html +1 -1
  84. package/apps/pwa/out/exec/index.txt +14 -14
  85. package/apps/pwa/out/field/index.html +1 -1
  86. package/apps/pwa/out/field/index.txt +14 -14
  87. package/apps/pwa/out/healthz +1 -1
  88. package/apps/pwa/out/index.html +1 -1
  89. package/apps/pwa/out/index.txt +14 -14
  90. package/apps/pwa/out/inside/index.html +1 -1
  91. package/apps/pwa/out/inside/index.txt +14 -14
  92. package/apps/pwa/out/intake/index.html +1 -1
  93. package/apps/pwa/out/intake/index.txt +14 -14
  94. package/apps/pwa/out/live/index.html +1 -1
  95. package/apps/pwa/out/live/index.txt +14 -14
  96. package/apps/pwa/out/live-run/index.html +1 -1
  97. package/apps/pwa/out/live-run/index.txt +14 -14
  98. package/apps/pwa/out/loops/index.html +1 -1
  99. package/apps/pwa/out/loops/index.txt +14 -14
  100. package/apps/pwa/out/market/index.html +1 -1
  101. package/apps/pwa/out/market/index.txt +14 -14
  102. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  103. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  104. package/apps/pwa/out/morning/index.html +1 -1
  105. package/apps/pwa/out/morning/index.txt +14 -14
  106. package/apps/pwa/out/observatory/index.html +1 -1
  107. package/apps/pwa/out/observatory/index.txt +14 -14
  108. package/apps/pwa/out/ops/checklist/index.html +1 -1
  109. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  110. package/apps/pwa/out/ops/release/index.html +1 -1
  111. package/apps/pwa/out/ops/release/index.txt +14 -14
  112. package/apps/pwa/out/ops/seats/index.html +1 -1
  113. package/apps/pwa/out/ops/seats/index.txt +14 -14
  114. package/apps/pwa/out/outputs/index.html +1 -1
  115. package/apps/pwa/out/outputs/index.txt +14 -14
  116. package/apps/pwa/out/reflection/index.html +1 -1
  117. package/apps/pwa/out/reflection/index.txt +15 -15
  118. package/apps/pwa/out/scheduler/index.html +1 -1
  119. package/apps/pwa/out/scheduler/index.txt +14 -14
  120. package/apps/pwa/out/sessions/index.html +1 -1
  121. package/apps/pwa/out/sessions/index.txt +14 -14
  122. package/apps/pwa/out/settings/devices/index.html +1 -1
  123. package/apps/pwa/out/settings/devices/index.txt +14 -14
  124. package/apps/pwa/out/settings/index.html +1 -1
  125. package/apps/pwa/out/settings/index.txt +14 -14
  126. package/apps/pwa/out/setup/done/index.html +1 -1
  127. package/apps/pwa/out/setup/done/index.txt +14 -14
  128. package/apps/pwa/out/setup/index.html +1 -1
  129. package/apps/pwa/out/setup/index.txt +14 -14
  130. package/apps/pwa/out/share/index.html +1 -1
  131. package/apps/pwa/out/share/index.txt +14 -14
  132. package/apps/pwa/out/showroom/index.html +1 -1
  133. package/apps/pwa/out/showroom/index.txt +14 -14
  134. package/apps/pwa/out/tasks/index.html +1 -1
  135. package/apps/pwa/out/tasks/index.txt +14 -14
  136. package/apps/pwa/out/term/index.html +1 -1
  137. package/apps/pwa/out/term/index.txt +14 -14
  138. package/apps/pwa/out/today/index.html +1 -1
  139. package/apps/pwa/out/today/index.txt +14 -14
  140. package/apps/pwa/out/trace/index.html +1 -1
  141. package/apps/pwa/out/trace/index.txt +14 -14
  142. package/apps/pwa/out/vault/index.html +1 -1
  143. package/apps/pwa/out/vault/index.txt +14 -14
  144. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  145. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  146. package/apps/pwa/out/workflows/index.html +1 -1
  147. package/apps/pwa/out/workflows/index.txt +14 -14
  148. package/apps/pwa/out/workspace/index.html +1 -1
  149. package/apps/pwa/out/workspace/index.txt +14 -14
  150. package/apps/pwa/out/worktrees/index.html +1 -1
  151. package/apps/pwa/out/worktrees/index.txt +14 -14
  152. package/graphs/catalog/catalog.yaml +1 -1
  153. package/graphs/drafts/recipes.yaml +2 -2
  154. package/graphs/test-diet/recipes.yaml +3 -0
  155. package/graphs/test-diet/test-diet.yaml +3 -1
  156. package/package.json +1 -1
  157. package/scripts/f-session-monitor.ts +69 -0
  158. package/scripts/hq/standby-snapshot.ts +7 -3
  159. package/scripts/knowledge-compress.ts +11 -0
  160. package/scripts/nightly-docops.ts +90 -3
  161. package/scripts/research-coo-admin-dates.ts +39 -0
  162. package/scripts/skills/public-check.ts +7 -2
  163. package/scripts/smart-merge/resync-shadow.ts +5 -3
  164. package/scripts/test-diet/lib.ts +81 -1
  165. package/scripts/test-diet/node.ts +48 -2
  166. package/scripts/verify-coo-admin-evidence.ts +83 -0
  167. package/src/acp/client.ts +4 -2
  168. package/src/agent/run-context.ts +4 -0
  169. package/src/agent-env/profile.ts +25 -5
  170. package/src/agent-mission/driver.ts +26 -12
  171. package/src/agent-mission/supervisor-metrics.ts +17 -2
  172. package/src/agent-substrate/acp-reviewer.ts +12 -5
  173. package/src/agent-substrate/pr-reviewer.ts +11 -4
  174. package/src/agent-substrate/review-intent.ts +102 -3
  175. package/src/autopilot/build/llm-conflict-merge.ts +22 -8
  176. package/src/autopilot/pr-manager.ts +18 -8
  177. package/src/away/release-watch.ts +47 -8
  178. package/src/briefing/brief-items.ts +104 -11
  179. package/src/chat/index.ts +1 -0
  180. package/src/cli/brief-cli.ts +20 -2
  181. package/src/cli/config-classify.ts +83 -0
  182. package/src/cli/control-cli.ts +14 -0
  183. package/src/cli/decisions-cli.ts +26 -16
  184. package/src/cli/logs-model-input.ts +1 -1
  185. package/src/cli/pr-cli.ts +65 -8
  186. package/src/cli/release-cli.ts +21 -4
  187. package/src/cli/tasks-cli.ts +108 -1
  188. package/src/connectors/types.ts +6 -6
  189. package/src/context-bus/context-now-surfaces.ts +18 -0
  190. package/src/context-bus/coord-events.ts +32 -4
  191. package/src/context-bus/long-term-memory.ts +23 -6
  192. package/src/context-bus/nightly-condense.ts +169 -21
  193. package/src/control/control-status.ts +87 -0
  194. package/src/control-plane/light-member.ts +5 -3
  195. package/src/control-plane/member.ts +172 -7
  196. package/src/core-turn/run-goal-loop.ts +22 -1
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
  198. package/src/decisions/decision-cards.ts +20 -1
  199. package/src/decisions/decision-ledger.ts +11 -8
  200. package/src/decisions/web-push-decision-cards.ts +19 -3
  201. package/src/discord-self-message.ts +2 -2
  202. package/src/domains/coo-admin-dates.evidence.json +876 -0
  203. package/src/domains/coo-admin-tool.ts +48 -14
  204. package/src/domains/ops-status.ts +53 -0
  205. package/src/domains/outbound-alert.ts +24 -1
  206. package/src/exec-requests/planner.ts +6 -0
  207. package/src/graph-kinds/registry.ts +6 -6
  208. package/src/graph-runner/graph-cli.ts +34 -2
  209. package/src/graph-runner/graph-run-api.ts +7 -0
  210. package/src/graph-runner/graph-tick.ts +73 -12
  211. package/src/graph-runner/runner.ts +43 -11
  212. package/src/graph-unify/wf-to-graph.ts +79 -0
  213. package/src/graph-wizard/generate.ts +34 -2
  214. package/src/graph-wizard/steps.ts +13 -0
  215. package/src/harness/harness-cli-command.ts +121 -23
  216. package/src/harness/harness-queue.ts +58 -14
  217. package/src/harness/launch-advice.ts +23 -0
  218. package/src/index.ts +36 -7
  219. package/src/knowledge/conclusion-compress.ts +93 -0
  220. package/src/knowledge/know-find.ts +9 -2
  221. package/src/knowledge/query.ts +20 -0
  222. package/src/llm.ts +26 -16
  223. package/src/loops/neighbor-runtime.ts +88 -0
  224. package/src/loops/orchestrator/author-depth.ts +29 -11
  225. package/src/loops/orchestrator/overlap.ts +7 -1
  226. package/src/loops/orchestrator/resync-shadow.ts +1 -1
  227. package/src/loops/orchestrator/tick.ts +35 -2
  228. package/src/loops/orchestrator/traffic.ts +2 -0
  229. package/src/loops/registry.ts +5 -0
  230. package/src/market/publish.ts +17 -1
  231. package/src/maturity/feature-maturity.ts +2 -1
  232. package/src/mss/logging/redaction.ts +17 -0
  233. package/src/nexus/api/draft-metrics.ts +22 -8
  234. package/src/nexus/api/graph-peer-edit.ts +77 -0
  235. package/src/nexus/api/graph-wizard-api.ts +9 -1
  236. package/src/nexus/api/graphs-api.ts +145 -7
  237. package/src/nexus/api/http-server.ts +93 -8
  238. package/src/nexus/api/plugins-market.ts +24 -0
  239. package/src/nexus/index.ts +3 -2
  240. package/src/oauth/claude-pty-maturity.ts +24 -0
  241. package/src/oauth/fallback-chain.ts +27 -14
  242. package/src/plugins/maker/market-bundle.ts +9 -0
  243. package/src/plugins/maker/plugin-maker.ts +9 -3
  244. package/src/plugins/maker/wizard-generate.ts +73 -9
  245. package/src/policy/llm-policy.ts +11 -6
  246. package/src/release-loop/checklist-board.ts +27 -0
  247. package/src/release-loop/checklist.ts +38 -0
  248. package/src/seat-address/seat-address.ts +1 -0
  249. package/src/seat-loop/seat-loop.ts +174 -20
  250. package/src/seat-loop/seat-report.ts +29 -1
  251. package/src/self-dev/dev-cli.ts +90 -8
  252. package/src/self-dev/dev-pipeline.ts +20 -1
  253. package/src/self-dev/draft-sweep.ts +45 -8
  254. package/src/self-dev/orchestrate.ts +3 -0
  255. package/src/self-dev/review-provider-fallback.ts +83 -0
  256. package/src/self-dev/run-supervisor.ts +5 -1
  257. package/src/self-implement/harness-policy-gates.ts +7 -5
  258. package/src/self-implement/host-regate.ts +28 -14
  259. package/src/self-implement/l8-merge-queue.ts +80 -8
  260. package/src/self-implement/orchestrator.ts +68 -27
  261. package/src/self-implement/seams.ts +5 -3
  262. package/src/self-implement/self-implement-cli.ts +4 -0
  263. package/src/shell-env-bootstrap.ts +19 -4
  264. package/src/steward/triage.ts +25 -8
  265. package/src/storage/file-lock.ts +33 -4
  266. package/src/task-agent/completion-evidence.ts +31 -3
  267. package/src/task-agent/cover.ts +61 -0
  268. package/src/task-agent/live-moves.ts +126 -3
  269. package/src/task-agent/shadow.ts +19 -4
  270. package/src/task-agent/task-hand.ts +2 -0
  271. package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
  272. package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
  273. package/src/telegram-commands.ts +8 -0
  274. package/src/telegram-project-command.ts +6 -0
  275. package/src/telegram.ts +2 -2
  276. package/src/typecheck-ratchet.ts +2 -1
  277. package/src/user-config.ts +35 -4
  278. package/src/version/packed-revision.json +1 -1
  279. package/src/workflow-runtime/executor.ts +24 -2
  280. package/src/workflow-runtime/triggers/registry.ts +27 -0
  281. package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
  282. package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
  313. package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
  314. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
  315. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { NEXT_MD_PATH, resolveNextMdConflict } from '../release-loop/next-md-merge.js';
6
+ import { defaultLlmResolve, mergeMainWithLlmResolve, type MergeGitSeam } from '../autopilot/build/llm-conflict-merge.js';
6
7
  import { getDefaultLogStore } from '../mss/logging/log-store.js';
7
8
 
8
9
  type Command = (bin: string, args: readonly string[], cwd: string) => { status: number | null; stdout: string; stderr: string };
@@ -30,7 +31,10 @@ export type L8MergeQueueDeps = {
30
31
 
31
32
  export type L8ShadowVerdict = { number: number; head: string; base?: string; verdict: 'pass' | 'conflict' | 'fail'; detail?: string; tests: string[]; at: string };
32
33
  export type L8ShadowQueueState = { pending: { number: number; head: string }[]; verdicts: L8ShadowVerdict[] };
33
- export type L8ShadowQueueDeps = L8MergeQueueDeps & { observe?: (verdict: L8ShadowVerdict) => void | Promise<void> };
34
+ export type L8ShadowQueueDeps = L8MergeQueueDeps & {
35
+ observe?: (verdict: L8ShadowVerdict) => void | Promise<void>;
36
+ resolveConflict?: (file: string, conflicted: string, target: string, worktree: string) => Promise<string>;
37
+ };
34
38
 
35
39
  function shadowCommand(execute: Command, bin: string, args: string[], cwd: string): string {
36
40
  const result = execute(bin, args, cwd);
@@ -129,7 +133,8 @@ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}
129
133
  if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
130
134
  verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
131
135
  const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
132
- let autoResolvedNextMd = false;
136
+ let integrated = merge.status === 0;
137
+ let resolvedNextMd = false;
133
138
  if (merge.status !== 0) {
134
139
  const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp).split('\n').filter(Boolean);
135
140
  if (!unresolved.length) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
@@ -148,14 +153,81 @@ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}
148
153
  writeFileSync(join(temp, NEXT_MD_PATH), resolved);
149
154
  run('git', ['add', '--', NEXT_MD_PATH], temp);
150
155
  run('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'commit', '-m', 'Integrate PR for L8 shadow evaluation'], temp);
151
- autoResolvedNextMd = true;
156
+ resolvedNextMd = true;
157
+ integrated = true;
152
158
  }
153
159
  } else {
154
- verdict.verdict = 'conflict';
155
- verdict.detail = unresolved.join('\n');
160
+ const unsafe = unresolved.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'));
161
+ if (unsafe) throw new Error('unsafe conflict path');
162
+ const staged = (stage: number, file: string): string | null => {
163
+ const result = execute('git', ['show', `:${stage}:${file}`], temp!);
164
+ return result.status === 0 ? result.stdout : null;
165
+ };
166
+ // Reject unsupported index shapes before consulting either resolver.
167
+ // A directory, submodule or unreadable path is an unsupported conflict shape, not an evaluator failure.
168
+ const hasTextMarkers = (file: string): boolean => {
169
+ try {
170
+ const path = join(temp!, file);
171
+ return existsSync(path) && statSync(path).isFile() && readFileSync(path, 'utf8').includes('<<<<<<< ');
172
+ } catch {
173
+ return false;
174
+ }
175
+ };
176
+ const unsupported = unresolved.some((file) => file !== NEXT_MD_PATH && !hasTextMarkers(file));
177
+ // A non-append-only next.md conflict must not fall through to the LLM even when other files conflict.
178
+ const nextStages = !unsupported && unresolved.includes(NEXT_MD_PATH)
179
+ ? [staged(1, NEXT_MD_PATH), staged(2, NEXT_MD_PATH), staged(3, NEXT_MD_PATH)] as const
180
+ : undefined;
181
+ const nextMd = nextStages
182
+ ? nextStages.some((value) => value === null) ? null
183
+ : resolveNextMdConflict(nextStages[0]!, nextStages[1]!, nextStages[2]!)
184
+ : undefined;
185
+ if (unsupported || nextMd === null || unresolved.some((file) => file !== NEXT_MD_PATH
186
+ && (staged(1, file) === null || staged(2, file) === null || staged(3, file) === null))) {
187
+ verdict.verdict = 'conflict';
188
+ verdict.detail = nextMd === null ? 'release/next.md conflict is not append-only' : unresolved.join('\n');
189
+ } else {
190
+ const git = (args: string[]) => execute('git', args, temp!);
191
+ const mergeGit: MergeGitSeam = {
192
+ isConfiguredRemote: () => false, // item.head is the fetched, pinned commit, not a remote branch.
193
+ fetch: () => false,
194
+ merge: () => ({ ok: false, conflict: true, stdout: merge.stdout, errorDetail: merge.stderr.trim() }),
195
+ conflictedFiles: () => run('git', ['diff', '--name-only', '--diff-filter=U'], temp!).split('\n').filter(Boolean),
196
+ readIndexStage: (_wt, stage, file) => {
197
+ const result = git(['show', `:${stage}:${file}`]);
198
+ if (result.status !== 0) throw new Error(`index stage ${stage} unavailable: ${file}`);
199
+ return result.stdout;
200
+ },
201
+ readFile: (file) => readFileSync(file, 'utf8'),
202
+ writeFile: (file, content) => writeFileSync(file, content),
203
+ add: (_wt, file) => { run('git', ['add', '--', file], temp!); },
204
+ commit: () => {
205
+ const result = git(['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'commit', '-m', 'Integrate PR for L8 shadow evaluation']);
206
+ return { ok: result.status === 0, errorDetail: result.stderr.trim() };
207
+ },
208
+ abort: () => { run('git', ['merge', '--abort'], temp!); },
209
+ };
210
+ const resolved = await mergeMainWithLlmResolve(temp, item.head, async (file, conflicted) => {
211
+ if (file === NEXT_MD_PATH) {
212
+ const next = resolveNextMdConflict(mergeGit.readIndexStage(temp!, 1, file), mergeGit.readIndexStage(temp!, 2, file), mergeGit.readIndexStage(temp!, 3, file));
213
+ if (next === null) throw new Error('release/next.md conflict is not append-only');
214
+ return next;
215
+ }
216
+ return (deps.resolveConflict ?? ((path, content, target, worktree) => defaultLlmResolve(path, content, target, { worktreePath: worktree, mode: 'on' })))(file, conflicted, item.head, temp!);
217
+ }, mergeGit);
218
+ if (resolved.status === 'conflict-unresolved') {
219
+ verdict.verdict = 'conflict';
220
+ verdict.detail = resolved.failedFile === NEXT_MD_PATH ? 'release/next.md conflict is not append-only'
221
+ : `${resolved.failedFile ?? 'merge conflict'}: ${resolved.reason ?? 'context resolution failed'}`;
222
+ } else if (resolved.status === 'error') {
223
+ throw new Error(`git merge failed: ${resolved.errorStep}: ${resolved.errorDetail ?? 'unknown error'}`);
224
+ }
225
+ integrated = resolved.status === 'llm-resolved';
226
+ resolvedNextMd = integrated && resolved.resolvedFiles?.includes(NEXT_MD_PATH) === true;
227
+ }
156
228
  }
157
229
  }
158
- if (merge.status === 0 || autoResolvedNextMd) {
230
+ if (integrated) {
159
231
  if (verdict.tests.length) {
160
232
  if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
161
233
  run('bun', ['install', '--frozen-lockfile'], temp);
@@ -174,7 +246,7 @@ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}
174
246
  } else {
175
247
  verdict.verdict = 'pass';
176
248
  }
177
- if (autoResolvedNextMd) {
249
+ if (resolvedNextMd) {
178
250
  verdict.detail = verdict.detail ? `release/next.md auto-resolved; ${verdict.detail}` : 'release/next.md auto-resolved';
179
251
  }
180
252
  }
@@ -434,7 +434,7 @@ function resolveDefaultBranchTarget(
434
434
  * 🔑 실패일 때만 두 칸을 더한다 — 성공 경로 payload 를 바꾸면 기존 소비자가 흔들린다.
435
435
  * `moduleLoadFailure` 는 ***「산출이 상했다」와 「그 순간 트리가 불일치했다」***를 가르는 값이다.
436
436
  */
437
- type PostSyncStatus = 'merged' | 'llm-resolved';
437
+ type PostSyncStatus = 'merged' | 'llm-resolved' | 'deterministic-resolved';
438
438
 
439
439
  type PostSyncGatePolicy = {
440
440
  exempted: boolean;
@@ -495,7 +495,7 @@ function postSyncGatePolicy(
495
495
  && !introduced && childResponsibility === 'none'
496
496
  && syncStatus === 'llm-resolved';
497
497
  const exempted = !passed
498
- && (timeoutOnly || (!introduced && childResponsibility === 'none' && syncStatus === 'merged'));
498
+ && (timeoutOnly || (!introduced && childResponsibility === 'none' && (syncStatus === 'merged' || syncStatus === 'deterministic-resolved')));
499
499
  return {
500
500
  exempted,
501
501
  exemptionWithheld,
@@ -2256,6 +2256,8 @@ export interface SelfImplementResult {
2256
2256
  branch?: string;
2257
2257
  gate?: SelfImplementGateResult;
2258
2258
  review?: SelfImplementReview;
2259
+ /** Worktree commit observed while this review ran; not inferred from the later PR head. */
2260
+ reviewedHeadCommit?: string;
2259
2261
  completionDisposition?: SelfImplementCompletionDisposition;
2260
2262
  /** Explicit supervisor rework verdict retained independently of workflow execution verdicts. */
2261
2263
  supervisorVerdict?: ReworkBudgetVerdict;
@@ -2362,7 +2364,18 @@ function truncatePrTitle(text: string): string {
2362
2364
  return points.length <= 72 ? text : `${points.slice(0, 69).join('')}...`;
2363
2365
  }
2364
2366
 
2365
- export function prTitle(feature: string): string {
2367
+ export function goalChecklistIds(feature: string, goalFile?: string): string[] {
2368
+ const document = goalFile ? goalDocumentForReleaseNote(goalFile) : feature;
2369
+ const declaration = document.split(/\r?\n/).find((line) => line.startsWith('- 칸 id: '));
2370
+ const declared = declaration?.slice('- 칸 id: '.length).trim();
2371
+ if (declared && declared !== 'not declared') {
2372
+ return [...new Set(declared.split(' · ').map((id) => id.trim()).filter(Boolean))];
2373
+ }
2374
+ const firstLine = feature.split(/\r?\n/, 1)[0] ?? '';
2375
+ return [...new Set(Array.from(firstLine.matchAll(/\b[A-Z][A-Z0-9]+-[A-Z0-9]+\b/g), (match) => match[0]))];
2376
+ }
2377
+
2378
+ function prTitleWithoutChecklistId(feature: string): string {
2366
2379
  const proseTitle = parseAskProseTitle(feature);
2367
2380
  if (proseTitle) return truncatePrTitle(proseTitle);
2368
2381
 
@@ -2405,6 +2418,13 @@ export function prTitle(feature: string): string {
2405
2418
  return truncatePrTitle(firstFileName);
2406
2419
  }
2407
2420
 
2421
+ export function prTitle(feature: string, goalFile?: string): string {
2422
+ const title = prTitleWithoutChecklistId(feature);
2423
+ const id = goalChecklistIds(feature, goalFile)[0];
2424
+ if (!id) return title;
2425
+ return truncatePrTitle(title.startsWith(`${id}: `) ? title : `${id}: ${title}`);
2426
+ }
2427
+
2408
2428
  const BLOCKED_DRAFT_PR_HUMAN_JUDGMENT_ANCHOR = '## 사람 판단 필요';
2409
2429
 
2410
2430
  /** 사람이 읽는 중단 사유 상한. 관측 `reason.slice(0, 240)` 과 같은 길이라 표시가 앞부분을 버리지 않는다. */
@@ -2665,6 +2685,7 @@ function prBody(feature: string, implSummary: string, gateLog?: string, review?:
2665
2685
  ? ['', '## 감독 수용 기준 완화', `- 대상: ${planRevision.relaxation.target}`, `- 이전: ${planRevision.relaxation.expected}`, `- 완화: ${planRevision.relaxation.replacement}`, `- 이유: ${planRevision.reason}`, `- 적용: ${planRevision.application?.status ?? 'failed'}${planRevision.application?.detail ? ` (${planRevision.application.detail})` : ''}`, ...(planRevision.disposition ? [`- 충돌 처분: ${planRevision.disposition}`] : [])]
2666
2686
  : [];
2667
2687
  return [
2688
+ `칸: ${goalChecklistIds(feature, goalFile).join(' · ') || '없음'}`,
2668
2689
  '## 요청',
2669
2690
  ...(goalFile ? [`- 골 파일: ${formatGoalFileForPrBody(goalFile)}`] : []),
2670
2691
  feature.trim(),
@@ -2882,6 +2903,7 @@ export function formatBlockedDraftClassificationSection(
2882
2903
 
2883
2904
  function blockedDraftPrBody(feature: string, implSummary: string, state: BlockedDraftPrState, goalFile?: string): string {
2884
2905
  return [
2906
+ `칸: ${goalChecklistIds(feature, goalFile).join(' · ') || '없음'}`,
2885
2907
  BLOCKED_DRAFT_PR_HUMAN_JUDGMENT_ANCHOR,
2886
2908
  state.salvageStatusExpected
2887
2909
  ? BLOCKED_DRAFT_PR_SALVAGE_STATUS_INSTRUCTION
@@ -3638,7 +3660,7 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3638
3660
  }
3639
3661
  try {
3640
3662
  const pushed = opts.seams.preserveBlockedBranch
3641
- ? await opts.seams.preserveBlockedBranch({ cwd: worktreePath, branch, title: prTitle(opts.feature), commit: preSyncCommit })
3663
+ ? await opts.seams.preserveBlockedBranch({ cwd: worktreePath, branch, title: prTitle(opts.feature, opts.goalFile), commit: preSyncCommit })
3642
3664
  : runGitCommand(worktreePath, ['push', '-u', 'origin', `${preSyncCommit}:refs/heads/${branch}`]).status === 0;
3643
3665
  if (!pushed) throw new Error('branch push failed');
3644
3666
  // The worktree is left untouched: a late merge may still be writing it. The harvest source is the pinned commit.
@@ -4963,7 +4985,7 @@ async function runSelfImplementInner(
4963
4985
  role: describeRole({ role: 'executor', executorKind: 'self' }),
4964
4986
  entry: siEntry, capabilities: [...siCaps.active], enhanceActive,
4965
4987
  });
4966
- progress('start', `🔨 self-implement 시작 — ${prTitle(opts.feature)}`);
4988
+ progress('start', `🔨 self-implement 시작 — ${prTitle(opts.feature, opts.goalFile)}`);
4967
4989
 
4968
4990
  let sessionId: string | undefined;
4969
4991
  if (opts.parentSessionId && s.forkSession) {
@@ -5202,6 +5224,7 @@ async function runSelfImplementInner(
5202
5224
  /** GATE-ENV-RETRY: 재시도 뒤에도 남은 환경 결손의 종류. 정의되면 이 라운드의 gate 실패는 «수확 대기»로 끝난다. */
5203
5225
  let gateEnvDeficitKind: GateEnvDeficitKind | undefined;
5204
5226
  let review: SelfImplementReview | undefined;
5227
+ let reviewedHeadCommit: string | undefined;
5205
5228
  /** Review-budget leftovers and the body that carried them, so a later merge drafts from the same text. */
5206
5229
  let preparedReviewBudgetPrBody: string | undefined;
5207
5230
  const reviewMustFixCountHistory: number[] = [];
@@ -5791,11 +5814,11 @@ async function runSelfImplementInner(
5791
5814
  throw new Error(`worktree not found: ${wt.path}`);
5792
5815
  }
5793
5816
  if (s.preserveBlockedBranch) {
5794
- pushed = await s.preserveBlockedBranch({ cwd: wt.path, branch: wt.branch, title: prTitle(opts.feature) });
5817
+ pushed = await s.preserveBlockedBranch({ cwd: wt.path, branch: wt.branch, title: prTitle(opts.feature, opts.goalFile) });
5795
5818
  if (!pushed) throw new Error('브랜치 push가 성공하지 않았음');
5796
5819
  } else {
5797
5820
  preservationErrorStep = 'commit';
5798
- const commit = commitWorktree(wt.path, prTitle(opts.feature));
5821
+ const commit = commitWorktree(wt.path, prTitle(opts.feature, opts.goalFile));
5799
5822
  if (!commit.ok && !/nothing to commit/.test(commit.out)) throw new Error(commit.out);
5800
5823
  committed = commit.ok;
5801
5824
  if (runGitCommand(wt.path, ['remote', 'get-url', 'origin']).status === 0) {
@@ -5875,10 +5898,10 @@ async function runSelfImplementInner(
5875
5898
  let salvageError: string | undefined;
5876
5899
  try {
5877
5900
  if (s.preserveBlockedBranch) {
5878
- if (!await s.preserveBlockedBranch({ cwd: wt.path, branch: salvageBranch, title: prTitle(opts.feature) })) throw new Error('수확 가지 push가 성공하지 않았음');
5901
+ if (!await s.preserveBlockedBranch({ cwd: wt.path, branch: salvageBranch, title: prTitle(opts.feature, opts.goalFile) })) throw new Error('수확 가지 push가 성공하지 않았음');
5879
5902
  } else {
5880
5903
  if (!existsSync(wt.path)) throw new Error(`worktree not found: ${wt.path}`);
5881
- const commit = commitWorktree(wt.path, prTitle(opts.feature));
5904
+ const commit = commitWorktree(wt.path, prTitle(opts.feature, opts.goalFile));
5882
5905
  if (!commit.ok && !/nothing to commit/.test(commit.out)) throw new Error(`로컬 커밋 실패: ${commit.out}`);
5883
5906
  const push = runGitCommand(wt.path, ['push', 'origin', `HEAD:refs/heads/${salvageBranch}`]);
5884
5907
  if (push.status !== 0) throw new Error(push.stderr || push.stdout || 'git push failed');
@@ -5915,10 +5938,10 @@ async function runSelfImplementInner(
5915
5938
  let pushed = false;
5916
5939
  try {
5917
5940
  if (s.preserveBlockedBranch) {
5918
- pushed = await s.preserveBlockedBranch({ cwd: wt.path, branch: wt.branch, title: prTitle(opts.feature) });
5941
+ pushed = await s.preserveBlockedBranch({ cwd: wt.path, branch: wt.branch, title: prTitle(opts.feature, opts.goalFile) });
5919
5942
  } else {
5920
5943
  if (commitFirst) {
5921
- const commit = commitWorktree(wt.path, prTitle(opts.feature));
5944
+ const commit = commitWorktree(wt.path, prTitle(opts.feature, opts.goalFile));
5922
5945
  if (!commit.ok && !/nothing to commit/.test(commit.out)) throw new Error(`로컬 브랜치 커밋 실패: ${commit.out}`);
5923
5946
  }
5924
5947
  const push = runGitCommand(wt.path, ['push', '-u', 'origin', `HEAD:refs/heads/${wt.branch}`]);
@@ -5942,7 +5965,7 @@ async function runSelfImplementInner(
5942
5965
  if (stage === 'review-blocked' && podReviewBlockedDraft) {
5943
5966
  let commitError: string | undefined;
5944
5967
  try {
5945
- const commit = commitWorktree(wt.path, prTitle(opts.feature));
5968
+ const commit = commitWorktree(wt.path, prTitle(opts.feature, opts.goalFile));
5946
5969
  if (!commit.ok && !/nothing to commit/.test(commit.out)) commitError = commit.out.trim() || 'git commit failed';
5947
5970
  } catch (error) {
5948
5971
  commitError = safeErrorDescription(error);
@@ -5954,7 +5977,7 @@ async function runSelfImplementInner(
5954
5977
  let pr: { url: string; number: number };
5955
5978
  try {
5956
5979
  pr = await withStepTimeout(s.openPr({
5957
- title: prTitle(opts.feature),
5980
+ title: prTitle(opts.feature, opts.goalFile),
5958
5981
  body: assembled.body,
5959
5982
  head: wt.branch,
5960
5983
  ...(opts.base ? { base: opts.base } : {}),
@@ -6110,7 +6133,7 @@ async function runSelfImplementInner(
6110
6133
  );
6111
6134
  preparedReviewBudgetPrBody = preparedPrBody.body;
6112
6135
  const pr = await withStepTimeout(s.openPr({
6113
- title: prTitle(opts.feature),
6136
+ title: prTitle(opts.feature, opts.goalFile),
6114
6137
  body: preparedPrBody.body,
6115
6138
  head: wt.branch,
6116
6139
  ...(opts.base ? { base: opts.base } : {}),
@@ -6826,7 +6849,7 @@ async function runSelfImplementInner(
6826
6849
  && targets.some((target) => changedFileIsInsideDeclaredTarget(path, target)));
6827
6850
  if (eligible.length) {
6828
6851
  inspectStaleReverts(revertGuardTargetPaths(opts.feature, goalDocument));
6829
- const message = `self-implement: ${prTitle(opts.feature)}`;
6852
+ const message = `self-implement: ${prTitle(opts.feature, opts.goalFile)}`;
6830
6853
  let outcome: { ok: boolean; out: string };
6831
6854
  try {
6832
6855
  outcome = (s.commitFinishedChildWork ?? ((cwd, paths, title) => {
@@ -7347,6 +7370,7 @@ async function runSelfImplementInner(
7347
7370
  lastReviewContext = reviewContext;
7348
7371
  reviewIntent = buildReviewIntent(reviewContext);
7349
7372
 
7373
+ reviewedHeadCommit = undefined;
7350
7374
  if (s.reviewDiff) {
7351
7375
  try {
7352
7376
  const stoppedBeforeReview = honorParentSoftStop('review');
@@ -7356,7 +7380,18 @@ async function runSelfImplementInner(
7356
7380
  }
7357
7381
  onNodeEntry(node('review'), round);
7358
7382
  progress('reviewing', '내부 리뷰 (리뷰어 비평)…');
7383
+ const readReviewHead = s.headCommit ?? ((cwd: string) => {
7384
+ const commit = runGitCommand(cwd, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8', timeout: 10_000 });
7385
+ return commit.status === 0 ? commit.stdout.trim() : undefined;
7386
+ });
7387
+ const headBeforeReview = readReviewHead(wt.path);
7388
+ const statusBeforeReview = runGitCommand(wt.path, ['status', '--porcelain'], { encoding: 'utf8', timeout: 10_000 });
7359
7389
  review = await withStepTimeout(s.reviewDiff(wt.path, reviewContext), T.review, 'review');
7390
+ const statusAfterReview = runGitCommand(wt.path, ['status', '--porcelain'], { encoding: 'utf8', timeout: 10_000 });
7391
+ reviewedHeadCommit = review.reviewed && headBeforeReview && /^[0-9a-f]{40}$/i.test(headBeforeReview)
7392
+ && statusBeforeReview.status === 0 && statusBeforeReview.stdout.trim() === ''
7393
+ && statusAfterReview.status === 0 && statusAfterReview.stdout.trim() === ''
7394
+ && headBeforeReview === readReviewHead(wt.path) ? headBeforeReview : undefined;
7360
7395
  } catch (error) {
7361
7396
  finalizeSupervisorDeliveries(error instanceof StepTimeoutError ? 'next-round-review-timeout' : 'next-round-review-failed');
7362
7397
  throw error;
@@ -7595,6 +7630,7 @@ async function runSelfImplementInner(
7595
7630
  ? { completionDisposition }
7596
7631
  : {};
7597
7632
  const reviewReal = !!review?.reviewed;
7633
+ const reviewedHeadResult = reviewReal && reviewedHeadCommit ? { reviewedHeadCommit } : {};
7598
7634
  const reviewDiffComplete = review?.diffTruncated === false;
7599
7635
  const requiredEvidenceMissing = evidenceCoverage !== undefined && evidenceCoverage.uncoveredCount > 0;
7600
7636
  const { latestSignalIncomplete } = signalIncompleteState(roundClassifications);
@@ -7680,7 +7716,7 @@ async function runSelfImplementInner(
7680
7716
  try { goalDocument = readFileSync(opts.goalFile, 'utf8'); } catch { /* no documented targets */ }
7681
7717
  }
7682
7718
  inspectStaleReverts(revertGuardTargetPaths(opts.feature, goalDocument));
7683
- s.commitWork(wt.path, prTitle(opts.feature));
7719
+ s.commitWork(wt.path, prTitle(opts.feature, opts.goalFile));
7684
7720
  // ⛔⭐ resume 경로와 «같은» 해석기를 쓴다 — 두 자리가 갈리면 같은 원인이 다른 얼굴로 나온다.
7685
7721
  const resolvedTarget = resolveDefaultBranchTarget(s.defaultBranchRef ?? defaultBranchRef, wt.path);
7686
7722
  const mergeTarget = resolvedTarget.target;
@@ -7725,9 +7761,9 @@ async function runSelfImplementInner(
7725
7761
  && await preserveMergeHarvest(sync.reason, sync.inputChars);
7726
7762
  return { ok: false, stage: 'merge-conflict', node: 'main-sync', ...resolveRunOutcome({ termination: 'abandoned' }), ...decisionSignalResult, sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...(harvestable ? { harvestable: true } : {}), detail: harvestable ? sync.reason === 'conflict-input-too-large' ? `충돌 큼 — 사람 수확 (${wt.branch}, 입력 ${sync.inputChars}자)` : `충돌 해결 중단 — 사람 수확 (${wt.branch})` : `pre-PR main sync ${sync.status}` };
7727
7763
  }
7728
- if (sync.status === 'llm-resolved' || sync.status === 'merged') {
7764
+ if (sync.status === 'llm-resolved' || sync.status === 'merged' || sync.status === 'deterministic-resolved') {
7729
7765
  preservationBase = opts.base ?? mergeTarget;
7730
- progress('gating', sync.status === 'llm-resolved'
7766
+ progress('gating', sync.status === 'llm-resolved' || sync.status === 'deterministic-resolved'
7731
7767
  ? `${mergeTarget} 충돌해결됨 — ${formatLlmMergeOutcome(sync)} — 통합 결과 full 재-gate…`
7732
7768
  : `${mergeTarget} 정합됨 — 통합 결과 full 재-gate…`);
7733
7769
  onNodeEntry('regate', round);
@@ -7880,7 +7916,7 @@ async function runSelfImplementInner(
7880
7916
  let pr: { url: string; number: number };
7881
7917
  try {
7882
7918
  pr = await withStepTimeout(s.openPr({
7883
- title: prTitle(opts.feature),
7919
+ title: prTitle(opts.feature, opts.goalFile),
7884
7920
  body: preparedPrBody.body,
7885
7921
  head: wt.branch,
7886
7922
  ...(opts.base ? { base: opts.base } : {}),
@@ -7902,6 +7938,11 @@ async function runSelfImplementInner(
7902
7938
  progress('aborted', formatPrOpenFailedProgress(location, prErrorFirstLine));
7903
7939
  throw new Error(`${location}\n${prError}`);
7904
7940
  }
7941
+ // PR creation may commit generated body/release notes or sync the base after review. Never bind an old review to that new head.
7942
+ if (reviewedHeadCommit) {
7943
+ const current = runGitCommand(wt.path, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8', timeout: 10_000 });
7944
+ if (current.status !== 0 || current.stdout.trim() !== reviewedHeadCommit) delete reviewedHeadResult.reviewedHeadCommit;
7945
+ }
7905
7946
  observe('pr-opened', {
7906
7947
  url: pr.url,
7907
7948
  number: pr.number,
@@ -7951,7 +7992,7 @@ async function runSelfImplementInner(
7951
7992
  }
7952
7993
  recordMergeDecision();
7953
7994
  progress('pr-opened', `OP 승인 대기 (#${pr.number}): ${releaseHoldPath}`);
7954
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason, prUrl: pr.url, prNumber: pr.number, detail: `OP approval required: ${releaseHoldPath}` };
7995
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason, prUrl: pr.url, prNumber: pr.number, detail: `OP approval required: ${releaseHoldPath}` };
7955
7996
  }
7956
7997
  if (!releasePathInspectionError && labels?.includes(AUTO_REVIEW_LABEL)) {
7957
7998
  try {
@@ -7969,7 +8010,7 @@ async function runSelfImplementInner(
7969
8010
  : 'release-path-inspection-failed';
7970
8011
  recordMergeDecision();
7971
8012
  progress('pr-opened', `OP 승인 대기 (#${pr.number}): PR 경로 조회 실패`);
7972
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason, prUrl: pr.url, prNumber: pr.number };
8013
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason, prUrl: pr.url, prNumber: pr.number };
7973
8014
  }
7974
8015
 
7975
8016
  if (canAuto && (s.mergePr || opts.mergeByHost)) {
@@ -8019,12 +8060,12 @@ async function runSelfImplementInner(
8019
8060
  // 판정(mergeReason)이 아니라 «집행 직전 가드»가 막았으므로 그 사실을 값으로 말한다.
8020
8061
  // ⛔ 안 그러면 마지막 줄이 침묵하고, 그건 이 착지가 없애려던 상태다(리뷰 must-fix 2R).
8021
8062
  const guardSkipReason = mergeSkipReason ?? (guardFailure ? 'merge-guard-unevaluated' : 'docs-deletion-threshold');
8022
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason: guardSkipReason, prUrl: pr.url, prNumber: pr.number, detail };
8063
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason: guardSkipReason, prUrl: pr.url, prNumber: pr.number, detail };
8023
8064
  }
8024
8065
  if (opts.mergeByHost) {
8025
8066
  observe('merge-ready', { number: pr.number, checkedHeadCommit });
8026
8067
  progress('pr-opened', `호스트 재게이트 대기 (#${pr.number})`);
8027
- return { ok: true, stage: 'merge-ready', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), checkedHeadCommit: checkedHeadCommit!, prUrl: pr.url, prNumber: pr.number };
8068
+ return { ok: true, stage: 'merge-ready', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, checkedHeadCommit: checkedHeadCommit!, prUrl: pr.url, prNumber: pr.number };
8028
8069
  }
8029
8070
  // Landing freeze: the in-flight marker is written before the freeze is read, so a `freeze on` racing this merge waits.
8030
8071
  // The resume sweep merges from the repository that owns the run worktree (the worktree itself may be cleaned up).
@@ -8036,13 +8077,13 @@ async function runSelfImplementInner(
8036
8077
  };
8037
8078
  const landing = admitLandingMerge(heldEntry, undefined, {}, { prNumber: pr.number, repoRoot: owningRepoRoot(wt.path) ?? wt.path, ...(checkedHeadCommit ? { headCommit: checkedHeadCommit } : {}) });
8038
8079
  if (landing.kind === 'held' && !(checkedHeadCommit && /^[0-9a-f]{40}$/i.test(checkedHeadCommit))) {
8039
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason: 'merge-guard-unevaluated', prUrl: pr.url, prNumber: pr.number, detail: 'freeze: checked head SHA unavailable; PR left ready for review' };
8080
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason: 'merge-guard-unevaluated', prUrl: pr.url, prNumber: pr.number, detail: 'freeze: checked head SHA unavailable; PR left ready for review' };
8040
8081
  }
8041
8082
  if (landing.kind !== 'merge') {
8042
8083
  const detail = landing.kind === 'held' ? landingFreezeMessage(landing.freeze) : 'freeze lifted; a resume sweep is merging this PR';
8043
8084
  debug.log('harness.merge', 'frozen', { pr: pr.number, ...(landing.kind === 'held' ? { reason: landing.freeze.reason, until: landing.freeze.until } : { resumedElsewhere: true }) });
8044
8085
  progress('pr-opened', `#${pr.number} ready · ${detail}`);
8045
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason: landing.kind === 'held' ? 'frozen' : 'resumed-elsewhere', prUrl: pr.url, prNumber: pr.number, detail };
8086
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason: landing.kind === 'held' ? 'frozen' : 'resumed-elsewhere', prUrl: pr.url, prNumber: pr.number, detail };
8046
8087
  }
8047
8088
  // The marker and claim live until the merge call itself settles — a step timeout does not stop a merge already
8048
8089
  // sent — and anything that throws before the merge call is made releases them.
@@ -8132,10 +8173,10 @@ async function runSelfImplementInner(
8132
8173
  // 그대로 두면 마지막 산출 줄이 «침묵»한다 — 이 착지가 없애려던 바로 그 상태다(리뷰 must-fix).
8133
8174
  // ⇒ 판정이 아니라 «집행»에서 막힌 것이므로 그 사실을 별도 값으로 말한다.
8134
8175
  const mergeAttemptSkipReason = mergeSkipReason ?? 'merge-attempt-failed';
8135
- return { ok: true, stage: 'pr-opened', node: 'merge', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason: mergeAttemptSkipReason, merged: false, prUrl: pr.url, prNumber: pr.number, ...(m.detail ? { detail: m.detail } : {}) };
8176
+ return { ok: true, stage: 'pr-opened', node: 'merge', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, mergeReason: mergeAttemptSkipReason, merged: false, prUrl: pr.url, prNumber: pr.number, ...(m.detail ? { detail: m.detail } : {}) };
8136
8177
  }
8137
8178
  progress('pr-opened', `✅ PR 개설 (#${pr.number})`);
8138
- return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...(lastSupervisorReason ? { supervisorReason: lastSupervisorReason } : {}), ...(mergeSkipReason ? { mergeReason: mergeSkipReason } : {}), prUrl: pr.url, prNumber: pr.number };
8179
+ return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...reviewedHeadResult, ...(lastSupervisorReason ? { supervisorReason: lastSupervisorReason } : {}), ...(mergeSkipReason ? { mergeReason: mergeSkipReason } : {}), prUrl: pr.url, prNumber: pr.number };
8139
8180
  }
8140
8181
 
8141
8182
  /** 결정적 요청 거부 — 같은 요청을 다시 보내면 «같은» 답이 온다(BACKLOG B6).
@@ -986,7 +986,7 @@ export interface DefaultSeamsOptions {
986
986
  runAndroidUnitTestGate?: (io: { args: readonly string[]; cwd: string }) => number;
987
987
  runIosUnitTestGate?: (io: { args: readonly string[]; cwd: string }) => number;
988
988
  /** Source-policy gate adapter for focused seam tests. */
989
- runHarnessPolicyGates?: typeof runHarnessPolicyGates;
989
+ runHarnessPolicyGates?: (input: Parameters<typeof runHarnessPolicyGates>[0]) => Awaited<ReturnType<typeof runHarnessPolicyGates>> | ReturnType<typeof runHarnessPolicyGates>;
990
990
  runExposeGate?: typeof runExposeGate;
991
991
  /** Dependency-wide validation adapter for focused seam tests. */
992
992
  runDependencyChangeGate?: typeof runDependencyChangeGate;
@@ -1020,6 +1020,8 @@ export interface DefaultSeamsOptions {
1020
1020
  * agent-substrate·staged 하니스와 동일 엔진) 배선. 미주입 시 reviewDiff 미노출(gate 통과=바로 병합결정·
1021
1021
  * 리뷰 스킵). dev-harness 의 llmReview 와 동일 형태(streamLLM 래퍼). */
1022
1022
  llmReview?: (prompt: string) => Promise<string>;
1023
+ /** Optional independent reviewer for reviewDiff only; other judgment calls retain llmReview. */
1024
+ subscriptionReviewLLM?: (prompt: string) => Promise<string>;
1023
1025
  /**
1024
1026
  * 이 리뷰 심이 파일을 스스로 읽을 수 있나. **선택 선언** — 안 주면 reviewDiff 가 칸을 만들지 않고
1025
1027
  * 오케스트레이터 관측은 `'unknown'` 이다(`false` 와 같은 값으로 접지 않는다).
@@ -2073,7 +2075,7 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
2073
2075
  // tsc-깨짐(F1 refFacts 미정의)을 결정론 차단. 변경 전체(.ts·테스트 포함)를 tsc 로 검사(변경 경로만 카운트).
2074
2076
  const tc = changedFileTypecheck(cwd, changedAll);
2075
2077
  debug.log('self-implement', 'gate.tsc', { checked: tc.checked, passed: tc.passed, errors: tc.errors, exempted: tc.exempted, noInspectionReason: tc.noInspectionReason }, { level: tc.passed ? 'info' : 'warn' });
2076
- const policy = (o.runHarnessPolicyGates ?? runHarnessPolicyGates)({ cwd, changedFiles: changedAll });
2078
+ const policy = await (o.runHarnessPolicyGates ?? runHarnessPolicyGates)({ cwd, changedFiles: changedAll });
2077
2079
  debug.log('self-implement', 'gate.policy', {
2078
2080
  passed: policy.passed, failures: policy.failures.map((failure) => failure.gate), skipped: policy.skipped,
2079
2081
  });
@@ -2530,7 +2532,7 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
2530
2532
  ...(ctx.round !== undefined ? { round: ctx.round } : {}),
2531
2533
  },
2532
2534
  } : {}),
2533
- }, o.llmReview!);
2535
+ }, o.subscriptionReviewLLM ?? o.llmReview!);
2534
2536
  debug.log('self-implement', 'review.done', {
2535
2537
  verdict: rr.verdict, reviewed: rr.reviewed ?? false, mustFix: rr.mustFix.length, shouldFix: rr.shouldFix.length,
2536
2538
  gateEvidenceLines: evidenceNote?.split(/\r?\n/).length ?? 0,
@@ -111,6 +111,7 @@ export function singleRunAsJobResult(feature: string, r: SelfImplementResultWith
111
111
  ...(r.harvestable === true && r.branch ? { harvestable: true } : {}),
112
112
  ...(r.worktreePath ? { worktreePath: r.worktreePath } : {}),
113
113
  ...(r.prUrl ? { prUrl: r.prUrl } : {}),
114
+ ...(r.prNumber !== undefined ? { prNumber: r.prNumber } : {}),
114
115
  // TA-LIVE-REVIEW-POD — 작업 트리 없는 런(Pod)의 live review 가 «런이 낸 PR 머리»를 확인하는 근거.
115
116
  ...(r.checkedHeadCommit !== undefined ? { checkedHeadCommit: r.checkedHeadCommit } : {}),
116
117
  ...(r.merged !== undefined ? { merged: r.merged } : {}),
@@ -121,6 +122,9 @@ export function singleRunAsJobResult(feature: string, r: SelfImplementResultWith
121
122
  ...(r.decomposeProposal ? { decomposeProposal: r.decomposeProposal } : {}),
122
123
  ...(r.goalPlanRevision ? { goalPlanRevision: r.goalPlanRevision } : {}),
123
124
  ...(r.salvage !== undefined ? { salvage: r.salvage } : {}),
125
+ ...(r.review?.reviewed === true && r.review.verdict === 'pass'
126
+ && r.reviewedHeadCommit && /^[0-9a-f]{40}$/i.test(r.reviewedHeadCommit)
127
+ ? { selfReview: { verdict: r.review.verdict, head: r.reviewedHeadCommit } } : {}),
124
128
  ...(r.review?.reviewed !== undefined ? { reviewed: r.review.reviewed } : {}),
125
129
  ...(r.review?.failureReason !== undefined ? { reviewReason: r.review.failureReason } : {}),
126
130
  // ⭐ 대표 지시(2026-09-08) — 걸음을 슈퍼바이저까지 나른다.
@@ -65,6 +65,12 @@ const POSIX_SHELLS = new Set(['zsh', 'bash', 'fish', 'sh', 'dash', 'ksh']);
65
65
  let cached: Record<string, string> | null = null;
66
66
  let captured = false;
67
67
 
68
+ // Both the daemon warm path (nexus/index.ts) and PTY's first synchronous capture
69
+ // pass through here. Keep the existing capture events intact; these are additive timings.
70
+ function logBootPhase(phase: string, start: number, outcome: string): void {
71
+ debug.log('boot.phase', phase, { ms: performance.now() - start, outcome });
72
+ }
73
+
68
74
  /** Parse `KEY=VALUE\n` output from `printenv`. Tolerates values that
69
75
  * themselves contain `=`; does not attempt to unescape newlines
70
76
  * (bash/zsh `printenv` emits them literally, which is fine for
@@ -121,10 +127,13 @@ function acceptCapture(stdout: string): Record<string, string> | null {
121
127
  }
122
128
 
123
129
  export function runPrintenvCapture(): Record<string, string> | null {
130
+ const seedStart = performance.now();
124
131
  const seed = captureSeed();
132
+ logBootPhase('login-env.seed', seedStart, seed ? 'ready' : 'unavailable');
125
133
  if (!seed) return null;
126
134
  const { shell, seedEnv } = seed;
127
135
 
136
+ const shellStart = performance.now();
128
137
  let res: ReturnType<typeof spawnSync>;
129
138
  try {
130
139
  res = spawnSync(shell, ['-l', '-i', '-c', 'printenv'], {
@@ -137,13 +146,14 @@ export function runPrintenvCapture(): Record<string, string> | null {
137
146
  // OSC sequences would corrupt our TUI output otherwise.
138
147
  });
139
148
  } catch {
149
+ logBootPhase('login-env.shell', shellStart, 'error');
140
150
  return null;
141
151
  }
142
152
  // ETIMEDOUT with status 0 = the shell outlived the timeout; never trust a late capture.
143
- if (res.error) return null;
144
- if (res.status !== 0 && res.status !== null) return null;
145
- if (res.signal) return null;
146
- return acceptCapture(typeof res.stdout === 'string' ? res.stdout : '');
153
+ const parsed = !res.error && (res.status === 0 || res.status === null) && !res.signal
154
+ ? acceptCapture(typeof res.stdout === 'string' ? res.stdout : '') : null;
155
+ logBootPhase('login-env.shell', shellStart, parsed ? 'ok' : 'failed');
156
+ return parsed;
147
157
  }
148
158
 
149
159
  /** Fill the login-env cache off the event loop (daemon boot). The first web-terminal spawn otherwise ran the
@@ -152,9 +162,12 @@ export function runPrintenvCapture(): Record<string, string> | null {
152
162
  export async function warmCapturedEnv(spawn: typeof Bun.spawn = Bun.spawn): Promise<boolean> {
153
163
  if (captured) return cached !== null;
154
164
  if (process.env.ELANOUS_SKIP_LOGIN_ENV === '1' || process.env.TERM_PROGRAM === 'monad-agent-nested') return false;
165
+ const seedStart = performance.now();
155
166
  const seed = captureSeed();
167
+ logBootPhase('login-env.seed', seedStart, seed ? 'ready' : 'unavailable');
156
168
  if (!seed) return false;
157
169
  const start = Date.now();
170
+ const shellStart = performance.now();
158
171
  try {
159
172
  const proc = spawn([seed.shell, '-l', '-i', '-c', 'printenv'], { env: seed.seedEnv as Record<string, string>, stdin: 'ignore', stdout: 'pipe', stderr: 'ignore' });
160
173
  // A grandchild (e.g. a pyenv rehash) can keep the stdout pipe open after the shell is killed,
@@ -167,6 +180,7 @@ export async function warmCapturedEnv(spawn: typeof Bun.spawn = Bun.spawn): Prom
167
180
  const result = await Promise.race([finished, timedOut]);
168
181
  clearTimeout(timer);
169
182
  const parsed = result && result[1] === 0 ? acceptCapture(result[0]) : null;
183
+ logBootPhase('login-env.shell', shellStart, parsed ? 'ok' : result ? 'failed' : 'timeout');
170
184
  if (captured) return cached !== null; // a synchronous capture won the race
171
185
  if (!parsed) return false;
172
186
  cached = parsed;
@@ -174,6 +188,7 @@ export async function warmCapturedEnv(spawn: typeof Bun.spawn = Bun.spawn): Prom
174
188
  debug.log('shell.envbootstrap.capture', 'warmed', { ok: true, ms: Date.now() - start, keyCount: Object.keys(parsed).length });
175
189
  return true;
176
190
  } catch {
191
+ logBootPhase('login-env.shell', shellStart, 'error');
177
192
  return false;
178
193
  }
179
194
  }
@@ -57,6 +57,8 @@ export interface StewardDeps {
57
57
  cardStore?: StewardCardDeps['store'];
58
58
  warn?: (message: string) => void;
59
59
  ask?: StewardAsk;
60
+ /** Maximum time for each working-backwards wish draft in schedule (milliseconds). */
61
+ draftItemMs?: number;
60
62
  launchSettings?: Pick<StewardSettings, 'mode' | 'launch' | 'maxParallel' | 'podPool'>;
61
63
  launchCommand?: LaunchDeps['command'];
62
64
  spawnLaunch?: LaunchDeps['spawn'];
@@ -413,20 +415,35 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
413
415
  const wishes = rows.filter(row => present.has(row.issue) && row.capability === 'new-capability' && row.rung !== 'hitl');
414
416
  const store = wishes.length ? deps.cardStore ?? new CardStore(root) : undefined;
415
417
  const invalid = new Set<string>();
418
+ const carried = new Set<string>();
416
419
  if (store) try {
417
420
  recordTriageOnCards(wishes, issues, { root, store, now: deps.now });
418
421
  for (const wish of wishes) {
419
- try { await recordWorkingBackwardsOnCards([wish], issues, store, deps.ask ?? askSteward); }
420
- catch (error) {
421
- if (!(error instanceof InvalidWorkingBackwardsDraft)) throw error;
422
- invalid.add(wish.issue);
423
- debug.log('steward.stage', 'draft-invalid', { issue: wish.issue });
424
- }
422
+ const timedOut = Symbol('draft-timeout');
423
+ let timer: ReturnType<typeof setTimeout> | undefined;
424
+ const timeout = new Promise<never>((_, reject) => {
425
+ timer = setTimeout(() => reject(timedOut), deps.draftItemMs ?? 120_000);
426
+ });
427
+ // The draft may finish without ever racing this promise (no ask needed) — never leave its rejection unhandled.
428
+ timeout.catch(() => undefined);
429
+ try {
430
+ // Bound the ask itself so a late answer cannot write to a store closed after this tick.
431
+ const ask: StewardAsk = (prompt, role) => Promise.race([(deps.ask ?? askSteward)(prompt, role), timeout]);
432
+ await recordWorkingBackwardsOnCards([wish], issues, store, ask);
433
+ } catch (error) {
434
+ if (error instanceof InvalidWorkingBackwardsDraft) {
435
+ invalid.add(wish.issue);
436
+ debug.log('steward.stage', 'draft-invalid', { issue: wish.issue });
437
+ } else {
438
+ carried.add(wish.issue);
439
+ debug.log('steward.schedule', 'draft-carried', { issue: wish.issue, reason: error === timedOut ? 'timeout' : 'error' });
440
+ }
441
+ } finally { if (timer) clearTimeout(timer); }
425
442
  }
426
443
  } finally { if (!deps.cardStore) store.close(); }
427
- if (invalid.size) rows = rows.map(row => invalid.has(row.issue)
444
+ if (invalid.size || carried.size) rows = rows.map(row => invalid.has(row.issue)
428
445
  ? { ...row, rung: 'hitl' as const, disposition: 'hitl' as const, hitlReason: 'other' as const, why: 'working-backwards draft invalid' }
429
- : row);
446
+ : carried.has(row.issue) ? { ...row, deferred: true as const, disposition: 'wait' as const } : row);
430
447
  }
431
448
  for (const row of rows) try { debug.log('steward.schedule', 'decision', {
432
449
  kind: row.disposition, target: row.issue, wouldAct: row.disposition === 'now' && liveLike(settings.mode),