elanous 0.2.15 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) 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-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
  55. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  56. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +15 -15
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +15 -15
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +15 -15
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +15 -15
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +15 -15
  67. package/apps/pwa/out/ceo/index.html +1 -1
  68. package/apps/pwa/out/ceo/index.txt +15 -15
  69. package/apps/pwa/out/chat/index.html +1 -1
  70. package/apps/pwa/out/chat/index.txt +15 -15
  71. package/apps/pwa/out/consult/index.html +1 -1
  72. package/apps/pwa/out/consult/index.txt +15 -15
  73. package/apps/pwa/out/control/index.html +1 -1
  74. package/apps/pwa/out/control/index.txt +15 -15
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +15 -15
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +15 -15
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +15 -15
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +15 -15
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +15 -15
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +15 -15
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +15 -15
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +15 -15
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +15 -15
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +15 -15
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +15 -15
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +15 -15
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +15 -15
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +15 -15
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +15 -15
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +16 -16
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +15 -15
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +15 -15
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +15 -15
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +15 -15
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +15 -15
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +15 -15
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +15 -15
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +15 -15
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +15 -15
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +15 -15
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +15 -15
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +15 -15
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +15 -15
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +15 -15
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +15 -15
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +15 -15
  146. package/graphs/doc-gen/doc-gen.yaml +24 -0
  147. package/graphs/doc-gen/recipes.yaml +3 -0
  148. package/graphs/intake/intake-daily.yaml +9 -2
  149. package/graphs/intake/news-sources.yaml +4 -0
  150. package/graphs/intake/recipes.yaml +6 -1
  151. package/graphs/orchestrator/orchestrator.yaml +7 -0
  152. package/graphs/orchestrator/recipes.yaml +3 -0
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/__fixtures__/intake-news.xml +7 -0
  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 +34 -15
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/goals/goal-census.ts +343 -0
  163. package/scripts/intake-news.ts +169 -0
  164. package/scripts/measure/author-section-contribution.ts +84 -0
  165. package/scripts/release-loop/cut-branch.ts +43 -13
  166. package/scripts/release-loop/docs-land-node.ts +69 -10
  167. package/scripts/release-loop/gate-node.ts +3 -1
  168. package/scripts/release-loop/publish-node.ts +2 -2
  169. package/scripts/release-loop/release-docs.ts +134 -0
  170. package/scripts/release-loop/vault-note-node.ts +19 -2
  171. package/scripts/release-loop/version-node.ts +7 -4
  172. package/scripts/release-story/draft.ts +42 -14
  173. package/scripts/rhythm/monthly.ts +97 -0
  174. package/src/acp/client.ts +17 -2
  175. package/src/agent-mission/driver.ts +104 -11
  176. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  177. package/src/agent-mission/supervisor-metrics.ts +218 -0
  178. package/src/autopilot/track-agent-shadow.ts +2 -1
  179. package/src/autopilot/track-agent.ts +5 -3
  180. package/src/briefing/brief-items.ts +191 -36
  181. package/src/budget/usage-outcomes.ts +204 -0
  182. package/src/cli/autopilot-cli.ts +66 -0
  183. package/src/cli/brief-cli.ts +73 -0
  184. package/src/cli/decisions-cli.ts +4 -1
  185. package/src/cli/intake-cli.ts +1 -1
  186. package/src/cli/know-cli.ts +20 -0
  187. package/src/cli/log-event-names.ts +1 -0
  188. package/src/cli/logs-cli.ts +43 -1
  189. package/src/cli/pr-cli.ts +92 -13
  190. package/src/cli/provider-cli.ts +14 -0
  191. package/src/cli/release-cli.ts +11 -8
  192. package/src/cli/self-update-canary.ts +46 -0
  193. package/src/cli/self-update.ts +399 -1
  194. package/src/cli/usage-cli.ts +26 -0
  195. package/src/context-card/index.ts +45 -0
  196. package/src/context-card/task.ts +76 -0
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  198. package/src/decisions/decision-ledger.ts +10 -1
  199. package/src/discord-self-message.ts +9 -0
  200. package/src/flow-loop/split.ts +24 -0
  201. package/src/harness/harness-cli-command.ts +178 -12
  202. package/src/harness/harness-pod-dispatch.ts +36 -55
  203. package/src/harness/harness-queue.ts +86 -7
  204. package/src/harness/harness-seams.ts +13 -3
  205. package/src/harness/helper-repair.ts +139 -8
  206. package/src/harness/helper-scan.ts +13 -2
  207. package/src/harness/nested-elanous-policy.ts +133 -0
  208. package/src/hq/fence-wrapper.ts +123 -0
  209. package/src/hq/hq.ts +16 -2
  210. package/src/hq/lease.ts +5 -3
  211. package/src/hq/transfer.ts +185 -0
  212. package/src/index.ts +168 -13
  213. package/src/intake-plane/digest.ts +85 -28
  214. package/src/knowledge/know-find.ts +105 -0
  215. package/src/llm.ts +75 -19
  216. package/src/loops/orchestrator/finish-rate.ts +14 -3
  217. package/src/loops/orchestrator/tick.ts +96 -25
  218. package/src/maturity/bot-slash-catalog.ts +11 -2
  219. package/src/maturity/feature-maturity.ts +2 -2
  220. package/src/maturity/tui-slash-availability.ts +9 -0
  221. package/src/nexus/api/grid.ts +61 -0
  222. package/src/nexus/api/http-server.ts +19 -1
  223. package/src/nexus/api/task-cards-api.ts +19 -0
  224. package/src/oauth/codex.ts +82 -2
  225. package/src/oauth/overload-failover-launch.ts +75 -0
  226. package/src/oauth/overload-failover.ts +137 -0
  227. package/src/release-loop/checklist.ts +10 -5
  228. package/src/release-loop/feature-store.ts +10 -9
  229. package/src/release-loop/landing-freeze.ts +18 -10
  230. package/src/release-loop/placement.ts +17 -8
  231. package/src/seat-dispatch/seat-ask.ts +33 -6
  232. package/src/seat-loop/seat-loop.ts +172 -21
  233. package/src/self-dev/ask-launch-flow.ts +48 -8
  234. package/src/self-dev/authored-goal-trace.ts +64 -0
  235. package/src/self-dev/dev-cli.ts +19 -1
  236. package/src/self-dev/dev-pipeline.ts +6 -5
  237. package/src/self-dev/entrance-registry.ts +10 -6
  238. package/src/self-dev/old-door.ts +73 -0
  239. package/src/self-dev/run-store.ts +5 -1
  240. package/src/self-dev/run-supervisor.ts +7 -1
  241. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  242. package/src/self-implement/app-api-budget.ts +166 -0
  243. package/src/self-implement/budget-gate.ts +7 -5
  244. package/src/self-implement/follow-up-goals.ts +289 -0
  245. package/src/self-implement/frozen-merges.ts +25 -20
  246. package/src/self-implement/goal-author-clarification.ts +81 -0
  247. package/src/self-implement/goal-author-cli.ts +33 -5
  248. package/src/self-implement/goal-author.ts +107 -25
  249. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  250. package/src/self-implement/goal-document-write.ts +28 -0
  251. package/src/self-implement/heal-intake.ts +4 -0
  252. package/src/self-implement/heal-triage.ts +44 -0
  253. package/src/self-implement/host-regate.ts +3 -1
  254. package/src/self-implement/orchestrator.ts +157 -8
  255. package/src/self-implement/run-ledger.ts +4 -0
  256. package/src/self-implement/seams.ts +44 -15
  257. package/src/session-runtime/index.ts +2 -0
  258. package/src/session-runtime/retry-api.ts +87 -0
  259. package/src/session-runtime/retry-policy.ts +107 -6
  260. package/src/steward/working-backwards.ts +16 -1
  261. package/src/task-agent/actions.ts +118 -0
  262. package/src/task-agent/judge.ts +96 -0
  263. package/src/task-cards/card-cli.ts +25 -0
  264. package/src/task-cards/card-store.ts +9 -4
  265. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  266. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  267. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  268. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  269. package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
  270. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  271. package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
  272. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  273. package/src/telegram-commands.ts +6 -1
  274. package/src/telegram-loops-command.ts +21 -14
  275. package/src/telegram.ts +47 -0
  276. package/src/user-config.ts +59 -4
  277. package/src/version/packed-revision.json +1 -1
  278. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  305. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  306. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  307. package/src/launch-head/seat-goal-queue.ts +0 -198
  308. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  309. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -22,6 +22,7 @@ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
