elanous 0.2.18 → 0.2.20

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-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `elanous harness salvage <runId...> [--json]` — 런마다 «PR #n | 수확 가지 <branch> (+a/-b · N files) | 없음 | 모름(<이유>)».
3
+ * SALVAGE-VISIBLE(0.2.19) — 판정은 `src/self-implement/salvage-branches.ts`, 여기는 배선·출력·관측만.
4
+ */
5
+ import type { Command } from 'commander';
6
+ import { debug } from '../debug/log.js';
7
+ import { normalizeRunId } from './harness-space.js';
8
+ import { loadFederatedRunLedger } from '../self-implement/run-ledger.js';
9
+ import { getUserConfig } from '../user-config.js';
10
+ import { resolveRepositoryName } from './repository-name.js';
11
+ import {
12
+ classifySalvageRetention,
13
+ formatSalvageRetention,
14
+ listDatedSalvageRefs,
15
+ listOpenPrRefs,
16
+ type DatedSalvageRef,
17
+ type SalvageReferenceSources,
18
+ type SalvageRetentionMode,
19
+ type SalvageRetentionReport,
20
+ } from '../self-implement/salvage-retention.js';
21
+ import {
22
+ decideRunSalvage,
23
+ findRunSalvageBranches,
24
+ gitSalvageRunner,
25
+ listRemoteSalvageRefs,
26
+ prStateFromLedger,
27
+ type RunPrState,
28
+ type RunSalvageVerdict,
29
+ type SalvageGitRunner,
30
+ } from '../self-implement/salvage-branches.js';
31
+
32
+ export interface HarnessSalvageDeps {
33
+ git?: SalvageGitRunner;
34
+ readPr?: (runId: string) => RunPrState;
35
+ print?: (line: string) => void;
36
+ }
37
+
38
+ function defaultReadPr(runId: string): RunPrState {
39
+ try {
40
+ return prStateFromLedger(loadFederatedRunLedger(runId, { includeTest: true }));
41
+ } catch (error) {
42
+ return { status: 'unknown', reason: `원장 읽기 실패 · ${error instanceof Error ? error.message : String(error)}` };
43
+ }
44
+ }
45
+
46
+ /** 여러 런을 ls-remote «한 번»으로 판정한다. PR 이 있는 런은 git 을 더 부르지 않는다. */
47
+ export function runHarnessSalvage(runIds: readonly string[], deps: HarnessSalvageDeps = {}): RunSalvageVerdict[] {
48
+ const git = deps.git ?? gitSalvageRunner(process.cwd());
49
+ const readPr = deps.readPr ?? defaultReadPr;
50
+ let listing: ReturnType<typeof listRemoteSalvageRefs> | undefined;
51
+ const verdicts: RunSalvageVerdict[] = [];
52
+ for (const raw of runIds) {
53
+ const runId = normalizeRunId(raw);
54
+ const pr = readPr(runId);
55
+ let verdict: RunSalvageVerdict;
56
+ if (pr.status === 'pr') {
57
+ verdict = decideRunSalvage(runId, pr, { status: 'not-checked' });
58
+ } else {
59
+ listing ??= listRemoteSalvageRefs(git);
60
+ verdict = decideRunSalvage(runId, pr, findRunSalvageBranches(runId, git, { listing }));
61
+ }
62
+ const top = verdict.salvage.status === 'found' ? verdict.salvage.branches[0] : undefined;
63
+ debug.log('harness.salvage', 'visible', {
64
+ runId,
65
+ outcome: verdict.outcome,
66
+ branch: top?.branch ?? null,
67
+ files: top?.diffstat.files ?? null,
68
+ ...(verdict.pr.status === 'pr' ? { prNumber: verdict.pr.number } : {}),
69
+ ...(verdict.salvage.status === 'unknown' ? { reason: verdict.salvage.reason } : {}),
70
+ ...(verdict.pr.status === 'unknown' ? { prReason: verdict.pr.reason } : {}),
71
+ });
72
+ verdicts.push(verdict);
73
+ }
74
+ return verdicts;
75
+ }
76
+
77
+ export function installHarnessSalvageCommand(harnessCmd: Command, deps: HarnessSalvageDeps = {}): Command {
78
+ return harnessCmd
79
+ .command('salvage')
80
+ .description('런 → PR | 수확 가지(salvage/*) | 없음 | 모름 — Pod 런이 멈추며 남긴 가지를 보인다')
81
+ .argument('<runIds...>', '런 id (run-xxxxxxxx-…)')
82
+ .option('--json', '런마다 { runId, outcome, pr, salvage } 를 JSON 배열로')
83
+ .action((runIds: string[], opts: { json?: boolean }) => {
84
+ const print = deps.print ?? ((line: string) => console.log(line));
85
+ const verdicts = runHarnessSalvage(runIds, deps);
86
+ if (opts.json) print(JSON.stringify(verdicts.map(({ line: _line, ...rest }) => rest)));
87
+ else for (const verdict of verdicts) print(verdict.line);
88
+ if (verdicts.some((verdict) => verdict.outcome === 'unknown')) process.exitCode = 1;
89
+ });
90
+ }
91
+
92
+ export interface HarnessSalvageRetentionDeps {
93
+ repository?: () => string;
94
+ listRefs?: (repository: string) => DatedSalvageRef[];
95
+ listOpenPrs?: (repository: string) => SalvageReferenceSources['openPrs'];
96
+ listCards?: () => SalvageReferenceSources['cards'];
97
+ mode?: () => SalvageRetentionMode;
98
+ now?: () => Date;
99
+ print?: (line: string) => void;
100
+ }
101
+
102
+ async function defaultCards(): Promise<SalvageReferenceSources['cards']> {
103
+ const { readTaskAgentState, taskAgentStatePath } = await import('../task-agent/task-hand.js');
104
+ const tasks = readTaskAgentState<{ tasks?: Record<string, { id: string; runId?: string }> }>(taskAgentStatePath()).tasks ?? {};
105
+ return Object.values(tasks).map((card) => ({ id: card.id, ...(card.runId ? { runId: card.runId } : {}), text: JSON.stringify(card) }));
106
+ }
107
+
108
+ /** SALVAGE-RETENTION — 그림자 보고(지우지 않는다). 조회가 하나라도 실패하면 수를 내지 않고 실패로 끝난다(⛔ 부분 수를 «전체»로 안 읽는다). */
109
+ export async function runHarnessSalvageRetention(deps: HarnessSalvageRetentionDeps = {}): Promise<SalvageRetentionReport> {
110
+ const repository = (deps.repository ?? (() => resolveRepositoryName({})))();
111
+ let mode: SalvageRetentionMode = 'shadow';
112
+ try {
113
+ mode = deps.mode ? deps.mode() : (getUserConfig().tools?.selfImplement?.salvageRetention?.mode ?? 'shadow');
114
+ } catch { mode = 'shadow'; }
115
+ const refs = (deps.listRefs ?? ((repo: string) => listDatedSalvageRefs(repo)))(repository);
116
+ const openPrs = (deps.listOpenPrs ?? ((repo: string) => listOpenPrRefs(repo)))(repository);
117
+ const cards = deps.listCards ? deps.listCards() : await defaultCards();
118
+ const report = classifySalvageRetention({ refs, sources: { openPrs, cards }, now: (deps.now ?? (() => new Date()))(), mode });
119
+ debug.log('self-implement.salvage-retention', 'shadow-report', { repository, ...report, openPrs: openPrs.length, cards: cards.length });
120
+ return report;
121
+ }
122
+
123
+ export function installHarnessSalvageRetentionCommand(harnessCmd: Command, deps: HarnessSalvageRetentionDeps = {}): Command {
124
+ return harnessCmd
125
+ .command('salvage-retention')
126
+ .description('원격 salvage/* 가지 보존 규칙(14일 초과 ⊕ 열린 PR·카드 참조 0) — 그림자: 지울 후보 수만 보고하고 지우지 않는다')
127
+ .option('--json', '{ mode, total, older, referenced, candidates, unknownAge, sample, deleted } JSON')
128
+ .action(async (opts: { json?: boolean }) => {
129
+ const print = deps.print ?? ((line: string) => console.log(line));
130
+ try {
131
+ const report = await runHarnessSalvageRetention(deps);
132
+ print(opts.json ? JSON.stringify(report) : formatSalvageRetention(report));
133
+ } catch (error) {
134
+ const reason = error instanceof Error ? error.message : String(error);
135
+ debug.log('self-implement.salvage-retention', 'shadow-report-failed', { error: reason });
136
+ print(`수확 가지 보존: 못 잼 — ${reason}`);
137
+ process.exitCode = 1;
138
+ }
139
+ });
140
+ }
@@ -2,6 +2,7 @@ import { createWorktree, type CreateWorktreeResult } from '../git-fs/worktree.js
2
2
  import { runGitCommand } from '../git-fs/runner.js';
