elanous 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -25,6 +25,8 @@ import {
25
25
  decideRetry,
26
26
  DoomLoopTracker,
27
27
  fingerprintError,
28
+ llmCallOutcomeKind,
29
+ type LlmCallOutcomeKind,
28
30
  type RetryAction,
29
31
  } from './retry-policy.js';
30
32
 
@@ -75,6 +77,35 @@ export interface FetchApiWithRetryOpts {
75
77
  * row" pattern within a single request. Pass a session-scoped
76
78
  * tracker if you want cross-request doom detection. */
77
79
  doomTracker?: DoomLoopTracker;
80
+ /** Fallbacks still unused. When > 0, k consecutive overload-class
81
+ * failures throw OverloadFailoverError before another retry sleep. */
82
+ remainingFallbacks?: number;
83
+ }
84
+
85
+ /** Raised when overload repeats k times and a fallback is still unused.
86
+ * The caller switches provider; this is not a retry exhaustion. */
87
+ export class OverloadFailoverError extends Error {
88
+ readonly provider: string;
89
+ readonly status: number;
90
+ readonly kind: LlmCallOutcomeKind;
91
+ readonly streak: number;
92
+ readonly cause: unknown;
93
+
94
+ constructor(opts: {
95
+ provider: string;
96
+ status: number;
97
+ kind: LlmCallOutcomeKind;
98
+ streak: number;
99
+ cause: unknown;
100
+ }) {
101
+ super(`overload failover after ${opts.streak} ${opts.kind} from ${opts.provider} (status ${opts.status})`);
102
+ this.name = 'OverloadFailoverError';
103
+ this.provider = opts.provider;
104
+ this.status = opts.status;
105
+ this.kind = opts.kind;
106
+ this.streak = opts.streak;
107
+ this.cause = opts.cause;
108
+ }
78
109
  }
79
110
 