22
22
  export type QueueItem = {
23
23
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
24
24
  at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
25
+ launchArgs?: string[]; launchCwd?: string; cellId?: string;
25
26
  };
26
27
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
27
28
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
@@ -53,6 +54,10 @@ export interface HarnessQueueDeps {
53
54
  cap?: (seat: QueueSeat) => number;
54
55
  pool?: () => QueuePool;
55
56
  launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
57
+ /** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
58
+ afterEnqueue?: (item: QueueItem) => void | Promise<void>;
59
+ /** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
60
+ launchCommand?: string;
56
61
  alive?: (pid: number) => boolean;
57
62
  processes?: () => readonly QueueProcess[];
58
63
  receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
@@ -143,6 +148,9 @@ function read(path: string): QueueItem[] {
143
148
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
144
149
  && (row.launchId === undefined || typeof row.launchId === 'string')
145
150
  && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
151
+ && (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
152
+ && (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
153
+ && (row.cellId === undefined || typeof row.cellId === 'string')
146
154
  && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
147
155
  throw new Error('harness queue: invalid queue file (no launch)');
148
156
  }
@@ -207,7 +215,24 @@ function seat(value: string): QueueSeat {
207
215
  return value as QueueSeat;
208
216
  }
209
217
 
210
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
218
+ /** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
219
+ export function queueCellId(text: string): string | undefined {
220
+ return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
221
+ }
222
+
223
+ /** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
224
+ function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
225
+ if (kind === 'say') return queueCellId(input);
226
+ try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
227
+ }
228
+
229
+ export class HarnessQueueDuplicateError extends Error {
230
+ constructor(existing: QueueItem, key: string) {
231
+ super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
232
+ }
233
+ }
234
+
235
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
211
236
  const assigned = seat(input.seat);
212
237
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
213
238
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -228,8 +253,24 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
228
253
  return existing;
229
254
  }
230
255
  }
256
+ // Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
257
+ const cellId = queueRowCellId(kind, value);
258
+ if (input.refuseDuplicate) {
259
+ const cell = cellId;
260
+ const existing = rows.find((row) => row.status !== 'finished'
261
+ && !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
262
+ && ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
263
+ if (existing) {
264
+ const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
265
+ observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
266
+ throw new HarnessQueueDuplicateError(existing, key);
267
+ }
268
+ }
231
269
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
232
270
  hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
271
+ ...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
272
+ ...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
273
+ ...(cellId === undefined ? {} : { cellId }),
233
274
  ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
234
275
  save(path, [...rows, item]);
235
276
  observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
@@ -318,6 +359,7 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
318
359
  }
319
360
 
320
361
  export function queueLaunchArgs(item: QueueItem): string[] {
362
+ if (item.launchArgs) return item.launchArgs;
321
363
  return ['harness', item.kind, item.input, '--substrate', 'pod',
322
364
  ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
323
365
  }
@@ -468,15 +510,19 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
468
510
  }
469
511
  }
470
512
 
471
- async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
513
+ function defaultLaunch(command?: string) {
514
+ return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
515
+ }
516
+
517
+ async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
472
518
  const repo = resolve(import.meta.dir, '../..');
473
519
  const logPath = join(root, 'harness', `${item.id}.log`);
474
520
  const fd = openSync(logPath, 'a', 0o600);
475
521
  try {
476
522
  const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
477
- harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
523
+ harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
478
524
  ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
479
- cwd: repo, detached: true, stdio: ['ignore', fd, fd],
525
+ cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
480
526
  env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
481
527
  ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
482
528
  });
@@ -675,7 +721,7 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
675
721
  }
676
722
  }
677
723
 
678
- export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
724
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
679
725
  const root = deps.root ?? effectiveInstanceRoot();
680
726
  return locked(root, async (path) => {
681
727
  try {
@@ -699,10 +745,20 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
699
745
  }
700
746
  const heads = new Map<QueueSeat, QueueItem>();
701
747
  for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
748
+ if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
749
+ const row = items.find((item) => item.id === requestedId);
750
+ if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
751
+ const reason = '앞선 대기열 항목 차례';
752
+ if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
753
+ observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
754
+ return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
755
+ }
702
756
  if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
703
757
  const lastSeat = lastLaunchedSeat(path);
704
758
  const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
705
759
  : queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
760
+ // A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
761
+ // head with room; an earlier seat's head with room means the requested row waits for its turn.
706
762
  const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
707
763
  .flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
708
764
  const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
@@ -729,7 +785,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
729
785
  ...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
730
786
  attributed, unattributed }, deps);
731
787
  }
732
- return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
788
+ const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
789
+ const subjectReason = blocked.get(subject.seat)!.reason;
790
+ return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
733
791
  };
734
792
  for (const item of candidates) {
735
793
  const injectedCap = deps.cap?.(item.seat);
@@ -799,11 +857,32 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
799
857
  + current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
800
858
  }
801
859
  if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
860
+ if (requestedId && item.id !== requestedId) {
861
+ const requested = current.find((row) => row.id === requestedId)!;
862
+ // The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
863
+ // evaluated yet; «another seat's turn» only when the requested seat itself has room.
864
+ const own = blocked.get(requested.seat)?.reason ?? (() => {
865
+ const ownCap = deps.cap?.(requested.seat);
866
+ const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
867
+ const charged = runs.filter((run) => !run.seat || run.seat === requested.seat);
868
+ const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
869
+ const active = charged.length + current.filter((row) => row.seat === requested.seat &&
870
+ (row.status === 'launching' || row.status === 'launched')
871
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
872
+ if (active >= details.cap) return seatCapReason(requested.seat, active, details);
873
+ const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
874
+ return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
875
+ })();
876
+ const reason = own ?? `다른 자리 차례 — ${item.seat}`;
877
+ if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
878
+ observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
879
+ return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
880
+ }
802
881
  const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
803
882
  delete launching.waitingReason;
804
883
  save(path, current.map((row) => row.id === item.id ? launching : row));
805
884
  try {
806
- const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
885
+ const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
807
886
  if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
808
887
  const launched: QueueItem = { ...launching, pid, status: 'launched' };
809
888
  save(path, current.map((row) => row.id === item.id ? launched : row));
@@ -727,10 +727,13 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
727
727
  let inspectionError: string | undefined;
728
728
  try {
729
729
  if (!deps.seams.readPrFiles) throw new Error('readPrFiles seam unavailable');
730
- const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd });
730
+ const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd, ...(deps.base ? { base: deps.base } : {}) });
731
731
  if (!Array.isArray(prFiles) || !prFiles.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
732
732
  holdPath = releasePathHold(prFiles);
733
- } catch (error) { inspectionError = String(error); }
733
+ } catch (error) {
734
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
735
+ inspectionError = rateLimited ? `rate-limit: ${String(error)}` : String(error);
736
+ }
734
737
  if (holdPath) {
735
738
  const protectedPath = holdPath;
736
739
  const prCwd = cwd;
@@ -745,7 +748,14 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
745
748
  observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-hold' });
746
749
  } else if (inspectionError) {
747
750
  observe('release-path-inspection-failed', { number: pr.number, error: inspectionError });
748
- observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-inspection-failed' });
751
+ observe('deployed-pr', {
752
+ number: pr.number,
753
+ url: pr.url,
754
+ autoReview: false,
755
+ mergeReason: inspectionError.startsWith('rate-limit:')
756
+ ? 'release-path-inspection-failed: rate-limit'
757
+ : 'release-path-inspection-failed',
758
+ });
749
759
  } else {
750
760
  let autoReviewLabeled = false;
751
761
  if (labels?.includes(AUTO_REVIEW_LABEL)) {
@@ -2,6 +2,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { debug } from '../debug/log.js';
5
+ import { releasePathHold, releasePrFilePaths } from '../self-dev/release-path-guard.js';
6
+ import { getUserConfig, type UserConfig } from '../user-config.js';
7
+ import type { QueueItem, QueueSeat } from './harness-queue.js';
5
8
  import type { RunGh, ScannedStoppedPr, StoppedPrCategory } from './helper-scan.js';
6
9
 
7
10
  export interface RepairGoalInput {
@@ -62,26 +65,154 @@ export function repairInputsFromBody(body: string): { originalRequest: string; m
62
65
 
63
66
  export type RepairShadowRow = { pr: number; category: StoppedPrCategory; goalHash: string; at: string };
64
67
 
65
- /** 그림자: 수리 골을 저작해 헬퍼 원장에만 남긴다(발사·라벨 0) · 같은 PR 은 한 번만. */
66
- export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: { runGh: RunGh; root: string; now?: Date }): Promise<RepairShadowRow[]> {
68
+ type RepairLedgerResult = 'shadow' | 'launched' | 'cap' | 'needs-human' | 'launch-failed';
69
+
70
+ type RepairLedgerLine = {
71
+ pr: number; category: StoppedPrCategory; goalHash: string; at: string;
72
+ mode: 'shadow' | 'live'; goal: string; result: RepairLedgerResult;
73
+ /** Harness queue item id — the run itself starts on a later queue tick. */ queueId?: string; reason?: string;
74
+ };
75
+
76
+ const RELEASE_FILE_PAGE = 100;
77
+
78
+ /** Changed files of the original PR. A failed read is a reason to hold, never a reason to launch. */
79
+ async function changedFiles(pr: number, runGh: RunGh): Promise<string[]> {
80
+ const pages: unknown[] = [];
81
+ for (let page = 1; ; page++) {
82
+ const raw = await runGh(['api', '--method', 'GET', '--raw-field', `per_page=${RELEASE_FILE_PAGE}`, '--raw-field', `page=${page}`, `repos/{owner}/{repo}/pulls/${pr}/files`]);
83
+ const parsed: unknown = JSON.parse(raw);
84
+ if (!Array.isArray(parsed)) throw new Error('gh api PR files returned an invalid file list');
85
+ pages.push(parsed);
86
+ if (parsed.length < RELEASE_FILE_PAGE) break;
87
+ }
88
+ return releasePrFilePaths(pages);
89
+ }
90
+
91
+ const QUEUE_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
92
+
93
+ /** 원 PR 의 자리 라벨(elanous:seat-<자리> 또는 자리 이름 라벨). 없거나 둘 이상이면 MK. */
94
+ function seatOf(labels: readonly { name?: string }[] | undefined): QueueSeat {
95
+ const found = new Set<QueueSeat>();
96
+ for (const label of labels ?? []) {
97
+ const name = label.name ?? '';
98
+ const prefixed = /^elanous:seat-(OP|TC|MK|UX)$/.exec(name)?.[1];
99
+ const bare = (QUEUE_SEATS as readonly string[]).includes(name) ? name : undefined;
100
+ const seat = prefixed ?? bare;
101
+ if (seat) found.add(seat as QueueSeat);
102
+ }
103
+ return found.size === 1 ? [...found][0]! : 'MK';
104
+ }
105
+
106
+ function helperSettings(config: UserConfig | undefined): { mode: 'shadow' | 'live'; perDay: number } {
107
+ const helper = config?.harness?.helper;
108
+ return {
109
+ mode: helper?.repair === 'live' ? 'live' : 'shadow',
110
+ perDay: typeof helper?.repairPerDay === 'number' && Number.isSafeInteger(helper.repairPerDay) && helper.repairPerDay >= 0
111
+ ? helper.repairPerDay : 3,
112
+ };
113
+ }
114
+
115
+ /** 그림자(기본): 수리 골을 원장에만 남긴다. live: 같은 함수로 대기열에 넣고 원 PR 을 superseded 로 표시한다. 같은 PR 은 평생 한 번. */
116
+ export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: {
117
+ runGh: RunGh; root: string; now?: Date;
118
+ config?: UserConfig;
119
+ /** 대기열 추가. 생략하면 addHarnessQueue — 실제 발사는 기존 대기열 틱이 한다. */
120
+ enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<Pick<QueueItem, 'id'>>;
121
+ }): Promise<RepairShadowRow[]> {
67
122
  const ledger = join(deps.root, 'helper', 'repairs.jsonl');
68
- const seen = new Set<number>();
123
+ const prior: RepairLedgerLine[] = [];
69
124
  if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
70
125
  if (!line.trim()) continue;
71
- try { seen.add((JSON.parse(line) as { pr: number }).pr); } catch { /* malformed line is skipped */ }
126
+ try { prior.push(JSON.parse(line) as RepairLedgerLine); } catch { /* malformed line is skipped */ }
127
+ }
128
+ const { mode, perDay } = helperSettings(deps.config ?? getUserConfig());
129
+ const day = (deps.now ?? new Date()).toISOString().slice(0, 10);
130
+ // Terminal results end a PR for good. A shadow line blocks only while still in shadow (live must still launch it),
131
+ // and a cap or failed launch blocks only for the same UTC day so the PR is retried tomorrow (ACP must-fix).
132
+ // Ledger lines written before results existed have no `result`; they were shadow records.
133
+ for (const line of prior) if (!line.result) line.result = 'shadow';
134
+ const seen = new Set(prior.filter((line) => line.result === 'launched' || line.result === 'needs-human'
135
+ || (line.result === 'shadow' && mode === 'shadow')
136
+ || ((line.result === 'cap' || line.result === 'launch-failed') && line.at.slice(0, 10) === day)).map((line) => line.pr));
137
+ // Distinct PRs: a follow-up repair line for an already launched PR is not a second launch.
138
+ let launchedToday = new Set(prior.filter((line) => line.result === 'launched' && line.at.slice(0, 10) === day).map((line) => line.pr)).size;
139
+ // A launched line whose label/comment failed is repaired here — the follow-up only, never a second enqueue.
140
+ if (mode === 'live') {
141
+ const latest = new Map<number, RepairLedgerLine>();
142
+ for (const line of prior) latest.set(line.pr, line);
143
+ for (const line of latest.values()) {
144
+ if (line.result !== 'launched' || !line.reason?.startsWith('후속 실패') || !line.queueId) continue;
145
+ const followups: string[] = [];
146
+ if (line.reason.includes('label ·')) {
147
+ try { await deps.runGh(['pr', 'edit', String(line.pr), '--add-label', 'elanous:superseded']); }
148
+ catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
149
+ }
150
+ if (line.reason.includes('comment ·')) {
151
+ try { await deps.runGh(['pr', 'comment', String(line.pr), '--body', `수리 대기열 항목 ${line.queueId} 로 대체(런은 대기열 틱에서 시작)`]); }
152
+ catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
153
+ }
154
+ const repaired: RepairLedgerLine = { ...line, at: (deps.now ?? new Date()).toISOString(), ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : { reason: '후속 복구' }) };
155
+ appendFileSync(ledger, JSON.stringify(repaired) + '\n');
156
+ debug.log('harness.helper', 'repair-followup-retried', { pr: line.pr, queueId: line.queueId, remaining: followups.length });
157
+ }
72
158
  }
