elanous 0.2.14 → 0.2.16

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 (350) 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-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -21,6 +21,7 @@ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePerson
21
21
  import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
22
22
  import { PersonaRegistry } from '../persona/registry.js';
23
23
  import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
24
+ import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
24
25
  import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
25
26
  import { queryRunningRuns } from '../self-implement/running-runs.js';
26
27
  import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
@@ -42,10 +43,18 @@ export type TcCandidate =
42
43
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
43
44
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
44
45
  export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
45
- export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
46
- export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
46
+ export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
47
+ export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
48
+
49
+ export function seatLoopTickLine(result: SeatLoopResult): string {
50
+ const suffix = (result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason
51
+ ? ` (live-safe 요청 · 이유: ${result.modeDowngradeReason})` : '';
52
+ return `seat loop ${result.seat}: ${result.status}${'runId' in result && result.runId ? ` ${result.runId}` : ''}${suffix}`;
53
+ }
47
54
  export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
48
55
  export type SeatDeps = {
56
+ /** Version stamp for decision cards the seat loop raises. Default = the ledger's resolver (`git reflog` on the repo, up to 10 s) — tests inject a fixed one. */
57
+ resolveDecisionVersion?: (at: string) => { released: string | null; dev: string | null; codename: string | null };
49
58
  personaConfig?: PersonaLoopConfig;
50
59
  personaRegistry?: PersonaRegistry;
51
60
  personaDir?: string;
@@ -77,6 +86,8 @@ export type SeatDeps = {
77
86
  stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
78
87
  /** Open cards from the decision ledger; injected only for isolated ledger tests. */
79
88
  pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
89
+ /** Running-run observation for OP cutoff; incomplete observations cannot establish that a run is absent. */
90
+ cutoffRuns?: (runIds: readonly string[]) => Pick<ReturnType<typeof queryRunningRuns>, 'completeness' | 'entries' | 'pty'>;
80
91
  /** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
81
92
  pullRequests?: () => Promise<TcPullRequest[]>;
82
93
  ledgerFiles?: (directory: string) => string[];
@@ -200,6 +211,10 @@ async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<Person
200
211
 
201
212
  export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
202
213
  const result = await runSeatLoopOnce(seat, deps);
214
+ if ((result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason) {
215
+ try { debug.log('seat-loop', 'mode-downgraded', { seat, requested: 'live-safe', effective: result.status, reason: result.modeDowngradeReason }); }
216
+ catch { /* logging cannot change execution */ }
217
+ }
203
218
  try { await runPersonaShadowOnce(deps); }
204
219
  catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
205
220
  try {
@@ -243,6 +258,59 @@ function queueKey(seat: string, item: SeatItem): string {
243
258
  return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
244
259
  }
245
260
 
261
+ /** A decision ledger that raises cards — forwards the injected version resolver so tests never run the repo's `git reflog`. */
262
+ function cardLedger(root: string, deps: SeatDeps, now?: Date): DecisionLedger {
263
+ return new DecisionLedger({ stateDir: root, ...(now ? { now: () => now } : {}), ...(deps.resolveDecisionVersion ? { resolveVersion: deps.resolveDecisionVersion } : {}) });
264
+ }
265
+
266
+ const DEFAULT_REPEAT_STOP = 3;
267
+
268
+ function repeatStopLimit(config: SeatLoopConfig): number {
269
+ const value = config.repeatStop;
270
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_REPEAT_STOP;
271
+ }
272
+
273
+ /** A finished attempt that never landed (no merged-PR evidence). `attempting` is the intent row of the same launch. A `refused` row whose reason is the AUTOQ retry note is the reconcile of a queued row, not a second try. */
274
+ function unlandedAttempt(entry: SeatEntry): boolean {
275
+ if (!entry.item || entry.action === 'seat-question' || entry.action === 'seat-answer' || entry.inquiry) return false;
276
+ if (entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'held' || entry.status === 'attempting') return false;
277
+ if (entry.status === 'refused' && entry.reason === 'AUTOQ failed or cancelled; eligible for retry') return false;
278
+ return entry.status === 'queued' || entry.status === 'refused' || entry.status === 'outcome-unknown';
279
+ }
280
+
281
+ function repeatStopReason(attempts: number): string {
282
+ return `같은 칸 ${attempts}회 착지 0 — 사람 판단`;
283
+ }
284
+
285
+ function personReopened(ledger: readonly SeatEntry[], seat: string, item: SeatItem, root: string): boolean {
286
+ const key = handledItemKey(item);
287
+ const lastHold = [...ledger].reverse().find((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && entry.status === 'held');
288
+ if (!lastHold) return false;
289
+ const heldAt = Date.parse(lastHold.at || lastHold.ts || '');
290
+ if (!Number.isFinite(heldAt)) return false;
291
+ const ref = decisionRef(seat, item);
292
+ return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).some((card) => {
293
+ if (!card.refs?.includes(ref) || card.status !== 'decided' || !card.decidedAt) return false;
294
+ const decidedAt = Date.parse(card.decidedAt);
295
+ return Number.isFinite(decidedAt) && decidedAt > heldAt;
296
+ });
297
+ }
298
+
299
+ /** Prior attempts of this seat and handled item that finished without a landing. A queued row whose queue is still retryable has not finished. Queue rows with the same key count when the ledger has no receipt. */
300
+ function unlandedAttemptCount(seat: string, item: SeatItem, ledger: readonly SeatEntry[], root: string, deps: SeatDeps): number {
301
+ const key = handledItemKey(item);
302
+ const idem = queueKey(seat, item);
303
+ const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
304
+ const finished = (entry: SeatEntry) => unlandedAttempt(entry) && !(entry.status === 'queued' && entry.queueId && outcome(entry.queueId, root) === 'retryable');
305
+ const fromLedger = ledger.filter((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && finished(entry));
306
+ const seen = new Set(fromLedger.flatMap((entry) => entry.queueId ? [entry.queueId] : []));
307
+ let rows: ReturnType<typeof listHarnessQueue> = [];
308
+ try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
309
+ catch (error) { observe('repeat-stop-queue-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
310
+ const fromQueue = rows.filter((row) => row.seat === seat && row.idempotencyKey === idem && row.status === 'finished' && !seen.has(row.id));
311
+ return fromLedger.length + fromQueue.length;
312
+ }
313
+
246
314
  /** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
247
315
  * seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
248
316
  function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
@@ -301,8 +369,18 @@ function decisionRef(seat: string, item: SeatItem): string {
301
369
  return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
302
370
  }
303
371
 
372
+ // The decision ledger counts Unicode characters and sentence boundaries before it stores a card.
373
+ function decisionSummary(text: string): string {
374
+ const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?。!?])\s+/u).slice(0, 2).join(' ');
375
+ return Array.from(sentences).slice(0, 240).join('').trim();
376
+ }
377
+
378
+ function decisionSourceRef(seat: string, item: SeatItem): string {
379
+ return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
380
+ }
381
+
304
382
  function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
305
- const ref = decisionRef(seat, item);
383
+ const ref = decisionSourceRef(seat, item);
306
384
  return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
307
385
  }
308
386
 
@@ -315,7 +393,7 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
315
393
  || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
316
394
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
317
395
  || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
318
- || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
396
+ || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown' || (entry.status === 'held' && !personReopened(ledger, entry.seat, entry.item!, root ?? ''))))
319
397
  || (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
320
398
  later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
321
399
  && later.item.version === entry.item?.version && later.item.id === entry.item?.id
@@ -456,6 +534,62 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
456
534
  return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
457
535
  }
458
536
 
537
+ const OP_CUTOFF_WINDOW_MS = 2 * 60 * 60_000;
538
+ const RUN_REFERENCE = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
539
+
540
+ function checkOpLandingCutoff(deps: SeatDeps, now: Date, path: string, stateRoot: string): SeatEntry | undefined {
541
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
542
+ const shipped = releasedVersion(releaseRoot);
543
+ const schedule = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
544
+ .filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && (!shipped || versionOrder(row.version, shipped) > 0))
545
+ .sort((a, b) => versionOrder(a.version, b.version))[0];
546
+ if (!schedule?.landBy) return;
547
+ const untilLanding = Date.parse(schedule.landBy) - now.getTime();
548
+ if (!Number.isFinite(untilLanding) || untilLanding < 0 || untilLanding > OP_CUTOFF_WINDOW_MS) return;
549
+ const append = deps.append ?? defaultAppend;
550
+ let items: ReturnType<NonNullable<SeatDeps['checklistItems']>>;
551
+ try { items = (deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items))(schedule.version); }
552
+ catch (error) {
553
+ const entry: SeatEntry = { seat: 'OP', ts: now.toISOString(), at: now.toISOString(), status: 'shadow',
554
+ reason: `체크리스트 못 읽음: ${schedule.version} (${String(error).slice(0, 200)})`,
555
+ modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' };
556
+ append(path, entry);
557
+ return entry;
558
+ }
559
+ const yellow = items.filter((item) => item.status === 'yellow');
560
+ const runIds = [...new Set(yellow.flatMap((item) => [...(item.evidence ?? '').matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase())))];
561
+ let running = new Set<string>();
562
+ if (runIds.length) {
563
+ try {
564
+ const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.op-cutoff' })))(runIds);
565
+ if (observed.completeness !== 'complete' || observed.pty.unreadable.length) {
566
+ observe('op-cutoff-runs-unreadable', { version: schedule.version });
567
+ return;
568
+ }
569
+ running = new Set(observed.entries.filter((row) => row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown')
570
+ .map((row) => row.runId.toLowerCase()));
571
+ } catch (error) {
572
+ observe('op-cutoff-runs-unreadable', { version: schedule.version, error: String(error).slice(0, 200) });
573
+ return;
574
+ }
575
+ }
576
+ const candidates = yellow.filter((item) => ![...(item.evidence ?? '').matchAll(RUN_REFERENCE)]
577
+ .some(([id]) => running.has(id.toLowerCase())))
578
+ .sort((a, b) => a.id.localeCompare(b.id))
579
+ .map(({ id, title, owner }) => ({ id, title, owner: owner ?? '미배정' }));
580
+ try { debug.log('seat-loop', 'op-cutoff-check', { version: schedule.version, landBy: schedule.landBy, candidates }); }
581
+ catch { /* Observation cannot change a request. */ }
582
+ if (!candidates.length) return;
583
+ const key = `seat-loop:op-cutoff:${schedule.version}:${seatDay(now)}`;
584
+ const requestPath = join(stateRoot, 'seat-requests', 'requests.jsonl');
585
+ withSeatRequestLedgerLock(requestPath, () => {
586
+ if (listSeatRequests(stateRoot).some((row) => row.key === key)) return;
587
+ appendSeatRequestRows(requestPath, [{ key, seat: 'OP', status: 'pending' as const, queuedAt: now.toISOString(),
588
+ text: `${schedule.version} 착지 마감 ${schedule.landBy} 전 노랑 칸 점검 — 다음 판으로 옮길지 OP 판단:\n${candidates.map((item) => `${item.id} · ${item.title} · 주인 ${item.owner}`).join('\n')}`,
589
+ version: schedule.version, landBy: schedule.landBy, candidates }]);
590
+ });
591
+ }
592
+
459
593
  function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
460
594
  const releaseRoot = deps.root ?? releaseLedgerRoot();
461
595
  const shipped = releasedVersion(releaseRoot);
@@ -802,7 +936,7 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
802
936
  if (judgment.state === 'absent') {
803
937
  if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
804
938
  const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
805
- new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
939
+ cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
806
940
  scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
807
941
  c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
808
942
  q: '결측 이웃의 업무를 어떻게 처리할까?' },
@@ -827,10 +961,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
827
961
  export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
828
962
  if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
829
963
  const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
830
- if (config.mode === 'off' || !(config.seats ?? ['MK']).includes(seat)) {
964
+ if (config.mode === 'off') {
831
965
  observe('skipped-off', { seat });
832
966
  return { seat, status: 'skipped-off' };
833
967
  }
968
+ if (!(config.seats ?? ['MK']).includes(seat)) {
969
+ // An excluded seat does not read inputs or run the shadow loop; never report it as shadow.
970
+ observe('skipped-off', { seat });
971
+ return { seat, status: 'skipped-off',
972
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: '자리 목록에 없음 (loops.seat.seats)' } : {}) };
973
+ }
834
974
  const now = (deps.now ?? (() => new Date()))();
835
975
  const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
836
976
  const directory = dirname(path);
@@ -928,6 +1068,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
928
1068
  return entry;
929
1069
  }
930
1070
  }
