elanous 0.2.14 → 0.2.15

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 (293) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -38,6 +38,21 @@ const PROTECTED_LABELS: ReadonlySet<string> = new Set<string>([...PR_LABELS
38
38
  const goalId = (branch: string): string | undefined => /(?:^|[-/])goalid-([a-f0-9]+)(?=-|\/|$)/i.exec(branch)?.[1]?.toLowerCase();
39
39
  export const branchStem = (branch: string): string | undefined => /^self-impl\/(.+)-[a-f0-9]{8}-r[a-f0-9]+$/i.exec(branch)?.[1];
40
40
  const slot = (body?: string): string | undefined => /^칸:\s*(.+?)\s*$/m.exec(body ?? '')?.[1];
41
+ const goalText = (body?: string): string | undefined => /^골:\s*(\S.*?)\s*$/m.exec(body ?? '')?.[1];
42
+ // Auto titles name only files («src/harness: harness-queue.ts») — different goals share them, so they never identify a goal.
43
+ export const isAutoTitle = (title: string): boolean =>
44
+ /^[\w./-]+:\s*[^\s,]+\.[a-z0-9]+(?:\s*,\s*[^\s,]+\.[a-z0-9]+)*$/i.test(title.trim());
45
+
46
+ /** Same goal (TC 10-05): ① both carry a goal identifier → compare it ② otherwise exact title ⊕ both harness drafts ⊕ same branch stem, never an auto title ③ else false. */
47
+ export function sameGoalPr(a: DraftTriagePr, b: DraftTriagePr): boolean {
48
+ const idA = goalId(a.branch), idB = goalId(b.branch);
49
+ if (idA && idB) return idA === idB;
50
+ const textA = goalText(a.body), textB = goalText(b.body);
51
+ if (textA && textB) return textA === textB;
52
+ if (!a.title || a.title !== b.title || isAutoTitle(a.title)) return false;
53
+ const stemA = branchStem(a.branch);
54
+ return Boolean(stemA && a.branch.startsWith('self-impl/') && b.branch.startsWith('self-impl/') && stemA === branchStem(b.branch));
55
+ }
41
56
  const referencesDraft = (text: string, number: number): boolean =>
42
57
  new RegExp(`(?:\\(수확\\s*#${number}\\s*\\)|\\bsuperseded\\s*#${number}(?!\\d))`, 'i').test(text);
43
58
  const landingCommentReferencesDraft = (text: string, number: number): boolean =>
@@ -55,28 +70,30 @@ export function decideDraft({ draft, runStatus, mergedTwins, openDrafts, ageHour
55
70
  return { action: 'keep', reason: 'live' };
56
71
  }
57
72
  const lineage = goalId(draft.branch);
58
- const twin = mergedTwins.find((pr) => pr.number !== draft.number && (
59
- (draft.title && pr.title === draft.title) || (lineage && goalId(pr.branch) === lineage)
60
- ));
61
- if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
73
+ const draftSlot = slot(draft.body);
62
74
  const stem = branchStem(draft.branch);
75
+ const compatible = (pr: DraftTriagePr): boolean =>
76
+ !(lineage && goalId(pr.branch) && goalId(pr.branch) !== lineage)
77
+ && !(draftSlot && slot(pr.body) && slot(pr.body) !== draftSlot);
78
+ const twin = mergedTwins.find((pr) => pr.number !== draft.number && compatible(pr) && sameGoalPr(draft, pr));
79
+ if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
63
80
  const createdAt = Date.parse(draft.createdAt ?? '');
64
81
  const newer = stem && draft.title && Number.isFinite(createdAt) ? openDrafts
65
- ?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem
82
+ ?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem && sameGoalPr(draft, pr)
66
83
  && Number.isFinite(Date.parse(pr.createdAt ?? '')) && Date.parse(pr.createdAt!) > createdAt)
67
84
  .sort((a, b) => Date.parse(b.createdAt!) - Date.parse(a.createdAt!) || b.number - a.number)[0] : undefined;
68
85
  if (newer) return { action: 'close', reason: `duplicate-of-open #${newer.number}` };
69
- const harvest = mergedTwins.find((pr) => pr.number !== draft.number && (
86
+ const harvest = mergedTwins.find((pr) => pr.number !== draft.number && compatible(pr) && (
70
87
  [pr.mergeCommitMessage, pr.body].some((text) => text && referencesDraft(text, draft.number))
71
88
  || pr.landingVerifiedComments?.some((text) => landingCommentReferencesDraft(text, draft.number))
72
89
  ));
73
90
  if (harvest) return { action: 'close', reason: `superseded-by #${harvest.number} (harvest #${draft.number})` };
74
91
  const created = Date.parse(draft.createdAt ?? '');
75
92
  const files = draft.changedFiles;
76
- const draftSlot = slot(draft.body);
77
93
  const later = mergedTwins.filter((pr) => pr.number !== draft.number && Number.isFinite(created)
78
94
  && Number.isFinite(Date.parse(pr.mergedAt ?? '')) && Date.parse(pr.mergedAt!) > created
79
- && ((lineage && goalId(pr.branch) === lineage) || (draftSlot && slot(pr.body) === draftSlot)));
95
+ && compatible(pr) && ((lineage && goalId(pr.branch) === lineage)
96
+ || (draftSlot && slot(pr.body) === draftSlot) || (stem && branchStem(pr.branch) === stem)));
80
97
  if (files?.length && files.every((file) => {
81
98
  const changed = Date.parse(draft.latestFileChanges?.[file] ?? '');
82
99
  return Number.isFinite(changed) && changed >= created && later.some((pr) =>
@@ -260,12 +260,23 @@ export interface SelfDevJobResult {
260
260
  * (merged / pr-opened / gate-failed / review-blocked / pr-declined). */
261
261
  stage?: string;
262
262
  branch?: string;
263
+ /** Child confirmed the finished implementation was pushed for manual harvest. */
264
+ harvestable?: true;
263
265
  worktreePath?: string;
264
266
  prUrl?: string;
265
267
  prNumber?: number;
266
268
  merged?: boolean;
267
269
  /** Child commit SHA, when observed by the Pod/host. */
268
270
  checkedHeadCommit?: string;
271
+ /**
272
+ * This run's own `--base` (not main). A non-main completion mode
273
+ * (worktree-only, draft, `--base` other than main) lands commits here.
274
+ */
275
+ base?: string;
276
+ /** Child finished with GOAL-COMPLETE (`ok=true`). Distinct from a main merge. */
277
+ ok?: boolean;
278
+ /** Commits this child added against its own `--base`. 0 is not completion progress. */
279
+ commitsAheadOfBase?: number;
269
280
  /** A dependency is done but its result is not on main; no child was launched. */
270
281
  blockReason?: 'parent-unlanded';
271
282
  parentPrNumber?: number;
@@ -405,6 +416,7 @@ export type ResumeDisposition = 'skip' | 'rerun' | 'rerun-duplicate-risk';
405
416
  */
406
417
  export function classifyResumeDisposition(result: SelfDevJobResult): ResumeDisposition {
407
418
  if (hasDelivered(result)) return 'skip';
419
+ if (result.harvestable === true && result.branch) return 'skip';
408
420
  if (result.stage === 'pr-opened') return 'rerun-duplicate-risk';
409
421
  if (result.merged === false) return 'rerun';
410
422
  if (result.status !== 'done') return 'rerun';
@@ -621,6 +633,7 @@ export function classifyFailure(
621
633
  ) {
622
634
  return 'awaiting-human';
623
635
  }
636
+ if (result.harvestable === true && result.branch) return 'main-sync-blocked';
624
637
  if (result.status === 'done' && !hasDelivered(result)) return convergedFailureKind();
625
638
  // ⭐ 상류가 죽어 «취소»된 조각 — 종전엔 status 가 failed 도 done 도 아니라 null 로 사라졌다.
626
639
  // ⛔ 이것을 못 보면 「무엇이 상류를 기다리는가」를 슈퍼바이저가 영영 모른다.
@@ -1479,6 +1492,7 @@ export function orchestrateSelfDev(opts: OrchestrateSelfDevOptions): Promise<Sel
1479
1492
  ...(resumeDisposition ? { resumeDisposition } : {}),
1480
1493
  ...(disp?.stage ? { stage: disp.stage } : {}),
1481
1494
  ...(disp?.branch ? { branch: disp.branch } : {}),
1495
+ ...(disp?.harvestable === true && disp.branch ? { harvestable: true } : {}),
1482
1496
  ...(disp?.worktreePath ? { worktreePath: disp.worktreePath } : {}),
1483
1497
  ...(disp?.prUrl ? { prUrl: disp.prUrl } : {}),
1484
1498
  ...(disp?.prNumber !== undefined ? { prNumber: disp.prNumber } : {}),
@@ -1526,7 +1540,7 @@ export function orchestrateSelfDev(opts: OrchestrateSelfDevOptions): Promise<Sel
1526
1540
  const rm = opts.removeWorktree ?? defaultRemoveWorktree;
1527
1541
  let removed = 0;
1528
1542
  for (const r of out) {
1529
- if (!r.worktreePath || r.prUrl) continue;
1543
+ if (!r.worktreePath || r.prUrl || r.harvestable) continue;
1530
1544
  try { rm(r.worktreePath); removed++; } catch { /* fail-soft */ }
1531
1545
  }
1532
1546
  if (removed > 0) debug.log('self-dev.orchestrate', 'teardown', { removed });
@@ -73,13 +73,18 @@ export function buildReviewProviderAttempts(
73
73
  const out: ReviewProviderAttempt[] = [primary];
74
74
  const seen = new Set([primary.model]);
75
75
  for (const f of fallbacks) {
76
- if (!f.model.trim() || seen.has(f.model)) continue;
76
+ if (!f.model.trim() || seen.has(f.model) || (primary.provider && f.provider?.name === primary.provider.name)) continue;
77
77
  seen.add(f.model);
78
78
  out.push(f);
79
79
  }
80
80
  return out;
81
81
  }
82
82
 
83
+ /** A distinct provider is available even when the optional config list is empty. */
84
+ export function defaultReviewFallbackModel(primaryProvider?: string): string {
85
+ return resolveModelAlias(primaryProvider === 'grok' ? 'claude' : 'grok')!;
86
+ }
87
+
83
88
  export interface ReviewFallbackObservation {
84
89
  readonly attempt: number;
85
90
  readonly total: number;
@@ -10,6 +10,14 @@ import { sweepFrozenMerges } from '../self-implement/frozen-merges.js';
10
10
  export interface SupervisorRound {
11
11
  round: number;
12
12
  landed: number;
13
+ /**
14
+ * Pieces that finished (`ok=true`) with at least one new commit against
15
+ * this run's own `--base` — not a main merge. Omitted on rounds that
16
+ * never measured it, so older history stays comparable.
17
+ */
18
+ baseCompleted?: number;
19
+ /** The run's own `--base` those completions were counted against. */
20
+ base?: string;
13
21
  actionable: number;
14
22
  reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'];
15
23
  }
@@ -22,6 +30,7 @@ export type SupervisorStopReason =
22
30
  | 'parent-signals-red'
23
31
  | 'needs-human'
24
32
  | 'no-actionable-work'
33
+ | 'harvestable-awaiting-human'
25
34
  | 'handed-off-to-salvage'
26
35
  | 'max-rounds'
27
36
  | 'no-progress'
@@ -39,6 +48,7 @@ export const SUPERVISOR_STOP_REASONS = [
39
48
  'parent-signals-red',
40
49
  'needs-human',
41
50
  'no-actionable-work',
51
+ 'harvestable-awaiting-human',
42
52
  'handed-off-to-salvage',
43
53
  'max-rounds',
44
54
  'no-progress',
@@ -105,9 +115,25 @@ export function countLanded(results: readonly SelfDevJobResult[]): number {
105
115
  return results.filter(hasDelivered).length;
106
116
  }
107
117
 
118
+ /** One base label for the round. Mixed or missing bases stay unnamed. */
119
+ export function roundBase(results: readonly SelfDevJobResult[]): string | undefined {
120
+ const bases = [...new Set(results.flatMap((result) => result.base === undefined ? [] : [result.base]))];
121
+ return bases.length === 1 ? bases[0] : undefined;
122
+ }
123
+
124
+ /**
125
+ * Finished pieces (`ok=true`) that added at least one commit against this
126
+ * run's own `--base`. A main merge is not required, and a commit without
127
+ * `ok=true` is not completion.
128
+ */
129
+ export function countBaseCompleted(results: readonly SelfDevJobResult[]): number {
130
+ return results.filter((result) => result.ok === true && (result.commitsAheadOfBase ?? 0) >= 1).length;
131
+ }
132
+
108
133
  export function madeProgress(prev: SupervisorRound | undefined, next: SupervisorRound): boolean {
109
134
  if (!prev) return true;
110
135
  if (next.landed > 0) return true;
136
+ if ((next.baseCompleted ?? 0) > (prev.baseCompleted ?? 0)) return true;
111
137
  if (next.actionable < prev.actionable) return true;
112
138
  return next.reviewMustFixTrend === 'improving';
113
139
  }
@@ -191,8 +217,16 @@ export function decideNextRun(input: {
191
217
  const t = triageRun(activeResults, deployFindings, observation === 'failed');
192
218
  const round = history.length;
193
219
  const landed = countLanded(results);
220
+ const baseCompleted = countBaseCompleted(results);
221
+ const completionBase = roundBase(results);
194
222
  const actionable = countActionable(t);
195
- const thisRound: SupervisorRound = { round, landed, actionable, reviewMustFixTrend };
223
+ const thisRound: SupervisorRound = {
224
+ round,
225
+ landed,
226
+ ...(baseCompleted > 0 || completionBase !== undefined ? { baseCompleted, ...(completionBase === undefined ? {} : { base: completionBase }) } : {}),
227
+ actionable,
228
+ reviewMustFixTrend,
229
+ };
196
230
  const goalPlanRevisionObservation = formatGoalPlanRevisionObservation(results);
197
231
  const withGoalPlanRevisionObservation = (why: string): string => goalPlanRevisionObservation
198
232
  && goalPlanRevisionObservation !== GOAL_PLAN_REVISION_OBSERVATION_UNAVAILABLE
@@ -226,6 +260,16 @@ export function decideNextRun(input: {
226
260
  };
227
261
  }
228
262
 
263
+ const harvestable = results.filter((result) => result.harvestable === true && !!result.branch);
264
+ if (harvestable.length > 0) {
265
+ return {
266
+ ...base,
267
+ action: 'stop',
268
+ stopReason: 'harvestable-awaiting-human',
269
+ why: withGoalPlanRevisionObservation(`구현 완료 · 수확 대기 — ${harvestable.map((result) => result.branch).join(' · ')}`),
270
+ };
271
+ }
272
+
229
273
  if (deliverableMergeCompleteness === 'complete') {
230
274
  return {
231
275
  ...base,
@@ -364,7 +408,7 @@ export function decideNextRun(input: {
364
408
  ...base,
365
409
  action: 'stop',
366
410
  stopReason: 'no-progress',
367
- why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 — 착지도 안 늘고 남은 조각도 안 줄었다(${actionable}). 골·불변식을 의심할 자리`),
411
+ why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 — base=${completionBase ?? 'main'} 기준 완료 ${baseCompleted} · 착지 ${landed} · 남은 조각도 안 줄었다(${actionable}). 골·불변식을 의심할 자리`),
368
412
  };
369
413
  }
370
414
  }
@@ -390,9 +434,12 @@ export function appendRound(
390
434
  reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'],
391
435
  ): SupervisorRound[] {
392
436
  const t = triageRun(results.filter((result) => result.salvage !== 'launched'));
437
+ const baseCompleted = countBaseCompleted(results);
438
+ const base = roundBase(results);
393
439
  return [...history, {
394
440
  round: history.length,
395
441
  landed: countLanded(results),
442
+ ...(baseCompleted > 0 || base !== undefined ? { baseCompleted, ...(base === undefined ? {} : { base }) } : {}),
396
443
  actionable: countActionable(t),
397
444
  ...(reviewMustFixTrend === undefined ? {} : { reviewMustFixTrend }),
398
445
  }];
@@ -7,7 +7,7 @@ import { debug } from '../debug/log.js';
7
7
  import { beginLandingMerge, readLandingFreeze, type LandingFreeze } from '../release-loop/landing-freeze.js';
8
8
 
9
9
  /** `manual`: the owning repository could not be determined — a sweep never resumes it; a person does. */
10
- export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true }
10
+ export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true; /** Host goal document, so a resumed merge can still apply its «이 칸 완료» declaration (CL-AUTO). */ goalFile?: string }
11
11
  /** What a resume sweep hands its merge function: the held entry, marked as already claimed by this sweep. */
12
12
  export type ResumedMerge = FrozenMerge & { resumed: true };
13
13
 
@@ -65,7 +65,8 @@ function readQueue(root: string): FrozenMerge[] {
65
65
  Number.isSafeInteger(item.prNumber) && item.prNumber > 0 &&
66
66
  typeof item.headCommit === 'string' && /^[a-f0-9]{40}$/i.test(item.headCommit) &&
67
67
  typeof item.repoRoot === 'string' && item.repoRoot.length > 0 &&
68
- (item.manual === undefined || item.manual === true))) throw new Error(`invalid frozen merge queue: ${path}`);
68
+ (item.manual === undefined || item.manual === true) &&
69
+ (item.goalFile === undefined || (typeof item.goalFile === 'string' && item.goalFile.length > 0)))) throw new Error(`invalid frozen merge queue: ${path}`);
69
70
  return data as FrozenMerge[];
70
71
  }
71
72
 
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'no
6
6
  import { groundMissionInCodebase, isRepositoryImplementationCandidate, type CodebaseGrounding } from '../autopilot/mission-codebase-gate.js';
7
7
  import { extractSlugSource, generateMissionSlug, slugify } from '../autopilot/mission-registry.js';
8
8
  import { enhancePrompt, type EnhanceOpts, type EnhanceResult } from '../prompt-enhance/enhance.js';
9
+ import type { LLMUsage } from '../prompt-cache/types.js';
9
10
  import { debug } from '../debug/log.js';
10
11
 
11
12
  function observeGoalAuthor(
@@ -324,9 +325,12 @@ function startGoalAuthorPhase(phase: GoalAuthorTimedPhase, authorRunId: string,
324
325
  return Date.now();
325
326
  }
326
327
 
327
- function endGoalAuthorPhase(phase: GoalAuthorTimedPhase, startedAt: number, authorRunId: string, onProgress?: GoalAuthorDeps['onProgress']): void {
328
+ type GoalAuthorTokens = { inputTokens: number | null; outputTokens: number | null };
329
+ const UNKNOWN_GOAL_AUTHOR_TOKENS: GoalAuthorTokens = { inputTokens: null, outputTokens: null };
330
+
331
+ function endGoalAuthorPhase(phase: GoalAuthorTimedPhase, startedAt: number, authorRunId: string, onProgress?: GoalAuthorDeps['onProgress'], tokens: GoalAuthorTokens = UNKNOWN_GOAL_AUTHOR_TOKENS): void {
328
332
  const elapsedMs = Date.now() - startedAt;
329
- observeGoalAuthor('goal-author', 'phase-end', { phase, authorRunId, elapsedMs });
333
+ observeGoalAuthor('goal-author', 'phase-end', { phase, authorRunId, elapsedMs, ...tokens });
330
334
  if (GOAL_AUTHOR_PHASES.includes(phase as GoalAuthorPhase)) {
331
335
  if (onProgress) onProgress(phase as GoalAuthorPhase, 'end');
332
336
  else process.stderr.write(`[goal-author] ${phase} ended in ${elapsedMs}ms\n`);
@@ -1156,6 +1160,21 @@ function observeAuthoredGroundingPathSections(document: string, paths: readonly
1156
1160
  }
1157
1161
  }
1158
1162
 
1163
+ function authoredGoalSectionChars(document: string): Record<string, number> {
1164
+ const headings = linesOutsideFencedCode(document)
1165
+ .filter(({ text }) => /^## (?:\S)/.test(text))
1166
+ .map(({ text, start }) => ({ heading: text.slice(3).trim(), start }));
1167
+ const sections: Record<string, number> = { preamble: [...document.slice(0, headings[0]?.start ?? document.length)].length };
1168
+ for (let index = 0; index < headings.length; index += 1) {
1169
+ const { heading, start } = headings[index];
1170
+ sections[heading] = (sections[heading] ?? 0) + [...document.slice(start, headings[index + 1]?.start ?? document.length)].length;
1171
+ }
1172
+ for (const heading of ['PROBLEM', 'WHAT TO BUILD', 'ACCEPTANCE CRITERIA', 'TRACED PATHS', 'SCOPE BOUNDARY', 'STEPS', '답하지 못하는 것']) {
1173
+ sections[heading] ??= 0;
1174
+ }
1175
+ return sections;
1176
+ }
1177
+
1159
1178
  function canonicalBlocks(document: string): string[] {
1160
1179
  return linesOutsideFencedCode(document)
1161
1180
  .filter((line) => /^##(?:\s|$)/.test(line.text))
@@ -4906,6 +4925,13 @@ async function authorGoalWithSupersededRootIntent(
4906
4925
  }
4907
4926
  // ⭐ 이 저작 «한 번»의 id. goalId 보다 «먼저» 있어야 ground·enhance 의 소요를 그 골에 붙일 수 있다.
4908
4927
  const authorRunId = generateGoalId();
4928
+ const authorStartedAt = Date.now();
4929
+ const enhanceTokens: GoalAuthorTokens = { inputTokens: null, outputTokens: null };
4930
+ const onEnhanceUsage = (usage: LLMUsage): void => {
4931
+ if (usage.inputTokens !== undefined) enhanceTokens.inputTokens = (enhanceTokens.inputTokens ?? 0) + usage.inputTokens;
4932
+ if (usage.outputTokens !== undefined) enhanceTokens.outputTokens = (enhanceTokens.outputTokens ?? 0) + usage.outputTokens;
4933
+ try { deps.enhanceOpts?.onUsage?.(usage); } catch { /* usage observation is fail-soft */ }
4934
+ };
4909
4935
  let facts: CodebaseGrounding | null = null;
4910
4936
  let groundingError = false;
4911
4937
  const persistentGrounding = resolveGoalAuthorPersistentGrounding('persistentGrounding' in deps ? deps.persistentGrounding : undefined);
@@ -4937,8 +4963,8 @@ async function authorGoalWithSupersededRootIntent(
4937
4963
  ? await deps.enhance(
4938
4964
  enhanceAsk,
4939
4965
  persistentFacts.length > 0
4940
- ? { ...deps.enhanceOpts, groundedFacts: persistentFacts }
4941
- : deps.enhanceOpts,
4966
+ ? { ...deps.enhanceOpts, groundedFacts: persistentFacts, onUsage: onEnhanceUsage }
4967
+ : { ...deps.enhanceOpts, onUsage: onEnhanceUsage },
4942
4968
  )
4943
4969
  : { original: ask, checklist: [], verbatimPreserved: true };
4944
4970
  // ⭐ goal-context 규범이 «어디에도 안 실린다»는 것을 관측으로 남긴다.
@@ -4956,7 +4982,7 @@ async function authorGoalWithSupersededRootIntent(
4956
4982
  placement: 'omitted',
4957
4983
  });
4958
4984
  } finally {
4959
- endGoalAuthorPhase('enhance', enhanceStartedAt, authorRunId, deps.onProgress);
4985
+ endGoalAuthorPhase('enhance', enhanceStartedAt, authorRunId, deps.onProgress, enhanceTokens);
4960
4986
  }
4961
4987
  if (!enhancement.verbatimPreserved || (enhanceAsk && enhancement.original !== enhanceAsk)) {
4962
4988
  throw new Error('goal author refused an enhancement that does not preserve its input verbatim');
@@ -5431,6 +5457,18 @@ async function authorGoalWithSupersededRootIntent(
5431
5457
  } finally {
5432
5458
  endGoalAuthorPhase('lint', lintStartedAt, authorRunId, deps.onProgress);
5433
5459
  }
5460
+ try {
5461
+ observeGoalAuthor('goal-author', 'summary', {
5462
+ authorRunId,
5463
+ elapsedMs: Date.now() - authorStartedAt,
5464
+ inputTokens: enhanceTokens.inputTokens,
5465
+ outputTokens: enhanceTokens.outputTokens,
5466
+ totalTokens: enhanceTokens.inputTokens === null || enhanceTokens.outputTokens === null
5467
+ ? null : enhanceTokens.inputTokens + enhanceTokens.outputTokens,
5468
+ goalChars: [...document].length,
5469
+ sectionChars: authoredGoalSectionChars(document),
5470
+ });
5471
+ } catch { /* observation is fail-soft */ }
5434
5472
  return { document, facts, grounded: !groundingError && hasEvidence, authorRunId };
5435
5473
  } finally {
5436
5474
  endAssembleSubphase();
@@ -35,6 +35,8 @@ type GoalClarificationAnsweredBy = 'human' | 'agent' | 'none' | 'mixed';
35
35
 
36
36
  export interface GoalClarificationEscalationInput {
37
37
  goalFile: string;
38
+ /** Explicit provenance for isolated test fixtures; never inferred from goalFile. */
39
+ testOrigin?: true;
38
40
  delivery?: GoalClarificationDelivery;
39
41
  resolvedDelivery?: GoalClarificationObservedDelivery;
40
42
  dispatch?: (
@@ -577,6 +579,9 @@ export async function escalateGoalDocumentClarifications(
577
579
  }],
578
580
  },
579
581
  input.dispatchContext?.sessionId,
582
+ {},
583
+ undefined,
584
+ { testOrigin: input.testOrigin },
580
585
  );
581
586
  pendingQuestionPersistence.write(pending);
582
587
  pendingWritten += 1;
@@ -1,7 +1,10 @@
1
- import { appendFileSync, mkdirSync, readFileSync, rmdirSync, statSync } from 'node:fs';
1
+ import { createHash } from 'node:crypto';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { effectiveInstanceRoot } from '../instance/resolve.js';
4
5
  import { debug } from '../debug/log.js';
6
+ import { getUserConfig } from '../user-config.js';
7
+ import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js';
5
8
 
6
9
  export type FailureSource = 'release-run' | 'harness-run' | 'loop-tick' | 'cron';
7
10
 
@@ -14,6 +17,114 @@ export interface FailureEvent {
14
17
  }
15
18
 
16
19
  const FOLD_WINDOW_MS = 60 * 60 * 1_000;
20
+ const HEAL_GRAPH = join(import.meta.dir, '../../graphs/heal/heal-loop.yaml');
21
+
22
+ type HealLoopOptions = { runId: string; input: { failureEvent: FailureEvent }; deps: { root: string } };
23
+ type StartHealLoop = (file: string, options: HealLoopOptions) => Promise<unknown>;
24
+
25
+ function healRunId(event: FailureEvent): string {
26
+ return `heal-${createHash('sha256').update(JSON.stringify([event.source, event.ref, event.at])).digest('hex')}`;
27
+ }
28
+
29
+ function maxConcurrentLoops(): number {
30
+ const raw = getUserConfig().raw.heal;
31
+ const cap = raw && typeof raw === 'object' && !Array.isArray(raw)
32
+ ? (raw as Record<string, unknown>).maxConcurrentLoops : undefined;
33
+ return typeof cap === 'number' && Number.isSafeInteger(cap) && cap > 0 ? cap : 2;
34
+ }
35
+
36
+ function ownerAlive(pid: unknown, pidStartedAt: unknown): boolean {
37
+ if (!Number.isSafeInteger(pid) || (pid as number) <= 0 || typeof pidStartedAt !== 'string') return false;
38
+ const started = Date.parse(pidStartedAt);
39
+ if (!Number.isFinite(started)) return false;
40
+ try {
41
+ const actual = psProcessStartMs(pid as number);
42
+ return actual !== null && Number.isFinite(actual) && Math.abs(actual - started) <= START_TOLERANCE_MS;
43
+ } catch { return false; }
44
+ }
45
+
46
+ function liveHealRuns(root: string): Set<string> {
47
+ const live = new Set<string>();
48
+ const dir = join(root, 'graph-runs', 'heal-loop');
49
+ let files: string[];
50
+ try { files = readdirSync(dir); }
51
+ catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return live;
53
+ throw error;
54
+ }
55
+ for (const file of files) {
56
+ if (!file.endsWith('.json')) continue;
57
+ try {
58
+ const state = JSON.parse(readFileSync(join(dir, file), 'utf8')) as Record<string, unknown>;
59
+ if (state.graphId === 'heal-loop' && state.runId === file.slice(0, -5) &&
60
+ state.status === 'running' && ownerAlive(state.pid, state.pidStartedAt)) live.add(state.runId as string);
61
+ } catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
63
+ }
64
+ }
65
+ return live;
66
+ }
67
+
68
+ /** A test process must never start the real heal loop — it spawns external grounding (10-05 load 150 incident). */
69
+ export async function startHealLoop(file: string, options: HealLoopOptions, env: NodeJS.ProcessEnv = process.env,
70
+ run: StartHealLoop = async (graph, args) => (await import('../graph-runner/runner.js')).runGraph(graph, args)): Promise<unknown> {
71
+ if (env.NODE_ENV === 'test' || env.ELANOUS_TEST_HOME) {
72
+ debug.log('heal.intake', 'loop-start-skipped-test', { runId: options.runId });
73
+ return undefined;
74
+ }
75
+ const root = options.deps.root;
76
+ const claimDir = join(root, 'heal', 'starts');
77
+ const selected = withInboxLock(inboxPath(root), () => {
78
+ mkdirSync(claimDir, { recursive: true });
79
+ const live = liveHealRuns(root);
80
+ const claimed = new Set<string>();
81
+ for (const name of readdirSync(claimDir)) {
82
+ if (!/^heal-[a-f0-9]{64}\.json$/.test(name)) continue;
83
+ const runId = name.slice(0, -5);
84
+ const path = join(claimDir, name);
85
+ const claim = JSON.parse(readFileSync(path, 'utf8')) as { active?: boolean; started?: boolean; pid?: number; pidStartedAt?: string };
86
+ const owner = claim.active && ownerAlive(claim.pid, claim.pidStartedAt);
87
+ if (!owner && !claim.started && !existsSync(join(root, 'graph-runs', 'heal-loop', `${runId}.json`))) {
88
+ unlinkSync(path);
89
+ continue;
90
+ }
91
+ claimed.add(runId);
92
+ if (owner && !live.has(runId)) live.add(runId);
93
+ }
94
+ const cap = maxConcurrentLoops();
95
+ if (live.size >= cap) {
96
+ debug.log('heal.intake', 'loop-start-deferred', { running: live.size, cap, runId: options.runId });
97
+ return undefined;
98
+ }
99
+ const next = readInbox(inboxPath(root)).find(event => !claimed.has(healRunId(event)) &&
100
+ !existsSync(join(root, 'graph-runs', 'heal-loop', `${healRunId(event)}.json`)));
101
+ if (!next) return undefined;
102
+ const runId = healRunId(next);
103
+ writeFileSync(join(claimDir, `${runId}.json`), JSON.stringify({ active: true, pid: process.pid,
104
+ pidStartedAt: new Date(Date.now() - process.uptime() * 1_000).toISOString() }), { flag: 'wx' });
105
+ return { event: next, runId };
106
+ });
107
+ if (!selected) return undefined;
108
+ let started = false;
109
+ try {
110
+ const result = await run(file, { runId: selected.runId, input: { failureEvent: selected.event }, deps: { root } });
111
+ started = true;
112
+ return result;
113
+ } finally {
114
+ withInboxLock(inboxPath(root), () => {
115
+ const claim = join(claimDir, `${selected.runId}.json`);
116
+ if (started || existsSync(join(root, 'graph-runs', 'heal-loop', `${selected.runId}.json`))) {
117
+ writeFileSync(claim, JSON.stringify({ active: false, started: true }));
118
+ } else {
119
+ unlinkSync(claim);
120
+ }
121
+ });
122
+ // A completed run opens a slot; a pre-ledger failure waits for the next intake rather than retrying in a tight loop.
123
+ if (started) void Promise.resolve().then(() => startHealLoop(file, options, env, run)).catch(error => {
124
+ debug.log('heal.intake', 'loop-start-failed', { runId: selected.runId, error: String(error) }, { level: 'error' });
125
+ });
126
+ }
127
+ }
17
128
 
18
129
  function inboxPath(root: string): string {
19
130
  return join(root, 'heal', 'inbox.jsonl');
@@ -55,7 +166,7 @@ function withInboxLock<T>(file: string, work: () => T): T {
55
166
  }
56
167
 
57
168
  /** One durable JSON line per failure; repeat source+ref within an hour is folded. */
58
- export function recordFailureEvent(event: FailureEvent, root = effectiveInstanceRoot()): { folded: boolean } {
169
+ export function recordFailureEvent(event: FailureEvent, root = effectiveInstanceRoot(), startLoop: StartHealLoop = startHealLoop): { folded: boolean } {
59
170
  if (!['release-run', 'harness-run', 'loop-tick', 'cron'].includes(event.source) ||
60
171
  !event.kind || !event.ref || !event.summary || !Number.isFinite(Date.parse(event.at))) {
61
172
  throw new Error('invalid heal failure event');
@@ -69,6 +180,13 @@ export function recordFailureEvent(event: FailureEvent, root = effectiveInstance
69
180
  return false;
70
181
  });
71
182
  debug.log('heal.intake', 'recorded', { source: event.source, kind: event.kind, ref: event.ref, folded });
183
+ if (!folded) {
184
+ const runId = healRunId(event);
185
+ void Promise.resolve().then(() => startLoop(HEAL_GRAPH, { runId, input: { failureEvent: event }, deps: { root } }))
186
+ .catch(error => {
187
+ debug.log('heal.intake', 'loop-start-failed', { source: event.source, kind: event.kind, ref: event.ref, runId, error: String(error) }, { level: 'error' });
188
+ });
189
+ }
72
190
  return { folded };
73
191
  }
74
192
 
@@ -20,7 +20,7 @@ export function needsPwaBuild(files: readonly string[]): boolean {
20
20
  return files.some((file) => file.startsWith('apps/pwa/') || (file.startsWith('src/') && !/\.test\.tsx?$/.test(file)));
21
21
  }
22
22
 
23
- export interface HostRegateInput { prNumber: number; headCommit: string; repoRoot: string; verifyOnly?: boolean; /** Called by a resume sweep that already holds this PR's claim. */ resumed?: true }
23
+ export interface HostRegateInput { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string; verifyOnly?: boolean; /** Called by a resume sweep that already holds this PR's claim. */ resumed?: true }
24
24
  export interface HostRegateResult { passed: boolean; failures: Array<{ step: string; detail: string }>; os: string; status?: 'passed' | 'failed' | 'unmeasured' | 'frozen'; /** verifyOnly: 실제로 얹어 잰 base 끝 */ baseCommit?: string }
25
25
  export type HostRegateDeps = {
26
26
  command?: (bin: string, args: readonly string[], cwd: string, env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
@@ -31,6 +31,7 @@ export type HostRegateDeps = {
31
31
  log?: (event: 'passed' | 'failed' | 'unmeasured' | 'base-raced', data: Record<string, unknown>) => void;
32
32
  freezeRoot?: string;
33
33
  runExposeGate?: typeof runExposeGate;
34
+ syncMergedChecklist?: typeof import('../release-loop/merged-pr-checklist.js').syncMergedPrChecklist;
34
35
  };
35
36
 
36
37
  const SPAWN_MAX_BUFFER = 256 * 1024 * 1024;
@@ -240,7 +241,7 @@ export async function runHostRegate(input: HostRegateInput, deps: HostRegateDeps
240
241
  // --match-head-commit pins the head. gh has no base pin, so the base was re-read just
241
242
  // above; the seconds between that read and the merge are checked after the fact below.
242
243
  if (input.verifyOnly) return result('passed');
243
- const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit });
244
+ const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot, ...(input.goalFile ? { goalFile: input.goalFile } : {}) }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit });
244
245
  if (landing.kind !== 'merge') {
245
246
  debug.log('harness.merge', 'frozen', { pr: input.prNumber, ...(landing.kind === 'held' ? { reason: landing.freeze.reason, until: landing.freeze.until } : { resumedElsewhere: true }) });
246
247
  return { passed: true, failures: [], os: process.platform, status: 'frozen' };
@@ -255,6 +256,12 @@ export async function runHostRegate(input: HostRegateInput, deps: HostRegateDeps
255
256
  catch (e) { return result('unmeasured', 'merge-confirm', String(e)); }
256
257
  if (merged.state !== 'MERGED') return result('unmeasured', 'merge-confirm', `PR state after merge is ${merged.state ?? 'unknown'}`);
257
258
  landed = true;
259
+ try {
260
+ const sync = deps.syncMergedChecklist ?? (await import('../release-loop/merged-pr-checklist.js')).syncMergedPrChecklist;
261
+ sync(input.prNumber, input.repoRoot, input.goalFile);
262
+ } catch (error) {
263
+ debug.log('release.checklist', 'merged-pr-sync-failed', { pr: input.prNumber, error: String(error) });
264
+ }
258
265
  const mergeCommit = merged.mergeCommit?.oid ?? '';
259
266
  let mergedOnto = '';
260
267
  try {