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
@@ -0,0 +1,118 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { FEATURE_MATURITY, type Maturity } from '../../src/maturity/feature-maturity.js';
4
+ import type { ChecklistItem } from '../../src/release-loop/checklist.js';
5
+
6
+ const DEFAULT_MAP = resolve(import.meta.dir, '../../docs/marketing/MAP-value-props-to-features.md');
7
+ const HELD = /특허\s*(?:보류|후보)|GOAL-CASCADE/i;
8
+ const PR = /(?:^|[^\w])#(\d+)\b|\/pull\/(\d+)\b/g;
9
+ type Grade = '된다' | '베타' | '로드맵';
10
+ type Row = { feature: string; grade: Grade; id: string; pr: string };
11
+ type Registry = typeof FEATURE_MATURITY;
12
+
13
+ function cells(line: string): string[] {
14
+ return line.slice(1, -1).split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, '|'));
15
+ }
16
+
17
+ function clean(text: string): string {
18
+ return text.replace(/\*\*/g, '').replace(/\s+/g, ' ').trim();
19
+ }
20
+
21
+ function gradeFor(feature: string, hint: string, registry: Registry): Maturity | undefined {
22
+ // A surface alone ("cli", "telegram") does not identify a feature in the registry.
23
+ const key = /\/(?:[\w-]+)(?:\/[\w-]+)*/.exec(hint)?.[0];
24
+ if (key && Object.prototype.hasOwnProperty.call(registry.pwaRoute, key))
25
+ return registry.pwaRoute[key as keyof Registry['pwaRoute']].pwa;
26
+ const named = /\b(cliRoot|tuiSlash|telegramCommand|discordCommand):([\w:-]+)/.exec(hint);
27
+ if (named) {
28
+ const group = registry[named[1] as Exclude<keyof Registry, 'pwaRoute'>] as Record<string, Maturity>;
29
+ return Object.prototype.hasOwnProperty.call(group, named[2]!) ? group[named[2]!] : undefined;
30
+ }
31
+ // Explicit command spelling in a feature name is unambiguous; a prose word is not.
32
+ const command = /`(?:elanous )?([\w:-]+)`/.exec(feature)?.[1];
33
+ if (command && Object.prototype.hasOwnProperty.call(registry.cliRoot, command))
34
+ return registry.cliRoot[command as keyof Registry['cliRoot']];
35
+ return undefined;
36
+ }
37
+
38
+ function prs(text: string): string[] {
39
+ return [...new Set([...text.matchAll(PR)].map((m) => `#${m[1] ?? m[2]}`))];
40
+ }
41
+
42
+ function escapeCell(text: string): string {
43
+ return text.replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ');
44
+ }
45
+
46
+ /** Checklist cells require green status; rows without checklist cells use the map's maturity and public marker. */
47
+ export function generateFeatureMap(input: { map: string; items: readonly ChecklistItem[]; registry: Registry; version: string }): string {
48
+ const checklist = new Map(input.items.map((item) => [item.id, item]));
49
+ const green = new Map(input.items.filter((item) => item.status === 'green').map((item) => [item.id, item]));
50
+ const rows: Row[] = [];
51
+ let section = '';
52
+ for (const line of input.map.split(/\r?\n/)) {
53
+ if (/^#{2,3} /.test(line)) section = line;
54
+ if (!section.startsWith('## 2. 기능 배치표 ') || !line.startsWith('|')) continue;
55
+ const columns = cells(line);
56
+ if (columns.length !== 8 || !/^P\d+(?:[·⊕]P\d+)*$/.test(clean(columns[0]!))) continue;
57
+ const [_, rawFeature, rawId, , hint, publication, evidence] = columns;
58
+ const feature = clean(rawFeature!);
59
+ if (!feature || feature === '—') continue;
60
+ const id = clean(rawId!);
61
+ const ids = id.split(/[·⊕]/).map((part) => part.trim()).filter(Boolean);
62
+ const hasChecklistCell = ids.some((part) => checklist.has(part));
63
+ const matched = ids.map((part) => green.get(part)).filter((item): item is ChecklistItem => !!item);
64
+ const complete = ids.length > 0 && ids.every((part) => green.has(part));
65
+ const maturity = hasChecklistCell ? gradeFor(feature, hint!, input.registry) : undefined;
66
+ const mapMaturity = /^(?:\/\S+\s+)?(stable|beta)\b/i.exec(clean(hint!))?.[1]?.toLowerCase();
67
+ const grade: Grade = hasChecklistCell
68
+ ? complete && maturity === 'stable' ? '된다' : complete && maturity === 'beta' ? '베타' : '로드맵'
69
+ : !clean(publication!).startsWith('✅') ? '로드맵'
70
+ : mapMaturity === 'stable' ? '된다' : mapMaturity === 'beta' ? '베타' : '로드맵';
71
+ const evidencePrs = hasChecklistCell
72
+ ? [...new Set(matched.flatMap((item) => prs(item.evidence ?? '')))] : prs(evidence!);
73
+ const held = HELD.test([...columns, ...ids.map((part) => {
74
+ const item = checklist.get(part);
75
+ return item ? `${item.title} ${item.evidence ?? ''}` : '';
76
+ })].join(' '));
77
+ rows.push({ feature: held ? '개념' : feature, grade: held ? '로드맵' : grade,
78
+ id: held ? '—' : hasChecklistCell ? id : '지도 판정', pr: held || !evidencePrs.length ? '근거 없음'
79
+ : !hasChecklistCell || complete ? evidencePrs.join(', ') : `부분: ${evidencePrs.join(', ')} · 나머지 칸 미완` });
80
+ }
81
+ const lines = [
82
+ '# FEATURE-MAP — 엘라누스가 할 수 있는 것',
83
+ '',
84
+ `판: ${escapeCell(input.version)} · 원천: src/maturity/feature-maturity.ts + 릴리스 체크리스트 green + docs/marketing/MAP-value-props-to-features.md §2`,
85
+ `재생성: \`bun scripts/marketing/generate-feature-map.ts --version ${escapeCell(input.version)} --out docs/marketing/FEATURE-MAP.md\``,
86
+ '내부 배치용: 로드맵은 현재 기능이 아니며, 된다/베타도 공개 판정·컷의 조상 PR 확인을 대신하지 않는다.',
87
+ '',
88
+ '| 기능 | 성숙도 | 칸 id | 근거 PR |',
89
+ '|---|---|---|---|',
90
+ ...rows.map((row) => `| ${escapeCell(row.feature)} | ${row.grade} | ${escapeCell(row.id)} | ${escapeCell(row.pr)} |`),
91
+ '',
92
+ ];
93
+ return lines.join('\n');
94
+ }
95
+
96
+ if (import.meta.main) {
97
+ const args = process.argv.slice(2);
98
+ const option = (name: string) => {
99
+ const index = args.indexOf(name);
100
+ if (index < 0) return undefined;
101
+ const value = args[index + 1];
102
+ if (!value || value.startsWith('--')) throw new Error(`${name} 값이 필요하다`);
103
+ args.splice(index, 2);
104
+ return value;
105
+ };
106
+ const version = option('--version');
107
+ const mapFile = option('--map') ?? DEFAULT_MAP;
108
+ const checklistFile = option('--checklist');
109
+ const out = option('--out');
110
+ if (!version || args.length) throw new Error('Usage: bun scripts/marketing/generate-feature-map.ts --version <v> [--map <file>] [--checklist <snapshot.json>] [--out <file>]');
111
+ const items = checklistFile
112
+ ? (JSON.parse(readFileSync(checklistFile, 'utf8')) as { items: ChecklistItem[] }).items
113
+ : (await import('../../src/release-loop/checklist.js')).listChecklist(version).items;
114
+ if (!Array.isArray(items)) throw new Error('체크리스트 items 배열이 없다');
115
+ const body = generateFeatureMap({ map: readFileSync(mapFile, 'utf8'), items, registry: FEATURE_MATURITY, version });
116
+ if (out) writeFileSync(out, body);
117
+ else process.stdout.write(body);
118
+ }
@@ -0,0 +1,84 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ export interface AuthorSummary {
5
+ authorRunId: string;
6
+ sectionChars: Record<string, number>;
7
+ }
8
+
9
+ export interface AssignedGoalEvent {
10
+ category: string;
11
+ event: string;
12
+ ts: string;
13
+ data: { authorRunId?: string; goalId?: string };
14
+ }
15
+
16
+ export interface ExecutionRun {
17
+ runId: string;
18
+ goalId: string;
19
+ startedAt: string;
20
+ outcome: string;
21
+ }
22
+
23
+ export interface SectionContribution {
24
+ authorRunId: string;
25
+ goalId: string;
26
+ runId: string;
27
+ section: string;
28
+ chars: number;
29
+ outcome: string;
30
+ }
31
+
32
+ /** Join an authored summary to the first execution starting after that author's assignment. */
33
+ export function measureAuthorSectionContribution(
34
+ summaries: readonly AuthorSummary[],
35
+ events: readonly AssignedGoalEvent[],
36
+ runs: readonly ExecutionRun[],
37
+ ): SectionContribution[] {
38
+ const rows: SectionContribution[] = [];
39
+ for (const summary of summaries) {
40
+ const assignment = events.filter((event) => event.category === 'goal-author'
41
+ && event.event === 'goal-id-assigned'
42
+ && event.data?.authorRunId === summary.authorRunId
43
+ && typeof event.data.goalId === 'string'
44
+ && Number.isFinite(Date.parse(event.ts)))
45
+ .sort((left, right) => Date.parse(left.ts) - Date.parse(right.ts)
46
+ || left.data.goalId!.localeCompare(right.data.goalId!))[0];
47
+ if (!assignment) continue;
48
+ const assignedAt = Date.parse(assignment.ts);
49
+ const run = runs.filter((candidate) => candidate.goalId === assignment.data.goalId
50
+ && Number.isFinite(Date.parse(candidate.startedAt))
51
+ && Date.parse(candidate.startedAt) >= assignedAt)
52
+ .sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt)
53
+ || left.runId.localeCompare(right.runId))[0];
54
+ if (!run) continue;
55
+ for (const [section, chars] of Object.entries(summary.sectionChars)) {
56
+ rows.push({ authorRunId: summary.authorRunId, goalId: run.goalId, runId: run.runId, section, chars, outcome: run.outcome });
57
+ }
58
+ }
59
+ return rows;
60
+ }
61
+
62
+ function readRunLedger(path: string): ExecutionRun[] {
63
+ const db = new Database(path, { readonly: true });
64
+ try {
65
+ const records = db.query('SELECT run_id, goal_id, doc FROM goal_run').all() as Array<{ run_id: string; goal_id: string; doc: string }>;
66
+ return records.map(({ run_id, goal_id, doc }) => {
67
+ const record = JSON.parse(doc) as { startedAt: string; outcome: string };
68
+ return { runId: run_id, goalId: goal_id, startedAt: record.startedAt, outcome: record.outcome };
69
+ });
70
+ } finally {
71
+ db.close();
72
+ }
73
+ }
74
+
75
+ if (import.meta.main) {
76
+ const [summaryFile, eventsFile, ledgerFile] = process.argv.slice(2);
77
+ if (!summaryFile || !eventsFile || !ledgerFile) throw new Error('usage: bun scripts/measure/author-section-contribution.ts <summaries.json> <events.jsonl> <goal-runs.db>');
78
+ const summaries = JSON.parse(readFileSync(summaryFile, 'utf8')) as AuthorSummary[];
79
+ const events = readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line) as AssignedGoalEvent);
80
+ const rows = measureAuthorSectionContribution(summaries, events, readRunLedger(ledgerFile));
81
+ console.log('| authorRunId | goalId | runId | section | chars | outcome |');
82
+ console.log('| --- | --- | --- | --- | ---: | --- |');
83
+ for (const row of rows) console.log(`| ${row.authorRunId} | ${row.goalId} | ${row.runId} | ${row.section} | ${row.chars} | ${row.outcome} |`);
84
+ }
@@ -1,27 +1,66 @@
1
1
  #!/usr/bin/env bun