1071
+ let tcDowngrade: SeatEntry | undefined;
931
1072
  if (seat === 'TC') {
932
1073
  const candidates = await tcCandidates(deps, now);
933
1074
  for (const candidate of candidates) {
@@ -938,19 +1079,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
938
1079
  && row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
939
1080
  if (!inLedger) {
940
1081
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1082
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC 타 자리 결함 질문은 shadow 기록만 구현' } : {}),
941
1083
  item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
942
1084
  action: 'seat-question', inquiry: { to: candidate.to, question } };
943
1085
  (deps.append ?? defaultAppend)(path, entry);
1086
+ tcDowngrade ??= entry;
944
1087
  }
945
1088
  if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
946
1089
  continue;
947
1090
  }
948
1091
  if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
949
- const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
1092
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1093
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC PR 판정은 shadow 기록만 구현' } : {}) };
950
1094
  (deps.append ?? defaultAppend)(path, entry);
1095
+ tcDowngrade ??= entry;
951
1096
  observe('tc-shadow-candidate', { seat, candidate });
952
1097
  }
953
1098
  }
1099
+ if (seat === 'OP' && config.mode === 'live-safe') {
1100
+ const unreadable = checkOpLandingCutoff(deps, now, path, stateRoot);
1101
+ if (unreadable) return unreadable;
1102
+ }
954
1103
  if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