80
111
  /** POST a fetch with the standard retry policy. On 2xx returns the
@@ -91,13 +122,17 @@ export async function fetchApiWithRetry(
91
122
  ): Promise<Response> {
92
123
  const maxAttempts = opts.maxAttempts ?? 4;
93
124
  const tracker = opts.doomTracker ?? new DoomLoopTracker();
125
+ const remainingFallbacks = opts.remainingFallbacks ?? 0;
94
126
  let attempt = 0;
127
+ let overloadStreak = 0;
95
128
  let lastErr: unknown;
96
129
  while (attempt < maxAttempts) {
97
130
  let response: Response | null = null;
98
131
  try {
99
132
  response = await fetch(url, init);
100
133
  if (response.ok) {
134
+ recordLlmCallOutcome(opts.provider, response.status, 'ok');
135
+ overloadStreak = 0;
101
136
  return response;
102
137
  }
103
138
  // Drain body so the error can carry the API's explanation. We
@@ -136,6 +171,11 @@ export async function fetchApiWithRetry(
136
171
  // would couple the helper to the caller's signal.
137
172
  if (isAbortLike(err)) throw err;
138
173
 
174
+ const status = err instanceof ApiHttpError ? err.status : 0;
175
+ const kind = llmCallOutcomeKind(status, err instanceof Error ? err.message : String(err));
176
+ recordLlmCallOutcome(opts.provider, status, kind);
177
+ if (kind === 'overloaded' || kind === '5xx' || kind === 'rate-limit') overloadStreak += 1;
178
+ else overloadStreak = 0;
139
179
  const fp = fingerprintError(err, `api:${opts.provider}`);
140
180
  const doomStatus = tracker.record(fp);
141
181
  const retryAfter = err instanceof ApiHttpError ? err.retryAfter : undefined;
@@ -143,6 +183,8 @@ export async function fetchApiWithRetry(
143
183
  attempt,
144
184
  doomStatus,
145
185
  retryAfter,
186
+ remainingFallbacks,
187
+ overloadStreak,
146
188
  });
147
189
  debug.log('llm.retry', decisionEvent(decision.action), {
148
190
  provider: opts.provider,
@@ -151,7 +193,19 @@ export async function fetchApiWithRetry(
151
193
  reason: decision.reason,
152
194
  delayMs: decision.delayMs,
153
195
  status: err instanceof ApiHttpError ? err.status : undefined,
196
+ kind,
197
+ overloadStreak,
198
+ remainingFallbacks,
154
199
  });
200
+ if (decision.action === 'switch-provider') {
201
+ throw new OverloadFailoverError({
202
+ provider: opts.provider,
203
+ status,
204
+ kind,
205
+ streak: overloadStreak,
206
+ cause: err,
207
+ });
208
+ }
155
209
  if (decision.action !== 'retry') {
156
210
  throw err;
157
211
  }
@@ -172,6 +226,39 @@ export async function fetchApiWithRetry(
172
226
  throw lastErr ?? new Error(`${opts.errorPrefix}: retry loop terminated unexpectedly`);
173
227
  }
174
228
 
229
+ type OutcomeWriter = (row: { provider: string; status: number; kind: LlmCallOutcomeKind }) => void;
230
+
231
+ let outcomeWriterForTesting: OutcomeWriter | undefined;
232
+
233
+ /** Test seam. Production uses the log store. `undefined` restores it. */
234
+ export function setLlmCallOutcomeWriterForTesting(writer: OutcomeWriter | undefined): void {
235
+ outcomeWriterForTesting = writer;
236
+ }
237
+
238
+ /** The one event launch failover is allowed to read.
239
+ * Category `llm.call`, event `outcome`, payload {provider,status,kind}.
240
+ * Written straight to the log store — `debug.log` drops the row when no
241
+ * sink is armed, which is the normal launch path. */
242
+ export function recordLlmCallOutcome(provider: string, status: number, kind: LlmCallOutcomeKind): void {
243
+ const data = { provider, status, kind };
244
+ // Trace only — a store sink would otherwise write a second `llm.call`/`outcome` row and halve the streak threshold.
245
+ debug.log('llm.call', 'outcome-trace', data);
246
+ if (outcomeWriterForTesting) {
247
+ outcomeWriterForTesting(data);
248
+ return;
249
+ }
250
+ if (process.env.NODE_ENV === 'test') return;
251
+ try {
252
+ const { getDefaultLogStore } = require('../mss/logging/log-store.js') as typeof import('../mss/logging/log-store.js');
253
+ getDefaultLogStore()?.insertBatch([{
254
+ rec: { ts: new Date().toISOString(), category: 'llm.call', event: 'outcome', data },
255
+ surface: 'llm',
256
+ }]);
257
+ } catch {
258
+ // A failed observation must not fail the call. Launch then sees no sample.
259
+ }
260
+ }
261
+
175
262
  function decisionEvent(action: RetryAction): string {
176
263
  // Stable category names for log filtering. The action is the most
177
264
  // useful axis ("how many turns hit retry vs abort"); the category
@@ -364,7 +364,61 @@ export function decideProviderFallback(input: {
364
364
  return { action: 'advance', nextProvider, category, attempts };
365
365
  }
366
366
 
367
- export type RetryAction = 'retry' | 'abort' | 'ask-user' | 'auto-undo';
367
+ /** Consecutive overload/5xx/rate-limit outcomes that switch the child off codex
368
+ * before the same provider spends its remaining retries. Other errors (400)
369
+ * never count. Recovery is this many later non-overload outcomes. */
370
+ export const OVERLOAD_FAILOVER_STREAK = 3;
371
+
372
+ export type LlmCallOutcomeKind = 'ok' | 'overloaded' | '5xx' | 'rate-limit' | 'other';
373
+
374
+ /** One finished LLM call. Launch failover reads only this shape. */
375
+ export interface LlmCallOutcome {
376
+ readonly provider: string;
377
+ readonly status: number;
378
+ readonly kind: LlmCallOutcomeKind;
379
+ }
380
+
381
+ const CODEX_CALL_PROVIDERS = new Set(['codex', 'openai-codex']);
382
+
383
+ export function llmCallOutcomeKind(status: number, message = ''): LlmCallOutcomeKind {
384
+ if (status >= 200 && status < 300) return 'ok';
385
+ const msg = message.toLowerCase();
386
+ if (status === 429 || msg.includes('rate limit') || msg.includes('too many requests')) return 'rate-limit';
387
+ if (
388
+ status === 500 || status === 502 || status === 503 || status === 504 ||
389
+ msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
390
+ ) {
391
+ return status >= 500 ? '5xx' : 'overloaded';
392
+ }
393
+ if (msg.includes('overloaded')) return 'overloaded';
394
+ return 'other';
395
+ }
396
+
397
+ export function isOverloadOutcomeKind(kind: LlmCallOutcomeKind): boolean {
398
+ return kind === 'overloaded' || kind === '5xx' || kind === 'rate-limit';
399
+ }
400
+
401
+ /** True when the newest `limit` codex outcomes are all overload-class.
402
+ * A non-codex row or a non-overload row breaks the streak. Empty input is false. */
403
+ export function codexOverloadStreakReached(
404
+ outcomes: readonly LlmCallOutcome[],
405
+ limit = OVERLOAD_FAILOVER_STREAK,
406
+ ): boolean {
407
+ if (limit < 1 || outcomes.length < limit) return false;
408
+ const tail = outcomes.slice(-limit);
409
+ return tail.every((row) => CODEX_CALL_PROVIDERS.has(row.provider) && isOverloadOutcomeKind(row.kind));
410
+ }
411
+
412
+ /** True when the newest `limit` outcomes are successful (any provider). */
413
+ export function overloadRecovered(
414
+ outcomes: readonly LlmCallOutcome[],
415
+ limit = OVERLOAD_FAILOVER_STREAK,
416
+ ): boolean {
417
+ if (limit < 1 || outcomes.length < limit) return false;
418
+ return outcomes.slice(-limit).every((row) => row.kind === 'ok');
419
+ }
420
+
421
+ export type RetryAction = 'retry' | 'abort' | 'ask-user' | 'auto-undo' | 'switch-provider';
368
422
 
369
423
  export interface RetryDecision {
370
424
  action: RetryAction;
@@ -417,6 +471,16 @@ export interface RetryContext {
417
471
  /** Optional: retry-after header string from a 429 response (ms or
418
472
  * seconds or HTTP date). decideRetry parses it. */
419
473
  retryAfter?: string;
474
+ /** Configured fallbacks still unused. When > 0, an overload-class
475
+ * failure that has already repeated `overloadStreak` times switches
476
+ * provider instead of spending another retry on the same one.
477
+ * Omitted or 0 keeps the historical retry-until-exhausted behaviour. */
478
+ remainingFallbacks?: number;
479
+ /** Consecutive overload-class failures already seen for this call,
480
+ * including the one under decision. Default threshold is
481
+ * OVERLOAD_FAILOVER_STREAK. */
482
+ overloadStreak?: number;
483
+ overloadStreakLimit?: number;
420
484
  }
421
485
 
422
486
  /** Classify an error into a RetryCategory without making an action
@@ -519,10 +583,42 @@ export function classifyError(err: unknown): RetryCategory {
519
583
  return 'unknown';
520
584
  }
521
585
 
586
+ function isOverloadSwitchCandidate(msg: string, code: string | null): boolean {
587
+ return (
588
+ code === '429' || code === '500' || code === '502' || code === '503' || code === '504' ||
589
+ msg.includes('rate limit') || msg.includes('too many requests') ||
590
+ msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
591
+ );
592
+ }
593
+
594
+ function overloadSwitchDecision(ctx: RetryContext): RetryDecision | undefined {
595
+ const remaining = ctx.remainingFallbacks ?? 0;
596
+ if (remaining <= 0) return undefined;
597
+ const limit = ctx.overloadStreakLimit ?? OVERLOAD_FAILOVER_STREAK;
598
+ const streak = ctx.overloadStreak ?? 1;
599
+ if (streak < limit) return undefined;
600
+ return {
601
+ action: 'switch-provider',
602
+ category: 'overloaded',
603
+ reason: `overload streak ${streak} with ${remaining} fallback(s) left; switch before spending retries`,
604
+ };
605
+ }
606
+
522
607
  /** Pure verdict. Caller sleeps + retries or aborts per the returned
523
608
  * action. Exponential backoff baseline: 500ms * 2^attempt, clamped
524
609
  * to [500, 30_000]. */
525
610
  export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
611
+ const msg = extractMessage(err).toLowerCase();
612
+ const code = extractErrorCode(err);
613
+
614
+ // Overload with a fallback still unused switches before the doom
615
+ // window (k === 3) turns the same 503 into auto-undo. A 400 is not
616
+ // a candidate. Doom still wins for every other repeated failure.
617
+ if (isOverloadSwitchCandidate(msg, code)) {
618
+ const switched = overloadSwitchDecision(ctx);
619
+ if (switched) return switched;
620
+ }
621
+
526
622
  // Doom-loop always wins — even a technically-retryable error
527
623
  // becomes auto-undo after N identical failures. The caller may
528
624
  // still degrade to ask-user when plan mode is active or when the
@@ -535,9 +631,6 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
535
631
  };
536
632
  }
537
633
 
538
- const msg = extractMessage(err).toLowerCase();
539
- const code = extractErrorCode(err);
540
-
541
634
  // Context window exhausted — never retry (the next call will also
542
635
  // be over-sized). Abort and surface the error to the caller; upstream
543
636
  // can trigger compaction.
@@ -589,8 +682,16 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
589
682
  };