3
3
  import { configuredWorktreeRoot } from '../user-config.js';
4
4
  import { debug } from '../debug/log.js';
5
+ import { join, resolve } from 'node:path';
5
6
 
6
7
  const OWNER_CONFIG_KEY = 'elanous.harness.owner';
7
8
  const COMMAND_CONFIG_KEY = 'elanous.harness.command';
@@ -87,8 +88,75 @@ function restoreWorktreeProvenance(worktreePath: string, snapshot: WorktreeConfi
87
88
  }
88
89
  }
89
90
 
91
+ /** Keys that must leave the shared config once `extensions.worktreeConfig` is on (`git help worktree` §CONFIGURATION FILE).
92
+ * `core.bare=false` is harmless when shared (linked trees default to non-bare), so only a true value moves. */
93
+ const MAIN_TREE_ONLY_CONFIG_KEYS = ['core.bare', 'core.worktree'] as const;
94
+
95
+ /** Moves `core.bare=true` / `core.worktree` from `$GIT_COMMON_DIR/config` to `$GIT_COMMON_DIR/config.worktree`.
96
+ * Runs every time (also when the extension is already on) and throws before the caller enables the extension.
97
+ * Incident 10-07 (HQ-WORKTREE-CONFIG): a shared `core.bare=true` made every linked worktree of a bare repo unusable. */
98
+ /** Values are moved byte-for-byte (a `core.worktree` path may carry edge whitespace), so read them NUL-terminated. */
99
+ function nulTerminatedValue(stdout: unknown, key: string): string {
100
+ const text = String(stdout ?? '');
101
+ if (!text.endsWith('\0')) throw new Error(`harness worktree ${key} read failed — git config emitted an unterminated value`);
102
+ return text.slice(0, -1);
103
+ }
104
+
105
+ function moveMainTreeOnlyConfig(worktreePath: string): void {
106
+ const common = gitAt(worktreePath, ['rev-parse', '--git-common-dir']);
107
+ const commonOut = String(common.stdout ?? '').trim();
108
+ if (common.status !== 0 || !commonOut) {
109
+ throw new Error(`harness worktree common git dir lookup failed — ${gitConfigFailure(common)}`);
110
+ }
111
+ const commonDir = resolve(worktreePath, commonOut);
112
+ const shared = join(commonDir, 'config');
113
+ const perTree = join(commonDir, 'config.worktree');
114
+ // `null` = absent. Typed reads judge meaning (`yes`/`TRUE` are true); raw reads carry the bytes that move.
115
+ const readValue = (file: string, key: string, typed: boolean): string | null => {
116
+ const read = gitAt(worktreePath, ['config', '--file', file, ...(typed ? ['--type=bool'] : []), '--null', '--get', key]);
117
+ if (read.status === 1) return null;
118
+ if (read.status !== 0) throw new Error(`harness worktree ${key} read failed (${file}) — ${gitConfigFailure(read)}`);
119
+ return nulTerminatedValue(read.stdout, key);
120
+ };
121
+ for (const key of MAIN_TREE_ONLY_CONFIG_KEYS) {
122
+ const typed = key === 'core.bare';
123
+ const meaning = readValue(shared, key, typed);
124
+ if (meaning === null) continue;
125
+ if (typed && meaning !== 'true') continue;
126
+ const raw = typed ? readValue(shared, key, false) : meaning;
127
+ if (raw === null) throw new Error(`harness worktree shared ${key} vanished during the move`);
128
+ // A valueless `bare` means true; writing its empty raw form would mean false.
129
+ const value = typed && raw === '' ? 'true' : raw;
130
+ // A conflicting per-tree value would flip the main tree once the extension is on — refuse instead of guessing.
131
+ // Every per-tree value must agree (not just the last one), so a mixed list also fails closed.
132
+ const existingRead = gitAt(worktreePath, ['config', '--file', perTree, ...(typed ? ['--type=bool'] : []), '--null', '--get-all', key]);
133
+ if (existingRead.status !== 0 && existingRead.status !== 1) {
134
+ throw new Error(`harness worktree ${key} read failed (${perTree}) — ${gitConfigFailure(existingRead)}`);
135
+ }
136
+ const existingValues = existingRead.status === 0 ? nulTerminatedValue(existingRead.stdout, key).split('\0') : [];
137
+ if (existingValues.some((existing) => existing !== meaning)) {
138
+ throw new Error(`harness worktree ${key} conflict — ${shared} has ${JSON.stringify(meaning)} but ${perTree} has ${JSON.stringify(existingValues)}; resolve by hand before enabling extensions.worktreeConfig`);
139
+ }
140
+ const existingMeaning = existingValues.length > 0 ? meaning : null;
141
+ if (existingMeaning === null) {
142
+ const write = gitAt(worktreePath, ['config', '--file', perTree, key, value]);
143
+ if (write.status !== 0) throw new Error(`harness worktree ${key} move failed — ${gitConfigFailure(write)}`);
144
+ }
145
+ const unset = gitAt(worktreePath, ['config', '--file', shared, '--unset-all', key]);
146
+ if (unset.status !== 0) throw new Error(`harness worktree shared ${key} unset failed — ${gitConfigFailure(unset)}`);
147
+ debug.log('harness.worktree', 'core-bare-moved', {
148
+ key,
149
+ value,
150
+ commonDir,
151
+ perTreeHadValue: existingMeaning !== null,
152
+ worktreePath,
153
+ }, { level: 'warn' });
154
+ }
155
+ }
156
+
90
157
  /** Records the shared worktree-scoped provenance used by both manual and self-implement creation paths. */