955
1104
  !hasSeatAnswer(stateRoot, question)
956
1105
  && !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
@@ -987,7 +1136,8 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
987
1136
  if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
988
1137
  }
989
1138
  }
990
- const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate }));
1139
+ const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1140
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' } : {}) }));
991
1141
  for (const entry of entries) {
992
1142
  const candidate = entry.candidate!;
993
1143
  const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
@@ -1003,7 +1153,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1003
1153
  }
1004
1154
  return entries[0]!;
1005
1155
  }
1156
+ const failedDecisions = new Set<string>();
1157
+ let lastDecisionFailure: SeatEntry | undefined;
1158
+ while (true) {
1006
1159
  const inputs = await gatherSeatInputs(seat, deps, ledger);
1160
+ inputs.requests = inputs.requests.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
1161
+ inputs.checklist = inputs.checklist.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
1007
1162
  const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
1008
1163
  const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
1009
1164
  const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
@@ -1085,6 +1240,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1085
1240
  entry.action = 'decision';
1086
1241
  }
1087
1242
  if (questionsMode !== 'on') {
1243
+ if (config.mode === 'live-safe') entry.modeDowngradeReason = '질문 전달 선행 조건 미충족 (questions shadow)';
1088
1244
  if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
1089
1245
  observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
1090
1246
  }
