elanous 0.2.17 → 0.2.18

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 (309) 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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  66. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  67. package/apps/pwa/out/approvals/index.html +1 -1
  68. package/apps/pwa/out/approvals/index.txt +14 -14
  69. package/apps/pwa/out/autopilot/index.html +1 -1
  70. package/apps/pwa/out/autopilot/index.txt +14 -14
  71. package/apps/pwa/out/board/index.html +1 -1
  72. package/apps/pwa/out/board/index.txt +14 -14
  73. package/apps/pwa/out/botlab/index.html +1 -1
  74. package/apps/pwa/out/botlab/index.txt +14 -14
  75. package/apps/pwa/out/bots/index.html +1 -1
  76. package/apps/pwa/out/bots/index.txt +14 -14
  77. package/apps/pwa/out/ceo/index.html +1 -1
  78. package/apps/pwa/out/ceo/index.txt +14 -14
  79. package/apps/pwa/out/chat/index.html +1 -1
  80. package/apps/pwa/out/chat/index.txt +14 -14
  81. package/apps/pwa/out/consult/index.html +1 -1
  82. package/apps/pwa/out/consult/index.txt +14 -14
  83. package/apps/pwa/out/control/index.html +1 -1
  84. package/apps/pwa/out/control/index.txt +14 -14
  85. package/apps/pwa/out/decisions/index.html +1 -0
  86. package/apps/pwa/out/decisions/index.txt +30 -0
  87. package/apps/pwa/out/design-check/index.html +1 -1
  88. package/apps/pwa/out/design-check/index.txt +14 -14
  89. package/apps/pwa/out/editor/index.html +1 -1
  90. package/apps/pwa/out/editor/index.txt +14 -14
  91. package/apps/pwa/out/exec/index.html +1 -1
  92. package/apps/pwa/out/exec/index.txt +14 -14
  93. package/apps/pwa/out/field/index.html +1 -1
  94. package/apps/pwa/out/field/index.txt +14 -14
  95. package/apps/pwa/out/healthz +1 -1
  96. package/apps/pwa/out/index.html +1 -1
  97. package/apps/pwa/out/index.txt +14 -14
  98. package/apps/pwa/out/inside/index.html +1 -1
  99. package/apps/pwa/out/inside/index.txt +14 -14
  100. package/apps/pwa/out/intake/index.html +1 -1
  101. package/apps/pwa/out/intake/index.txt +14 -14
  102. package/apps/pwa/out/live/index.html +1 -1
  103. package/apps/pwa/out/live/index.txt +14 -14
  104. package/apps/pwa/out/loops/index.html +1 -1
  105. package/apps/pwa/out/loops/index.txt +14 -14
  106. package/apps/pwa/out/market/index.html +1 -1
  107. package/apps/pwa/out/market/index.txt +14 -14
  108. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  109. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  110. package/apps/pwa/out/morning/index.html +1 -1
  111. package/apps/pwa/out/morning/index.txt +14 -14
  112. package/apps/pwa/out/observatory/index.html +1 -1
  113. package/apps/pwa/out/observatory/index.txt +14 -14
  114. package/apps/pwa/out/ops/checklist/index.html +1 -1
  115. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  116. package/apps/pwa/out/ops/release/index.html +1 -1
  117. package/apps/pwa/out/ops/release/index.txt +14 -14
  118. package/apps/pwa/out/ops/seats/index.html +1 -1
  119. package/apps/pwa/out/ops/seats/index.txt +14 -14
  120. package/apps/pwa/out/outputs/index.html +1 -1
  121. package/apps/pwa/out/outputs/index.txt +14 -14
  122. package/apps/pwa/out/reflection/index.html +1 -1
  123. package/apps/pwa/out/reflection/index.txt +15 -15
  124. package/apps/pwa/out/scheduler/index.html +1 -1
  125. package/apps/pwa/out/scheduler/index.txt +14 -14
  126. package/apps/pwa/out/sessions/index.html +1 -1
  127. package/apps/pwa/out/sessions/index.txt +14 -14
  128. package/apps/pwa/out/settings/devices/index.html +1 -1
  129. package/apps/pwa/out/settings/devices/index.txt +14 -14
  130. package/apps/pwa/out/settings/index.html +1 -1
  131. package/apps/pwa/out/settings/index.txt +14 -14
  132. package/apps/pwa/out/setup/done/index.html +1 -1
  133. package/apps/pwa/out/setup/done/index.txt +14 -14
  134. package/apps/pwa/out/setup/index.html +1 -1
  135. package/apps/pwa/out/setup/index.txt +14 -14
  136. package/apps/pwa/out/share/index.html +1 -1
  137. package/apps/pwa/out/share/index.txt +14 -14
  138. package/apps/pwa/out/showroom/index.html +1 -1
  139. package/apps/pwa/out/showroom/index.txt +14 -14
  140. package/apps/pwa/out/tasks/index.html +1 -1
  141. package/apps/pwa/out/tasks/index.txt +14 -14
  142. package/apps/pwa/out/term/index.html +1 -1
  143. package/apps/pwa/out/term/index.txt +14 -14
  144. package/apps/pwa/out/today/index.html +1 -1
  145. package/apps/pwa/out/today/index.txt +14 -14
  146. package/apps/pwa/out/trace/index.html +1 -1
  147. package/apps/pwa/out/trace/index.txt +14 -14
  148. package/apps/pwa/out/vault/index.html +1 -1
  149. package/apps/pwa/out/vault/index.txt +14 -14
  150. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  151. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  152. package/apps/pwa/out/workflows/index.html +1 -1
  153. package/apps/pwa/out/workflows/index.txt +14 -14
  154. package/apps/pwa/out/workspace/index.html +1 -1
  155. package/apps/pwa/out/workspace/index.txt +14 -14
  156. package/apps/pwa/out/worktrees/index.html +1 -1
  157. package/apps/pwa/out/worktrees/index.txt +14 -14
  158. package/package.json +2 -1
  159. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  160. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  161. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  162. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  163. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  164. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  165. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  166. package/scripts/ci-public-export-import-gate.ts +7 -2
  167. package/scripts/ci-typecheck-changed.ts +21 -6
  168. package/scripts/discord-seat-probe.ts +3 -2
  169. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  170. package/scripts/goals/authoring-metrics.ts +73 -0
  171. package/scripts/landing-heal/must-fix-match.ts +7 -7
  172. package/scripts/landing-heal/run-mission.ts +3 -2
  173. package/scripts/public-export-test-run.ts +37 -2
  174. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  175. package/scripts/release-loop/cut-branch.ts +2 -1
  176. package/scripts/release-loop/cutoff.ts +4 -2
  177. package/scripts/release-loop/docs-land-node.ts +8 -1
  178. package/scripts/release-loop/docs-node.ts +1 -1
  179. package/scripts/release-loop/gate-node.ts +99 -14
  180. package/scripts/release-loop/node-verdict.ts +2 -1
  181. package/scripts/release-loop/npm-publish-node.ts +11 -7
  182. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  183. package/scripts/release-loop/preflight.ts +246 -0
  184. package/scripts/release-loop/publish-node.ts +8 -1
  185. package/scripts/release-loop/release-readiness.ts +11 -6
  186. package/scripts/release-loop/release-version.ts +48 -0
  187. package/scripts/release-loop/resume-release.ts +115 -0
  188. package/scripts/release-loop/unattended-release.ts +43 -7
  189. package/scripts/release-loop/version-node.ts +145 -40
  190. package/scripts/rhythm/daily.ts +25 -4
  191. package/scripts/skills/public-check.ts +62 -12
  192. package/scripts/test-deterministic.ts +51 -33
  193. package/scripts/test-diet/effectiveness.ts +62 -1
  194. package/scripts/test-diet/lib.ts +1 -1
  195. package/scripts/test-diet/node.ts +8 -1
  196. package/src/agent-mission/review-loop.ts +4 -3
  197. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  198. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  199. package/src/briefing/brief-items.ts +86 -17
  200. package/src/cli/autopilot-cli.ts +30 -11
  201. package/src/cli/brief-cli.ts +36 -2
  202. package/src/cli/freeze-cli.ts +3 -2
  203. package/src/cli/intake-cli.ts +73 -1
  204. package/src/cli/notify-cli.ts +75 -0
  205. package/src/cli/provider-cli.ts +12 -2
  206. package/src/cli/release-cli.ts +101 -17
  207. package/src/cli/tasks-cli.ts +309 -3
  208. package/src/context-bus/condense.ts +19 -3
  209. package/src/context-bus/context-day.ts +77 -24
  210. package/src/context-bus/context-first.ts +33 -0
  211. package/src/discord-self-message.ts +20 -2
  212. package/src/discord-slash-wire.ts +1 -0
  213. package/src/domains/outbound-alert.ts +200 -27
  214. package/src/domains/outbound-log-probe.ts +16 -1
  215. package/src/domains/release-tool.ts +2 -2
  216. package/src/execution-loop/gates.ts +11 -2
  217. package/src/flow-loop/split.ts +8 -4
  218. package/src/git-fs/worktree.ts +23 -3
  219. package/src/harness/harness-cli-command.ts +10 -2
  220. package/src/harness/harness-queue.ts +388 -33
  221. package/src/harness/helper-scan.ts +10 -3
  222. package/src/hq/seats.ts +369 -0
  223. package/src/index.ts +133 -15
  224. package/src/intake-plane/adopt-gate.ts +49 -0
  225. package/src/intake-plane/check.ts +3 -0
  226. package/src/intake-plane/collect-github.ts +57 -25
  227. package/src/intake-plane/discord-seat-work.ts +8 -2
  228. package/src/intake-plane/requirement-funnel.ts +316 -0
  229. package/src/knowledge/know-find.ts +10 -1
  230. package/src/loops/budget.ts +36 -0
  231. package/src/loops/orchestrator/finish-rate.ts +73 -1
  232. package/src/loops/orchestrator/grid-place.ts +11 -0
  233. package/src/loops/orchestrator/tick.ts +295 -20
  234. package/src/maturity/feature-maturity.ts +3 -2
  235. package/src/nexus/api/decisions-route.ts +11 -1
  236. package/src/nexus/api/loop-edges.ts +69 -13
  237. package/src/nexus/api/ops-seats-sources.ts +2 -2
  238. package/src/nexus/api/workflow-templates.ts +3 -0
  239. package/src/oauth/codex.ts +38 -0
  240. package/src/onboarding/first-chat-gate.ts +29 -0
  241. package/src/release-loop/checklist.ts +104 -6
  242. package/src/release-loop/feature-store.ts +10 -9
  243. package/src/release-loop/landing-freeze.ts +6 -4
  244. package/src/release-loop/placement.ts +13 -3
  245. package/src/release-loop/rubric.ts +80 -0
  246. package/src/seat-dispatch/seat-ask.ts +79 -16
  247. package/src/seat-loop/seat-loop.ts +12 -6
  248. package/src/self-dev/ask-launch-flow.ts +24 -3
  249. package/src/self-dev/ask-path-candidates.ts +68 -0
  250. package/src/self-dev/dev-pipeline.ts +4 -0
  251. package/src/self-dev/draft-triage-rules.ts +1 -1
  252. package/src/self-dev/orchestrate-cli.ts +22 -0
  253. package/src/self-dev/round-pr-supersede.ts +244 -0
  254. package/src/self-dev/run-supervisor.ts +14 -0
  255. package/src/self-implement/follow-up-goals.ts +2 -2
  256. package/src/self-implement/gate-remote.ts +427 -0
  257. package/src/self-implement/goal-author.ts +8 -3
  258. package/src/self-implement/l8-merge-queue.ts +29 -5
  259. package/src/self-implement/orchestrator.ts +59 -12
  260. package/src/self-implement/prior-draft-findings.ts +4 -3
  261. package/src/skills/skill-feedback.ts +76 -0
  262. package/src/task-agent/actions.ts +39 -6
  263. package/src/task-agent/judge.ts +31 -5
  264. package/src/task-agent/mission.ts +423 -0
  265. package/src/task-agent/shadow.ts +90 -0
  266. package/src/task-agent/task-hand.ts +235 -0
  267. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  268. package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
  269. package/src/telegram-loops-command.ts +1 -1
  270. package/src/telegram.ts +5 -20
  271. package/src/user-config.ts +63 -3
  272. package/src/version/packed-revision.json +1 -1
  273. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  307. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  308. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  309. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -81,9 +81,16 @@ export function classifyStoppedPr({ body, comments }: { body: string; comments:
81
81
  action = '증거 재측 — 인용된 판정 명령을 호스트에서 다시 돌려 결과를 PR 에 남김; 코드 수정 아님';
82
82
  } else if (/review-budget/i.test(stopEvidence) || basis === 'must-fix-reported') {
83
83
  // Real supervisor bodies (UNCONVERGEABLE · implementation-deficit) carry `classificationBasis: must-fix-reported` instead of the RFC word.
84
- if (basis === 'must-fix-reported') signals.push('classificationBasis=must-fix-reported → review-budget');
85
- category = 'review-budget';
86
- action = latest?.count ? '헬퍼 자식 spawn — must-fix 원문과 기존 시험 설계로 한 라운드' : '하지 않음 — must-fix 0 또는 미관측; 수리 자식 대상 아님';
84
+ const previous = latest && reviews.find((review) => review.round === latest.round - 1);
85
+ if (latest && previous && latest.round >= 2 && latest.count > 0 && latest.count >= previous.count) {
86
+ category = 'review-oscillation';
87
+ signals.push(`must-fix ${previous.count}→${latest.count} (round ${previous.round}→${latest.round}) → review-oscillation`);
88
+ action = '헬퍼 자식 spawn — 기존 시험 설계 보존 후 한 라운드';
89
+ } else {
90
+ if (basis === 'must-fix-reported') signals.push('classificationBasis=must-fix-reported → review-budget');
91
+ category = 'review-budget';
92
+ action = latest?.count ? '헬퍼 자식 spawn — must-fix 원문과 기존 시험 설계로 한 라운드' : '하지 않음 — must-fix 0 또는 미관측; 수리 자식 대상 아님';
93
+ }
87
94
  } else if (/no-progress/i.test(stopEvidence) || basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') {
88
95
  if (basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') signals.push(`classificationBasis=${basis} → no-progress`);
89
96
  category = 'no-progress';
@@ -0,0 +1,369 @@
1
+ // HQ seat folders are disposable views of one bare repository. No scheduler or human checkout is modified here.
2
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { runGitCommand } from '../git-fs/runner.js';
5
+
6
+ type HqSeatRole = 'OP' | 'MK' | 'TC' | 'UX';
7
+ const ROLES: readonly string[] = ['OP', 'MK', 'TC', 'UX'];
8
+ interface HouseLedger {
9
+ home: { kind: 'home'; path: string; source: 'cli-resolved' };
10
+ // `reclaiming` = the merged tip a `work done` proved before it started removing; it lets an interrupted done resume.
11
+ sandboxes: Record<string, { kind: 'sandbox'; path: string; owner: HqSeatRole; reclaiming?: string }>;
12
+ }
13
+
14
+ function hqRoot(home = process.env.HOME): string {
15
+ if (!home) throw new Error('HOME is required (or pass --root)');
16
+ return join(home, 'elanous-hq');
17
+ }
18
+
19
+ // A first fetch or checkout of the real monorepo takes minutes; only fetch gets the long ceiling.
20
+ function git(cwd: string, ...args: string[]): string {
21
+ const result = runGitCommand(cwd, args, { encoding: 'utf8', timeout: args.includes('fetch') ? 1_800_000 : 600_000 });
22
+ if (result.status !== 0) throw new Error(`git ${args.join(' ')}: ${result.stderr.trim() || `exit ${result.status}`}`);
23
+ return result.stdout.trim();
24
+ }
25
+
26
+ function repository(root: string): string {
27
+ const repo = join(root, 'repo.git');
28
+ if (git(repo, 'rev-parse', '--is-bare-repository') !== 'true') throw new Error(`not a bare repository: ${repo}`);
29
+ return repo;
30
+ }
31
+
32
+ function ledgerPath(root: string): string { return join(root, 'house.json'); }
33
+ function readLedger(root: string): HouseLedger {
34
+ const ledger = JSON.parse(readFileSync(ledgerPath(root), 'utf8')) as HouseLedger;
35
+ if (!ledger.home?.path || !ledger.sandboxes || typeof ledger.sandboxes !== 'object') throw new Error('invalid house.json; refusing to guess houses');
36
+ return ledger;
37
+ }
38
+ function saveLedger(root: string, ledger: HouseLedger): void {
39
+ const path = ledgerPath(root);
40
+ const tmp = `${path}.${process.pid}.tmp`;
41
+ writeFileSync(tmp, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
42
+ try { renameSync(tmp, path); }
43
+ catch (error) { rmSync(tmp, { force: true }); throw error; }
44
+ }
45
+ // The lock covers the read as well as the atomic rename: a rename alone cannot prevent lost updates.
46
+ // It also serializes repo.git worktree/branch mutations: the git runner retries lock contention, and a
47
+ // retried `worktree add -b` that already created its branch fails with "branch already exists".
48
+ function withLedgerLock<T>(root: string, update: () => T, waitMs = 120_000): T {
49
+ const lock = `${ledgerPath(root)}.lock`;
50
+ const deadline = Date.now() + waitMs;
51
+ while (true) {
52
+ try { mkdirSync(lock); break; }
53
+ catch (error) {
54
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
55
+ if (Date.now() >= deadline) throw new Error(`house.json lock timed out: ${lock}`);
56
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
57
+ }
58
+ }
59
+ try { return update(); }
60
+ finally { rmdirSync(lock); }
61
+ }
62
+ /** Caller must hold the ledger lock. */
63
+ function updateLedgerLocked(root: string, update: (ledger: HouseLedger) => void): void {
64
+ const ledger = readLedger(root);
65
+ update(ledger);
66
+ saveLedger(root, ledger);
67
+ }
68
+ function roleOf(value: string): HqSeatRole {
69
+ if (!ROLES.includes(value)) throw new Error(`invalid seat role: ${value} (OP|MK|TC|UX)`);
70
+ return value as HqSeatRole;
71
+ }
72
+ function workName(name: string): string {
73
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) throw new Error(`invalid work name: ${name}`);
74
+ return name;
75
+ }
76
+ function clean(path: string): boolean { return git(path, 'status', '--porcelain') === ''; }
77
+ function assertRegisteredWorktree(repo: string, path: string): void {
78
+ // The path itself must be the registered worktree: a symlink to another registered worktree
79
+ // (e.g. seats/TC -> work/TC-fix1) would otherwise pass and let a refresh detach that work.
80
+ if (lstatSync(path).isSymbolicLink()) throw new Error(`not a worktree of ${repo}: ${path} (symlink)`);
81
+ const own = join(realpathSync(dirname(path)), basename(path));
82
+ const entries = git(repo, 'worktree', 'list', '--porcelain').split('\n');
83
+ const registered = entries.some(line => line.startsWith('worktree ') && line.slice(9) === own);
84
+ if (!registered) throw new Error(`not a worktree of ${repo}: ${path}`);
85
+ if (realpathSync(git(path, 'rev-parse', '--git-common-dir')) !== realpathSync(repo)) {
86
+ throw new Error(`not a worktree of ${repo}: ${path}`);
87
+ }
88
+ }
89
+ const SANDBOX_IGNORE = '/.elanous-test/';
90
+ // The sandbox is filled by ordinary work, so every view of repo.git ignores it: `status` stays clean and
91
+ // `worktree remove` (no --force) deletes it with the worktree instead of refusing a merged work.
92
+ function ensureSandboxIgnored(repo: string): void {
93
+ const exclude = join(repo, 'info', 'exclude');
94
+ const current = existsSync(exclude) ? readFileSync(exclude, 'utf8') : '';
95
+ if (current.split('\n').includes(SANDBOX_IGNORE)) return;
96
+ mkdirSync(join(repo, 'info'), { recursive: true });
97
+ writeFileSync(exclude, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${SANDBOX_IGNORE}\n`);
98
+ }
99
+ // A local relative origin (`../remote.git`) is relative to the calling checkout, not to repo.git. Git itself
100
+ // resolves it from the checkout's top level even when run from a subdirectory, so the same base is used here.
101
+ function callerOrigin(): string {
102
+ const origin = git(process.cwd(), 'remote', 'get-url', 'origin');
103
+ const url = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(origin);
104
+ const scpLike = /^[^/]*:/.test(origin);
105
+ if (url || scpLike || isAbsolute(origin)) return origin;
106
+ return resolve(git(process.cwd(), 'rev-parse', '--show-toplevel'), origin);
107
+ }
108
+ function mergedIntoMain(repo: string, commit: string): boolean {
109
+ const merged = runGitCommand(repo, ['merge-base', '--is-ancestor', commit, 'refs/remotes/origin/main'], { encoding: 'utf8' });
110
+ if (merged.status !== 0 && merged.status !== 1) throw new Error(`cannot verify merge: ${merged.stderr.trim()}`);
111
+ return merged.status === 0;
112
+ }
113
+ // Git lock files are created exclusively; ours carry our pid. A lock left by a killed `work done` is never
114
+ // removed automatically (between our check and a delete, a live git could have re-taken it): the error names it.
115
+ function takeLock(lock: string): void {
116
+ try { writeFileSync(lock, `hq-done ${process.pid}\n`, { flag: 'wx' }); }
117
+ catch (error) {
118
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
119
+ let content = '';
120
+ try { content = readFileSync(lock, 'utf8').trim(); } catch { /* gone or unreadable: report as held */ }
121
+ const owner = /^hq-done (\d+)$/.exec(content);
122
+ if (owner && !processAlive(Number(owner[1]))) throw new Error(`stale lock from a stopped hq work done (pid ${owner[1]}): remove ${lock} and rerun`);
123
+ throw error;
124
+ }
125
+ }
126
+ function processAlive(pid: number): boolean {
127
+ try { process.kill(pid, 0); return true; }
128
+ catch (error) { return (error as NodeJS.ErrnoException).code === 'EPERM'; }
129
+ }
130
+ // Scoped `worktree prune`: drop only the admin entry whose checkout is `path` and is already gone.
131
+ function removeStaleWorktreeAdmin(repo: string, path: string): void {
132
+ const admins = join(repo, 'worktrees');
133
+ if (!existsSync(admins) || existsSync(path)) return;
134
+ for (const name of readdirSync(admins)) {
135
+ const gitdir = join(admins, name, 'gitdir');
136
+ if (existsSync(gitdir) && readFileSync(gitdir, 'utf8').trim() === join(path, '.git')) rmSync(join(admins, name), { recursive: true, force: true });
137
+ }
138
+ }
139
+ function quoteShell(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; }
140
+
141
+ /** Origin is read from the calling checkout; nothing in that checkout is moved or reconfigured. */
142
+ export function hqInit(options: { root?: string; home?: string } = {}): { root: string; created: boolean; cron: string } {
143
+ const root = resolve(options.root ?? hqRoot());
144
+ const repo = join(root, 'repo.git');
145
+ if (!options.home) throw new Error('resolved home house is required to register house.json');
146
+ mkdirSync(options.home, { recursive: true });
147
+ const home = realpathSync(options.home);
148
+ mkdirSync(root, { recursive: true });
149
+ // Concurrent inits serialize here: the repo.git setup and the first house.json registration are both
150
+ // decided under the ledger lock, so neither side can overwrite the other or miss a home mismatch.
151
+ const created = withLedgerLock(root, () => {
152
+ if (existsSync(ledgerPath(root))) {
153
+ const registered = readLedger(root).home.path;
154
+ if (registered !== home) throw new Error(`house.json home mismatch: registered ${registered}; requested ${home}`);
155
+ }
156
+ const fresh = !existsSync(repo);
157
+ if (fresh) {
158
+ const source = callerOrigin();
159
+ git(root, 'init', '--bare', repo);
160
+ git(repo, 'remote', 'add', 'origin', source);
161
+ }
162
+ repository(root);
163
+ if (!fresh) {
164
+ // An existing mirror must be of the repository init is run for; never re-point or reuse another one.
165
+ const mirrored = runGitCommand(repo, ['remote', 'get-url', 'origin'], { encoding: 'utf8' });
166
+ const caller = callerOrigin();
167
+ if (mirrored.status === 0 && mirrored.stdout.trim() !== caller) throw new Error(`repo.git mirrors ${mirrored.stdout.trim()}, not ${caller}; refusing to reuse ${root}`);
168
+ }
169
+ // An interrupted first fetch leaves a bare repository but no origin/main. Finish it on retry.
170
+ const remote = runGitCommand(repo, ['remote', 'get-url', 'origin'], { encoding: 'utf8' });
171
+ if (remote.status !== 0) {
172
+ // Only an empty, just-initialized repo.git (an init stopped before `remote add`) is finished here.
173
+ if (git(repo, 'for-each-ref') !== '' || existsSync(join(repo, 'worktrees'))) throw new Error(`repo.git has no origin but is not empty; refusing to adopt ${repo}`);
174
+ const source = callerOrigin();
175
+ git(repo, 'remote', 'add', 'origin', source);
176
+ }
177
+ const main = runGitCommand(repo, ['rev-parse', '--verify', 'refs/remotes/origin/main'], { encoding: 'utf8' });
178
+ if (fresh || remote.status !== 0 || main.status !== 0) {
179
+ git(repo, 'config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*');
180
+ git(repo, 'fetch', 'origin');
181
+ }
182
+ git(repo, 'rev-parse', '--verify', 'refs/remotes/origin/main');
183
+ if (!existsSync(ledgerPath(root))) {
184
+ saveLedger(root, { home: { kind: 'home', path: home, source: 'cli-resolved' }, sandboxes: {} });
185
+ }
186
+ return fresh;
187
+ }, 2_100_000); // longer than the 30-minute first fetch, so a concurrent init reaches the home check
188
+ // Print only: OP installs the line. Only existing role views can be refreshed; init never creates seats.
189
+ // cron runs with a minimal PATH, so the line names this runtime and CLI entry by absolute path.
190
+ const cli = [process.execPath, process.argv[1]].filter((part): part is string => Boolean(part)).map(part => quoteShell(resolve(part))).join(' ');
191
+ const script = `git --git-dir=${quoteShell(repo)} fetch origin && ${ROLES.map(role => `( [ ! -d ${quoteShell(join(root, 'seats', role))} ] || ${cli} hq seat ${role} --refresh --root ${quoteShell(root)} )`).join(' && ')}`;
192
+ const cron = `*/5 * * * * ${script}`;
193
+ return { root, created, cron };
194
+ }
195
+
196
+ export function hqSeat(value: string, options: { root?: string; refresh?: boolean } = {}): { path: string; outcome: 'created' | 'unchanged' | 'refreshed' | 'dirty' } {
197
+ const role = roleOf(value);
198
+ const root = resolve(options.root ?? hqRoot());
199
+ const repo = repository(root);
200
+ readLedger(root);
201
+ const path = join(root, 'seats', role);
202
+ // A new seat starts from the current origin/main, not from whatever the mirror last fetched.
203
+ if (!existsSync(path)) git(repo, 'fetch', 'origin');
204
+ const created = withLedgerLock(root, () => {
205
+ if (existsSync(path)) return false;
206
+ mkdirSync(join(root, 'seats'), { recursive: true });
207
+ // A seat folder deleted by hand leaves its git registration behind; drop only that one so the seat is recreated.
208
+ removeStaleWorktreeAdmin(repo, join(realpathSync(join(root, 'seats')), role));
209
+ git(repo, 'worktree', 'add', '--detach', path, 'refs/remotes/origin/main');
210
+ return true;
211
+ });
212
+ if (created) return { path, outcome: 'created' };
213
+ // Refuse an unrelated checkout even when it is clean; do not touch its HEAD.
214
+ assertRegisteredWorktree(repo, path);
215
+ // Seats are always detached; a worktree on a branch here (e.g. a work moved into seats/) is someone's work.
216
+ const onBranch = runGitCommand(path, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { encoding: 'utf8' });
217
+ if (onBranch.status === 0) throw new Error(`not a seat: ${path} is on branch ${onBranch.stdout.trim()}; refusing to touch it`);
218
+ // A dirty seat must be byte-for-byte untouched, including its detached HEAD.
219
+ if (options.refresh && !clean(path)) return { path, outcome: 'dirty' };
220
+ if (!options.refresh) return { path, outcome: 'unchanged' };
221
+ git(repo, 'fetch', 'origin');
222
+ // Concurrent refreshes of one seat (cron and a person) serialize on the house lock; each re-checks first.
223
+ return withLedgerLock(root, () => {
224
+ assertRegisteredWorktree(repo, path);
225
+ if (runGitCommand(path, ['symbolic-ref', '--quiet', 'HEAD'], { encoding: 'utf8' }).status === 0) throw new Error(`not a seat: ${path} is on a branch; refusing to touch it`);
226
+ if (!clean(path)) return { path, outcome: 'dirty' as const };
227
+ // Commits made on the detached seat are not on origin/main; moving HEAD would orphan them. Treat as dirty.
228
+ if (!mergedIntoMain(repo, git(path, 'rev-parse', 'HEAD'))) return { path, outcome: 'dirty' as const };
229
+ git(path, 'checkout', '--detach', 'refs/remotes/origin/main');
230
+ return { path, outcome: 'refreshed' as const };
231
+ });
232
+ }
233
+
234
+ export function hqWork(action: 'new' | 'done', value: string, nameValue: string, options: { root?: string } = {}): { path: string; outcome: 'created' | 'removed' } {
235
+ const role = roleOf(value);
236
+ const name = workName(nameValue);
237
+ const root = resolve(options.root ?? hqRoot());
238
+ const repo = repository(root);
239
+ readLedger(root);
240
+ const branch = `work/${role}-${name}`;
241
+ const path = join(root, 'work', `${role}-${name}`);
242
+ if (action === 'new') {
243
+ if (existsSync(path)) throw new Error(`work already exists: ${path}`);
244
+ // fetch stays outside the lock: it only moves remote-tracking refs, is idempotent and lock-retried, and the
245
+ // cron line runs it unlocked anyway. Branch, worktree and ledger changes below are all under the lock.
246
+ git(repo, 'fetch', 'origin');
247
+ withLedgerLock(root, () => {
248
+ if (existsSync(path)) throw new Error(`work already exists: ${path}`);
249
+ mkdirSync(join(root, 'work'), { recursive: true });
250
+ ensureSandboxIgnored(repo);
251
+ const base = git(repo, 'rev-parse', '--verify', 'refs/remotes/origin/main');
252
+ git(repo, 'worktree', 'add', '-b', branch, path, base);
253
+ try {
254
+ const sandbox = join(path, '.elanous-test');
255
+ mkdirSync(sandbox);
256
+ updateLedgerLocked(root, ledger => {
257
+ ledger.sandboxes[path] = { kind: 'sandbox', path: realpathSync(sandbox), owner: role };
258
+ });
259
+ } catch (error) {
260
+ // Roll back only a work that is still exactly what was just created (clean, at base, branch at base),
261
+ // without --force and with a compare-and-delete; anything a hook or another writer added is kept.
262
+ let untouched = false;
263
+ // `--ignored` also sees files in the (ignored) sandbox; only the empty sandbox dir we made is invisible to it.
264
+ try { untouched = git(path, 'rev-parse', '--verify', 'HEAD') === base && git(path, 'status', '--porcelain', '--ignored') === ''; } catch { /* unreadable: keep it */ }
265
+ const removed = untouched ? runGitCommand(repo, ['worktree', 'remove', path], { encoding: 'utf8' }) : undefined;
266
+ const unbranched = removed?.status === 0 ? runGitCommand(repo, ['update-ref', '-d', `refs/heads/${branch}`, base], { encoding: 'utf8' }) : undefined;
267
+ const rolledBack = !untouched ? `work kept: it changed after creation: ${path}`
268
+ : removed?.status !== 0 ? `work kept: ${removed?.stderr.trim()}`
269
+ : unbranched?.status !== 0 ? `rollback incomplete; branch kept: ${unbranched?.stderr.trim()}`
270
+ : 'rolled back';
271
+ throw new Error(`work registration failed (${rolledBack}): ${error instanceof Error ? error.message : String(error)}`);
272
+ }
273
+ });
274
+ return { path, outcome: 'created' };
275
+ }
276
+ git(repo, 'fetch', 'origin');
277
+ withLedgerLock(root, () => {
278
+ if (!existsSync(path)) {
279
+ // Reconcile a done that removed the worktree but failed before the ledger or branch caught up.
280
+ if (!readLedger(root).sandboxes[path]) throw new Error(`work does not exist: ${path}`);
281
+ // A moved worktree (`git worktree move`) is still live work on this branch: never reclaim it from here.
282
+ // Only this work's own stale registration is cleaned below; other (maybe briefly missing) worktrees are left.
283
+ const own = join(existsSync(dirname(path)) ? realpathSync(dirname(path)) : dirname(path), basename(path));
284
+ const elsewhere = git(repo, 'worktree', 'list', '--porcelain').split('\n\n')
285
+ .filter(block => block.split('\n').includes(`branch refs/heads/${branch}`))
286
+ .map(block => block.split('\n')[0]!.slice(9))
287
+ .find(location => location !== own);
288
+ if (elsewhere) throw new Error(`work branch ${branch} is checked out at ${elsewhere}; refusing to reclaim ${path}`);
289
+ const ref = runGitCommand(repo, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { encoding: 'utf8' });
290
+ if (ref.status !== 0) {
291
+ // Without the branch, the only merge proof is the tip an interrupted done recorded before removing.
292
+ const recorded = readLedger(root).sandboxes[path]?.reclaiming;
293
+ if (!recorded || !mergedIntoMain(repo, recorded)) throw new Error(`cannot verify merge: branch ${branch} is missing; house.json entry kept: ${path}`);
294
+ updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
295
+ removeStaleWorktreeAdmin(repo, own);
296
+ return;
297
+ }
298
+ const tip = ref.stdout.trim();
299
+ const merged = runGitCommand(repo, ['merge-base', '--is-ancestor', tip, 'refs/remotes/origin/main'], { encoding: 'utf8' });
300
+ if (merged.status !== 0) throw new Error(merged.status === 1 ? `work is not merged into origin/main: ${branch}` : `cannot verify merge: ${merged.stderr}`);
301
+ // Same order as a normal done: the entry goes first, so a save that fails again keeps the branch (the proof).
302
+ const entry = readLedger(root).sandboxes[path];
303
+ updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
304
+ const deleted = runGitCommand(repo, ['update-ref', '-d', `refs/heads/${branch}`, tip], { encoding: 'utf8' });
305
+ if (deleted.status !== 0) {
306
+ // The branch moved after the merge check: put the registration back so done can be retried.
307
+ updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = entry; });
308
+ throw new Error(`work branch moved after the merge check; registration kept: ${branch}: ${deleted.stderr.trim()}`);
309
+ }
310
+ removeStaleWorktreeAdmin(repo, own);
311
+ return;
312
+ }
313
+ // Only work that `hq work new` registered is reclaimed; a hand-made worktree at the same path is not ours.
314
+ const entry = readLedger(root).sandboxes[path];
315
+ if (entry?.owner !== role) throw new Error(`work is not registered in house.json; refusing to remove: ${path}`);
316
+ assertRegisteredWorktree(repo, path);
317
+ if (!clean(path)) throw new Error(`work is dirty; refusing to remove: ${path}`);
318
+ const ref = `refs/heads/${branch}`;
319
+ const attached = runGitCommand(path, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { encoding: 'utf8' });
320
+ const branchTip = runGitCommand(repo, ['rev-parse', '--verify', '--quiet', ref], { encoding: 'utf8' });
321
+ // Resume: an interrupted done left the work detached at its recorded merged tip, with the branch either
322
+ // still at that tip (stopped before the delete) or already deleted (stopped before the removal).
323
+ const branchPresent = branchTip.status === 0;
324
+ const resuming = attached.status !== 0 && entry.reclaiming !== undefined
325
+ && git(path, 'rev-parse', 'HEAD') === entry.reclaiming
326
+ && (!branchPresent || branchTip.stdout.trim() === entry.reclaiming);
327
+ if (!resuming && (attached.status !== 0 || attached.stdout.trim() !== branch)) throw new Error(`work branch mismatch: ${path}`);
328
+ const tip = resuming ? entry.reclaiming! : branchTip.stdout.trim();
329
+ // The ancestor check is against origin/main, not bare HEAD (which may still point to master).
330
+ if (!mergedIntoMain(repo, tip)) throw new Error(`work is not merged into origin/main: ${branch}`);
331
+ // Removal order keeps every interruption resumable: record the proven tip → detach → hold the work's git
332
+ // locks → compare-and-delete the branch → remove the worktree → drop the registration last.
333
+ // Holding this worktree's HEAD.lock and index.lock makes git refuse every commit and `add` in it until the
334
+ // removal ends (`worktree remove` itself does not need them), so no commit can land between the last
335
+ // check and the removal. The locks live in the worktree's admin dir, which the removal deletes.
336
+ updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = { ...entry, reclaiming: tip }; });
337
+ const admin = git(path, 'rev-parse', '--absolute-git-dir');
338
+ const held: string[] = [];
339
+ const release = (): void => { for (const lock of held.splice(0)) rmSync(lock, { force: true }); };
340
+ const restore = (): string => {
341
+ release();
342
+ const head = runGitCommand(path, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8' });
343
+ runGitCommand(repo, ['update-ref', ref, head.status === 0 ? head.stdout.trim() : tip, ''], { encoding: 'utf8' });
344
+ const reattached = runGitCommand(path, ['symbolic-ref', 'HEAD', ref], { encoding: 'utf8' }).status === 0;
345
+ // If HEAD could not be re-attached (e.g. a stale HEAD.lock), keep `reclaiming` so the next done resumes.
346
+ const restored = reattached ? { kind: entry.kind, path: entry.path, owner: entry.owner } : { ...entry, reclaiming: tip };
347
+ try { updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = restored; }); return reattached ? 'registration restored' : 'left resumable'; }
348
+ catch (error) { return `REGISTRATION NOT RESTORED (fix ${path} in house.json by hand): ${error instanceof Error ? error.message : String(error)}`; }
349
+ };
350
+ const fail = (message: string): never => { throw new Error(`${message}; nothing removed: ${path} · ${restore()}`); };
351
+ if (!resuming) git(path, 'checkout', '--quiet', '--detach');
352
+ for (const name of ['HEAD.lock', 'index.lock']) {
353
+ const lock = join(admin, name);
354
+ try { takeLock(lock); held.push(lock); }
355
+ catch (error) { fail(`work is busy (another git process holds ${name}): ${error instanceof Error ? error.message : String(error)}`); }
356
+ }
357
+ if (git(path, 'rev-parse', 'HEAD') !== tip) fail('work gained a commit after the merge check');
358
+ if (branchPresent) {
359
+ const deleted = runGitCommand(repo, ['update-ref', '-d', ref, tip], { encoding: 'utf8' });
360
+ if (deleted.status !== 0) fail(`work branch moved after the merge check: ${deleted.stderr.trim()}`);
361
+ }
362
+ if (!clean(path)) fail('work changed during done');
363
+ // Without --force, git also refuses a worktree that became dirty after the check above.
364
+ const removed = runGitCommand(repo, ['worktree', 'remove', path], { encoding: 'utf8' });
365
+ if (removed.status !== 0) fail(`git worktree remove failed: ${removed.stderr.trim()}`);
366
+ updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
367
+ });
368
+ return { path, outcome: 'removed' };
369
+ }
package/src/index.ts CHANGED
@@ -74,6 +74,7 @@ import { registerModelWatchCommand } from './cli/model-watch-cli.js';
74
74
  import { registerIntakeCommands } from './cli/intake-cli.js';
75
75
  import { registerTasksCommands } from './cli/tasks-cli.js';
76
76
  import { registerBriefCommands } from './cli/brief-cli.js';
77
+ import { registerNotifyCommand } from './cli/notify-cli.js';
77
78
  import { registerLabelsCommands } from './cli/labels-cli.js';
78
79
  import { registerMsgCommands } from './cli/msg-cli.js';
79
80
  import { registerDoctorCommand } from './cli/doctor-cli.js';
@@ -135,6 +136,7 @@ import type { ReviewResult } from './agent-substrate/pr-reviewer.js';
135
136
  import type { ReviewVerdict } from './agent-mission/review-loop.js';
136
137
  import type { SyncMode } from './types.js';
137
138
  import { runOnboarding, runOnboardingStep, runOnboardingNonInteractive, needsOnboarding, needsFirstRun, handleOnboardingRefusal, type OnboardingStepId } from './onboarding.js';
139
+ import { firstChatGate } from './onboarding/first-chat-gate.js';
138
140
  import {
139
141
  getUserConfig, reloadUserConfig, userConfigPath, saveUserConfig,
140
142
  backupUserConfig, backupConfigPath, addRotationEntry, type RotationEntry,
@@ -888,6 +890,7 @@ registerLiveCommands(program);
888
890
  registerResearchCommand(program);
889
891
  registerReleaseCommands(program);
890
892
  registerKnowCommand(program);
893
+ registerNotifyCommand(program);
891
894
  registerFreezeCommands(program);
892
895
  registerFlowCommands(program);
893
896
  // HQ-HB · HQ-FENCE (10-04): one writer across mbp · node-b · cloud-vm — lease on the arbiter, 2-of-3 quorum, generation fencing.
@@ -985,6 +988,37 @@ hqCmd.command('seen').description('이 호스트가 본 가장 높은 임대 세
985
988
  process.exitCode = result.seen ? 0 : 3;
986
989
  });
987
990
  registerHqMovePlanCommand(hqCmd);
991
+ hqCmd.command('init').description('한 저장소의 bare 미러와 집 원장을 만들고 5분 fetch 크론 줄만 출력')
992
+ .option('--root <dir>', 'HQ 폴더 (기본 ~/elanous-hq)')
993
+ .action(async (opts: { root?: string }) => {
994
+ try {
995
+ const { hqInit } = await import('./hq/seats.js');
996
+ const { getElanousConfigDir } = await import('./elanous-config-dir.js');
997
+ const result = hqInit({ ...opts, home: getElanousConfigDir() });
998
+ console.log(`hq init: ${result.created ? 'created' : 'already exists'} ${result.root}\n# OP installs this line; nothing was installed:\n${result.cron}`);
999
+ } catch (error) { console.error(`hq init: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1000
+ });
1001
+ hqCmd.command('seat <role>').description('OP|MK|TC|UX 자리 워크트리 (깨끗할 때만 갱신)')
1002
+ .option('--refresh', 'origin/main으로 깨끗한 자리만 최신화')
1003
+ .option('--root <dir>', 'HQ 폴더')
1004
+ .action(async (role: string, opts: { refresh?: boolean; root?: string }) => {
1005
+ try {
1006
+ const { hqSeat } = await import('./hq/seats.js');
1007
+ const result = hqSeat(role, opts);
1008
+ if (result.outcome === 'dirty') console.warn(`hq seat ${role}: dirty — refresh skipped; move edits to hq work new`);
1009
+ else console.log(`hq seat ${role}: ${result.outcome} ${result.path}`);
1010
+ } catch (error) { console.error(`hq seat: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1011
+ });
1012
+ const hqWorkCmd = hqCmd.command('work').description('고칠 때만 작업 워크트리를 만들고 병합 후 회수');
1013
+ for (const action of ['new', 'done'] as const) hqWorkCmd.command(`${action} <role> <name>`)
1014
+ .option('--root <dir>', 'HQ 폴더')
1015
+ .action(async (role: string, name: string, opts: { root?: string }) => {
1016
+ try {
1017
+ const { hqWork } = await import('./hq/seats.js');
1018
+ const result = hqWork(action, role, name, opts);
1019
+ console.log(`hq work ${action}: ${result.outcome} ${result.path}`);
1020
+ } catch (error) { console.error(`hq work ${action}: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1021
+ });
988
1022
  const fenceWrapperCmd = hqCmd.command('fence-wrapper').description('설치본 HQ 울타리 크론 래퍼');
989
1023
  fenceWrapperCmd.command('install').description('래퍼 제안 또는 백업 후 원자적 설치')
990
1024
  .option('--dry-run', '변경 내용만 출력 (기본)')
@@ -1042,6 +1076,40 @@ hqCmd.command('fence').description('이 호스트가 임대를 쥐었을 때만
1042
1076
  });
1043
1077
  const seatCmd = program.command('seat').description('분배된 자리의 하루 루프와 보고');
1044
1078
  registerSeatRequestsCommands(seatCmd);
1079
+ seatCmd.command('asks').description('최근 CTO 되묻기 왕복과 10분 판정')
1080
+ .option('--since <duration>', '조회 창 (예: 24h)', '24h')
1081
+ .option('--json', 'JSON 출력')
1082
+ .action(async (opts: { since: string; json?: boolean }) => {
1083
+ try {
1084
+ const duration = /^(\d+)([mhd])$/.exec(opts.since);
1085
+ if (!duration || !Number.isSafeInteger(Number(duration[1]))) throw new Error('--since: 24h, 30m, 7d 형식 필요');
1086
+ const ms = Number(duration[1]) * ({ m: 60_000, h: 3_600_000, d: 86_400_000 }[duration[2]!] ?? 0);
1087
+ if (!Number.isSafeInteger(ms) || ms <= 0) throw new Error('--since: 양의 시간 필요');
1088
+ const { defaultMsgStorePath } = await import('./msg/msg-store.js');
1089
+ const path = defaultMsgStorePath();
1090
+ if (!(await import('node:fs')).existsSync(path)) {
1091
+ if (opts.json) await writeStdoutJson('[]\n');
1092
+ else console.log(`원장 없음 — ${path}`);
1093
+ return;
1094
+ }
1095
+ const { listSeatAskRoundTrips } = await import('./seat-dispatch/seat-ask.js');
1096
+ const now = Date.now();
1097
+ const rows = listSeatAskRoundTrips({ since: now - ms, now });
1098
+ if (!rows.length) {
1099
+ if (opts.json) await writeStdoutJson('[]\n');
1100
+ else console.log('이 창에 되묻기 0건');
1101
+ return;
1102
+ }
1103
+ if (opts.json) { await writeStdoutJson(`${JSON.stringify(rows)}\n`); return; }
1104
+ const clock = (value: number | null) => value === null ? '-' : new Intl.DateTimeFormat('ko-KR', {
1105
+ timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
1106
+ }).format(value);
1107
+ console.log('id · 표면 · 물음 HH:MM KST · 답 · 도착 · 왕복 분 · 판정');
1108
+ for (const row of rows) console.log(`${row.id} · ${row.surface} · ${clock(row.askedAt)} KST · ${clock(row.answeredAt)} · ${clock(row.deliveredAt)} · ${row.roundTripMs === null ? '-' : (row.roundTripMs / 60_000).toFixed(1)}분 · ${row.verdict}`);
1109
+ const count = (verdict: string) => rows.filter(row => row.verdict === verdict).length;
1110
+ console.log(`10분 안 ${count('10분 안')} / 넘음 ${count('10분 넘음')} / 미답 ${count('미답')} / 대기 ${count('답 대기') + count('전달 대기')} / 미상 ${count('시각 미상')}`);
1111
+ } catch (error) { console.error(`seat asks: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1112
+ });
1045
1113
  seatCmd.command('answer <askId> <answer>').description('자리 질문에 답을 남기고 원래 표면으로 회신 예약')
1046
1114
  .action(async (askId: string, answer: string) => {
1047
1115
  try {
@@ -3299,11 +3367,20 @@ contextCmd.command('day')
3299
3367
  .description('자리 넷과 외부 세션의 최근 맥락 이벤트 수·마지막 시각 (읽기 전용)')
3300
3368
  .option('--since <hours>', '최근 시간 창 (예: 24h)', '24h')
3301
3369
  .option('--json', '구조화된 JSON 출력')
3302
- .action(async (opts: { since: string; json?: boolean }) => {
3370
+ .option('--timeline', '시간순 사건 한 줄씩 (KST)')
3371
+ .option('--seat <source>', '한 출처만 (OP|TC|MK|UX|claude-code|codex-agent-mission|harness-child)')
3372
+ .action(async (opts: { since: string; json?: boolean; timeline?: boolean; seat?: string }) => {
3303
3373
  try {
3304
- const { contextDay, renderContextDay } = await import('./context-bus/context-day.js');
3305
- const report = contextDay(opts.since);
3306
- await writeStdoutJson(`${opts.json ? JSON.stringify(report) : renderContextDay(report)}\n`);
3374
+ const { contextDay, contextDayTimeline, renderContextDay, renderContextDayTimeline, CONTEXT_DAY_SOURCES } = await import('./context-bus/context-day.js');
3375
+ if (opts.seat && !CONTEXT_DAY_SOURCES.some(source => source === opts.seat)) throw new Error(`unknown --seat: ${opts.seat}`);
3376
+ if (opts.seat && !opts.timeline) throw new Error('--seat requires --timeline');
3377
+ if (opts.timeline) {
3378
+ const report = contextDayTimeline(opts.since, { seat: opts.seat as typeof CONTEXT_DAY_SOURCES[number] | undefined });
3379
+ await writeStdoutJson(`${opts.json ? JSON.stringify(report) : renderContextDayTimeline(report)}\n`);
3380
+ } else {
3381
+ const report = contextDay(opts.since);
3382
+ await writeStdoutJson(`${opts.json ? JSON.stringify(report) : renderContextDay(report)}\n`);
3383
+ }
3307
3384
  } catch (error) {
3308
3385
  console.error(`context day: ${error instanceof Error ? error.message : String(error)}`);
3309
3386
  process.exitCode = 2;
@@ -4337,16 +4414,52 @@ selfCmd
4337
4414
  .option('--base <ref>', '커밋됐지만 PR 없는 변경의 비교 기준(ref...HEAD)')
4338
4415
  .option('--pr <number>', '열린 PR 번호에서 gh pr view files로 변경 파일을 읽는다')
4339
4416
  .option('--shards [number]', '메모리 예산에 맞춰 시험을 별도 프로세스로 분할한다 (기본 2)')
4340
- .action((opts: { changed?: boolean; base?: string; pr?: string; shards?: string | boolean }) => {
4417
+ .option('--remote [host]', 'GATE-REMOTE — 같은 커밋을 원격 호스트(기본 config gateRemote.host=node-b)에서 돌리고 결과만 가져온다(깨끗한 트리 · --pr 은 로컬)')
4418
+ .option('--local', '원격 자동 분배(로컬 1분 부하 > gateRemote.loadThreshold=20)를 끄고 로컬에서 돌린다')
4419
+ .action(async (opts: { changed?: boolean; base?: string; pr?: string; shards?: string | boolean; remote?: string | boolean; local?: boolean }) => {
4341
4420
  if (!opts.changed && opts.shards === undefined) { ui.error('self gate requires --changed or --shards'); process.exitCode = 2; return; }
4421
+ // The root strips `--local` from argv anywhere (remote-bookmark attach · src/cli/remote-resolve.ts) before Commander
4422
+ // sees it, so this subcommand reads it from the untouched process.argv.
4423
+ opts.local = !!opts.local || process.argv.slice(2).includes('--local');
4424
+ if (opts.remote !== undefined && opts.local) { ui.error('self gate: --remote and --local cannot be combined'); process.exitCode = 2; return; }
4425
+ if (typeof opts.remote === 'string') {
4426
+ const { isValidGateHost } = await import('./self-implement/gate-remote.js');
4427
+ if (!isValidGateHost(opts.remote)) { ui.error(`self gate: invalid --remote host: ${opts.remote}`); process.exitCode = 2; return; }
4428
+ }
4429
+ const shards = opts.shards === undefined ? undefined : opts.shards === true ? 2 : Number(opts.shards);
4430
+ const runLocal = () => {
4431
+ try {
4432
+ const { runSelfGateCli } = require('./self-implement/gate-cli.js') as typeof import('./self-implement/gate-cli.js');
4433
+ const result = runSelfGateCli(process.cwd(), { base: opts.base, pr: opts.pr, shards });
4434
+ console.log(result.lines.join('\n'));
4435
+ return result.exitCode;
4436
+ } catch (error) {
4437
+ ui.error(`self gate failed: ${error instanceof Error ? error.message : String(error)}`);
4438
+ return 1;
4439
+ }
4440
+ };
4342
4441
  try {
4343
- const { runSelfGateCli } = require('./self-implement/gate-cli.js') as typeof import('./self-implement/gate-cli.js');
4344
- const result = runSelfGateCli(process.cwd(), { base: opts.base, pr: opts.pr, shards: opts.shards === undefined ? undefined : opts.shards === true ? 2 : Number(opts.shards) });
4345
- console.log(result.lines.join('\n'));
4346
- process.exitCode = result.exitCode;
4442
+ const { dispatchHeavyCheck } = await import('./self-implement/gate-remote.js');
4443
+ const { execFileSync } = await import('node:child_process');
4444
+ const repo = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
4445
+ // Only a committed --base range is reproducible on another host: the default mode measures uncommitted files, --pr
4446
+ // reads files through gh on this host, and the remote checkout runs from the repository root — those stay local.
4447
+ const { realpathSync } = await import('node:fs');
4448
+ const localOnlyReason = opts.pr ? 'self gate --pr reads PR files through gh on this host'
4449
+ : !opts.base ? 'self gate without --base measures uncommitted files on this host'
4450
+ : realpathSync(repo) !== realpathSync(process.cwd()) ? 'self gate run from a sub-directory' : undefined;
4451
+ // origin/main is pinned to this machine's origin/main on the host; any other base is passed as its resolved sha.
4452
+ const remoteArgv = (sha: (ref: string) => string) => ['bun', 'bin/elanous.mjs', 'self', 'gate', '--local', ...(opts.changed ? ['--changed'] : []),
4453
+ ...(opts.base ? ['--base', opts.base === 'origin/main' ? opts.base : sha(opts.base)] : []), ...(shards !== undefined ? ['--shards', String(shards)] : [])];
4454
+ process.exitCode = await dispatchHeavyCheck({
4455
+ tool: 'self-gate', repo, installPwa: true, remoteArgv, refs: opts.base ? [opts.base] : [],
4456
+ localOnlyReason,
4457
+ flags: { remote: typeof opts.remote === 'string' ? opts.remote : opts.remote ? true : undefined, local: !!opts.local },
4458
+ runLocal,
4459
+ });
4347
4460
  } catch (error) {
4348
- ui.error(`self gate failed: ${error instanceof Error ? error.message : String(error)}`);
4349
- process.exitCode = 1;
4461
+ ui.error(`self gate dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
4462
+ process.exitCode = runLocal();
4350
4463
  }
4351
4464
  });
4352
4465
 
@@ -8599,10 +8712,15 @@ program
8599
8712
  }
8600
8713
  if (opts.tools) announceChatToolsCompatibility();
8601
8714
  const cfg = getUserConfig();
8602
- if (needsOnboarding(cfg)) {
8603
- if (cfg.onboarding.webFirst === false) ui.info('No config yet — launching setup wizard first.');
8604
- if (!await runFirstSetupForCli(cfg, true)) return;
8605
- }
8715
+ if (await firstChatGate(cfg, {
8716
+ isTTY: process.stdin.isTTY === true,
8717
+ runFirstRun: async (deps) => (await import('./onboarding/first-run.js')).runFirstRun(deps),
8718
+ runInteractive: () => {
8719
+ if (cfg.onboarding.webFirst === false) ui.info('No config yet — launching setup wizard first.');
8720
+ return runFirstSetupForCli(cfg, true);
8721
+ },
8722
+ print: (line) => console.log(line),
8723
+ }) === 'stop') return;
8606
8724
  const refreshed = reloadUserConfig();
8607
8725
  // ★ 관측갭 수리(2026-07-21·제1원칙·트랙A) — self-implement 자식 goal-loop(`chat --goal-loop`)은 데몬과
8608
8726
  // 별개 독립 프로세스라 nexus StoreSink 를 상속 안 한다. 부모 `self implement`(위 self 커맨드)는 sink 를