91
158
  export function recordHarnessWorktreeProvenance(worktreePath: string, provenance: HarnessWorktreeProvenance): void {
159
+ moveMainTreeOnlyConfig(worktreePath);
92
160
  const enable = gitAt(worktreePath, ['config', 'extensions.worktreeConfig', 'true']);
93
161
  if (enable.status !== 0) {
94
162
  throw new Error(`harness worktree owner declaration failed — ${gitConfigFailure(enable)}`);
@@ -1,19 +1,7 @@
1
- import { realpathSync } from 'node:fs';
2
- import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
1
+ import { isAbsolute, relative, sep } from 'node:path';
3
2
  import { debug } from '../debug/log.js';
4
3
  import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
5
-
6
- function physicalPath(path: string): string {
7
- const absolute = resolve(path);
8
- try {
9
- return realpathSync(absolute);
10
- } catch (error) {
11
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
12
- const parent = dirname(absolute);
13
- if (parent === absolute) throw error;
14
- return resolve(physicalPath(parent), relative(parent, absolute));
15
- }
16
- }
4
+ import { isInsideOpsRoot, physicalPath, testProcessSignal } from '../instance/test-write-guard.js';
17
5
 
18
6
  /** Refuse a test's write to the production instance, including an explicitly supplied ledger directory. */
19
7
  export function refuseProductionLedgerWriteInTest(
@@ -23,11 +11,13 @@ export function refuseProductionLedgerWriteInTest(
23
11
  productionRoot = prodInstanceRoot(),
24
12
  instanceRoot = effectiveInstanceRoot(),
25
13
  ): boolean {
26
- if (env.NODE_ENV !== 'test' && !env.ELANOUS_TEST_HOME) return false;
14
+ if (!testProcessSignal(env, env === process.env ? undefined : '')) return false;
27
15
  const target = physicalPath(root);
28
16
  const production = physicalPath(productionRoot);
29
17
  const withinProduction = relative(production, target);
30
- if (withinProduction === '..' || withinProduction.startsWith(`..${sep}`) || isAbsolute(withinProduction)) return false;
18
+ const outsideHomeRoot = withinProduction === '..' || withinProduction.startsWith(`..${sep}`) || isAbsolute(withinProduction);
19
+ // The account's real ~/.elanous is production too when a runner redirected HOME (TEST-PROD-LEAK).
20
+ if (outsideHomeRoot && !isInsideOpsRoot(root)) return false;
31
21
  try {
32
22
  debug.log('harness.incidents', 'write-refused', { store, root: target, effectiveInstanceRoot: instanceRoot });
33
23
  } catch { /* Observation cannot reopen a closed write boundary. */ }
@@ -0,0 +1,63 @@
1
+ // LOOPCHECK-FENCE (0.2.20): an hq-fence cron execution is one process, not a loop.
2
+ // Each wrapped run leaves its rc in a per-role streak file; the wrapper's owner alert
3
+ // raises a seat request only when the same role failed in consecutive runs, keyed per
4
+ // role so the seat request journal holds at most one `loopcheck:hq-fence:<role>` per day.
5
+ // No loops/ import here: hq.ts records through this module and loops/registry imports hq.ts.
6
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { Database } from 'bun:sqlite';
9
+ import { debug } from '../debug/log.js';
10
+
11
+ /** The role is the file name; `lastAt` is shown in the owner alert so TC sees when the streak last grew. */
12
+ export interface FenceOutcome { consecutiveFailures: number; lastRc: number; lastAt: string }
13
+
14
+ /** Two failures in a row: one rc=1 from a job that recovers on the next fire is not a fence fault. */
15
+ export const FENCE_FAILURE_STREAK = 2;
16
+
17
+ const safeRole = (role: string): string => role.replace(/[^\w.-]/g, '_') || 'unknown';
18
+
19
+ export function fenceOutcomePath(dir: string, role: string): string {
20
+ return join(dir, `${safeRole(role)}.json`);
21
+ }
22
+
23
+ /** Default directory next to the host-local lease view (`<config>/hq/fence-outcomes`). */
24
+ export function fenceOutcomeDir(localPath: string): string {
25
+ return join(dirname(localPath), 'fence-outcomes');
26
+ }
27
+
28
+ export function readFenceOutcome(dir: string, role: string): FenceOutcome | null {
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(fenceOutcomePath(dir, role), 'utf8')) as Partial<FenceOutcome>;
31
+ return typeof parsed.consecutiveFailures === 'number' ? parsed as FenceOutcome : null;
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
34
+ debug.log('hq.fence-outcome', 'read-failed', { role, error: String(error) });
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** rc 0 resets the streak; any other rc extends it. A skipped (not-holder) run is not recorded.
40
+ * Overlapping runs of one role serialize the read-modify-write on an SQLite lock (released even on SIGKILL). */
41
+ export function recordFenceOutcome(dir: string, role: string, rc: number, now: Date = new Date()): FenceOutcome {
42
+ const path = fenceOutcomePath(dir, role);
43
+ mkdirSync(dirname(path), { recursive: true });
44
+ const lock = new Database(`${path}.lock.sqlite`);
45
+ try {
46
+ lock.exec('PRAGMA busy_timeout = 10000');
47
+ lock.exec('BEGIN EXCLUSIVE');
48
+ try {
49
+ const previous = readFenceOutcome(dir, role);
50
+ const outcome: FenceOutcome = { consecutiveFailures: rc === 0 ? 0 : (previous?.consecutiveFailures ?? 0) + 1, lastRc: rc, lastAt: now.toISOString() };
51
+ const tmp = `${path}.${process.pid}.tmp`;
52
+ writeFileSync(tmp, JSON.stringify(outcome));
53
+ renameSync(tmp, path);
54
+ lock.exec('COMMIT');
55
+ return outcome;
56
+ } catch (error) { lock.exec('ROLLBACK'); throw error; }
57
+ } finally { lock.close(); }
58
+ }
59
+
60
+ /** `hq-fence: fence failed (rc=1, role=seat-loop)` → `seat-loop`; other wrapper reasons have no role. */
61
+ export function fenceAlertRole(reason: string): string | null {
62
+ return /\brole=([\w.-]+)/.exec(reason)?.[1] ?? null;
63
+ }
@@ -3,6 +3,9 @@ import { chmodSync, constants, copyFileSync, lstatSync, mkdirSync, mkdtempSync,
3
3
  import { dirname, isAbsolute, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { createPatch } from 'diff';
6
+ import { debug } from '../debug/log.js';
7
+ import { notifyOwners } from '../loops/checker.js';
8
+ import { FENCE_FAILURE_STREAK, fenceAlertRole, readFenceOutcome } from './fence-outcomes.js';
6
9
 
7
10
  const quote = (value: string): string => `'${value.replaceAll("'", "'\"'\"'")}'`;
8
11
 
@@ -121,3 +124,33 @@ export function installHqFenceWrapper(configDir: string, body: string, yes: bool
121
124
  } finally { rmSync(tempDir, { recursive: true, force: true }); }
122
125
  return { path, preview, ...(backup ? { backup } : {}) };
123
126
  }
127
+
128
+ export interface FenceAlertDeps { root: string; outcomeDir: string; now?: Date }
129
+
130
+ /**
131
+ * The wrapper's owner alert. Returns the number of seat requests queued (0 or 1).
132
+ * - `fence failed (rc, role)`: queued only when that role's streak reached FENCE_FAILURE_STREAK
133
+ * (an unknown streak is not read as «first failure» — it is queued, still capped per role/day).
134
+ * - Any other wrapper failure (entry missing, bun missing, role missing): queued under `hq-fence:wrapper`.
135
+ */
136
+ export function hqFenceAlert(reason: string, deps: FenceAlertDeps): number {
137
+ const now = deps.now ?? new Date();
138
+ const fenceFailure = /fence failed \(rc=/.test(reason);
139
+ const role = fenceAlertRole(reason);
140
+ let streak: number | null = null;
141
+ let lastAt: string | undefined;
142
+ if (fenceFailure && role) {
143
+ const outcome = readFenceOutcome(deps.outcomeDir, role);
144
+ streak = outcome?.consecutiveFailures ?? null;
145
+ lastAt = outcome?.lastAt;
146
+ if (streak !== null && streak < FENCE_FAILURE_STREAK) {
147
+ debug.log('hq.fence-wrapper', 'alert-suppressed', { role, streak, threshold: FENCE_FAILURE_STREAK, reason });
148
+ return 0;
149
+ }
150
+ }
151
+ const id = `hq-fence:${fenceFailure && role ? role : 'wrapper'}`;
152
+ const text = streak !== null ? `${reason} · ${streak} consecutive runs (last ${lastAt ?? 'unknown'})` : reason;
153
+ const added = notifyOwners([{ id, owner: 'TC', enabled: true, registered: true, state: 'failing', reason: text }], { root: deps.root, now });
154
+ debug.log('hq.fence-wrapper', added ? 'alert-queued' : 'alert-deduplicated', { id, role, streak, reason });
155
+ return added;
156
+ }
package/src/hq/hq.ts CHANGED
@@ -10,6 +10,7 @@ import { debug } from '../debug/log.js';
10
10
  import { DEFAULT_NEXUS_HTTP_PORT } from '../nexus/default-port.js';
11
11
  import { getElanousConfigDir } from '../elanous-config-dir.js';
12
12
  import { getUserConfig, type HqConfig, type HqFenceRole } from '../user-config.js';
13
+ import { fenceOutcomeDir, recordFenceOutcome } from './fence-outcomes.js';
13
14
  import { promoteHq, proposeHqPromotion, writeOpSeatRequest } from './promote.js';
14
15
  import type { HqOpRequestWriter, PromoteResult } from './promote.js';
15
16
  import {
@@ -42,6 +43,8 @@ export interface HqDeps {
42
43
  /** Holder/standby views and the arbiter's check use this tailnet probe — never ssh (OP 10-04 09:04). */
43
44
  probe?: HostProbe;
44
45
  localPath?: string;
46
+ /** Per-role streak files of wrapped-run rc (LOOPCHECK-FENCE · default next to localPath). */
47
+ outcomeDir?: string;
45
48
  /** Host-local identity, deliberately outside the replicated config/state directory. */
46
49
  hostPath?: string;
47
50
  /** Seen-generation marker (default hq.seenGenerationFile · ~/.elanous-hq/seen-generation). */
@@ -108,7 +111,7 @@ function resolve(deps: HqDeps = {}): Resolved {
108
111
  ssh,
109
112
  probe: deps.probe ?? tailnetProbe(config),
110
113
  localPath: deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'),
111
- seenPath: deps.seenPath ?? config.seenGenerationFile ?? (deps.localPath ? `${deps.localPath}.seen-generation` : join(homedir(), '.elanous-hq', 'seen-generation')),
114
+ seenPath: deps.seenPath ?? (deps.localPath && !config.seenGenerationFile ? `${deps.localPath}.seen-generation` : hqSeenGenerationPath(config)),
112
115
  now: deps.now ?? (() => Math.floor(Date.now() / 1000)),
113
116
  log,
114
117
  };
@@ -161,6 +164,11 @@ function writeLocal(path: string, state: LocalHqState): void {
161
164
  }
162
165
 
163
166
  /** Highest lease generation this host ever observed (null = never). */
167
+ /** The seen-generation marker this host keeps (hq.seenGenerationFile · default ~/.elanous-hq/seen-generation). */
168
+ export function hqSeenGenerationPath(config: Pick<HqConfig, 'seenGenerationFile'>): string {
169
+ return config.seenGenerationFile ?? join(homedir(), '.elanous-hq', 'seen-generation');
170
+ }
171
+
164
172
  export function readSeenGeneration(path: string): number | null {
165
173
  try {
166
174
  if (!existsSync(path)) return null;
@@ -386,5 +394,12 @@ export function hqFenceRun(role: HqFenceRole, command: string[], deps: HqDeps =
386
394
  const [cmd, ...args] = command;
387
395
  if (!cmd) throw new Error('hq fence: command required after --');
388
396
  const r = spawnSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ELANOUS_HQ_GENERATION: String(decided.generation ?? '') } });
389
- return r.status ?? 1;
397
+ const rc = r.status ?? 1;
398
+ // LOOPCHECK-FENCE: the wrapper's alert reads this streak, so a lone failure is not a seat request.
399
+ try {
400
+ const dir = deps.outcomeDir ?? fenceOutcomeDir(deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'));
401
+ const outcome = recordFenceOutcome(dir, role, rc, new Date(deps.now?.() ?? Date.now()));
402
+ if (rc !== 0) debug.log('hq.fence', 'run-failed', { role, rc, consecutiveFailures: outcome.consecutiveFailures });
403
+ } catch (error) { try { debug.log('hq.fence', 'outcome-record-failed', { role, rc, error: String(error) }); } catch { /* fail-soft */ } }
404
+ return rc;
390
405
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import type { SelfDevRunParticipant, SelfDevRunState } from './self-dev/run-stor
7
7
  import { applyConfigDirFlagFromArgv } from './cli/config-dir-flag.js';
8
8
  import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
9
9
  import { registerDocsCommands } from './cli/docs-cli.js';
10
+ import { registerRetroCommands } from './cli/retro-cli.js';
10
11
  import { registerA2ACommands } from './cli/a2a-cli.js';
11
12
  import { registerPluginCommands } from './cli/plugin-cli.js';
12
13
  import { registerMakeCommand } from './cli/make-cli.js';
@@ -51,6 +52,7 @@ import { registerRoleCommands } from './cli/role-cli.js';
51
52
  import { registerMachineCommands } from './cli/machine-cli.js';
52
53
  import { registerAgentEnvCommands } from './cli/agent-env-cli.js';
53
54
  import { registerScheduleCommands } from './cli/schedule-cli.js';
55
+ import { registerGuardianCommands } from './cli/guardian-cli.js';
54
56
  export { runSchedule, scheduleCreatePlan } from './cli/schedule-cli.js';
55
57
  export type { ScheduleDispatch } from './cli/schedule-cli.js';
56
58
  import { registerReviewLoopOptions, buildReviewLoopOpts } from './agent-mission/review-loop-cli.js';
@@ -75,6 +77,7 @@ import { registerIntakeCommands } from './cli/intake-cli.js';
75
77
  import { registerTasksCommands } from './cli/tasks-cli.js';
76
78
  import { registerBriefCommands } from './cli/brief-cli.js';
77
79
  import { registerNotifyCommand } from './cli/notify-cli.js';
80
+ import { registerAwayCommand } from './cli/away-cli.js';
78
81
  import { registerLabelsCommands } from './cli/labels-cli.js';
79
82
  import { registerMsgCommands } from './cli/msg-cli.js';
80
83
  import { registerDoctorCommand } from './cli/doctor-cli.js';
@@ -215,6 +218,24 @@ export { cliVersion, setInstallMetadataRootForTesting };
215
218
  export const program = new Command();
216
219
  program.enablePositionalOptions();
217
220
 
221
+ program.command('preview <target>')
222
+ .description('Isolated PR/branch-head TUI + PWA preview; publish the TUI-COMFORT friction table to a PR')
223
+ .option('--scenario <id>', 'TUI-COMFORT scenario IDs (comma-separated; default S2)', 'S2')
224
+ .option('--flag <path>', 'Enable an existing config feature flag in main-based preview (repeatable)', (value: string, all: string[]) => [...all, value], [] as string[])
225
+ .option('--bundle-pr <number>', 'Merge PR head into isolated main preview (repeatable; no flags)', (value: string, all: number[]) => [...all, Number(value)], [] as number[])
226
+ .option('--comment-pr <number>', 'PR to receive a branch/main preview report', (value: string) => Number(value))
227
+ .action(async (target: string, opts: { scenario: string; flag: string[]; bundlePr: number[]; commentPr?: number }) => {
228
+ try {
229
+ const { runPrPreview } = await import('./self-dev/pr-preview.js');
230
+ const result = await runPrPreview({ target, repoRoot: process.cwd(), scenario: opts.scenario, flags: opts.flag, bundlePrs: opts.bundlePr, ...(opts.commentPr === undefined ? {} : { commentPr: opts.commentPr }) });
231
+ console.log(`preview ${result.status} · ${result.head} · PR #${result.prNumber} · comment posted`);
232
+ if (result.status !== 'passed') process.exitCode = 1;
233
+ } catch (error) {
234
+ console.error(error instanceof Error ? error.message : String(error));
235
+ process.exitCode = 1;
236
+ }
237
+ });
238
+
218
239
  export { SELF_SEND_RECENT_FRAME_WINDOW_MS, SELF_SEND_STALE_HEARTBEAT_MS, formatSelfSendCandidateDisplay } from './harness/self-send-decide.js';
219
240
  export type { SelfSendCandidate, SelfSendCandidateDisplay } from './harness/self-send-decide.js';
220
241
  import { decideSelfSend, type SelfSendCandidate } from './harness/self-send-decide.js';
@@ -891,6 +912,7 @@ registerResearchCommand(program);
891
912
  registerReleaseCommands(program);
892
913
  registerKnowCommand(program);
893
914
  registerNotifyCommand(program);
915
+ registerAwayCommand(program);
894
916
  registerFreezeCommands(program);
895
917
  registerFlowCommands(program);
896
918
  // HQ-HB · HQ-FENCE (10-04): one writer across mbp · node-b · cloud-vm — lease on the arbiter, 2-of-3 quorum, generation fencing.
@@ -1039,10 +1061,12 @@ fenceWrapperCmd.command('install').description('래퍼 제안 또는 백업 후
1039
1061
  fenceWrapperCmd.command('alert <reason>').description('HQ 울타리 실패를 기존 루프 소유자 요청 원장에 적재')
1040
1062
  .action(async (reason: string) => {
1041
1063
  try {
1042
- const { notifyOwners } = await import('./loops/checker.js');
1064
+ // LOOPCHECK-FENCE: keyed per role (not per pid) and only after consecutive failures of that role.
1065
+ const { fenceOutcomeDir } = await import('./hq/fence-outcomes.js');
1066
+ const { hqFenceAlert } = await import('./hq/fence-wrapper.js');
1043
1067
  const { getElanousConfigDir } = await import('./elanous-config-dir.js');
1044
- notifyOwners([{ id: `hq-fence:${Date.now()}:${process.pid}`, owner: 'TC', enabled: true, registered: true,
1045
- state: 'failing', reason }], { root: getElanousConfigDir() });
1068
+ const root = getElanousConfigDir();
1069
+ hqFenceAlert(reason, { root, outcomeDir: fenceOutcomeDir(_joinPath(root, 'hq', 'local.json')) });
1046
1070
  } catch (error) { console.error(`hq fence-wrapper alert: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1047
1071
  });
1048
1072
  hqCmd.command('fence-audit').description('운영 crontab 의 HQ 울타리 밖 쓰기 잡 목록과 감싼 줄 제안(읽기 전용)')
@@ -1885,6 +1909,7 @@ program
1885
1909
  });
1886
1910
 
1887
1911
  registerAutopilotCommands(program);
1912
+ registerRetroCommands(program);
1888
1913
 
1889
1914
  // ── memory ──
1890
1915
  const memCmd = program.command('memory').description('Persistent memories injected into every chat turn (user / feedback / project / reference)');
@@ -5979,6 +6004,7 @@ program
5979
6004
  });
5980
6005
 
5981
6006
  registerScheduleCommands(program);
6007
+ registerGuardianCommands(program);
5982
6008
 
5983
6009
  /** `elanous decide*` 공통 — 보낼 곳과 자격을 정한다(설정 decide.endpoint > ELANOUS_JEV_ENDPOINT > Typesafe). 없으면 안내하고 rc 2. */
5984
6010
  async function loadJevAccessOrExit(): Promise<import('./decide/jev.js').JevAccess> {
@@ -7173,7 +7199,7 @@ const selfDevCmd = program
7173
7199
  const members = parsePodPool(devAskSubstrate.pool!);
7174
7200
  const host = podPoolHostLease(members);
7175
7201
  while (!authoringLease) {
7176
- const status = recommendConcurrency(measurePoolLease(members), { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
7202
+ const status = recommendConcurrency(measurePoolLease(members, { measureUsage: true }), { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
7177
7203
  if (status.recommended !== null && status.recommended > 0) {
7178
7204
  const { leaseHasPendingPod } = await import('./pod-lease/host-lease.js');
7179
7205
  const pendingJobs = status.pendingJobs ?? [];
@@ -7328,7 +7354,23 @@ const selfDevCmd = program
7328
7354
  if (hasExplicitReviewerContext && loaded.failed.length > 0) throw new DevPipelineError(contextStatus);
7329
7355
  if (loaded.items.length > 0) spec = { ...spec, reviewerContext: loaded.items };
7330
7356
  }
7331
- if (devOpts.worktree === true) {
7357
+ // ⭐ TUI-ONE-WORD-LAUNCH — 명시 격리 우주에서 `--cwd`·`--worktree` 둘 다 없으면 «자동» 갈래로
7358
+ // 같은 `prepareDevWorktree` 를 탄다(새 생성 경로 없음). 명시 `--cwd` 는 그대로 이긴다.
7359
+ const autoWorktreeDecision = await (async () => {
7360
+ if (!devOpts.elanous || devOpts.worktree === true || devOpts.cwd !== undefined) {
7361
+ return devCli.decideAutoWorktree({ explicitlyIsolated: false, ...devOpts });
7362
+ }
7363
+ const [{ resolveCurrentInstance }, { isExplicitlyIsolated }] = await Promise.all([
7364
+ import('./instance/current.js'),
7365
+ import('./cli/pty-drive-cli.js'),
7366
+ ]);
7367
+ return devCli.decideAutoWorktree({
7368
+ explicitlyIsolated: isExplicitlyIsolated(resolveCurrentInstance(), devOpts.isolatedRoot),
7369
+ ...devOpts,
7370
+ });
7371
+ })();
7372
+ debug.log('dev-pipeline', 'auto-worktree-decision', { runId: devRunId, decision: autoWorktreeDecision });
7373
+ if (autoWorktreeDecision === 'requested' || autoWorktreeDecision === 'auto') {
7332
7374
  const { prepareDevWorktree, renderPreparedDevWorktree } = await import('./harness/harness-worktree-auto.js');
7333
7375
  const goal = 'file' in input
7334
7376
  ? await (async () => {
@@ -7338,13 +7380,25 @@ const selfDevCmd = program
7338
7380
  return { id: goalId, file: input.file };
7339
7381
  })()
7340
7382
  : undefined;
7341
- const prepared = prepareDevWorktree(process.cwd(), devRunId, invokedAsDrive ? 'drive' : 'dev', goal);
7383
+ const prepared = (() => {
7384
+ try {
7385
+ return prepareDevWorktree(process.cwd(), devRunId, invokedAsDrive ? 'drive' : 'dev', goal);
7386
+ } catch (error) {
7387
+ if (autoWorktreeDecision !== 'auto') throw error;
7388
+ debug.log('dev-pipeline', 'auto-worktree-failed', { runId: devRunId, cwd: process.cwd(), msg: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
7389
+ throw new DevPipelineError(devCli.formatAutoWorktreeFailure(error));
7390
+ }
7391
+ })();
7342
7392
  preparedWorktree = prepared;
7343
7393
  if (spec.elanous) spec = { ...spec, elanous: { ...spec.elanous, cwd: prepared.worktree.path } };
7344
7394
  else if (spec.drive) spec = { ...spec, drive: { ...spec.drive, cwd: prepared.worktree.path } };
7345
7395
  else throw new DevPipelineError('--worktree 는 elanous 또는 셸 drive child에만 유효');
7346
7396
  if (!opts.json) console.log(renderPreparedDevWorktree(prepared).join('\n'));
7347
7397
  debug.log('dev-pipeline', 'auto-worktree-prepared', { runId: devRunId, ...prepared.environment, worktree: prepared.worktree });
7398
+ if (autoWorktreeDecision === 'auto') {
7399
+ if (!opts.json) console.log(devCli.formatAutoWorktreeNotice(prepared.worktree.path));
7400
+ debug.log('dev-pipeline', 'auto-worktree-implied', { runId: devRunId, worktree: prepared.worktree.path });
7401
+ }
7348
7402
  }
7349
7403
  // ⛔ 종전의 `const plan = planDevPipeline(spec)` 를 지운다(무인 리뷰 must-fix) — 쓰이지 않는
7350
7404
  // 데다 **잔여 조회보다 먼저 던질 수 있어** 관측 자체를 건너뛰게 만든다. 계획은 어차피
@@ -7630,6 +7684,11 @@ configCmd
7630
7684
  .description('Print all config or one dotted path, e.g. llm.provider (secrets redacted by default)')
7631
7685
  .option('--reveal', 'Print unredacted output and record the reveal')
7632
7686
  .action(async (path: string | undefined, opts: { reveal?: boolean }) => {
7687
+ const { resolveCurrentInstance } = await import('./instance/current.js');
7688
+ const instance = resolveCurrentInstance();
7689
+ if (instance.kind === 'test' && instance.layer === 'tree-derived') {
7690
+ console.error('⚠️ test 우주의 config를 읽습니다 — 운영 값은 --config-dir ~/.elanous');
7691
+ }
7633
7692
  const cfg = getUserConfig() as unknown as Record<string, unknown>;
7634
7693
  const value = path ? getConfigPath(cfg, path) : cfg;
7635
7694
  if (value === undefined) {
@@ -7724,6 +7783,59 @@ configCmd
7724
7783
  ui.info(`unset ${path}`);
7725
7784
  });
7726
7785
 
7786
+ // ── config drift / explain (CFG-STORE1 · 0.2.20 · 2026-10-08) — 읽기 전용 «유효 config ⊕ 출처» ──
7787
+ // 집(~/.elanous)과 파생 시험 우주(<트리>/.elanous-test) config 사본의 드리프트를 한 명령으로.
7788
+ // ⛔ 쓰기 0 · 못 읽음 ≠ 같음 · 비밀 값 가림. 계산 = src/cli/config-drift.ts.
7789
+ async function configDriftDiscover(o: { registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) {
7790
+ const { discoverUniverses } = await import('./cli/config-drift.js');
7791
+ return discoverUniverses({
7792
+ ...(o.registry === false ? { registryPath: null } : typeof o.registry === 'string' ? { registryPath: o.registry } : {}),
7793
+ ...(o.treeScan === false ? { treeScanRoots: [] } : o.scanRoot?.length ? { treeScanRoots: o.scanRoot } : {}),
7794
+ });
7795
+ }
7796
+
7797
+ configCmd
7798
+ .command('drift')
7799
+ .description('읽기 전용 — 운영 config 와 파생 시험 우주(.elanous-test) config 사본이 다른 키 표 (CFG-STORE1)')
7800
+ .option('--key <key>', '이 키(또는 그 아래)만')
7801
+ .option('--json', 'JSON 출력')
7802
+ .option('--limit <n>', '표시할 키 수 (0=전부 · 기본 50)', '50')
7803
+ .option('--registry <path>', '인스턴스 레지스트리 경로 (기본 ~/.elanous/logs/instances.json)')
7804
+ .option('--scan-root <dir...>', '명시 트리 뿌리 (기본 = config-drift.ts defaultTreeScanRoots)')
7805
+ .option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
7806
+ .option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리(자리·작업 트리)만')
7807
+ .option('--include-expected', 'sync-test 의 test-safe 변환(비밀 제거·발송 끔)과 _test 메타 키 차이도 드리프트로 센다')
7808
+ .action(async (o: { key?: string; json?: boolean; limit: string; includeExpected?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
7809
+ const { computeDrift, renderDrift } = await import('./cli/config-drift.js');
7810
+ const report = computeDrift(await configDriftDiscover(o), { key: o.key, includeExpected: o.includeExpected });
7811
+ debug.log('config.drift', 'computed', {
7812
+ key: o.key ?? null, rows: report.rows.length, prodStatus: report.prodStatus, ...report.universes,
7813
+ });
7814
+ debug.flush();
7815
+ console.log(o.json ? JSON.stringify(report, null, 2) : renderDrift(report, { limit: Number(o.limit) || 0 }));
7816
+ if (report.prodStatus !== 'ok') process.exit(1);
7817
+ });
7818
+
7819
+ configCmd
7820
+ .command('explain <key>')
7821
+ .description('읽기 전용 — 운영 값 ⊕ 출처 층 ⊕ 파생 시험 우주별 값 (CFG-STORE1)')
7822
+ .option('--json', 'JSON 출력')
7823
+ .option('--registry <path>', '인스턴스 레지스트리 경로')
7824
+ .option('--scan-root <dir...>', '명시 트리 뿌리')
7825
+ .option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
7826
+ .option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리만')
7827
+ .action(async (key: string, o: { json?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
7828
+ const { computeExplain, renderExplain } = await import('./cli/config-drift.js');
7829
+ const { buildUserConfig } = await import('./user-config.js');
7830
+ const report = computeExplain(await configDriftDiscover(o), key, {
7831
+ codeDefaults: () => buildUserConfig('/nonexistent/elanous-config-drift/config.json'),
7832
+ });
7833
+ debug.log('config.explain', 'computed', { key, layer: report.prod.layer, ...report.universes });
7834
+ debug.flush();
7835
+ console.log(o.json ? JSON.stringify(report, null, 2) : renderExplain(report));
7836
+ if (report.prod.layer === 'prod-unreadable') process.exit(1);
7837
+ });
7838
+
7727
7839
  // ── config 격리 sync (ISO-1 · 2026-07-13) — 운영→테스트 물질화 동기화 ──
7728
7840
  // 격리 테스트 인스턴스는 운영 config 를 공유하지 않는다(overlay 은퇴 방향).
7729
7841
  configCmd