@@ -1192,9 +1348,11 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1192
1348
  if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
1193
1349
  observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
1194
1350
  }
1351
+ if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
1195
1352
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
1196
1353
  ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
1197
1354
  if (config.mode === 'shadow' || !item) {
1355
+ if (!item && lastDecisionFailure) return lastDecisionFailure;
1198
1356
  append(path, entry);
1199
1357
  if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
1200
1358
  return entry;
@@ -1282,11 +1440,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1282
1440
  append(path, { ...entry, status: 'attempting' });
1283
1441
  try {
1284
1442
  if (action.kind === 'decision') {
1285
- const title = `${seat}: ${action.text.replace(/[\r\n]+/g, ' ')}`;
1443
+ const title = `${seat}: ${decisionSummary(action.text)}`;
1444
+ const source = decisionSourceRef(seat, item);
1286
1445
  const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
1287
- '--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1446
+ '--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
1447
+ '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1288
1448
  '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
1289
- '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--json']);
1449
+ '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1450
+ '--agent', 'seat-loop', '--ref', source, '--json']);
1290
1451
  const decision: unknown = JSON.parse(raised.trim());
1291
1452
  if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
1292
1453
  throw new Error('decisions raise returned no decision id');
@@ -1297,6 +1458,38 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1297
1458
  return entry;
1298
1459
  }
