elanous 0.2.15 → 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 (254) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { findNativeTool, isNativeToolModelExposed } from '../native-tool-catalog.js';
2
2
 
3
- export type EntranceStatus = 'live' | 'retired';
3
+ export type EntranceStatus = 'live' | 'retired' | 'closed';
4
4
 
5
5
  export type EntranceStampability = 'stampable' | 'structurally-unstampable' | 'unknown';
6
6
 
@@ -181,12 +181,14 @@ const ENTRANCE_DECLARATIONS = [
181
181
  // `dev --ask` 와 ***같은*** `runAskLaunchFlow` 를 탄다. 두 문의 차이는 `entrance` 태그뿐이다.
182
182
  // ⇒ 판정값이 `'verified' | 'unknown'` 둘뿐이라 「구조적 부재」를 적을 칸이 없다. `unknown` 으로 둔다.
183
183
  // ⛔ 그러니 이 `unknown` 을 「미완」으로 읽지 마라 — 그 구분은 이 주석이 canonical 이다.
184
- { id: 'cli-dev-ask', surface: 'cli', status: 'live', verification: { actualMission: 'verified', legacyParity: 'unknown' } },
184
+ // OLD-DOOR-CLOSE — a direct `dev --ask` from a terminal or seat session refuses.
185
+ // Harness-internal calls still pass, marked with the ONEDOOR-1 entrance stamp.
186
+ { id: 'cli-dev-ask', surface: 'cli', status: 'closed', verification: { actualMission: 'verified', legacyParity: 'unknown' } },
185
187
  // `elanous drive` 는 `elanous dev` 의 Commander alias 로 살아 있고, buildDriveAliasDevSpec 이 entrance 를
186
188
  // `cli-drive` 로 넘기며 completion 을 `worktree-only` 로 못 박는다. 그래서 별도 무인 권한 축이 아니다.
187
189
  { id: 'cli-drive', surface: 'cli', status: 'live' },
188
- { id: 'cli-self-implement', surface: 'cli', status: 'live' },
189
- { id: 'cli-self-orchestrate', surface: 'cli', status: 'live' },
190
+ { id: 'cli-self-implement', surface: 'cli', status: 'closed' },
191
+ { id: 'cli-self-orchestrate', surface: 'cli', status: 'closed' },
190
192
  // ⭐ 2026-08-22 · 🅣 — ***사람이 «돌려 보고» 적은 판정***이다(⛔ 도구가 추측한 값이 아니다).
191
193
  // actualMission: 그 입구로 실제 골을 «끝까지» 돌렸다 — 17발 · 자동 병합 5 · 회수 착지 7.
192
194
  // 📏 원장 각인이 그것을 말한다: `"entrance":"cli-harness-ask"` (아침엔 0건이었다).
@@ -331,6 +333,7 @@ export function summarizeEntrances(registry: readonly EntranceDeclaration[] = EN
331
333
  readonly total: number;
332
334
  readonly live: number;
333
335
  readonly retired: number;
336
+ readonly closed: number;
334
337
  readonly bySurface: ReadonlyArray<{ surface: EntranceSurface; total: number; retired: number }>;
335
338
  } {
336
339
  const surfaces = [...new Set(registry.map((e) => e.surface))];
@@ -338,6 +341,7 @@ export function summarizeEntrances(registry: readonly EntranceDeclaration[] = EN
338
341
  total: registry.length,
339
342
  live: registry.filter((e) => e.status === 'live').length,
340
343
  retired: registry.filter((e) => e.status === 'retired').length,
344
+ closed: registry.filter((e) => e.status === 'closed').length,
341
345
  bySurface: surfaces.map((surface) => ({
342
346
  surface,
343
347
  total: registry.filter((e) => e.surface === surface).length,
@@ -350,12 +354,12 @@ export function summarizeEntrances(registry: readonly EntranceDeclaration[] = EN
350
354
  export function renderLaunchEntrances(registry: readonly EntranceDeclaration[] = ENTRANCE_REGISTRY): string {
351
355
  const summary = summarizeEntrances(registry);
352
356
  const rows = listEntrancesWithModelExposure(registry)
353
- .map((e) => ` ${e.status === 'retired' ? '🪦' : '✅'} ${e.surface.padEnd(7)} ${e.id} (${e.stampability}) verification=actualMission:${e.verification.actualMission},legacyParity:${e.verification.legacyParity} imprintEvidence=${e.imprintEvidence} evidenceSource=${e.stampabilityEvidenceSource} evidenceSourceFilePath=${e.evidenceSourceFilePath ?? 'missing'} modelExposed=${e.modelExposed ?? 'missing'}`);
357
+ .map((e) => ` ${e.status === 'retired' ? '🪦' : e.status === 'closed' ? '닫힘' : '✅'} ${e.surface.padEnd(7)} ${e.id} (${e.stampability}) verification=actualMission:${e.verification.actualMission},legacyParity:${e.verification.legacyParity} imprintEvidence=${e.imprintEvidence} evidenceSource=${e.stampabilityEvidenceSource} evidenceSourceFilePath=${e.evidenceSourceFilePath ?? 'missing'} modelExposed=${e.modelExposed ?? 'missing'}`);
354
358
  const surfaces = summary.bySurface
355
359
  .map((s) => `${s.surface}=${s.total}${s.retired > 0 ? `(은퇴 ${s.retired})` : ''}`)
356
360
  .join(' · ');
357
361
  return [
358
- `launch entrances: ${summary.total} (live ${summary.live} · retired ${summary.retired})`,
362
+ `launch entrances: ${summary.total} (live ${summary.live} · retired ${summary.retired} · closed ${summary.closed})`,
359
363
  ` 표면별: ${surfaces}`,
360
364
  ...rows,
361
365
  ].join('\n');
@@ -0,0 +1,73 @@
1
+ /**
2
+ * OLD-DOOR-CLOSE — the three outside doors refuse a direct call.
3
+ *
4
+ * `elanous dev --ask`, `elanous self implement`, and `elanous self orchestrate`
5
+ * stay callable from inside the harness. The harness marks those calls with
6
+ * the same stamp ONEDOOR-1 uses for a door (`ELANOUS_HARNESS_ENTRANCE`) or with
7
+ * the explicit internal flag `--harness-internal`. A terminal or seat session
8
+ * carries neither, so it exits non-zero and is told where to go.
9
+ */
10
+ import type { LaunchDoor } from '../harness/launch-stamp.js';
11
+
12
+ export const OLD_DOORS = ['dev-ask', 'self-implement', 'self-orchestrate'] as const;
13
+ export type OldDoor = (typeof OLD_DOORS)[number];
14
+
15
+ /** Same env ONEDOOR-1 already stamps onto a harness launch. */
16
+ export const OLD_DOOR_STAMP_ENV = 'ELANOUS_HARNESS_ENTRANCE';
17
+ /** Same meaning as the env, for a caller that can pass an argument but not an env. */
18
+ export const OLD_DOOR_STAMP_FLAG = '--harness-internal';
19
+
20
+ const STAMP_BY_DOOR: Readonly<Record<OldDoor, readonly string[]>> = {
21
+ 'dev-ask': ['dev-ask', 'cli-dev-ask', 'cli-drive', 'harness-say', 'cli-harness-say', 'harness-ask', 'cli-harness-ask', 'queue-tick', 'agent-mission', 'daemon-tool'],
22
+ 'self-implement': ['self-implement', 'cli-self-implement', 'nl-self-implement', 'daemon-self-implement', 'daemon-tool', 'harness-say', 'cli-harness-say', 'harness-ask', 'cli-harness-ask', 'queue-tick', 'agent-mission'],
23
+ 'self-orchestrate': ['self-orchestrate', 'cli-self-orchestrate', 'nl-self-orchestrate', 'cli-harness-orchestrate', 'harness-say', 'cli-harness-say', 'harness-ask', 'cli-harness-ask', 'queue-tick', 'agent-mission', 'daemon-tool'],
24
+ };
25
+
26
+ export interface OldDoorRefusal {
27
+ readonly door: OldDoor;
28
+ readonly caller: 'outside';
29
+ readonly exitCode: 1;
30
+ readonly message: string;
31
+ }
32
+
33
+ export function oldDoorStampAccepted(door: OldDoor, stamp: string | undefined | null): boolean {
34
+ const value = stamp?.trim();
35
+ if (!value) return false;
36
+ return (STAMP_BY_DOOR[door] as readonly string[]).includes(value);
37
+ }
38
+
39
+ /** A harness-internal call carries the stamp in the environment or on the argv. Anything else is outside. */
40
+ export function isOldDoorInternalCall(
41
+ door: OldDoor,
42
+ env: NodeJS.ProcessEnv = process.env,
43
+ argv: readonly string[] = process.argv,
44
+ ): boolean {
45
+ if (argv.includes(OLD_DOOR_STAMP_FLAG)) return true;
46
+ return oldDoorStampAccepted(door, env[OLD_DOOR_STAMP_ENV]);
47
+ }
48
+
49
+ /** Env a harness parent sets so the child door stays open. */
50
+ export function oldDoorInternalEnv(door: LaunchDoor | OldDoor): Record<string, string> {
51
+ return { [OLD_DOOR_STAMP_ENV]: door };
52
+ }
53
+
54
+ const REPLACEMENT: Readonly<Record<OldDoor, string>> = {
55
+ 'dev-ask': 'elanous harness ask <goal.md>',
56
+ 'self-implement': 'elanous harness say "<sentence>"',
57
+ 'self-orchestrate': 'elanous harness say "<sentence>"',
58
+ };
59
+
60
+ const DOOR_LABEL: Readonly<Record<OldDoor, string>> = {
61
+ 'dev-ask': 'elanous dev --ask',
62
+ 'self-implement': 'elanous self implement',
63
+ 'self-orchestrate': 'elanous self orchestrate',
64
+ };
65
+
66
+ /** One line: the closed door, the harness door to use, and a command of the same meaning. */
67
+ export function oldDoorRefusalMessage(door: OldDoor): string {
68
+ return `${DOOR_LABEL[door]} 는 닫혔습니다 — harness say / harness ask 로. 예: ${REPLACEMENT[door]}`;
69
+ }
70
+
71
+ export function refuseOldDoor(door: OldDoor): OldDoorRefusal {
72
+ return { door, caller: 'outside', exitCode: 1, message: oldDoorRefusalMessage(door) };
73
+ }
@@ -0,0 +1,166 @@
1
+ import { execFile as execFileCb } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { debug } from '../debug/log.js';
4
+ import { releaseGitDiffPaths, releasePrFilePaths } from '../self-dev/release-path-guard.js';
5
+
6
+ const defaultExecFile = promisify(execFileCb);
7
+ const GIT_TIMEOUT = 20_000;
8
+
9
+ export type AppApiExecFile = (
10
+ file: string,
11
+ args: readonly string[],
12
+ opts: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer?: number; env?: NodeJS.ProcessEnv },
13
+ ) => Promise<{ stdout: string; stderr?: string }>;
14
+
15
+ let execFileImpl: AppApiExecFile = (file, args, opts) => defaultExecFile(file, [...args], opts);
16
+
17
+ /** Tests inject the `gh` runner. Production keeps `execFile`. */
18
+ export function setAppApiExecFileForTest(impl: AppApiExecFile | undefined): void {
19
+ execFileImpl = impl ?? ((file, args, opts) => defaultExecFile(file, [...args], opts));
20
+ }
21
+
22
+ /** App-token `gh` calls only. Caller names are fixed strings; the token value is never logged. */
23
+ const GH_APP_CALLERS = {
24
+ 'post-pr-comment-history': 'postPrComment',
25
+ 'post-pr-comment': 'postPrComment',
26
+ 'find-pr-comment': 'findPrComment',
27
+ 'edit-pr-comment': 'editPrComment',
28
+ 'add-pr-label-list': 'addPrLabel',
29
+ 'add-pr-label-create': 'addPrLabel',
30
+ 'add-pr-label': 'addPrLabel',
31
+ 'read-pr-files': 'readPrFiles',
32
+ } as const;
33
+
34
+ export type GhAppCaller = keyof typeof GH_APP_CALLERS;
35
+
36
+ export const APP_API_RATE_LIMIT_RETRY_CAP_MS = 15 * 60_000;
37
+
38
+ export class AppApiRateLimitError extends Error {
39
+ readonly rateLimited = true as const;
40
+ readonly status: 403 | 429;
41
+ readonly resetAtMs: number | undefined;
42
+ constructor(message: string, status: 403 | 429, resetAtMs: number | undefined) {
43
+ super(message);
44
+ this.name = 'AppApiRateLimitError';
45
+ this.status = status;
46
+ this.resetAtMs = resetAtMs;
47
+ }
48
+ }
49
+
50
+ export function isAppApiRateLimitFailure(status: number | undefined, text: string, remaining?: string): boolean {
51
+ if (remaining === '0') return true;
52
+ if (status !== undefined && status !== 403 && status !== 429) return false;
53
+ return /API rate limit exceeded|rate limit/i.test(text) && (status === 403 || status === 429 || status === undefined);
54
+ }
55
+
56
+ function ghHeader(stderr: string, name: string): string | undefined {
57
+ const match = new RegExp(`^${name}:\\s*(.*)$`, 'im').exec(stderr);
58
+ return match?.[1]?.trim();
59
+ }
60
+
61
+ function ghStatusCode(stderr: string, error: unknown): number | undefined {
62
+ const fromHeader = ghHeader(stderr, 'HTTP') ?? ghHeader(stderr, 'status');
63
+ const headerCode = fromHeader ? Number.parseInt(fromHeader, 10) : NaN;
64
+ if (Number.isFinite(headerCode)) return headerCode;
65
+ const text = `${stderr}\n${error instanceof Error ? error.message : String(error ?? '')}`;
66
+ const match = /\b(403|429)\b/.exec(text);
67
+ return match ? Number(match[1]) : undefined;
68
+ }
69
+
70
+ /** Wait until X-RateLimit-Reset, never longer than 15 minutes. Returns false when the reset is already past or unreadable. */
71
+ export async function waitForAppApiRateLimitReset(
72
+ resetAtMs: number | undefined,
73
+ now = Date.now(),
74
+ sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
75
+ ): Promise<boolean> {
76
+ if (resetAtMs === undefined || !Number.isFinite(resetAtMs)) return false;
77
+ const waitMs = Math.min(Math.max(0, resetAtMs - now), APP_API_RATE_LIMIT_RETRY_CAP_MS);
78
+ if (waitMs <= 0) return false;
79
+ await sleep(waitMs);
80
+ return true;
81
+ }
82
+
83
+ export async function runGhApp(
84
+ caller: GhAppCaller,
85
+ args: readonly string[],
86
+ opts: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer?: number; env: NodeJS.ProcessEnv },
87
+ ): Promise<{ stdout: string; stderr: string }> {
88
+ let stdout = '';
89
+ let stderr = '';
90
+ let status: number | undefined;
91
+ let error: unknown;
92
+ try {
93
+ const result = await execFileImpl('gh', args, opts);
94
+ stdout = result.stdout;
95
+ stderr = result.stderr ?? '';
96
+ status = 200;
97
+ } catch (caught) {
98
+ error = caught;
99
+ const failed = caught as { stdout?: string; stderr?: string };
100
+ stdout = typeof failed.stdout === 'string' ? failed.stdout : '';
101
+ stderr = typeof failed.stderr === 'string' ? failed.stderr : '';
102
+ status = ghStatusCode(stderr, caught);
103
+ }
104
+ const remaining = ghHeader(stderr, 'X-RateLimit-Remaining');
105
+ debug.log('github.app-api', 'call', {
106
+ caller: GH_APP_CALLERS[caller],
107
+ endpoint: args.filter((arg) => !arg.startsWith('-')).at(-1) ?? 'gh',
108
+ status: status ?? 0,
109
+ remaining: remaining ?? '',
110
+ });
111
+ if (error) {
112
+ const text = `${stderr}\n${error instanceof Error ? error.message : String(error)}`;
113
+ if (isAppApiRateLimitFailure(status, text, remaining)) {
114
+ const reset = ghHeader(stderr, 'X-RateLimit-Reset');
115
+ const resetSeconds = reset ? Number(reset) : NaN;
116
+ throw new AppApiRateLimitError(
117
+ text.trim() || 'API rate limit exceeded',
118
+ status === 429 ? 429 : 403,
119
+ Number.isFinite(resetSeconds) ? resetSeconds * 1000 : undefined,
120
+ );
121
+ }
122
+ throw error;
123
+ }
124
+ return { stdout, stderr };
125
+ }
126
+
127
+ export async function readPrFilesWithRateLimitFallback(input: {
128
+ number: number;
129
+ cwd: string;
130
+ base?: string;
131
+ env: NodeJS.ProcessEnv;
132
+ sleep?: (ms: number) => Promise<void>;
133
+ now?: () => number;
134
+ }): Promise<string[]> {
135
+ const env = input.env;
136
+ const endpoint = [
137
+ 'api', '--paginate', '--slurp', '--method', 'GET', '-f', 'per_page=100',
138
+ `repos/{owner}/{repo}/pulls/${input.number}/files`,
139
+ ];
140
+ const ghOpts = { cwd: input.cwd, encoding: 'utf8' as const, timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env };
141
+ const fromApi = async () => releasePrFilePaths(JSON.parse((await runGhApp('read-pr-files', endpoint, ghOpts)).stdout));
142
+ const fromGit = async (): Promise<string[]> => {
143
+ const base = input.base ?? 'origin/main';
144
+ const { stdout } = await execFileImpl('git', ['diff', '--find-renames', '--name-status', '-z', `${base}...HEAD`], {
145
+ cwd: input.cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024,
146
+ });
147
+ return releaseGitDiffPaths(stdout);
148
+ };
149
+ try {
150
+ return await fromApi();
151
+ } catch (error) {
152
+ if (!(error instanceof AppApiRateLimitError)) throw error;
153
+ try {
154
+ return await fromGit();
155
+ } catch (gitError) {
156
+ const waited = await waitForAppApiRateLimitReset(error.resetAtMs, input.now?.() ?? Date.now(), input.sleep);
157
+ if (!waited) throw error;
158
+ try {
159
+ return await fromApi();
160
+ } catch (retryError) {
161
+ if (retryError instanceof AppApiRateLimitError) throw retryError;
162
+ throw gitError;
163
+ }
164
+ }
165
+ }
166
+ }
@@ -9,6 +9,7 @@ import type { UsageSnapshot } from '../budget/types.js';
9
9
  import { debug } from '../debug/log.js';
10
10
  import {
11
11
  DEFAULT_BUDGET_GATE_MAX_USED_PERCENT,
12
+ LAUNCH_GROK_USED_PERCENT_CAP,
12
13
  type BudgetGateMaxUsedPercent,
13
14
  type BudgetGateOnShortfall,
14
15
  } from '../user-config.js';
@@ -137,16 +138,17 @@ function slotUsable(
137
138
  return { usable: measuredUsable || unmeasured, reason: formatCodexReason(input.codexCandidates, cap), ...(unmeasured ? { unmeasured: true } : {}) };
138
139
  }
139
140
  if (provider === GROK_PROVIDER) {
140
- const cap = capFor(input.maxUsedPercent, GROK_PROVIDER);
141
+ const configured = capFor(input.maxUsedPercent, GROK_PROVIDER);
142
+ // 발사 관문은 `elanous usage` 와 같은 주간 사용률에 80% 상한을 실제로 적용한다.
143
+ // «못 쟀다»(undefined)는 통과가 아니다. decideBudget 은 설정 상한을 그대로 쓴다.
144
+ const cap = allowUnmeasured ? LAUNCH_GROK_USED_PERCENT_CAP : configured;
141
145
  const known = typeof input.grokUsedPercent === 'number' && Number.isFinite(input.grokUsedPercent);
142
- const unmeasured = allowUnmeasured && !known;
143
- const usable = unmeasured || (cap === undefined
146
+ const usable = cap === undefined
144
147
  ? known
145
- : known && (input.grokUsedPercent as number) < cap);
148
+ : known && (input.grokUsedPercent as number) < cap;
146
149
  return {
147
150
  usable,
148
151
  reason: formatGrokReason(input.grokUsedPercent, cap),
149
- ...(unmeasured ? { unmeasured: true } : {}),
150
152
  };
151
153
  }
152
154
  return { usable: false, reason: `${provider}: 예산 판정 없음` };
@@ -0,0 +1,289 @@
1
+ // FOLLOWUP-LOOP① — after a harness PR merges, leftover findings become one
2
+ // follow-up goal draft. Default mode is shadow: record only. `live` also
3
+ // enqueues through the same function as `harness queue add`.
4
+ //
5
+ // The test process injects every store. Nothing here opens the operational
6
+ // ledger or queue unless the caller passes those seams.
7
+
8
+ import { createHash } from 'node:crypto';
9
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { addHarnessQueue, type QueueSeat } from '../harness/harness-queue.js';
12
+ import { debug } from '../debug/log.js';
13
+ import { withFileLockSync } from '../storage/file-lock.js';
14
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
15
+ import { devVersion, listChecklist, parseOwner } from '../release-loop/checklist.js';
16
+ import { getUserConfig } from '../user-config.js';
17
+
18
+ export const FOLLOW_UP_DEPTH_LIMIT = 3;
19
+ export const FOLLOW_UP_OP_CARD_REQUIRED = 'OP 카드 필요';
20
+
21
+ const REMAINING_SECTION = '남은 조각';
22
+ const SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
23
+
24
+ export type FollowUpMode = 'shadow' | 'live';
25
+
26
+ export interface FollowUpDraftRecord {
27
+ prNumber: number;
28
+ cellId: string;
29
+ depth: number;
30
+ draftHash: string;
31
+ kind: 'draft' | 'op-card-required';
32
+ originalAskFirstLine: string;
33
+ remainings: readonly string[];
34
+ draft?: string;
35
+ note?: string;
36
+ queued: boolean;
37
+ /** live 대기열 넣기가 실패했을 때의 이유(초안은 그대로 남는다). */
38
+ enqueueError?: string;
39
+ seat?: QueueSeat;
40
+ }
41
+
42
+ export interface FollowUpMergeInput {
43
+ prNumber: number;
44
+ /** Result field set when the review budget is exhausted. */
45
+ followUpMustFix?: readonly string[];
46
+ /** Last review's non-blocking findings. */
47
+ shouldFix?: readonly string[];
48
+ /** Opened PR body — the «남은 조각» section is read verbatim. */
49
+ prBody?: string;
50
+ /** Original request. Only its first line enters the draft. */
51
+ feature: string;
52
+ /** Release-cell id. Absent means the goal file's GoalId, else the feature's first token. */
53
+ cellId?: string;
54
+ goalFile?: string;
55
+ }
56
+
57
+ export interface FollowUpSeams {
58
+ stateRoot?: string;
59
+ mode?: FollowUpMode;
60
+ /**
61
+ * Prior follow-up records for this cell, oldest first.
62
+ * Absent means the caller did not hand over a store — the test process writes nothing.
63
+ * Production `defaultSeams` always sets this to the state-root ledger.
64
+ */
65
+ readDrafts?: (cellId: string) => readonly FollowUpDraftRecord[];
66
+ appendDraft?: (record: FollowUpDraftRecord) => void;
67
+ /** Same function `harness queue add` calls. Injected in tests so the real queue is never opened. */
68
+ enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<unknown> | unknown;
69
+ /** Release-cell owner (`TC`, `TC/rel`, …). Absent means look up the developing checklist. */
70
+ cellOwner?: (cellId: string) => string | undefined;
71
+ log?: (category: string, event: string, data?: unknown) => void;
72
+ }
73
+
74
+ export function followUpDraftsPath(stateRoot = elanousStateRoot()): string {
75
+ return join(stateRoot, 'follow-up', 'drafts.jsonl');
76
+ }
77
+
78
+ export function readFollowUpMode(read: () => { harness?: { followUp?: unknown } } = getUserConfig): FollowUpMode {
79
+ return read().harness?.followUp === 'live' ? 'live' : 'shadow';
80
+ }
81
+
82
+ /** Lines under a markdown heading «남은 조각», verbatim, without the bullet mark. */
83
+ export function remainingPiecesFromPrBody(body: string | undefined): string[] {
84
+ if (!body) return [];
85
+ const lines = body.split(/\r?\n/);
86
+ const start = lines.findIndex((line) => line.trim() === `## ${REMAINING_SECTION}`);
87
+ if (start < 0) return [];
88
+ const out: string[] = [];
89
+ for (const line of lines.slice(start + 1)) {
90
+ if (/^#{1,6}\s/.test(line)) break;
91
+ const item = line.match(/^\s*[-*]\s+(.+?)\s*$/)?.[1];
92
+ if (item) out.push(item);
93
+ }
94
+ return out;
95
+ }
96
+
97
+ export function collectRemainings(input: Pick<FollowUpMergeInput, 'followUpMustFix' | 'shouldFix' | 'prBody'>): string[] {
98
+ const seen = new Set<string>();
99
+ const out: string[] = [];
100
+ for (const item of [
101
+ ...(input.followUpMustFix ?? []),
102
+ ...(input.shouldFix ?? []),
103
+ ...remainingPiecesFromPrBody(input.prBody),
104
+ ]) {
105
+ const text = item.trim();
106
+ if (!text || seen.has(text)) continue;
107
+ seen.add(text);
108
+ out.push(text);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ export function followUpCellId(input: Pick<FollowUpMergeInput, 'cellId' | 'goalFile' | 'feature'>, readGoal: (path: string) => string = (path) => readFileSync(path, 'utf8')): string | undefined {
114
+ const explicit = input.cellId?.trim();
115
+ if (explicit) return explicit;
116
+ if (input.goalFile) {
117
+ try {
118
+ const goalId = readGoal(input.goalFile).match(/^- GoalId:\s*(\S+)\s*$/m)?.[1];
119
+ if (goalId) return goalId;
120
+ } catch { /* a missing goal file is not a cell id */ }
121
+ }
122
+ // 자리 발사 머리(`[MK 자리 · 0.2.16 체크리스트 칸 FOLLOWUP-LOOP · …]`)가 칸을 «명시»한 경우만 읽는다.
123
+ // 추정(요청문 첫 단어)으로는 기록하지 않는다 — 다른 골이 한 칸으로 합쳐져 깊이·담당이 틀린다.
124
+ // 자리 머리 줄만 본다 — 본문이 다른 칸을 언급해도 그 칸으로 기록하지 않는다.
125
+ // 저작 Pod 규칙(OP 10-06)에서 첫 줄은 `대상 경로:` 이고 머리는 둘째 줄이라, 앞의 `대상 경로:` 줄 하나는 건너뛴다.
126
+ const lines = input.feature.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
127
+ const header = /^대상 경로:/u.test(lines[0] ?? '') ? lines[1] : lines[0];
128
+ return header?.match(/^\[(?:OP|MK|TC|UX) 자리 · [^\]]*?체크리스트 칸 ([A-Z][A-Z0-9-]*[A-Z0-9])(?: ·|\])/u)?.[1];
129
+ }
130
+
131
+ export function originalAskFirstLine(feature: string): string {
132
+ return feature.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? '';
133
+ }
134
+
135
+ export function followUpDraftText(firstLine: string, remainings: readonly string[]): string {
136
+ return [
137
+ firstLine,
138
+ ...remainings,
139
+ 'origin/main 에서 시작 · 남은 것만',
140
+ ].join('\n');
141
+ }
142
+
143
+ export function followUpDraftHash(text: string): string {
144
+ return createHash('sha256').update(text).digest('hex').slice(0, 16);
145
+ }
146
+
147
+ /** Chain length of follow-up goals already recorded for this cell, before this one. */
148
+ export function followUpChainDepth(prior: readonly Pick<FollowUpDraftRecord, 'kind' | 'prNumber'>[]): number {
149
+ // 한 PR 이 «기록 → 대기열 확정» 두 줄을 남길 수 있으니 PR 번호로 센다.
150
+ return new Set(prior.filter((record) => record.kind === 'draft' || record.kind === 'op-card-required').map((record) => record.prNumber)).size;
151
+ }
152
+
153
+ /** Seat = the cell's owner. `TC/rel` keeps the seat `TC`. Unknown or absent owners go to OP (ONEDOOR). */
154
+ export function seatForCellOwner(owner: string | undefined): QueueSeat {
155
+ if (!owner) return 'OP';
156
+ try {
157
+ const seat = parseOwner(owner.trim()).seat;
158
+ if ((SEATS as readonly string[]).includes(seat)) return seat as QueueSeat;
159
+ } catch { /* not a seat */ }
160
+ const head = owner.trim().split(/[/\s·]/)[0] ?? '';
161
+ return (SEATS as readonly string[]).includes(head) ? head as QueueSeat : 'OP';
162
+ }
163
+
164
+ function defaultReadDrafts(stateRoot: string, cellId: string): FollowUpDraftRecord[] {
165
+ let text = '';
166
+ try { text = readFileSync(followUpDraftsPath(stateRoot), 'utf8'); } catch { return []; }
167
+ const out: FollowUpDraftRecord[] = [];
168
+ for (const line of text.split('\n')) {
169
+ if (!line.trim()) continue;
170
+ try {
171
+ const record = JSON.parse(line) as FollowUpDraftRecord;
172
+ if (record.cellId === cellId) out.push(record);
173
+ } catch { /* a torn line does not invent a chain */ }
174
+ }
175
+ return out;
176
+ }
177
+
178
+ /** 같은 PR 은 칸과 무관하게 한 번 — 칸 id 가 달리 들어와도 다시 만들지 않는다. */
179
+ function defaultPrRecorded(stateRoot: string, prNumber: number): boolean {
180
+ let text = '';
181
+ try { text = readFileSync(followUpDraftsPath(stateRoot), 'utf8'); } catch { return false; }
182
+ return text.split('\n').some((line) => {
183
+ if (!line.trim()) return false;
184
+ try { return (JSON.parse(line) as FollowUpDraftRecord).prNumber === prNumber; } catch { return false; }
185
+ });
186
+ }
187
+
188
+ function defaultAppendDraft(stateRoot: string, record: FollowUpDraftRecord): void {
189
+ const path = followUpDraftsPath(stateRoot);
190
+ mkdirSync(dirname(path), { recursive: true });
191
+ appendFileSync(path, `${JSON.stringify(record)}\n`);
192
+ }
193
+
194
+ function defaultCellOwner(cellId: string): string | undefined {
195
+ try {
196
+ return listChecklist(devVersion()).items.find((item) => item.id === cellId)?.owner;
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Record one follow-up draft for a merged PR. Same PR is recorded once.
204
+ * Depth above {@link FOLLOW_UP_DEPTH_LIMIT} records «OP 카드 필요» instead of a draft.
205
+ * `live` enqueues; `shadow` (the default) only records. Returns null when nothing remains.
206
+ * Every failure is `debug.log` only — the merge result is the caller's to return unchanged.
207
+ */
208
+ export async function recordFollowUpOnMerge(input: FollowUpMergeInput, seams: FollowUpSeams = {}): Promise<FollowUpDraftRecord | null> {
209
+ const log = seams.log ?? ((category, event, data) => { debug.log(category, event, data); });
210
+ try {
211
+ const remainings = collectRemainings(input);
212
+ if (remainings.length === 0) return null;
213
+ // No injected store and no explicit state root: this is the test process.
214
+ // Do not open the operational ledger or the harness queue.
215
+ // A test process handed the operational root (defaultSeams) is refused the same way.
216
+ const testProcess = process.env.NODE_ENV === 'test' || !!process.env.ELANOUS_TEST_HOME;
217
+ if (!seams.readDrafts && !seams.appendDraft && (!seams.stateRoot || (testProcess && seams.stateRoot === elanousStateRoot()))) return null;
218
+ const stateRoot = seams.stateRoot ?? elanousStateRoot();
219
+ const cellId = followUpCellId(input);
220
+ if (!cellId) {
221
+ log('self-implement.follow-up', 'draft-skipped-no-cell', { prNumber: input.prNumber });
222
+ return null;
223
+ }
224
+ const prior = (seams.readDrafts ?? ((id) => defaultReadDrafts(stateRoot, id)))(cellId);
225
+ if (prior.some((record) => record.prNumber === input.prNumber)) return null;
226
+ if (!seams.readDrafts && defaultPrRecorded(stateRoot, input.prNumber)) return null;
227
+ const depth = followUpChainDepth(prior) + 1;
228
+ const firstLine = originalAskFirstLine(input.feature);
229
+ const mode = seams.mode ?? readFollowUpMode();
230
+ const overDepth = depth > FOLLOW_UP_DEPTH_LIMIT;
231
+ const draft = overDepth ? undefined : followUpDraftText(firstLine, remainings);
232
+ const record: FollowUpDraftRecord = {
233
+ prNumber: input.prNumber,
234
+ cellId,
235
+ depth,
236
+ draftHash: followUpDraftHash(draft ?? `${firstLine}\n${FOLLOW_UP_OP_CARD_REQUIRED}`),
237
+ kind: overDepth ? 'op-card-required' : 'draft',
238
+ originalAskFirstLine: firstLine,
239
+ remainings,
240
+ ...(draft ? { draft } : {}),
241
+ ...(overDepth ? { note: FOLLOW_UP_OP_CARD_REQUIRED } : {}),
242
+ queued: false,
243
+ };
244
+ // 운영 원장은 «같은 PR 확인 ⊕ 첫 기록»을 한 잠금 안에서 한다 — 동시 병합 둘이 초안 둘을 만들지 않게.
245
+ const append = seams.appendDraft ?? ((row: FollowUpDraftRecord) => defaultAppendDraft(stateRoot, row));
246
+ const appendFirst = seams.appendDraft
247
+ ? (row: FollowUpDraftRecord) => { append(row); return true; }
248
+ : (row: FollowUpDraftRecord) => (mkdirSync(dirname(followUpDraftsPath(stateRoot)), { recursive: true }), withFileLockSync(`${followUpDraftsPath(stateRoot)}.lock`, () => {
249
+ if (defaultPrRecorded(stateRoot, row.prNumber)) return false;
250
+ defaultAppendDraft(stateRoot, row);
251
+ return true;
252
+ }));
253
+ if (!overDepth && mode === 'live' && draft) record.seat = seatForCellOwner((seams.cellOwner ?? defaultCellOwner)(cellId));
254
+ // 원장 기록을 먼저 확정한다 — 기록이 실패하면 대기열에 넣지 않는다(같은 PR 한 번 · 원장 밖 연결골 0).
255
+ if (!appendFirst(record)) return null;
256
+ if (!overDepth && mode === 'live' && draft && record.seat) {
257
+ const seat = record.seat;
258
+ if (testProcess && !seams.enqueue) return record;
259
+ try {
260
+ await (seams.enqueue ?? ((item) => addHarnessQueue({ seat: item.seat, say: item.say, idempotencyKey: item.idempotencyKey })))({
261
+ seat,
262
+ say: draft,
263
+ idempotencyKey: `follow-up:${input.prNumber}`,
264
+ });
265
+ record.queued = true;
266
+ } catch (error) {
267
+ // 대기열 넣기가 실패해도 초안은 남긴다 — 같은 PR 은 다시 오지 않으므로 여기서 잃으면 영영 없다.
268
+ // queued=false ⊕ enqueueError 로 원장에 남기고, 재투입은 같은 idempotencyKey 로 사람이·다음 수리 노드가 한다.
269
+ record.enqueueError = (error instanceof Error ? error.message : String(error)).slice(0, 300);
270
+ log('self-implement.follow-up', 'enqueue-failed', { prNumber: input.prNumber, seat, error: record.enqueueError });
271
+ }
272
+ // 대기열 결과를 원장에 한 줄 더 — 이 쓰기의 실패는 대기열 실패와 다른 사건이다.
273
+ try {
274
+ append({ ...record });
275
+ } catch (error) {
276
+ log('self-implement.follow-up', 'ledger-confirm-failed', {
277
+ prNumber: input.prNumber, seat, queued: record.queued, error: error instanceof Error ? error.message : String(error),
278
+ });
279
+ }
280
+ }
281
+ return record;
282
+ } catch (error) {
283
+ log('self-implement.follow-up', 'draft-failed', {
284
+ prNumber: input.prNumber,
285
+ error: error instanceof Error ? error.message : String(error),
286
+ });
287
+ return null;
288
+ }
289
+ }
@@ -5452,23 +5452,29 @@ async function authorGoalWithSupersededRootIntent(
5452
5452
  assembleCompleted = true;
5453
5453
  endGoalAuthorPhase('assemble', assembleStartedAt, authorRunId, deps.onProgress);
5454
5454
  const lintStartedAt = startGoalAuthorPhase('lint', authorRunId, deps.onProgress);
5455
+ let lintPassed = false;
5455
5456
  try {
5456
5457
  assertRequiredBlocks(document);
5458
+ lintPassed = true;
5457
5459
  } finally {
5458
- endGoalAuthorPhase('lint', lintStartedAt, authorRunId, deps.onProgress);
5460
+ try {
5461
+ endGoalAuthorPhase('lint', lintStartedAt, authorRunId, deps.onProgress);
5462
+ } finally {
5463
+ try {
5464
+ observeGoalAuthor('goal-author', 'summary', {
5465
+ authorRunId,
5466
+ elapsedMs: Date.now() - authorStartedAt,
5467
+ inputTokens: enhanceTokens.inputTokens,
5468
+ outputTokens: enhanceTokens.outputTokens,
5469
+ totalTokens: enhanceTokens.inputTokens === null || enhanceTokens.outputTokens === null
5470
+ ? null : enhanceTokens.inputTokens + enhanceTokens.outputTokens,
5471
+ goalChars: [...document].length,
5472
+ sectionChars: authoredGoalSectionChars(document),
5473
+ ...(!lintPassed ? { outcome: 'lint-failed' } : {}),
5474
+ });
5475
+ } catch { /* observation is fail-soft */ }
5476
+ }
5459
5477
  }
5460
- try {
5461
- observeGoalAuthor('goal-author', 'summary', {
5462
- authorRunId,
5463
- elapsedMs: Date.now() - authorStartedAt,
5464
- inputTokens: enhanceTokens.inputTokens,
5465
- outputTokens: enhanceTokens.outputTokens,
5466
- totalTokens: enhanceTokens.inputTokens === null || enhanceTokens.outputTokens === null
5467
- ? null : enhanceTokens.inputTokens + enhanceTokens.outputTokens,
5468
- goalChars: [...document].length,
5469
- sectionChars: authoredGoalSectionChars(document),
5470
- });
5471
- } catch { /* observation is fail-soft */ }
5472
5478
  return { document, facts, grounded: !groundingError && hasEvidence, authorRunId };
5473
5479
  } finally {
5474
5480
  endAssembleSubphase();