159
+ const enqueue = deps.enqueue ?? (async (input) => {
160
+ const { addHarnessQueue } = await import('./harness-queue.js');
161
+ return addHarnessQueue(input, { root: deps.root });
162
+ });
73
163
  const written: RepairShadowRow[] = [];
74
164
  for (const row of rows) {
75
165
  if ((row.category !== 'review-budget' && row.category !== 'review-oscillation') || seen.has(row.pr)) continue;
76
166
  const view = JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'body'])) as { body: string };
167
+ // 자리 라벨은 본문 조회와 분리한다 — 기존 본문 조회 계약(--json body)을 그대로 둔다. 못 읽으면 MK.
168
+ let labels: { name?: string }[] | undefined;
169
+ if (mode === 'live') {
170
+ try { labels = (JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'labels'])) as { labels?: { name?: string }[] }).labels; }
171
+ catch { labels = undefined; }
172
+ }
77
173
  const goal = authorRepairGoal({ pr: row.pr, category: row.category, ...repairInputsFromBody(view.body) });
78
174
  if (!goal) continue;
79
- const entry: RepairShadowRow = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at: (deps.now ?? new Date()).toISOString() };
175
+ const at = (deps.now ?? new Date()).toISOString();
176
+ const base = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at, goal };
177
+ let record: RepairLedgerLine = { ...base, mode: 'shadow', result: 'shadow' };
178
+ if (mode === 'live') {
179
+ let files: string[] | undefined;
180
+ try { files = await changedFiles(row.pr, deps.runGh); }
181
+ catch (error) { files = undefined; debug.log('harness.helper', 'repair-files-unreadable', { pr: row.pr, reason: String(error) }); }
182
+ const release = files && releasePathHold(files);
183
+ // 상한은 «발사한 수»의 상한이다. 사람 승인이 필요한 PR 은 그 수를 쓰지 않으므로 상한보다 먼저 가른다.
184
+ if (files && !release && launchedToday >= perDay) {
185
+ record = { ...base, mode: 'live', result: 'cap', reason: '상한' };
186
+ } else if (!files) {
187
+ // An unreadable file list is transient: hold today, retry on a later day (never a permanent needs-human) (ACP must-fix).
188
+ record = { ...base, mode: 'live', result: 'launch-failed', reason: '변경 파일 조회 실패 · 다음 날 재시도' };
189
+ } else if (release) {
190
+ record = { ...base, mode: 'live', result: 'needs-human', reason: '사람 승인 필요' };
191
+ } else {
192
+ let item: Pick<QueueItem, 'id'> | undefined;
193
+ try {
194
+ item = await enqueue({ seat: seatOf(labels), say: goal, idempotencyKey: `helper-repair-${row.pr}` });
195
+ // The queue item exists from here on: a later label/comment failure is recorded on the launched line,
196
+ // never as launch-failed (which would let the next scan enqueue a second repair) (ACP must-fix).
197
+ const followups: string[] = [];
198
+ try { await deps.runGh(['pr', 'edit', String(row.pr), '--add-label', 'elanous:superseded']); }
199
+ catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
200
+ try { await deps.runGh(['pr', 'comment', String(row.pr), '--body', `수리 대기열 항목 ${item.id} 로 대체(런은 대기열 틱에서 시작)`]); }
201
+ catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
202
+ record = { ...base, mode: 'live', result: 'launched', queueId: item.id, ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : {}) };
203
+ launchedToday += 1;
204
+ debug.log('harness.helper', 'repair-launched', { pr: row.pr, queueId: item.id, mode: 'live', followupFailures: followups.length });
205
+ } catch (error) {
206
+ record = { ...base, mode: 'live', result: 'launch-failed', reason: `발사 실패 · ${error instanceof Error ? error.message : String(error)}` };
207
+ debug.log('harness.helper', 'repair-launch-failed', { pr: row.pr, reason: record.reason });
208
+ }
209
+ }
210
+ }
80
211
  mkdirSync(dirname(ledger), { recursive: true });