1299
1460
  if (config.mode === 'live-safe') {
1461
+ const limit = repeatStopLimit(config);
1462
+ const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
1463
+ if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
1464
+ const reason = repeatStopReason(attempts);
1465
+ entry.status = 'held';
1466
+ entry.reason = reason;
1467
+ const ref = decisionRef(seat, item);
1468
+ const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
1469
+ if (!prior) {
1470
+ cardLedger(stateRoot, deps, now).raiseOnce({
1471
+ title: `${seat}: ${item.id} 반복 착지 0`,
1472
+ category: 'other',
1473
+ scqa: {
1474
+ s: `${seat} 칸 ${item.id} 가 착지 없이 ${attempts}회 끝났다.`,
1475
+ c: '같은 칸을 다시 발사하지 않고 사람 판단을 기다린다.',
1476
+ q: '이 칸을 계속할까, 멈출까?',
1477
+ },
1478
+ options: [
1479
+ { key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
1480
+ { key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
1481
+ ],
1482
+ recommendation: { skipped: true, reason: '착지 없는 반복만으로 계속할지 자동 결정하지 않는다' },
1483
+ raisedBy: { agent: 'seat-loop' },
1484
+ refs: [ref],
1485
+ raisedAt: now.toISOString(),
1486
+ crossCheckSkipped: '반복 정지는 사람 판단',
1487
+ }, ref);
1488
+ }
1489
+ append(path, entry);
1490
+ observe('repeat-stopped', { seat, item: item.id, attempts, lastQueueId: [...ledger].reverse().find((row) => row.seat === seat && row.item && handledItemKey(row.item) === handledItemKey(item) && row.queueId)?.queueId ?? null });
1491
+ return entry;
1492
+ }
1300
1493
  let queued: { id: string };
1301
1494
  try {
1302
1495
  queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
@@ -1332,9 +1525,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1332
1525
  observe('launched', { seat, item, runId });
1333
1526
  return entry;
1334
1527
  } catch (error) {
1335
- append(path, { ...entry, status: 'outcome-unknown' });
1528
+ const failure: SeatEntry = { ...entry, status: 'outcome-unknown' };
1529
+ append(path, failure);
1530
+ if (action.kind === 'decision') {
1531
+ lastDecisionFailure = failure;
1532
+ const source = decisionSourceRef(seat, item);
1533
+ try { debug.log('seat-loop', 'decision-raise-failed', { source, error: String(error) }); }
1534
+ catch { /* A failed observation cannot interrupt the remaining items. */ }
1535
+ failedDecisions.add(itemKey(item));
1536
+ continue;
1537
+ }
1336
1538
  throw error;
1337
1539
  }
1540
+ }
1338
1541
  } catch (error) {
1339
1542
  turnError = error;
1340
1543
  throw error;
@@ -11,6 +11,7 @@
11
11
  // ⛔ 종료(`process.exit`)는 여기가 «안» 한다 — 호출자가 자기 표면에 맞게 끝낸다.
12
12
  // 그래서 결과를 «세 값»으로 낸다: 발사 / 저작 전 중단 / 전제 검사 중단.
13
13
  import { linesOutsideFencedCode } from '../self-implement/goal-author.js';
14
+ import { recordAuthoredGoal } from './authored-goal-trace.js';
14
15
  import { retiredEntranceNotice, type EntranceDeclaration } from './entrance-registry.js';
15
16
  import {
16
17
  classifyConcurrentAuthoring,
@@ -58,7 +59,7 @@ function resolveSelfResolveClarifications(explicit: boolean | undefined): Resolv
58
59
  /** 저작기 산출 — `runGoalAuthorCli` 의 반환에서 이 흐름이 «실제로 쓰는» 것만. */
59
60
  export interface AskAuthoredGoal {
60
61
  readonly path: string;
61
- readonly authored?: { readonly document?: string; readonly grounded?: boolean | null } | null;
62
+ readonly authored?: { readonly document?: string; readonly grounded?: boolean | null; readonly authorRunId?: string } | null;
62
63
  }
63
64
 
64
65
  export type LaunchingTreeProbe =
@@ -158,6 +159,8 @@ export interface AskLaunchFlowDeps {
158
159
  /** ⛔ 못 얻으면 `null` — 같은 이유. */
159
160
  recentAuthoringSamples(): RecentAuthoringSample[] | null;
160
161
  authorGoal(args: readonly string[], options: Record<string, unknown>): Promise<AskAuthoredGoal>;
162
+ /** AUTHOR-TRACE seam (tests). */
163
+ recordAuthoredGoal?: typeof recordAuthoredGoal;
161
164
  /** Pod launches reserve a host slot before goal authoring begins. */
162
165
  beforeAuthoring?(): Promise<void>;
163
166
  /** 발사 전 권고용 분해 seam. 생략하면 전용 관측명을 준 기존 분해기를 쓴다. */
@@ -861,6 +864,24 @@ export async function runAskLaunchFlow(
861
864
  selfResolveClarifications: selfResolveClarifications.value,
862
865
  });
863
866
  deps.print(`[ask] 저작됨: ${authored.path}`);
867
+ // AUTHOR-TRACE: the authored goal file can be cleaned up later — keep path ⊕ sha256 ⊕ size in the run ledger and
868
+ // archive the body, so authoring length is measurable after the fact (GOAL-BENCH · AUTHOR-LITE).
869
+ const traceIds = {
870
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
871
+ ...(authored.authored?.authorRunId === undefined ? {} : { authorRunId: authored.authored.authorRunId }),
872
+ };
873
+ if (authored.authored?.document !== undefined && (traceIds.runId !== undefined || traceIds.authorRunId !== undefined)) {
874
+ try {
875
+ const trace = (deps.recordAuthoredGoal ?? recordAuthoredGoal)(traceIds, authored.path, authored.authored.document);
876
+ deps.log('goal-authored-trace', { ...traceIds, ...trace }, 'info');
877
+ } catch (error) {
878
+ deps.log('goal-authored-trace-failed', { ...traceIds, error: error instanceof Error ? error.message : String(error) }, 'warn');
879
+ }
880
+ } else {
881
+ deps.log('goal-authored-trace-skipped', {
882
+ reason: authored.authored?.document === undefined ? 'no-authored-document' : 'no-run-or-author-id', path: authored.path,
883
+ }, 'warn');
884
+ }
864
885
  const markerCounts = authored.authored?.document === undefined
865
886
  ? null
866
887
  : countMissingAuthoredConstraintMarkers(input.askText, authored.authored.document);
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { appendRunLedgerEntry, runLedgerDir } from '../self-implement/run-ledger.js';
5
+ import { debug } from '../debug/log.js';
6
+
7
+ /** AUTHOR-TRACE: what `harness say`/`ask` authored, kept with the run — the goal file itself may be cleaned up later,
8
+ * so the ledger carries path ⊕ sha256 ⊕ size and the body is archived next to the ledger. */
9
+ export interface AuthoredGoalTrace {
10
+ path: string;
11
+ sha256: string;
12
+ chars: number;
13
+ sections: number;
14
+ archivedAt: string;
15
+ }
16
+
17
+ /** `## ` headings outside fenced code (a fence closes only on the same marker, at least as long). */
18
+ function countSections(document: string): number {
19
+ let fence: { char: string; length: number } | null = null;
20
+ let count = 0;
21
+ for (const line of document.split('\n')) {
22
+ const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
23
+ if (fence) {
24
+ if (marker && marker[1]![0] === fence.char && marker[1]!.length >= fence.length && line.trim() === marker[1]) fence = null;
25
+ continue;
26
+ }
27
+ if (marker) { fence = { char: marker[1]![0]!, length: marker[1]!.length }; continue; }
28
+ if (/^ {0,3}##\s/.test(line)) count += 1;
29
+ }
30
+ return count;
31
+ }
32
+
33
+ export function authoredGoalTrace(path: string, document: string, archivedAt: string): AuthoredGoalTrace {
34
+ return {
35
+ path,
36
+ sha256: createHash('sha256').update(document).digest('hex'),
37
+ chars: [...document].length,
38
+ sections: countSections(document),
39
+ archivedAt,
40
+ };
41
+ }
42
+
43
+ /** Archive the body, emit one `goal-author`/`goal-authored` observation and — when the run id is already known —
44
+ * append a `goal-authored` run-ledger event. At `harness say` authoring time the run id usually does not exist yet;
45
+ * the authoring run id (`authorRunId`, joined to the goal by `goal-id-assigned`) keys the record then. */
46
+ export function recordAuthoredGoal(
47
+ ids: { runId?: string; authorRunId?: string },
48
+ path: string,
49
+ document: string,
50
+ options: { ledgerDir?: string; archiveDir?: string } = {},
51
+ ): AuthoredGoalTrace {
52
+ const key = ids.runId ?? ids.authorRunId;
53
+ if (!key) throw new Error('recordAuthoredGoal needs a runId or an authorRunId');
54
+ const ledgerDir = options.ledgerDir ?? runLedgerDir();
55
+ const archiveDir = options.archiveDir ?? join(dirname(ledgerDir), 'authored-goals');
56
+ const digest = createHash('sha256').update(document).digest('hex');
57
+ const archived = join(archiveDir, `${key}-${digest.slice(0, 12)}.md`);
58
+ if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
59
+ writeFileSync(archived, document, 'utf8');
60
+ const trace = authoredGoalTrace(path, document, archived);
61
+ debug.log('goal-author', 'goal-authored', { ...ids, ...trace });
62
+ if (ids.runId) appendRunLedgerEntry({ runId: ids.runId, event: 'goal-authored', data: { ...trace, ...(ids.authorRunId ? { authorRunId: ids.authorRunId } : {}) } }, ledgerDir);
63
+ return trace;
64
+ }
@@ -43,6 +43,7 @@ import { observeDeliverables, type DeliverableObservationResult, type Deliverabl
43
43
  import { debug } from '../debug/log.js';
44
44
  import { getUserConfig, type UserConfig } from '../user-config.js';
45
45
  import { resolveChildLlmPreference, type ChildLlmPreferenceInput, type ResolvedChildLlmPreference } from '../self-implement/child-llm-preference.js';
46
+ import { applyLaunchOverloadFailover, codexProviderNamed } from '../oauth/overload-failover-launch.js';
46
47
  import { readCachedGrokQuota } from '../oauth/codex-account-store.js';
47
48
  import { queryAbandonedDraftPrs, type AbandonedDraftPr } from '../cli/logs-abandoned-draft-prs.js';
48
49
  import { branchLineageSlug } from '../cli/pr-lineage.js';
@@ -1631,6 +1632,18 @@ function resolveChildLlmFromParts(
1631
1632
  return undefined;
1632
1633
  }
1633
1634
 
1635
+ /** Pinned `tools.selfImplement.childLlm` naming codex. Fallback-chain codex is not pinned. */
1636
+ function childLlmCodexPinned(flagProvider: string | undefined): boolean {
1637
+ if (flagProvider !== undefined) return false;
1638
+ try {
1639
+ const preference = resolveChildLlmPreference(getUserConfig());
1640
+ const provider = preference.chain[0]?.provider;
1641
+ return preference.mode === 'pinned' && preference.source.chain === 'pinned' && codexProviderNamed(provider);
1642
+ } catch {
1643
+ return false;
1644
+ }
1645
+ }
1646
+
1634
1647
  export function buildChildLlmSelection(
1635
1648
  opts: Pick<DevCliOpts, 'childLlmProvider' | 'childLlmModel' | 'childLlmEffort'> & {
1636
1649
  /** 발사 인자가 있을 때 관측용 선호를 읽는다. 없으면 getUserConfig. 기존 두 인자 시그니처는 그대로다. */
@@ -1967,7 +1980,12 @@ export function buildDevCliSpec(
1967
1980
  }
1968
1981
 
1969
1982
  if (path === 'self-mission') {
1970
- const childLlm = buildChildLlmSelection({ ...opts, readPreference: () => getUserConfig() });
1983
+ const builtChild = buildChildLlmSelection({ ...opts, readPreference: () => getUserConfig() });
1984
+ const childLlm = applyLaunchOverloadFailover(builtChild, {
1985
+ provider: opts.childLlmProvider,
1986
+ model: opts.childLlmModel,
1987
+ codexPinned: childLlmCodexPinned(opts.childLlmProvider),
1988
+ });
1971
1989
  // 잔량 조회(약 3초)는 한 발사에 한 번만 — 자식·부모가 같은 값을 쓴다.
1972
1990
  let grokQuotaOnce: ReturnType<typeof readCachedGrokQuota> | undefined;
1973
1991
  const readGrokQuotaOnce = () => (grokQuotaOnce ??= readGrokQuota());
@@ -35,6 +35,7 @@ import type { DocumentReferenceStatus } from '../self-implement/self-implement-r
35
35
  import type { SelfDevGoal, OrchestrateSelfDevOptions, SelfDevJobResult } from './orchestrate.js';
36
36
  import { launchCapabilityObservation, resolveLaunchCapabilities } from './launch-capabilities.js';
37
37
  import { lookupEntrance, type EntranceId } from './entrance-registry.js';
38
+ import { resolveLaunchStamp } from '../harness/launch-stamp.js';
38
39
  import type { IngestionEntry } from '../agent-substrate/execution/ingestion-policy.js';
39
40
  import type { ReviewerContextItem } from '../agent-substrate/pr-reviewer.js';
40
41
  import { resolveRunIdentity, type RunIdSource } from '../harness/harness-space.js';
@@ -42,7 +43,7 @@ import { addSelfDevRunParticipant, processBirthId, saveSelfDevRun } from './run-
42
43
  import { getUserConfig, resolveRoleLlm } from '../user-config.js';
43
44
  import { reviewReasoningEffort } from '../model-tier/review-effort.js';
44
45
  import { getProvider, inferProviderFromModel } from '../llm.js';
45
- import { buildReviewProviderAttempts, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
46
+ import { buildReviewProviderAttempts, defaultReviewFallbackModel, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
46
47
  import type { DefaultSeamsOptions } from '../self-implement/seams.js';
47
48
  import { harnessTargetOptions, resolveHarnessTarget, revalidateHarnessTarget, type HarnessTargetResolution } from '../self-implement/harness-target-options.js';
48
49
  import { provisionRepository, type RepoProvisionResult } from '../self-implement/repo-provision.js';
@@ -609,6 +610,7 @@ export function toSelfImplementOptions(text: string, plan: ResolvedDevPlan, seam
609
610
  ...(s.correlationId !== undefined ? { correlationId: s.correlationId } : {}),
610
611
  ...(s.branchName ? { branchName: s.branchName } : {}),
611
612
  ...(s.maxReworkRounds !== undefined ? { maxReworkRounds: s.maxReworkRounds } : {}),
613
+ launch: resolveLaunchStamp({ entrance: plan.entrance, queueId: process.env.ELANOUS_HARNESS_QUEUE_LAUNCH }),
612
614
  };
613
615
  }
614
616
 
@@ -1093,10 +1095,12 @@ export async function buildDefaultSelfImplementSeams(
1093
1095
  provider: inferProviderFromModel(model) ? getProvider(model) : undefined,
1094
1096
  label,
1095
1097
  });
1098
+ const primaryReviewAttempt = reviewAttemptForModel(reviewModel, 'primary');
1096
1099
  const reviewAttempts = buildReviewProviderAttempts(
1097
- reviewAttemptForModel(reviewModel, 'primary'),
1098
- reviewFallbackModels.map((model) => reviewAttemptForModel(model, model)),
1099
- );
1100
+ primaryReviewAttempt,
1101
+ [...reviewFallbackModels, defaultReviewFallbackModel(primaryReviewAttempt.provider?.name)]
1102
+ .map((model) => reviewAttemptForModel(model, model)),
1103
+ ).slice(0, 2);
1100
1104
  const defaultApiReview = async (prompt: string): Promise<string> => {
1101
1105
  const r = await runReviewWithFallback(
1102
1106
  reviewAttempts,
@@ -1859,9 +1863,9 @@ async function observeDevTaskDispatch(spec: DevPipelineSpec, plan: ResolvedDevPl
1859
1863
  }
1860
1864
  }
1861
1865
 
1862
- async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string): Promise<string | undefined> {
1866
+ async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string, base?: string): Promise<string | undefined> {
1863
1867
  if (!seams.readPrFiles) throw new Error('release-path inspection unavailable — automatic merge refused');
1864
- const files = await seams.readPrFiles({ number, cwd });
1868
+ const files = await seams.readPrFiles({ number, cwd, ...(base ? { base } : {}) });
1865
1869
  if (!Array.isArray(files) || !files.every((path) => typeof path === 'string')) throw new Error('invalid PR file list — automatic merge refused');
1866
1870
  const path = releasePathHold(files);
1867
1871
  if (!path) return undefined;
@@ -2103,10 +2107,11 @@ async function runDevPipelineDispatch(
2103
2107
  ...assembledSeams,
2104
2108
  ...(assembledSeams.mergePr ? { mergePr: async (input) => {
2105
2109
  let path: string | undefined;
2106
- try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd); }
2110
+ try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd, plan.base); }
2107
2111
  catch (error) {
2108
- debug.log('self-dev.merge', 'release-path-inspection-failed', { number: input.number, error: String(error) });
2109
- return { merged: false, detail: `automatic merge refused: ${String(error)}` };
2112
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
2113
+ debug.log('self-dev.merge', 'release-path-inspection-failed', { number: input.number, error: String(error), ...(rateLimited ? { rateLimit: true } : {}) });
2114
+ return { merged: false, detail: rateLimited ? `automatic merge refused: rate-limit: ${String(error)}` : `automatic merge refused: ${String(error)}` };
2110
2115
  }
2111
2116
  if (path) return { merged: false, detail: `OP approval required: ${path}` };
2112
2117
  return assembledSeams.mergePr!(input);