2
- import { addItem, checklistGate, listChecklist, summarizeChecklist } from '../../src/release-loop/checklist.js';
2
+ import { checklistGate, listChecklist, summarizeChecklist } from '../../src/release-loop/checklist.js';
3
+ import * as store from '../../src/release-loop/feature-store.js';
4
+ import { getSchedule } from '../../src/release-loop/release-schedule.js';
5
+ import { debug } from '../../src/debug/log.js';
3
6
  import { emitNodeResult, errorResult, readGraphContext, type GraphContext } from './node-verdict.js';
4
7
 
5
- export function runChecklistGate(context: GraphContext = readGraphContext()) {
8
+ export function runChecklistGate(context: GraphContext = readGraphContext(), now: Date = new Date()) {
6
9
  const version = context.input.version;
7
10
  const data = listChecklist(version);
11
+ const schedule = getSchedule(version);
12
+ const deadline = schedule?.landBy;
13
+ const overdue = deadline !== null && deadline !== undefined && now.getTime() > Date.parse(deadline);
14
+ const autoMoved = overdue ? data.items.filter((item) => item.status === 'yellow' && item.priority !== 'P0' && item.disposition === undefined).map((item) => item.id) : [];
8
15
  const gate = checklistGate(version);
9
- const counts = summarizeChecklist(data);
10
- const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}${gate.parity?.length ? ` · ⚠ 짝 경고 ${gate.parity.length}(${gate.parity.map((p) => p.id).join(', ')})` : ''}`;
11
- if (gate.ok && gate.moved.length) {
12
- const [major, minor, patch] = version.split('.').map(Number);
13
- const next = `${major}.${minor}.${patch! + 1}`;
14
- // 이월은 «같은 칸»을 다음 판으로 옮기는 것 — 다른 판 id 충돌 검사(CKID #23407)의 대상이 아니다.
16
+ const p0 = data.items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
17
+ gate.undecided = gate.undecided.filter((id) => !autoMoved.includes(id) && !p0.includes(id));
18
+ gate.moved = gate.moved.filter((id) => !p0.includes(id)).concat(autoMoved);
19
+ gate.knownIssues = gate.knownIssues.filter((item) => !p0.includes(item.id));
20
+ gate.blocked.push(...p0.filter((id) => !gate.blocked.includes(id)));
21
+ gate.ok = gate.red.length === 0 && gate.undecided.length === 0 && gate.blocked.length === 0;
22
+ const [major, minor, patch] = version.split('.').map(Number);
23
+ const next = `${major}.${minor}.${patch! + 1}`;
24
+ const carried: string[] = [];
25
+ let carryFailure: string | undefined;
26
+ if (gate.moved.length) {
27
+ const nextChecklist = listChecklist(next);
28
+ const existing = new Map(nextChecklist.items.map((item) => [item.id, item]));
29
+ const movedFromHere = new Set(nextChecklist.history.filter((entry) => entry.field === 'move' && entry.from === version).map((entry) => entry.id));
30
+ const sameCarry = new Set<string>();
15
31
  for (const id of gate.moved) {
16
- const item = data.items.find((candidate) => candidate.id === id)!;
17
- if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
18
- try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }, { allowDuplicateId: true }); }
19
- catch (error) {
20
- if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
32
+ const original = data.items.find((item) => item.id === id)!;
33
+ const destination = existing.get(id);
34
+ if (!destination) continue;
35
+ if (destination.title === original.title && destination.owner === original.owner && (movedFromHere.has(id) || destination.evidence?.split('\n').some((line) => line.startsWith(`${version}에서 이월`)))) {
36
+ sameCarry.add(id);
37
+ } else {
38
+ carryFailure = `이월 충돌: ${id} — 다음 판에 다른 칸이 같은 ID`;
39
+ debug.log('release-loop.checklist-gate', 'carry-collision', { id, from: version, to: next });
40
+ break;
41
+ }
42
+ }
43
+ if (!carryFailure) for (const id of gate.moved) {
44
+ try {
45
+ if (sameCarry.has(id)) {
46
+ store.remove(version, id, 'release-loop.checklist-gate');
47
+ debug.log('release-loop.checklist-gate', 'carry-closed', { id, from: version, to: next });
48
+ } else {
49
+ const reason = autoMoved.includes(id) ? `컷 자동 이월 · 마감 ${new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(deadline!))}` : '컷 자동 이월 · 처분 move';
50
+ store.move(id, version, next, 'release-loop.checklist-gate', undefined, undefined, reason, { clearDisposition: true });
51
+ carried.push(id);
52
+ debug.log('release-loop.checklist-gate', 'carried', { id, from: version, to: next });
53
+ }
54
+ } catch (error) {
55
+ carryFailure = `이월 실패: ${id} — ${error instanceof Error ? error.message : String(error)}`;
56
+ break;
21
57
  }
22
58
  }
23
59
  }
24
- return { outcome: gate.ok ? 'ok' as const : 'fail' as const, verdict: gate.ok ? 'pass' as const : 'fail' as const, summary, ...gate };
60
+ if (carryFailure) gate.ok = false;
61
+ const counts = summarizeChecklist(data);
62
+ const summary = `${carryFailure ? `${carryFailure} · ` : ''}확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}${gate.parity?.length ? ` · ⚠ 짝 경고 ${gate.parity.length}(${gate.parity.map((p) => p.id).join(', ')})` : ''} · 이월 ${carried.length}(${carried.join(', ') || '-'})`;
63
+ return { outcome: gate.ok ? 'ok' as const : 'fail' as const, verdict: gate.ok ? 'pass' as const : 'fail' as const, summary, ...gate, carried };
25
64
  }
26
65
 
27
66
  if (import.meta.main) {
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { debug } from '../../src/debug/log.js';
6
6
 
7
- export interface CutBranchOptions { version: string; base: string; pick: string[]; dryRun?: boolean; repoRoot?: string; log?: (line: string) => void }
7
+ export interface CutBranchOptions { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; repoRoot?: string; log?: (line: string) => void }
8
8
 
9
9
  // git-spawn-allow: release branch operations happen only in an isolated temporary worktree; main is never checked out or changed.
10
10
  function git(repo: string, args: string[]): string {
@@ -23,8 +23,8 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
23
23
  const repo = resolve(opts.repoRoot ?? process.cwd());
24
24
  const branch = `release/${opts.version}`;
25
25
  const log = opts.log ?? console.log;
26
- const report = (event: 'created' | 'refused' | 'conflict', reason?: string, commit?: string) =>
27
- debug.log('release-loop.cut-branch', event, { version: opts.version, branch, ...(commit ? { commit } : {}), ...(reason ? { reason } : {}) });
26
+ const report = (event: 'created' | 'refused' | 'conflict' | 'appended', reason?: string, commit?: string, extra?: Record<string, string | string[]>) =>
27
+ debug.log('release-loop.cut-branch', event, { version: opts.version, branch, ...(commit ? { commit } : {}), ...(reason ? { reason } : {}), ...extra });
28
28
  try {
29
29
  if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(opts.version)) throw new Error(`invalid release version: ${opts.version}`);
30
30
  if (!opts.pick.length) throw new Error('at least one --pick commit is required');
@@ -37,18 +37,31 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
37
37
  for (const sha of picks) if (ancestor(repo, sha, base)) throw new Error(`pick ${sha} is already in base ${base}`);
38
38
  if (new Set(picks).size !== picks.length) throw new Error('duplicate --pick commit');
39
39
  for (const sha of picks) if (git(repo, ['rev-list', '--parents', '-n', '1', sha]).split(' ').length !== 2) throw new Error(`pick ${sha} must have exactly one parent`);
40
- if (git(repo, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`])) throw new Error(`${branch} already exists on origin`);
40
+ const remoteLine = git(repo, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`]);
41
+ if (remoteLine && !opts.append) throw new Error(`${branch} already exists on origin — use --append to fast-forward more picks onto it`);
42
+ if (!remoteLine && opts.append) throw new Error(`${branch} does not exist on origin — omit --append to create it`);
41
43
  if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: repo }).status === 0) throw new Error(`${branch} already exists locally`);
42
- const pkg = JSON.parse(git(repo, ['show', `${base}:package.json`])) as { version?: unknown };
43
- if (pkg.version !== opts.version) throw new Error(`base package.json version ${pkg.version} is not ${opts.version}`);
44
- log(`${branch}: ${base} + ${picks.join(' + ')}${opts.dryRun ? ' (dry-run)' : ''}`);
44
+ let remoteTip: string | undefined;
45
+ if (opts.append) {
46
+ const advertised = remoteLine.split(/\s+/)[0] ?? '';
47
+ if (!/^[0-9a-f]{40}$/i.test(advertised)) throw new Error(`${branch} remote tip is not a commit SHA`);
48
+ git(repo, ['fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
49
+ remoteTip = git(repo, ['rev-parse', '--verify', `refs/remotes/origin/${branch}^{commit}`]);
50
+ if (advertised !== remoteTip) throw new Error(`${branch} remote tip moved — fast-forward refused (no force push)`);
51
+ for (const sha of picks) if (ancestor(repo, sha, remoteTip)) throw new Error(`pick ${sha} is already in ${branch}`);
52
+ }
53
+ const versionAt = remoteTip ?? base;
54
+ const pkg = JSON.parse(git(repo, ['show', `${versionAt}:package.json`])) as { version?: unknown };
55
+ if (pkg.version !== opts.version) throw new Error(`${opts.append ? 'release branch' : 'base'} package.json version ${pkg.version} is not ${opts.version}`);
56
+ const start = remoteTip ?? base;
57
+ log(`${branch}: ${start} + ${picks.join(' + ')}${opts.append ? ' (append)' : ''}${opts.dryRun ? ' (dry-run)' : ''}`);
45
58
  if (opts.dryRun) return { branch, commit: null, dryRun: true };
46
59
  const temp = mkdtempSync(join(tmpdir(), 'release-cut-branch-'));
47
60
  const tree = join(temp, 'tree');
48
61
  let added = false;
49
62
  let commit: string | undefined;
50
63
  try {
51
- git(repo, ['worktree', 'add', '--detach', tree, base]);
64
+ git(repo, ['worktree', 'add', '--detach', tree, start]);
52
65
  added = true;
53
66
  for (const sha of picks) {
54
67
  try { git(tree, ['cherry-pick', sha]); }
@@ -68,13 +81,30 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
68
81
  } finally { rmSync(temp, { recursive: true, force: true }); }
69
82
  }
70
83
  if (!commit) throw new Error('cherry-pick produced no commit');
71
- const push = git(repo, ['push', '--porcelain', `--force-with-lease=refs/heads/${branch}:`, 'origin', `${commit}:refs/heads/${branch}`]);
72
- const newBranch = push.split('\n').some((line) => {
84
+ const lease = opts.append ? remoteTip! : '';
85
+ let push: string;
86
+ try {
87
+ push = git(repo, ['push', '--porcelain', `--force-with-lease=refs/heads/${branch}:${lease}`, 'origin', `${commit}:refs/heads/${branch}`]);
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ if (opts.append && /stale info|rejected|non-fast-forward|failed to push/i.test(message)) {
91
+ throw new Error(`${branch} remote tip moved — fast-forward refused (no force push)`);
92
+ }
93
+ throw error;
94
+ }
95
+ const landed = push.split('\n').some((line) => {
73
96
  const [flag, ref, summary] = line.split('\t');
74
- return flag === '*' && ref?.endsWith(`:refs/heads/${branch}`) && summary === '[new branch]';
97
+ if (flag !== ' ' && flag !== '*') return false;
98
+ if (!ref?.endsWith(`:refs/heads/${branch}`)) return false;
99
+ if (!opts.append) return flag === '*' && summary === '[new branch]';
100
+ const range = summary?.match(/^([0-9a-f]+)\.\.([0-9a-f]+)$/i);
101
+ return flag === ' ' && !!range && remoteTip!.startsWith(range[1]!) && commit.startsWith(range[2]!);
75
102
  });
76
- if (!newBranch) throw new Error(`${branch} already exists on origin (push did not create a new branch)`);
77
- report('created', undefined, commit);
103
+ if (!landed) throw new Error(opts.append
104
+ ? `${branch} remote tip moved — fast-forward refused (no force push)`
105
+ : `${branch} already exists on origin (push did not create a new branch)`);
106
+ if (opts.append) report('appended', undefined, commit, { from: remoteTip!, to: commit, picks });
107
+ else report('created', undefined, commit);
78
108
  log(`${branch} → ${commit}`);
79
109
  return { branch, commit, dryRun: false };
80
110
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { spawnSync } from 'node:child_process';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
3
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { randomUUID } from 'node:crypto';
@@ -19,10 +19,10 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
19
19
  import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
20
20
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
21
21
 
22
- interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }> }
22
+ interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }>; timedOut?: boolean }
23
23
  export interface GateRunner {
24
- command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
25
- localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
24
+ command(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
25
+ localCommand(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
26
26
  sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
27
27
  add(tree: string, commit: string): Promise<void>;
28
28
  remove(tree: string): Promise<void>;
@@ -52,7 +52,7 @@ export interface GateResult {
52
52
  fixed: number;
53
53
  knownEnv: number;
54
54
  knownEnvCleared: string[];
55
- stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
55
+ stalledEnv: Array<{ file: string; reason: 'no-output' | 'isolated-timeout'; local: 'passed' | 'failed' | 'timeout' }>;
56
56
  baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
57
57
  durationMs: number;
58
58
  error?: string;
@@ -189,17 +189,65 @@ class StalledPodShards extends Error {
189
189
  }
190
190
  }
191
191
 
192
+ /** Isolated re-runs (host stall re-run · cut and baseline re-checks) are time-limited; sweeps and installs keep their full time. */
193
+ export const GATE_ISOLATED_TIMEOUT_MS = 300_000;
194
+
195
+ /**
196
+ * Runs a limited local command in its own process group. On timeout: SIGTERM the group (test-deterministic forwards it to its own
197
+ * `bun test` group — Chrome included), SIGKILL after a grace, and return after a hard deadline even if a foreign-group process still holds the pipe.
198
+ */
199
+ export function limitedLocalCommand(cmd: string, args: string[], cwd: string, limitMs: number, graceMs = 10_000): Promise<CommandResult> {
200
+ return new Promise((resolveRun) => {
201
+ const chunks: string[] = [];
202
+ let timedOut = false;
203
+ let done = false;
204
+ const timers: ReturnType<typeof setTimeout>[] = [];
205
+ const child = spawn(cmd, args, { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
206
+ let kept = 0;
207
+ // Same ceiling as the unlimited spawnSync path (maxBuffer 128MB): drop the oldest output beyond it, keep the tail.
208
+ const keep = (d: string) => { chunks.push(d); kept += d.length; while (kept > 128 * 1024 * 1024 && chunks.length > 1) kept -= chunks.shift()!.length; };
209
+ child.stdout?.setEncoding('utf8').on('data', keep);
210
+ child.stderr?.setEncoding('utf8').on('data', keep);
211
+ const signalGroup = (signal: NodeJS.Signals) => { try { if (child.pid) process.kill(-child.pid, signal); } catch { /* group already gone */ } };
212
+ const finish = (code: number | null, error?: Error) => {
213
+ if (done) return;
214
+ done = true;
215
+ for (const timer of timers) clearTimeout(timer);
216
+ child.stdout?.destroy();
217
+ child.stderr?.destroy();
218
+ const tail = error ? `\n${error}` : timedOut ? `\nkilled after ${limitMs}ms (process group)` : '';
219
+ resolveRun({ rc: code ?? 2, output: `${chunks.join('')}${tail}`, ...(timedOut ? { timedOut } : {}) });
220
+ };
221
+ timers.push(setTimeout(() => {
222
+ timedOut = true;
223
+ signalGroup('SIGTERM');
224
+ timers.push(setTimeout(() => signalGroup('SIGKILL'), graceMs));
225
+ timers.push(setTimeout(() => finish(null), graceMs + 5_000));
226
+ }, limitMs));
227
+ child.on('error', (error) => finish(null, error));
228
+ child.on('close', (code) => finish(code));
229
+ });
230
+ }
231
+
192
232
  export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
193
233
  let remoteMirror: string | undefined;
194
- const localCommand: GateRunner['command'] = async (cmd, args, cwd) => {
234
+ const localCommand: GateRunner['localCommand'] = async (cmd, args, cwd, limitMs) => {
235
+ if (limitMs) return limitedLocalCommand(cmd, args, cwd, limitMs);
195
236
  const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
196
237
  return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
197
238
  };
198
239
  const command: GateRunner['command'] = commandOverride ?? (remote
199
- ? async (cmd, args, cwd) => {
200
- const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`],
201
- { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
202
- return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
240
+ ? async (cmd, args, cwd, limitMs) => {
241
+ // A limited remote run is killed on the remote host; the local ssh deadline is a backstop.
242
+ // GNU timeout signals its own process group: -s KILL would kill timeout too (ssh 255), so TERM first then KILL after 10s (124 / 137).
243
+ // No coreutils timeout on the remote → run unlimited there and rely on the local ssh deadline.
244
+ const limitPrefix = limitMs ? `$(command -v timeout >/dev/null 2>&1 && echo 'timeout -k 10 ${Math.ceil(limitMs / 1000)}') ` : '';
245
+ // Start in /tmp, not the caller checkout. macOS resolves that symlink to /private/tmp before the child sees it;
246
+ // the remote command still `cd`s to the quoted path, so either spelling is the same directory.
247
+ const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${limitPrefix}${[cmd, ...args].map(quote).join(' ')}`],
248
+ { cwd: '/tmp', encoding: 'utf8', maxBuffer: 128 * 1024 * 1024, ...(limitMs ? { timeout: limitMs + 60_000, killSignal: 'SIGKILL' as const } : {}) });
249
+ const timedOut = !!limitMs && (run.status === 124 || run.status === 137 || (run.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT');
250
+ return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}`, ...(timedOut ? { timedOut } : {}) };
203
251
  }
