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
@@ -3,10 +3,17 @@ import { lstatSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { debug } from '../debug/log.js';
5
5
 
6
- export type RevertGuardResult = { reverted: string[]; protected: string[]; warning?: string };
6
+ export type RevertGuardResult = { reverted: string[]; protected: string[]; staleDeleted?: string[]; outsideDeleted?: string[]; warning?: string };
7
+
8
+ function isTargetPath(file: string, targets: readonly string[]): boolean {
9
+ return targets.some((target) => {
10
+ const path = target.replace(/^\.\//, '').replace(/\/+$/, '');
11
+ return path !== '' && (file === path || file.startsWith(`${path}/`));
12
+ });
13
+ }
7
14
 
8
15
  /** Compare the worktree's exact bytes with blobs from recent HEAD ancestry, before add -A. */
9
- export function dropStaleReverts(cwd: string, targets: readonly string[], runId: string, depth = 50): RevertGuardResult {
16
+ export function dropStaleReverts(cwd: string, targets: readonly string[], runId: string, depth = 50, base?: string): RevertGuardResult {
10
17
  const result: RevertGuardResult = { reverted: [], protected: [] };
11
18
  const git = (...args: string[]): Buffer => execFileSync('git', args, { cwd, timeout: 20_000, maxBuffer: 16 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
12
19
  const reason = (error: unknown): string => (error instanceof Error ? error.message : String(error)).replace(/\s+/g, ' ').slice(0, 240);
@@ -15,6 +22,38 @@ export function dropStaleReverts(cwd: string, targets: readonly string[], runId:
15
22
  const commits = git('rev-list', `--max-count=${limit + 1}`, 'HEAD').toString('utf8').trim().split('\n');
16
23
  const changed = git('diff', '--name-only', '--diff-filter=M', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean);
17
24
  const staged = git('diff', '--cached', '--name-only', '--diff-filter=M', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean);
25
+ const deleted = new Set([
26
+ ...git('diff', '--name-only', '--diff-filter=D', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean),
27
+ ...git('diff', '--cached', '--name-only', '--diff-filter=D', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean),
28
+ ]);
29
+ for (const file of deleted) {
30
+ if (isTargetPath(file, targets)) continue;
31
+ try {
32
+ // add -A stages the worktree: even a dangling symlink replaces the staged deletion.
33
+ try {
34
+ lstatSync(join(cwd, file));
35
+ continue;
36
+ } catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
38
+ }
39
+ // ls-tree returning an empty entry means absent; a failed lookup must not look like absence.
40
+ const headEntry = git('ls-tree', 'HEAD', '--', file).toString('utf8').trim();
41
+ if (!/^\d+ blob [0-9a-f]+\t/.test(headEntry)) continue;
42
+ const baseEntry = base ? git('ls-tree', base, '--', file).toString('utf8').trim() : null;
43
+ if (baseEntry === '' && base) {
44
+ git('checkout', 'HEAD', '--', file);
45
+ (result.staleDeleted ??= []).push(file);
46
+ result.reverted.push(file);
47
+ debug.log('self-implement.revert-guard', 'reverted-stale-delete', { runId, files: [file] });
48
+ } else {
49
+ (result.outsideDeleted ??= []).push(file);
50
+ debug.log('self-implement.revert-guard', 'outside-delete', { runId, files: [file] }, { level: 'warn' });
51
+ }
52
+ } catch (error) {
53
+ result.warning = `git inspection/checkout failed for ${file}: ${reason(error)}`;
54
+ debug.log('self-implement.revert-guard', 'fail-open', { runId, reason: result.warning }, { level: 'warn' });
55
+ }
56
+ }
18
57
  for (const file of new Set([...changed, ...staged])) {
19
58
  try {
20
59
  if (!lstatSync(join(cwd, file)).isFile()) continue;
@@ -37,11 +76,7 @@ export function dropStaleReverts(cwd: string, targets: readonly string[], runId:
37
76
  }
38
77
  }
39
78
  if (!matchedCommit) continue;
40
- const scoped = targets.some((target) => {
41
- const path = target.replace(/^\.\//, '').replace(/\/+$/, '');
42
- return path !== '' && (file === path || file.startsWith(`${path}/`));
43
- });
44
- if (scoped) {
79
+ if (isTargetPath(file, targets)) {
45
80
  result.protected.push(file);
46
81
  debug.log('self-implement.revert-guard', 'protected-target', { runId, files: [file], matchedCommit }, { level: 'warn' });
47
82
  } else {
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { refuseProductionLedgerWriteInTest } from '../harness/ledger-write-guard.js';
3
4
  import { tracedPathReferences, verbatimOriginalAsk } from './goal-author.js';
4
5
  import { parseAskTargetPathHintsResult } from '../self-dev/launch-preflight.js';
5
6
  import { isRunStatus } from './run-status-mapping.js';
@@ -402,6 +403,7 @@ export function describeFederatedMissingRunLedger(runId: string, options: Federa
402
403
 
403
404
  /** Append one observer event. Callers own fail-soft handling. */
404
405
  export function appendRunLedgerEntry(entry: RunLedgerEntry, dir = runLedgerDir()): void {
406
+ if (refuseProductionLedgerWriteInTest(dir, 'run-ledger')) return;
405
407
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
406
408
  const live = runLedgerPath(entry.runId, dir);
407
409
  withRunLedgerLock(dir, entry.runId, () => {
@@ -16,6 +16,7 @@ export type SelfImplementStage =
16
16
  | 'worktree-completed'
17
17
  | 'gate-failed'
18
18
  | 'review-blocked'
19
+ | 'no-changes'
19
20
  | 'merge-conflict'
20
21
  | 'aborted'
21
22
  | 'timed-out'
@@ -30,6 +31,7 @@ const STAGE_OUTCOMES = {
30
31
  'worktree-completed': { runStatus: 'completed' },
31
32
  'gate-failed': { runStatus: 'failed', failureKind: 'gate' },
32
33
  'review-blocked': { runStatus: 'failed', failureKind: 'review' },
34
+ 'no-changes': { runStatus: 'failed', failureKind: 'review' },
33
35
  'merge-conflict': { runStatus: 'failed', failureKind: 'merge-conflict' },
34
36
  aborted: { runStatus: 'failed', failureKind: 'aborted' },
35
37
  'timed-out': { runStatus: 'failed', failureKind: 'timed-out' },
@@ -2513,7 +2513,7 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
2513
2513
  verdict: rr.verdict,
2514
2514
  mustFix: rr.mustFix,
2515
2515
  shouldFix: rr.shouldFix,
2516
- summary: renderReview(rr).slice(0, 2000),
2516
+ summary: rr.reviewed ? renderReview(rr).slice(0, 2000) : `리뷰 못 함(사유: ${rr.failureReason ?? '사유 미상'})`,
2517
2517
  reviewed: rr.reviewed ?? false,
2518
2518
  // ⛔⭐ 이 한 줄이 `orchestrator.ts` 의 「찍는 줄」에 값을 «흘린다». 빼면 그 줄은 살아 있는데 값이 안 온다.
2519
2519
  ...(rr.failureReason !== undefined ? { failureReason: rr.failureReason } : {}),
@@ -108,6 +108,7 @@ export function singleRunAsJobResult(feature: string, r: SelfImplementResultWith
108
108
  stage: r.stage,
109
109
  ...(!r.ok && r.detail ? { error: { message: r.detail } } : {}),
110
110
  ...(r.branch ? { branch: r.branch } : {}),
111
+ ...(r.harvestable === true && r.branch ? { harvestable: true } : {}),
111
112
  ...(r.worktreePath ? { worktreePath: r.worktreePath } : {}),
112
113
  ...(r.prUrl ? { prUrl: r.prUrl } : {}),
113
114
  ...(r.merged !== undefined ? { merged: r.merged } : {}),
@@ -0,0 +1,338 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { prodInstanceRoot } from '../instance/resolve.js';
6
+ import { seatOfTree } from '../loops/orchestrator/traffic.js';
7
+ import { getUserConfig, ORCHESTRATOR_DEFAULTS } from '../user-config.js';
8
+ import { isRunStatus } from './run-status-mapping.js';
9
+ import { loadRunLedger, runLedgerDir, type RunLedgerEntry, type RunLedgerReader } from './run-ledger.js';
10
+
11
+ export type SpeedClaimsOutcome = 'merged' | 'failed' | 'abandoned' | 'completed-without-merge';
12
+ export type SpeedClaimsSeat = 'OP' | 'TC' | 'MK' | 'UX' | 'unknown';
13
+ export interface SpeedClaimsRow {
14
+ seat: SpeedClaimsSeat;
15
+ outcome: SpeedClaimsOutcome;
16
+ count: number;
17
+ underOneMinuteCount: number;
18
+ medianMinutes: number;
19
+ q1Minutes: number;
20
+ q3Minutes: number;
21
+ }
22
+ export interface SpeedClaimsMeasurement {
23
+ scope: 'self-implement-run-ledger';
24
+ ledgerDirectory: string;
25
+ status: 'measured' | 'cannot-measure';
26
+ completedCount: number;
27
+ inProgressCount: number;
28
+ cannotMeasureCount: number;
29
+ unknownSeatCount: number;
30
+ rows: SpeedClaimsRow[];
31
+ note: string;
32
+ parent: ParentSpeedClaimsMeasurement;
33
+ }
34
+ export interface ParentSpeedClaimsEvent {
35
+ ts: string;
36
+ category: string;
37
+ event: string;
38
+ data: unknown;
39
+ }
40
+
41
+ export interface ParentSpeedClaimsRow {
42
+ seat: 'OP' | 'TC' | 'MK' | 'UX' | 'direct' | 'unknown';
43
+ stage: string;
44
+ merged: boolean;
45
+ count: number;
46
+ medianMinutes: number;
47
+ q1Minutes: number;
48
+ q3Minutes: number;
49
+ }
50
+
51
+ export interface ParentSpeedClaimsMeasurement {
52
+ scope: 'self-dev.orchestrate-parent-logs';
53
+ status: 'measured' | 'cannot-measure';
54
+ completedCount: number;
55
+ inProgressCount: number;
56
+ cannotMeasureCount: number;
57
+ rows: ParentSpeedClaimsRow[];
58
+ note: string;
59
+ }
60
+
61
+ export interface SpeedClaimsOptions {
62
+ dir?: string;
63
+ list?: (path: string) => string[];
64
+ read?: RunLedgerReader;
65
+ readSeat?: (runId: string, ledgerDirectory: string, entries: readonly RunLedgerEntry[]) => SpeedClaimsSeat;
66
+ readParentEvents?: () => readonly ParentSpeedClaimsEvent[];
67
+ readParentQueue?: () => unknown;
68
+ }
69
+
70
+ const NOTE = 'Child-ledger elapsed minutes from the first child event (or checkpoint creation when unavailable) to the recorded child end, including retries and resumes; this is not the parent launch-to-done interval. Only finished runs enter quartiles (linear interpolation at ranks (n-1)*p). In-progress runs (including parked) and missing, unreadable or invalid timestamps are excluded, not assigned zero. Seats come from the same state root checkpoint, launch queue or recorded launch tree; unresolved seats are counted separately. A completed run without a recorded merge is not a merge.';
71
+ const CANONICAL_RUN_ID = /^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
72
+ const SEATS: readonly SpeedClaimsSeat[] = ['OP', 'TC', 'MK', 'UX', 'unknown'];
73
+
74
+ function checkpointFor(runId: string, ledgerDirectory: string): Record<string, unknown> | null {
75
+ try {
76
+ const value: unknown = JSON.parse(readFileSync(join(dirname(ledgerDirectory), 'self-dev-runs', `${runId}.json`), 'utf8'));
77
+ return value && typeof value === 'object' && !Array.isArray(value) && 'runId' in value && value.runId === runId
78
+ ? value as Record<string, unknown> : null;
79
+ } catch { return null; }
80
+ }
81
+
82
+ function seatFromLaunch(runId: string, ledgerDirectory: string, entries: readonly RunLedgerEntry[]): SpeedClaimsSeat {
83
+ const checkpoint = checkpointFor(runId, ledgerDirectory);
84
+ if (SEATS.includes(checkpoint?.seat as SpeedClaimsSeat) && checkpoint?.seat !== 'unknown') return checkpoint!.seat as SpeedClaimsSeat;
85
+ // Queue ids and launch ids are not run ids. Only an exact, unique ask-file match can attribute a queued launch.
86
+ const goalFiles = new Set(entries.filter((entry) => entry.event === 'start')
87
+ .map((entry) => entry.data.goalFile).filter((file): file is string => typeof file === 'string' && isAbsolute(file)));
88
+ if (goalFiles.size === 1) {
89
+ try {
90
+ const queue: unknown = JSON.parse(readFileSync(join(dirname(ledgerDirectory), 'harness', 'queue.json'), 'utf8'));
91
+ if (Array.isArray(queue)) {
92
+ const matched = queue.filter((row) => row && typeof row === 'object' && row.kind === 'ask'
93
+ && (row.status === 'launching' || row.status === 'launched' || row.status === 'finished')
94
+ && row.input === [...goalFiles][0] && SEATS.includes(row.seat));
95
+ if (matched.length === 1 && matched[0]!.seat !== 'unknown') return matched[0]!.seat as SpeedClaimsSeat;
96
+ }
97
+ } catch { /* An absent or unreadable queue cannot establish attribution. */ }
98
+ }
99
+ try {
100
+ const cfg = getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
101
+ const paths = entries.flatMap((entry) => entry.event === 'start' ? [entry.data.targetRoot] : entry.event === 'worktree' ? [entry.data.path] : []);
102
+ const seats = new Set<SpeedClaimsSeat>();
103
+ for (const path of paths) {
104
+ if (typeof path !== 'string' || !isAbsolute(path)) continue;
105
+ try {
106
+ const seat = seatOfTree(path, cfg);
107
+ if (seat) seats.add(seat);
108
+ } catch { /* A removed or unreadable launch tree does not prove a seat. */ }
109
+ }
110
+ if (seats.size === 1) return [...seats][0]!;
111
+ } catch { /* Unavailable seat configuration does not erase a readable run. */ }
112
+ return 'unknown';
113
+ }
114
+
115
+ function parseTime(timestamp: unknown): number | null {
116
+ if (typeof timestamp !== 'string') return null;
117
+ const match = /^(\d{4})-(\d\d)-(\d\d)T\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.exec(timestamp);
118
+ if (!match) return null;
119
+ const year = Number(match[1]);
120
+ const month = Number(match[2]);
121
+ const day = Number(match[3]);
122
+ const days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
123
+ if (month < 1 || month > 12 || day < 1 || day > days[month - 1]!) return null;
124
+ const parsed = Date.parse(timestamp);
125
+ return Number.isFinite(parsed) ? parsed : null;
126
+ }
127
+
128
+ function time(entry: RunLedgerEntry | undefined): number | null {
129
+ return parseTime(entry?.timestamp);
130
+ }
131
+
132
+ function percentile(sorted: readonly number[], p: number): number {
133
+ const rank = (sorted.length - 1) * p;
134
+ const low = Math.floor(rank);
135
+ return sorted[low]! + (sorted[Math.ceil(rank)]! - sorted[low]!) * (rank - low);
136
+ }
137
+
138
+ function endOfRun(entries: readonly RunLedgerEntry[]): { entry: RunLedgerEntry; outcome: SpeedClaimsOutcome } | null {
139
+ let end: { entry: RunLedgerEntry; outcome: SpeedClaimsOutcome } | null = null;
140
+ for (const entry of entries) {
141
+ if (entry.event === 'start') { end = null; continue; }
142
+ if (entry.event === 'merged' && entry.data.merged === true) end = { entry, outcome: 'merged' };
143
+ if (end?.outcome === 'merged') continue;
144
+ if (entry.event === 'human-stop') end = { entry, outcome: 'abandoned' };
145
+ if (entry.event === 'run-status' && isRunStatus(entry.data.runStatus)) {
146
+ const status = entry.data.runStatus;
147
+ if (status === 'completed') end = { entry, outcome: 'completed-without-merge' };
148
+ else if (status === 'failed') end = { entry, outcome: entry.data.stage === 'aborted' ? 'abandoned' : 'failed' };
149
+ else if (status === 'cancelled') end = { entry, outcome: 'abandoned' };
150
+ else end = null;
151
+ }
152
+ }
153
+ return end;
154
+ }
155
+
156
+ const PARENT_NOTE = 'Elapsed minutes from the earliest valid parent self-dev.orchestrate start log to the latest valid done log, grouped by that done.data.outcomes[].stage and merged flag. Only runs with both events and valid timestamps enter quartiles. A start without done is in progress. Seats use the same-runId queue seat when available, or direct when a readable queue has no matching entry; an unreadable queue remains unknown. Parent log queries are read-only and independent of child run ledgers.';
157
+ type ParentSeat = ParentSpeedClaimsRow['seat'];
158
+ const PARENT_SEATS: readonly ParentSeat[] = ['OP', 'TC', 'MK', 'UX', 'direct', 'unknown'];
159
+
160
+ function object(value: unknown): Record<string, unknown> | null {
161
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
162
+ }
163
+
164
+ /** Read the first-class logs CLI rather than opening logs.db in the measurement process. */
165
+ function readParentEventsFromLogs(): ParentSpeedClaimsEvent[] {
166
+ const command = fileURLToPath(new URL('../../bin/elanous.mjs', import.meta.url));
167
+ const events: ParentSpeedClaimsEvent[] = [];
168
+ let before: string | undefined;
169
+ const seenCursors = new Set<string>();
170
+ for (;;) {
171
+ const result = spawnSync(process.execPath, [command, `--test=${fileURLToPath(new URL('../../', import.meta.url))}`, 'logs', '--instance', 'prod', '--category', 'self-dev.orchestrate', '--json', '--json-data', '--limit', '5000', ...(before ? ['--before', before] : [])], {
172
+ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
173
+ });
174
+ if (result.error || result.status !== 0) throw new Error(result.stderr || result.error?.message || 'parent logs query failed');
175
+ const lines = result.stdout.trim().split('\n').filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
176
+ if (lines.some((line) => object(line._meta)?.type === 'log-query-unreadable-instances')) throw new Error('parent logs query incomplete');
177
+ events.push(...lines.filter((line): line is Record<string, unknown> & ParentSpeedClaimsEvent =>
178
+ line.store === 'prod' && line.category === 'self-dev.orchestrate' && typeof line.ts === 'string' && typeof line.event === 'string'));
179
+ const limit = lines.find((line) => object(line._meta)?.type === 'log-query-limit');
180
+ if (!limit) return events;
181
+ const meta = object(limit._meta);
182
+ const cursor = meta?.nextCursors ?? meta?.nextCursor;
183
+ if (typeof cursor !== 'number' && !object(cursor)) throw new Error('parent logs pagination incomplete');
184
+ const next = typeof cursor === 'number' ? String(cursor) : JSON.stringify(cursor);
185
+ if (seenCursors.has(next)) throw new Error('parent logs pagination repeated a cursor');
186
+ seenCursors.add(next);
187
+ before = next;
188
+ }
189
+ }
190
+
191
+ function readParentQueueFromState(): unknown {
192
+ return JSON.parse(readFileSync(join(prodInstanceRoot(), 'harness', 'queue.json'), 'utf8')) as unknown;
193
+ }
194
+
195
+ /** Parent launch-to-done census; no child ledger id is used as a join key. */
196
+ export function measureParentSpeedClaims(
197
+ events: readonly ParentSpeedClaimsEvent[], queue?: unknown,
198
+ ): ParentSpeedClaimsMeasurement {
199
+ const result: ParentSpeedClaimsMeasurement = {
200
+ scope: 'self-dev.orchestrate-parent-logs', status: 'measured', completedCount: 0,
201
+ inProgressCount: 0, cannotMeasureCount: 0, rows: [], note: PARENT_NOTE,
202
+ };
203
+ const queueRows = Array.isArray(queue) ? queue : object(queue)?.items;
204
+ const queueByRun = new Map<string, ParentSeat | null>();
205
+ if (Array.isArray(queueRows)) for (const raw of queueRows) {
206
+ const entry = object(raw);
207
+ if (entry && typeof entry.runId === 'string' && PARENT_SEATS.includes(entry.seat as ParentSeat) && entry.seat !== 'direct') {
208
+ const seat = entry.seat as ParentSeat;
209
+ queueByRun.set(entry.runId, queueByRun.has(entry.runId) && queueByRun.get(entry.runId) !== seat ? null : seat);
210
+ }
211
+ }
212
+ const runs = new Map<string, { start?: number; end?: number; outcomes?: unknown }>();
213
+ for (const event of events) {
214
+ if (event.category !== 'self-dev.orchestrate' || (event.event !== 'start' && event.event !== 'done')) continue;
215
+ const data = object(event.data);
216
+ const runId = data?.runId;
217
+ if (typeof runId !== 'string' || !CANONICAL_RUN_ID.test(runId)) { result.cannotMeasureCount++; continue; }
218
+ const run = runs.get(runId) ?? {};
219
+ const at = parseTime(event.ts);
220
+ if (event.event === 'start') {
221
+ if (at !== null) run.start = run.start === undefined || !Number.isFinite(run.start) ? at : Math.min(run.start, at);
222
+ else if (run.start === undefined) run.start = NaN;
223
+ } else if (at !== null && (run.end === undefined || !Number.isFinite(run.end) || at > run.end)) {
224
+ run.end = at;
225
+ run.outcomes = data?.outcomes;
226
+ } else if (at === null && run.end === undefined) {
227
+ run.end = NaN;
228
+ }
229
+ runs.set(runId, run);
230
+ }
231
+ const groups = new Map<string, { seat: ParentSeat; stage: string; merged: boolean; values: number[] }>();
232
+ for (const [runId, run] of runs) {
233
+ if (run.start === undefined && run.end !== undefined) { result.cannotMeasureCount++; continue; }
234
+ if (run.end === undefined) { result.inProgressCount++; continue; }
235
+ if (run.start === undefined || !Number.isFinite(run.start) || !Number.isFinite(run.end) || run.end < run.start || !Array.isArray(run.outcomes) || run.outcomes.length === 0) {
236
+ result.cannotMeasureCount++;
237
+ continue;
238
+ }
239
+ const seat = queueByRun.has(runId) ? (queueByRun.get(runId) ?? 'unknown') : Array.isArray(queueRows) ? 'direct' : 'unknown';
240
+ const outcomes = run.outcomes.map(object);
241
+ if (outcomes.some((outcome) => !outcome)) {
242
+ result.cannotMeasureCount++;
243
+ continue;
244
+ }
245
+ for (const outcome of outcomes) {
246
+ const stage = typeof outcome!.stage === 'string' ? outcome!.stage : 'unknown';
247
+ const merged = outcome!.merged === true;
248
+ const key = JSON.stringify([seat, stage, merged]);
249
+ const group = groups.get(key) ?? { seat, stage, merged, values: [] };
250
+ group.values.push((run.end - run.start!) / 60_000);
251
+ groups.set(key, group);
252
+ }
253
+ result.completedCount++;
254
+ }
255
+ result.rows = [...groups.values()].map(({ seat, stage, merged, values }) => {
256
+ values.sort((a, b) => a - b);
257
+ return { seat, stage, merged, count: values.length, medianMinutes: percentile(values, 0.5), q1Minutes: percentile(values, 0.25), q3Minutes: percentile(values, 0.75) };
258
+ }).sort((a, b) => PARENT_SEATS.indexOf(a.seat) - PARENT_SEATS.indexOf(b.seat) || a.stage.localeCompare(b.stage));
259
+ if (result.cannotMeasureCount > 0) result.status = 'cannot-measure';
260
+ return result;
261
+ }
262
+
263
+ /** Read-only elapsed-time census of canonical local run ledgers, including unsuccessful runs. */
264
+ export function measureSpeedClaims(options: SpeedClaimsOptions = {}): SpeedClaimsMeasurement {
265
+ const ledgerDirectory = resolve(options.dir ?? runLedgerDir());
266
+ let parent: ParentSpeedClaimsMeasurement;
267
+ try {
268
+ const events = (options.readParentEvents ?? readParentEventsFromLogs)();
269
+ let queue: unknown;
270
+ try { queue = (options.readParentQueue ?? readParentQueueFromState)(); } catch { /* Queue absence does not erase parent observations or establish a direct launch. */ }
271
+ parent = measureParentSpeedClaims(events, queue);
272
+ } catch {
273
+ parent = { ...measureParentSpeedClaims([]), status: 'cannot-measure' };
274
+ }
275
+ const result: SpeedClaimsMeasurement = {
276
+ scope: 'self-implement-run-ledger', ledgerDirectory, status: 'measured',
277
+ completedCount: 0, inProgressCount: 0, cannotMeasureCount: 0, unknownSeatCount: 0, rows: [], note: NOTE, parent,
278
+ };
279
+ let files: string[];
280
+ try { files = (options.list ?? readdirSync)(ledgerDirectory); }
281
+ catch { return { ...result, status: 'cannot-measure' }; }
282
+
283
+ const groups = new Map<string, { seat: SpeedClaimsSeat; outcome: SpeedClaimsOutcome; values: number[] }>();
284
+ for (const file of files.sort()) {
285
+ const runId = file.endsWith('.jsonl') ? file.slice(0, -'.jsonl'.length) : '';
286
+ if (!CANONICAL_RUN_ID.test(runId)) continue;
287
+ try {
288
+ const entries = loadRunLedger(runId, ledgerDirectory, options.read);
289
+ if (!entries || entries.length === 0) { result.cannotMeasureCount++; continue; }
290
+ const end = endOfRun(entries);
291
+ const first = time(entries[0]);
292
+ const createdAt = checkpointFor(runId, ledgerDirectory)?.createdAt;
293
+ const creation = typeof createdAt === 'number' && Number.isFinite(createdAt) && createdAt >= 0 ? createdAt : null;
294
+ const start = first === null ? creation : first;
295
+ const endTime = end ? time(end.entry) : null;
296
+ if (start === null || (end && (endTime === null || endTime < start))) {
297
+ result.cannotMeasureCount++;
298
+ continue;
299
+ }
300
+ if (!end) { result.inProgressCount++; continue; }
301
+ const seat = (options.readSeat ?? seatFromLaunch)(runId, ledgerDirectory, entries);
302
+ const safeSeat = SEATS.includes(seat) ? seat : 'unknown';
303
+ if (safeSeat === 'unknown') result.unknownSeatCount++;
304
+ const key = `${safeSeat}:${end.outcome}`;
305
+ const group = groups.get(key) ?? { seat: safeSeat, outcome: end.outcome, values: [] };
306
+ group.values.push((endTime! - start) / 60_000);
307
+ groups.set(key, group);
308
+ result.completedCount++;
309
+ } catch { result.cannotMeasureCount++; }
310
+ }
311
+ result.rows = [...groups.values()].map(({ seat, outcome, values }) => {
312
+ values.sort((a, b) => a - b);
313
+ return { seat, outcome, count: values.length, underOneMinuteCount: values.filter((minutes) => minutes < 1).length, medianMinutes: percentile(values, 0.5), q1Minutes: percentile(values, 0.25), q3Minutes: percentile(values, 0.75) };
314
+ }).sort((a, b) => SEATS.indexOf(a.seat) - SEATS.indexOf(b.seat) || a.outcome.localeCompare(b.outcome));
315
+ if (result.cannotMeasureCount > 0) result.status = 'cannot-measure';
316
+ return result;
317
+ }
318
+
319
+ export function renderSpeedClaims(result: SpeedClaimsMeasurement): string {
320
+ return [
321
+ 'child run-ledger (child start → child end):',
322
+ `ledger directory: ${result.ledgerDirectory}`,
323
+ `status: ${result.status}`,
324
+ `finished (quartile denominator): ${result.completedCount}`,
325
+ `in progress (excluded): ${result.inProgressCount}`,
326
+ `cannot measure (excluded): ${result.cannotMeasureCount}`,
327
+ `unknown seat (finished): ${result.unknownSeatCount}`,
328
+ ...result.rows.map((row) => `seat=${row.seat} outcome=${row.outcome} count=${row.count} underOneMinute=${row.underOneMinuteCount} median=${row.medianMinutes}m q1=${row.q1Minutes}m q3=${row.q3Minutes}m`),
329
+ `note: ${result.note}`,
330
+ 'parent launch → done (self-dev.orchestrate):',
331
+ `parent status: ${result.parent.status}`,
332
+ `parent finished (quartile denominator): ${result.parent.completedCount}`,
333
+ `parent in progress (excluded): ${result.parent.inProgressCount}`,
334
+ `parent cannot measure (excluded): ${result.parent.cannotMeasureCount}`,
335
+ ...result.parent.rows.map((row) => `seat=${row.seat} stage=${row.stage} merged=${row.merged} count=${row.count} median=${row.medianMinutes}m q1=${row.q1Minutes}m q3=${row.q3Minutes}m`),
336
+ `parent note: ${result.parent.note}`,
337
+ ].join('\n');
338
+ }
@@ -260,9 +260,56 @@ function skipOverlappingRun(root: string, open: FailureStreak['open'] & { pid?:
260
260
  }
261
261
  debug.log('steward.stage', 'skipped-overlap', { openStage: open.stage, pid: open.pid });
262
262
  writeFileSync(skippedPath(root), JSON.stringify({ openStage: open.stage, pid: open.pid }));
263
+ logTickDecision(root, 'skipped', `overlapping ${open.stage} (pid ${open.pid ?? 'unknown'})`);
263
264
  }
264
265
 
265
266
  function reportPath(root: string): string { return join(root, 'steward', 'observe.json'); }
267
+
268
+ type TickDecision = { verdict: 'launched' | 'failed'; reason: string; goalId?: string };
269
+
270
+ function logTickDecision(root: string | undefined, verdict: 'launched' | 'skipped' | 'reported' | 'failed', reason: string, goalId?: string): void {
271
+ const runId = graphRunId();
272
+ const marker = root && runId && existsSync(join(root, 'steward')) ? join(root, 'steward', `decision-logged-${runId}.json`) : undefined;
273
+ const decision = { verdict, reason: redactSecretText(reason), ...(goalId ? { goalId } : {}), ...(runId ? { runId } : {}) };
274
+ if (marker) try {
275
+ const previous = JSON.parse(readFileSync(marker, 'utf8')) as { verdict?: string; reason?: string };
276
+ // A replay keeps the first success; a failure replaces a success; a success after a failure replaces the failure.
277
+ if (previous.verdict === decision.verdict && previous.reason === decision.reason) return;
278
+ if (previous.verdict !== 'failed' && verdict !== 'failed') return;
279
+ } catch { /* Missing or stale marker: retry the observation. */ }
280
+ try {
281
+ debug.log('loop.steward', 'decision', decision);
282
+ if (marker) writeFileSync(marker, JSON.stringify(decision));
283
+ } catch { /* Observation must not change the tick outcome. */ }
284
+ }
285
+
286
+ function tickDecisionPath(root: string, runId: string): string {
287
+ return join(root, 'steward', `decision-${runId}.json`);
288
+ }
289
+ function rememberLaunchDecision(root: string, entry: TickDecision): void {
290
+ const runId = graphRunId();
291
+ if (!runId) return;
292
+ const path = tickDecisionPath(root, runId);
293
+ try {
294
+ let current: TickDecision | undefined;
295
+ try { current = JSON.parse(readFileSync(path, 'utf8')) as TickDecision; }
296
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return; }
297
+ // Keep the first failure (and its goal/cause), even if a retried schedule later succeeds.
298
+ if (current?.verdict === 'failed' || current?.verdict === entry.verdict) return;
299
+ writeFileSync(path, JSON.stringify(entry));
300
+ } catch { /* An unavailable observation store must not block execution. */ }
301
+ }
302
+ function readLaunchDecision(root: string): TickDecision | undefined {
303
+ const runId = graphRunId();
304
+ if (!runId) return undefined;
305
+ try { return JSON.parse(readFileSync(tickDecisionPath(root, runId), 'utf8')) as TickDecision; }
306
+ catch { return undefined; }
307
+ }
308
+ function logStageFailure(root: string, stage: StewardStage, reason: string): void {
309
+ const runId = graphRunId();
310
+ if (runId) try { rmSync(tickDecisionPath(root, runId), { force: true }); } catch { /* Observation must not change the failure. */ }
311
+ logTickDecision(root, 'failed', `${stage}: ${reason}`);
312
+ }
266
313
  function readState(path: string): { issues: Record<string, string>; digestDay?: string } {
267
314
  try { return JSON.parse(readFileSync(path, 'utf8')) as ReturnType<typeof readState>; }
268
315
  catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { issues: {} }; throw error; }
@@ -396,10 +443,18 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
396
443
  const entry = raiseHitl(issue, row, launchDeps);
397
444
  recordHitlOnCard(issue, entry, cardDeps);
398
445
  }
446
+ let launchDecision: TickDecision | undefined;
399
447
  for (const item of plan.launches) {
400
448
  const entry = launch(item, launchDeps);
449
+ const decision: TickDecision | undefined = entry.status === 'launched'
450
+ ? { verdict: 'launched', reason: 'harness run spawned', goalId: `linear:${item.issue.identifier}` }
451
+ : entry.status === 'failed'
452
+ ? { verdict: 'failed', reason: entry.reason ?? 'launch failed', goalId: `linear:${item.issue.identifier}` }
453
+ : undefined;
454
+ if (decision && (!launchDecision || (decision.verdict === 'failed' && launchDecision.verdict !== 'failed'))) launchDecision = decision;
401
455
  recordLaunchOnCard(item.issue, entry, cardDeps);
402
456
  }
457
+ if (launchDecision) rememberLaunchDecision(root, launchDecision);
403
458
  if (settings.mode === 'shadow') {
404
459
  const { recordShadowTick } = await import('./readiness.js');
405
460
  const tick = recordShadowTick(root, rows, plan, (deps.now ?? (() => new Date()))());
@@ -459,12 +514,18 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
459
514
  }
460
515
  }
461
516
  const day = at.slice(0, 10);
517
+ let digested = false;
462
518
  if (state.digestDay !== day) {
463
519
  const send = deps.sendDigest ?? sendStewardDigest;
464
520
  await send(`스튜어드 ${day}: ${rows.map(row => `${row.issue} ${row.disposition}`).join(' · ')}`);
465
521
  state.digestDay = day;
466
522
  writeFileSync(path, JSON.stringify(state));
523
+ digested = true;
467
524
  }
525
+ const launched = readLaunchDecision(root);
526
+ if (launched) logTickDecision(root, launched.verdict, launched.reason, launched.goalId);
527
+ else if (outcomes.length || digested) logTickDecision(root, 'reported', outcomes.length ? `${outcomes.length} outcome(s) reported` : 'daily digest sent');
528
+ else logTickDecision(root, 'skipped', 'no new outcome or digest');
468
529
  }
469
530
  outcome = 'ok';
470
531
  } finally {
@@ -513,10 +574,14 @@ async function withStageSlot<T>(root: string, run: () => Promise<T>, onBusy?: ()
513
574
 
514
575
  /** The command-node boundary records both normal exits and an unclosed stage from a killed process. */
515
576
  export async function runStewardStageCommand(stage: StewardStage, deps: StewardCommandDeps = {}): Promise<0 | 1> {
516
- if (resolvedStewardSettings(deps.launchSettings).mode === 'off') return 0;
577
+ if (resolvedStewardSettings(deps.launchSettings).mode === 'off') {
578
+ if (stage === 'sync') logTickDecision(deps.root ?? effectiveInstanceRoot(), 'skipped', 'steward mode off');
579
+ return 0;
580
+ }
517
581
  const root = deps.root ?? effectiveInstanceRoot();
518
582
  if (stage !== 'sync' && existsSync(skippedPath(root))) return 0;
519
- return withStageSlot(root, async () => {
583
+ try {
584
+ return await withStageSlot(root, async () => {
520
585
  const now = deps.now ?? (() => new Date());
521
586
  const threshold = stewardSettings().alertAfterFailures ?? 3;
522
587
  const alertIfNeeded = async (state: FailureStreak): Promise<void> => {
@@ -564,10 +629,16 @@ export async function runStewardStageCommand(stage: StewardStage, deps: StewardC
564
629
  if (!ok) {
565
630
  console.error(`steward ${stage}: stage failed — ${reason}`);
566
631
  debug.log('steward.stage', 'failed', { stage, reason });
632
+ if (stage === 'sync' || !existsSync(skippedPath(root))) logStageFailure(root, stage, reason);
567
633
  return 1;
568
634
  }
569
635
  return 0;
570
- }, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
636
+ }, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
637
+ } catch (error) {
638
+ try { if (stage === 'sync' || !existsSync(skippedPath(root))) logStageFailure(root, stage, error instanceof Error ? error.message : String(error)); }
639
+ catch { /* Observation must not change the stage error. */ }
640
+ throw error;
641
+ }
571
642
  }
572
643
 
573
644
  export async function runStewardNodeCommand(
@@ -53,7 +53,10 @@ export async function recordWorkingBackwardsOnCards(
53
53
  }
54
54
  const card = store.createCard({ goalId: `linear:${issue.identifier}`, title: redactSecrets(issue.title) });
55
55
  if (card.sections.some(section => section.key.startsWith('prfaq:')) && card.sections.some(section => section.key.startsWith('manual:'))) continue;
56
- if (card.sections.some(section => section.key.startsWith('launch:'))) throw new Error(`Working-backwards draft follows launch: ${issue.identifier}`);
56
+ if (card.sections.some(section => section.key.startsWith('launch:'))) {
57
+ debug.log('steward.working-backwards', 'skipped-launched-before-draft', { issueId: issue.identifier });
58
+ continue;
59
+ }
57
60
  const draft = await draftWorkingBackwards(issue, ask);
58
61
  for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
59
62
  const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;