590
683
  }
591
684
 
592
- // Overloaded / server busy.
593
- if (msg.includes('overloaded') || msg.includes('server_busy') || code === '503') {
685
+ // Overloaded / server busy / 5xx.
686
+ if (
687
+ msg.includes('overloaded') ||
688
+ msg.includes('server_busy') ||
689
+ msg.includes('server_error') ||
690
+ code === '500' ||
691
+ code === '502' ||
692
+ code === '503' ||
693
+ code === '504'
694
+ ) {
594
695
  return {
595
696
  action: 'retry',
596
697
  delayMs: backoffMs(ctx.attempt),
@@ -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,8 +53,26 @@ 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}`);
57
- const draft = await draftWorkingBackwards(issue, ask);
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
+ }
60
+ // An invalid draft for the same issue text is not re-asked every tick — each ask is a 70-100 s LLM call (10-05 ELA-29 timed the schedule stage out 3×).
61
+ const invalidKey = `hitl:draft-invalid-${createHash('sha256').update(JSON.stringify({ title: issue.title, body: issue.body ?? '' })).digest('hex').slice(0, 16)}`;
62
+ if (card.sections.some(section => section.key === invalidKey)) {
63
+ debug.log('steward.working-backwards', 'skipped-known-invalid', { issueId: issue.identifier });
64
+ throw new InvalidWorkingBackwardsDraft();
65
+ }
66
+ let draft: WorkingBackwardsDraft;
67
+ try { draft = await draftWorkingBackwards(issue, ask); }
68
+ catch (error) {
69
+ if (error instanceof InvalidWorkingBackwardsDraft) {
70
+ // The memo is best-effort — failing to write it must not replace the invalid-draft error the caller routes to hitl.
71
+ try { store.appendSection(card.id, { key: invalidKey, owner: 'steward', content: 'working-backwards draft invalid for this issue text — not re-asked until the issue changes' }); }
72
+ catch (memoError) { debug.log('steward.working-backwards', 'invalid-memo-failed', { issueId: issue.identifier, error: memoError instanceof Error ? memoError.message : String(memoError) }); }
73
+ }
74
+ throw error;
75
+ }
58
76
  for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
59
77
  const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
60
78
  if (!card.sections.some(item => item.key === key)) store.appendSection(card.id, { key, owner: 'steward', content });
@@ -46,6 +46,31 @@ export function registerCardCommand(program: Command, deps: CardCliDeps = {}): C
46
46
  }
47
47
  });
48
48
 
49
+ card.command('close <id>')
50
+ .description('Close a task card with a reason (owner operation)')
51
+ .requiredOption('--reason <text>', 'One-line close reason')
52
+ .action((id: string, options: { reason: string }) => {
53
+ const store = createStore();
54
+ try {
55
+ const reason = options.reason.trim();
56
+ if (!reason || /[\r\n]/.test(options.reason)) throw new Error('Close reason must be one line');
57
+ const existing = store.getCard(id);
58
+ if (!existing) {
59
+ process.stderr.write(`Card not found: ${id}\n`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ if (existing.status === 'closed') {
64
+ write(`이미 닫힘: ${id}\n`);
65
+ return;
66
+ }
67
+ store.closeCard(id, reason);
68
+ write(`닫힘: ${id}\n`);
69
+ } finally {
70
+ store.close();
71
+ }
72
+ });
73
+
49
74
  card.command('list')
50
75
  .description('List task cards')
51
76
  .option('--open', 'Only show open cards')
@@ -32,7 +32,7 @@ export interface AppendSectionInput {
32
32
  type CardEvent =
33
33
  | { type: 'created'; id: string; goalId: string; title: string; createdAt: string }
34
34
  | { type: 'section'; key: string; owner: string; content: string; createdAt: string }
35
- | { type: 'closed'; createdAt: string };
35
+ | { type: 'closed'; createdAt: string; reason?: string };
36
36
 
37
37
  /** RFC §3 (내부 문서 `RFC-execution-and-landing-loop-agents-parallel-first-2026-09-29`) — each card section has one owner.
38
38
  * A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
@@ -197,13 +197,15 @@ export class CardStore {
197
197
  }).immediate();
198
198
  }
199
199
 
200
- closeCard(id: string): TaskCard {
200
+ closeCard(id: string, reason?: string): TaskCard {
201
+ if (reason !== undefined && !reason.trim()) throw new Error('Close reason is required');
201
202
  return this.db.transaction(() => {
202
203
  this.reconcile();
203
204
  const card = this.readCard(id);
204
205
  if (!card) throw new Error(`Card not found: ${id}`);
205
206
  if (card.status === 'closed') return card;
206
- this.append(id, { type: 'closed', createdAt: new Date().toISOString() });
207
+ this.append(id, { type: 'closed', createdAt: new Date().toISOString(),
208
+ ...(reason === undefined ? {} : { reason: redactSecrets(reason.trim()) }) });
207
209
  return { ...card, status: 'closed' as const };
208
210
  }).immediate();
209
211
  }
@@ -1,9 +1,9 @@
1
- import { lstatSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
3
3
  import { gunzipSync } from 'node:zlib';
4
4
  import { debug } from '../../debug/log.js';
5
5
  import { getDefaultLogStore, type LogStore } from '../../mss/logging/log-store.js';
6
- import type { LogRecord } from '../../mss/logging/record.js';
6
+ import { fromJsonl, type LogRecord } from '../../mss/logging/record.js';
7
7
 
8
8
  const FILE_LIMIT = 5 * 1024 * 1024;
9
9
  const TOTAL_LIMIT = 20 * 1024 * 1024;
@@ -83,50 +83,58 @@ export function parsePodArtifactChunks(logs: string): ParseResult {
83
83
 
84
84
  const POD_LOGS_PATH = 'pod-logs/logs.jsonl';
85
85
 
86
- function childRunId(data: unknown): string | undefined {
87
- if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;
88
- const value = (data as Record<string, unknown>).runId;
89
- return typeof value === 'string' && value.length > 0 ? value : undefined;
90
- }
91
-
92
- function decisionRecord(line: string): { rec: LogRecord; surface: string } | undefined {
93
- let parsed: unknown;
94
- try { parsed = JSON.parse(line); } catch { return undefined; }
95
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
96
- const row = parsed as Record<string, unknown>;
97
- if (row.category !== 'harness.decision') return undefined;
98
- if (typeof row.ts !== 'string' || typeof row.event !== 'string') return undefined;
99
- const runId = childRunId(row.data);
100
- if (!runId) return undefined;
101
- const data = { ...(row.data as Record<string, unknown>), podJob: '', importedFrom: 'pod' as const };
102
- return {
103
- rec: {
104
- ts: row.ts,
105
- category: 'harness.decision',
106
- event: row.event,
107
- data,
108
- ...(typeof row.level === 'string' ? { level: row.level as LogRecord['level'] } : {}),
109
- ...(typeof row.trace_id === 'string' ? { trace_id: row.trace_id } : {}),
110
- },
111
- surface: typeof row.surface === 'string' && row.surface ? row.surface : 'pod',
112
- };
86
+ /** Last measured child ledger context when a failed Pod emitted no terminal JSON. */
87
+ export function podNoResultDiagnostic(ledger: string): string | null {
88
+ let stage: string | undefined;
89
+ let round: number | undefined;
90
+ let mustFix: string | undefined;
91
+ for (const line of ledger.split(/\r?\n/)) {
92
+ try {
93
+ const entry = JSON.parse(line) as { event?: unknown; data?: Record<string, unknown> };
94
+ if (!entry || typeof entry !== 'object' || !entry.data || typeof entry.data !== 'object' || typeof entry.event !== 'string' || entry.event === 'pod-ledger-incomplete') continue;
95
+ if (typeof entry.data.round === 'number' && Number.isSafeInteger(entry.data.round)) round = entry.data.round;
96
+ if (entry.event !== 'run-origin') stage = entry.event;
97
+ // `reviewed` carries pointers only (count + finding ids); the text stays in the review artifact.
98
+ if (entry.event === 'reviewed' && typeof entry.data.mustFix === 'number') {
99
+ const ids = Array.isArray(entry.data.findingIds) ? entry.data.findingIds.filter((id): id is string => typeof id === 'string') : [];
100
+ mustFix = `${entry.data.mustFix}${ids.length ? ` ${JSON.stringify(ids)}` : ''}`;
101
+ }
102
+ } catch { /* incomplete ledger line */ }
103
+ }
104
+ return stage ? `child terminal result missing; last ledger stage=${stage}; round=${round ?? 'unknown'}; mustFix=${mustFix ?? 'unknown'}` : null;
113
105
  }
114
106
 
115
- /** Import only harness.decision rows from a returned pod log into the launching host store. */
116
- export function importPodDecisionLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
107
+ /** Import valid JSONL log records into the launching host store, with durable per-job line receipts. */
108
+ export function importPodLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
117
109
  if (!store) return { imported: 0, skipped: 0 };
118
- const text = bytes.toString('utf8');
119
- const rows: Array<{ rec: LogRecord; surface: string }> = [];
110
+ const rows: Array<{ rec: LogRecord; surface: string; line: number }> = [];
120
111
  let skipped = 0;
121
- for (const line of text.split(/\r?\n/)) {
112
+ for (const [lineNumber, line] of bytes.toString('utf8').split(/\r?\n/).entries()) {
122
113
  if (!line) continue;
123
- const row = decisionRecord(line);
124
- if (!row) { skipped += 1; continue; }
125
- row.rec.data = { ...(row.rec.data as Record<string, unknown>), podJob: job };
126
- rows.push(row);
114
+ const rec = fromJsonl(line);
115
+ if (!rec || !Number.isFinite(Date.parse(rec.ts))) { skipped++; continue; }
116
+ const data = rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
117
+ ? rec.data as Record<string, unknown>
118
+ : rec.data === undefined ? {} : { originalData: rec.data };
119
+ const surface = (rec as LogRecord & { surface?: unknown }).surface;
120
+ rows.push({
121
+ rec: { ...rec, data: { ...data, origin: 'pod', podJob: job } },
122
+ surface: typeof surface === 'string' && surface ? surface : 'pod',
123
+ line: lineNumber,
124
+ });
125
+ }
126
+ return { imported: store.insertPodBatch(job, rows), skipped };
127
+ }
128
+
129
+ function importReturnedLogs(bytes: Buffer, job: string, store: LogStore | null,
130
+ log: (category: string, event: string, data: Record<string, unknown>) => void): void {
131
+ try {
132
+ if (!store) throw new Error('host log store unavailable');
133
+ const imported = importPodLogs(bytes, job, store);
134
+ log('self-implement.pod', 'pod-logs-imported', { job, imported: imported.imported, skipped: imported.skipped });
135
+ } catch (error) {
136
+ debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
127
137
  }
128
- if (rows.length > 0) store.insertBatch(rows);
129
- return { imported: rows.length, skipped };
130
138
  }
131
139
 
132
140
  /** Reject symlink parents as well as lexical traversal; never replace a host file. */
@@ -176,17 +184,24 @@ export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) =>
176
184
  podLogsReturned = true;
177
185
  const lines = bytes.length ? bytes.toString('utf8').split('\n').length - (bytes.at(-1) === 10 ? 1 : 0) : 0;
178
186
  log('self-implement.pod', 'pod-logs-returned', { job, path: destination, lines });
179
- try {
180
- const imported = importPodDecisionLogs(bytes, job, logStore);
181
- log('self-implement.pod', 'pod-decisions-imported', { job, imported: imported.imported, skipped: imported.skipped });
182
- } catch (error) {
183
- log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
184
- }
187
+ importReturnedLogs(bytes, job, logStore, log);
185
188
  }
186
189
  } catch (e) {
187
190
  const reason = (e as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : e instanceof Error ? e.message : String(e);
188
- if ((e as NodeJS.ErrnoException).code === 'EEXIST') log('self-implement.pod', 'artifact-collect-skipped', { job, path, reason });
189
- else log('self-implement.pod', 'artifact-collect-incomplete', { job, path, reason });
191
+ if ((e as NodeJS.ErrnoException).code === 'EEXIST') {
192
+ log('self-implement.pod', 'artifact-collect-skipped', { job, path, reason });
193
+ if (path === podLogsPath) {
194
+ try {
195
+ const existing = resolve(dir, job, path);
196
+ if (!lstatSync(existing).isSymbolicLink() && readFileSync(existing).equals(bytes)) {
197
+ podLogsReturned = true;
198
+ importReturnedLogs(bytes, job, logStore, log);
199
+ }
200
+ } catch (error) {
201
+ debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
202
+ }
203
+ }
204
+ } else log('self-implement.pod', 'artifact-collect-incomplete', { job, path, reason });
190
205
  if (path === podLogsPath) podLogsMissingReason = reason;
191
206
  }
192
207
  }