204
252
  : localCommand);
205
253
  const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
@@ -630,7 +678,15 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
630
678
  }
631
679
  for (const { file, reason } of stalled) {
632
680
  if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
633
- const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree);
681
+ // 0.2.15: InsidePage.test.tsx gave no Pod output and then hung on the host with headless Chrome — the run could only fail.
682
+ const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree, GATE_ISOLATED_TIMEOUT_MS);
683
+ // A file that stalls in its Pod shard and again on the host is an environment stall: record it, measure nothing from it.
684
+ if (isolated.timedOut) {
685
+ // Grandchildren (e.g. headless Chrome) can outlive the kill — the event lets the next release see a repeat.
686
+ debug.log('release-loop.gate', 'isolated-timeout', { file, label, limitMs: GATE_ISOLATED_TIMEOUT_MS });
687
+ local.push({ file, reason, local: 'timeout' });
688
+ continue;
689
+ }
634
690
  const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
635
691
  if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
636
692
  measured.failures.push(...parsed.failures);
@@ -688,14 +744,26 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
688
744
  result.fixed = diff.fixed.length;
689
745
  result.preexisting = diff.common.length;
690
746
  for (const file of new Set(diff.newFailures.map(fileOf))) {
691
- const isolatedCut = cutHostFailed.has(file)
747
+ // 0.2.15: the cut re-check of a new-failure file (InsidePage.test.tsx · headless Chrome) hung for good — limit it like the host re-run.
748
+ const cutCheck = cutHostFailed.has(file) ? undefined : await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
749
+ if (cutCheck?.timedOut) {
750
+ debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'cut isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
751
+ result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
752
+ continue;
753
+ }
754
+ const isolatedCut = cutCheck === undefined
692
755
  ? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
693
- : failuresOf(await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree), `cut isolated ${file}`);
756
+ : failuresOf(cutCheck, `cut isolated ${file}`);
694
757
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
695
758
  const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
696
759
  if (candidates.length === 0) continue;
697
760
  const baselineTree = await getBaseTree();
698
- const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
761
+ const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree, GATE_ISOLATED_TIMEOUT_MS);
762
+ if (previous.timedOut) {
763
+ debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'baseline isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
764
+ result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
765
+ continue;
766
+ }
699
767
  let oldFailures: Set<string>;
700
768
  if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
701
769
  const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);