81
- appendFileSync(ledger, JSON.stringify({ ...entry, mode: 'shadow', goal }) + '\n');
82
- debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
212
+ appendFileSync(ledger, JSON.stringify(record) + '\n');
213
+ if (record.result === 'shadow') debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
83
214
  seen.add(row.pr);
84
- written.push(entry);
215
+ written.push({ pr: base.pr, category: base.category, goalHash: base.goalHash, at: base.at });
85
216
  }
86
217
  return written;
87
218
  }
@@ -139,8 +139,19 @@ if (import.meta.main) {
139
139
  // 명시적 그림자 기록에서만 추가 PR 조회와 헬퍼 원장 쓰기를 수행한다(발사 0).
140
140
  const { recordRepairShadows } = await import('./helper-repair.js');
141
141
  const { effectiveInstanceRoot } = await import('../instance/resolve.js');
142
- const shadows = await recordRepairShadows(rows, { runGh, root: effectiveInstanceRoot() });
143
- if (shadows.length) console.log(`수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
142
+ const { getUserConfig } = await import('../user-config.js');
143
+ const root = effectiveInstanceRoot();
144
+ const config = getUserConfig();
145
+ const live = config.harness?.helper?.repair === 'live';
146
+ const shadows = await recordRepairShadows(rows, {
147
+ runGh, root, config,
148
+ // live 만 대기열에 넣는다. 실제 발사는 기존 대기열 틱이 한다(새 발사 경로 0).
149
+ ...(live ? { enqueue: async (input) => {
150
+ const { addHarnessQueue } = await import('./harness-queue.js');
151
+ return addHarnessQueue(input, { root });
152
+ } } : {}),
153
+ });
154
+ if (shadows.length) console.log(live ? `수리 골 ${shadows.length}건 → helper/repairs.jsonl` : `수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
144
155
  }
145
156
  if (json) console.log(JSON.stringify(rows, null, 2));
146
157
  else for (const row of rows) {
@@ -0,0 +1,133 @@
1
+ // Nested elanous launch policy.
2
+ //
3
+ // Codex children drop parent env under shell_environment_policy inherit=core, so a
4
+ // depth marker that only lives in the environment never arrives. Depth is therefore
5
+ // carried on the codex argv (`-c shell_environment_policy.set.ELANOUS_NESTED_DEPTH=<n>`).
6
+ //
7
+ // Unset ELANOUS_NESTED_DEPTH is depth 0 (the outer launcher). A missing marker is
8
+ // not "depth unknown" and does not refuse an ordinary mission spawn.
9
+ // Refusal for a backend that cannot carry the marker applies only when a nested
10
+ // elanous launch was actually requested (allow flag or config allow).
11
+ //
12
+ // Only depth 0 (the outer launcher) may allow a nested elanous launch. At depth >= 1
13
+ // an allow flag is ignored and the launch stays refused.
14
+
15
+ import { debug } from '../debug/log.js';
16
+ import { getUserConfig } from '../user-config.js';
17
+
18
+ export const ELANOUS_NESTED_DEPTH_ENV = 'ELANOUS_NESTED_DEPTH';
19
+
20
+ /** Codex config key that sets the depth marker inside the child's shell policy. */
21
+ export const CODEX_NESTED_DEPTH_CONFIG_KEY = `shell_environment_policy.set.${ELANOUS_NESTED_DEPTH_ENV}`;
22
+
23
+ export type NestedElanousBackendName = 'codex' | 'claude' | 'gemini' | 'grok' | 'aside' | (string & {});
24
+
25
+ export interface NestedElanousDecision {
26
+ readonly allowed: boolean;
27
+ readonly depth: number;
28
+ /** True when depth >= 1 supplied allow and it was ignored. */
29
+ readonly allowIgnored: boolean;
30
+ readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored' | 'depth-cap';
31
+ }
32
+
33
+ /** Current process depth from the nested-depth marker. Unset or unreadable = 0 (outer launcher). */
34
+ export function readNestedElanousDepth(env: Record<string, string | undefined> = process.env): number {
35
+ const raw = env[ELANOUS_NESTED_DEPTH_ENV];
36
+ if (raw === undefined || raw.trim() === '') return 0;
37
+ if (!/^\d+$/.test(raw.trim())) return 0;
38
+ return Number(raw);
39
+ }
40
+
41
+ /** True when this backend can carry the depth marker to its child. Only codex can. */
42
+ export function backendCarriesNestedDepth(backendName: string): boolean {
43
+ return backendName === 'codex';
44
+ }
45
+
46
+ /**
47
+ * Codex argv fragment that sets the child's depth marker.
48
+ * `depth` is the child's depth (parent + 1). Non-codex backends return [] — they cannot carry it.
49
+ */
50
+ export function nestedDepthCodexArgs(backendName: string, childDepth: number): string[] {
51
+ if (backendName !== 'codex') return [];
52
+ if (!Number.isInteger(childDepth) || childDepth < 0) return [];
53
+ return ['-c', `${CODEX_NESTED_DEPTH_CONFIG_KEY}=${childDepth}`];
54
+ }
55
+
56
+ /**
57
+ * Allow decision. Depth 0 allows only when `allow` is true.
58
+ * Depth >= 1 always refuses; a supplied allow is ignored and logged.
59
+ */
60
+ /** Nested depth cap: explicit value, else `harness.nestedElanousMaxDepth`, else 2. */
61
+ export function resolveNestedDepthCap(maxDepth?: number): number {
62
+ const configured = maxDepth ?? getUserConfig().harness?.nestedElanousMaxDepth;
63
+ return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 2;
64
+ }
65
+
66
+ export function decideNestedElanousLaunch(input: {
67
+ readonly depth: number;
68
+ readonly allow?: boolean;
69
+ readonly maxDepth?: number;
70
+ /** False for an ordinary mission spawn that did not request a nested launch — no cap log. */
71
+ readonly logCap?: boolean;
72
+ }): NestedElanousDecision {
73
+ const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
74
+ const cap = resolveNestedDepthCap(input.maxDepth);
75
+ if (depth >= cap) {
76
+ if (input.logCap !== false) debug.log('agent-mission.nested', 'depth-cap', { depth, cap });
77
+ return { allowed: false, depth, allowIgnored: input.allow === true, reason: 'depth-cap' };
78
+ }
79
+ if (depth >= 1) {
80
+ if (input.allow === true) {
81
+ debug.log('agent-mission.nested', 'allow-ignored', { depth });
82
+ return { allowed: false, depth, allowIgnored: true, reason: 'allow-ignored' };
83
+ }
84
+ return { allowed: false, depth, allowIgnored: false, reason: 'nested-refused' };
85
+ }
86
+ if (input.allow === true) {
87
+ return { allowed: true, depth, allowIgnored: false, reason: 'depth-0-allow' };
88
+ }
89
+ return { allowed: false, depth, allowIgnored: false, reason: 'depth-0' };
90
+ }
91
+
92
+ /** Config `harness.nestedElanous`. Only `'allow'` counts, and only at depth 0. */
93
+ export function configAllowsNestedElanous(
94
+ config: { harness?: { nestedElanous?: string } } | undefined,
95
+ depth: number,
96
+ ): boolean {
97
+ if (depth !== 0) return false;
98
+ return config?.harness?.nestedElanous === 'allow';
99
+ }
100
+
101
+ /**
102
+ * Gate a nested elanous launch.
103
+ *
104
+ * Ordinary mission spawn is not this gate: call it only when a nested elanous
105
+ * launch was requested. A backend that cannot carry the depth marker is then
106
+ * refused (`depth-unknown`) — including depth 0 with allow — because the child
107
+ * would not know its own depth. Unset depth is 0, not unknown.
108
+ *
109
+ * `allow` is the flag. Config allow counts only at depth 0 and never overrides
110
+ * a depth >= 1 refusal.
111
+ */
112
+ export function gateNestedElanousLaunch(input: {
113
+ readonly backendName: string;
114
+ readonly depth: number;
115
+ /** True only when this call is gating a requested nested elanous launch. */
116
+ readonly nestedLaunchRequested: boolean;
117
+ readonly allow?: boolean;
118
+ readonly configAllow?: boolean;
119
+ readonly maxDepth?: number;
120
+ }): NestedElanousDecision {
121
+ const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
122
+ const allow = input.nestedLaunchRequested && (input.allow === true || (depth === 0 && input.configAllow === true));
123
+ if (!input.nestedLaunchRequested) {
124
+ return decideNestedElanousLaunch({ depth, allow: false, logCap: false, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
125
+ }
126
+ if (!backendCarriesNestedDepth(input.backendName)) {
127
+ // The depth-unknown refusal stands; reaching the cap is still recorded.
128
+ const cap = resolveNestedDepthCap(input.maxDepth);
129
+ if (depth >= cap) debug.log('agent-mission.nested', 'depth-cap', { depth, cap, backend: input.backendName });
130
+ return { allowed: false, depth, allowIgnored: false, reason: 'depth-unknown' };
131
+ }
132
+ return decideNestedElanousLaunch({ depth, allow, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
133
+ }