elanous 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -0,0 +1,114 @@
1
+ // Nested elanous launch policy.
2
+ //
3
+ // Codex children drop parent env under shell_environment_policy inherit=core, so a
4
+ // depth marker that only lives in the environment never arrives. Depth is therefore
5
+ // carried on the codex argv (`-c shell_environment_policy.set.ELANOUS_NESTED_DEPTH=<n>`).
6
+ //
7
+ // Unset ELANOUS_NESTED_DEPTH is depth 0 (the outer launcher). A missing marker is
8
+ // not "depth unknown" and does not refuse an ordinary mission spawn.
9
+ // Refusal for a backend that cannot carry the marker applies only when a nested
10
+ // elanous launch was actually requested (allow flag or config allow).
11
+ //
12
+ // Only depth 0 (the outer launcher) may allow a nested elanous launch. At depth >= 1
13
+ // an allow flag is ignored and the launch stays refused.
14
+
15
+ import { debug } from '../debug/log.js';
16
+
17
+ export const ELANOUS_NESTED_DEPTH_ENV = 'ELANOUS_NESTED_DEPTH';
18
+
19
+ /** Codex config key that sets the depth marker inside the child's shell policy. */
20
+ export const CODEX_NESTED_DEPTH_CONFIG_KEY = `shell_environment_policy.set.${ELANOUS_NESTED_DEPTH_ENV}`;
21
+
22
+ export type NestedElanousBackendName = 'codex' | 'claude' | 'gemini' | 'grok' | 'aside' | (string & {});
23
+
24
+ export interface NestedElanousDecision {
25
+ readonly allowed: boolean;
26
+ readonly depth: number;
27
+ /** True when depth >= 1 supplied allow and it was ignored. */
28
+ readonly allowIgnored: boolean;
29
+ readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored';
30
+ }
31
+
32
+ /** Current process depth from the nested-depth marker. Unset or unreadable = 0 (outer launcher). */
33
+ export function readNestedElanousDepth(env: Record<string, string | undefined> = process.env): number {
34
+ const raw = env[ELANOUS_NESTED_DEPTH_ENV];
35
+ if (raw === undefined || raw.trim() === '') return 0;
36
+ if (!/^\d+$/.test(raw.trim())) return 0;
37
+ return Number(raw);
38
+ }
39
+
40
+ /** True when this backend can carry the depth marker to its child. Only codex can. */
41
+ export function backendCarriesNestedDepth(backendName: string): boolean {
42
+ return backendName === 'codex';
43
+ }
44
+
45
+ /**
46
+ * Codex argv fragment that sets the child's depth marker.
47
+ * `depth` is the child's depth (parent + 1). Non-codex backends return [] — they cannot carry it.
48
+ */
49
+ export function nestedDepthCodexArgs(backendName: string, childDepth: number): string[] {
50
+ if (backendName !== 'codex') return [];
51
+ if (!Number.isInteger(childDepth) || childDepth < 0) return [];
52
+ return ['-c', `${CODEX_NESTED_DEPTH_CONFIG_KEY}=${childDepth}`];
53
+ }
54
+
55
+ /**
56
+ * Allow decision. Depth 0 allows only when `allow` is true.
57
+ * Depth >= 1 always refuses; a supplied allow is ignored and logged.
58
+ */
59
+ export function decideNestedElanousLaunch(input: {
60
+ readonly depth: number;
61
+ readonly allow?: boolean;
62
+ }): NestedElanousDecision {
63
+ const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
64
+ if (depth >= 1) {
65
+ if (input.allow === true) {
66
+ debug.log('agent-mission.nested', 'allow-ignored', { depth });
67
+ return { allowed: false, depth, allowIgnored: true, reason: 'allow-ignored' };
68
+ }
69
+ return { allowed: false, depth, allowIgnored: false, reason: 'nested-refused' };
70
+ }
71
+ if (input.allow === true) {
72
+ return { allowed: true, depth, allowIgnored: false, reason: 'depth-0-allow' };
73
+ }
74
+ return { allowed: false, depth, allowIgnored: false, reason: 'depth-0' };
75
+ }
76
+
77
+ /** Config `harness.nestedElanous`. Only `'allow'` counts, and only at depth 0. */
78
+ export function configAllowsNestedElanous(
79
+ config: { harness?: { nestedElanous?: string } } | undefined,
80
+ depth: number,
81
+ ): boolean {
82
+ if (depth !== 0) return false;
83
+ return config?.harness?.nestedElanous === 'allow';
84
+ }
85
+
86
+ /**
87
+ * Gate a nested elanous launch.
88
+ *
89
+ * Ordinary mission spawn is not this gate: call it only when a nested elanous
90
+ * launch was requested. A backend that cannot carry the depth marker is then
91
+ * refused (`depth-unknown`) — including depth 0 with allow — because the child
92
+ * would not know its own depth. Unset depth is 0, not unknown.
93
+ *
94
+ * `allow` is the flag. Config allow counts only at depth 0 and never overrides
95
+ * a depth >= 1 refusal.
96
+ */
97
+ export function gateNestedElanousLaunch(input: {
98
+ readonly backendName: string;
99
+ readonly depth: number;
100
+ /** True only when this call is gating a requested nested elanous launch. */
101
+ readonly nestedLaunchRequested: boolean;
102
+ readonly allow?: boolean;
103
+ readonly configAllow?: boolean;
104
+ }): NestedElanousDecision {
105
+ const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
106
+ if (!input.nestedLaunchRequested) {
107
+ return decideNestedElanousLaunch({ depth, allow: false });
108
+ }
109
+ if (!backendCarriesNestedDepth(input.backendName)) {
110
+ return { allowed: false, depth, allowIgnored: false, reason: 'depth-unknown' };
111
+ }
112
+ const allow = input.allow === true || (depth === 0 && input.configAllow === true);
113
+ return decideNestedElanousLaunch({ depth, allow });
114
+ }
package/src/hq/hq.ts CHANGED
@@ -206,7 +206,7 @@ function mutate(r: Resolved, decide: (record: LeaseRecord | null, now: number) =
206
206
  }
207
207
 
208
208
  export type LeaseAction = 'acquire' | 'renew' | 'status' | 'release';
209
- export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: string } = {}) {
209
+ export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: string; expectedHolder?: string; expectedGeneration?: number } = {}) {
210
210
  const r0 = resolve(deps);
211
211
  const r = opts.host ? { ...r0, me: opts.host } : r0;
212
212
  if (action === 'status') {
@@ -215,9 +215,15 @@ export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: s
215
215
  noteSeen(r, record);
216
216
  return { ok: true as const, action, record, ageSeconds: record ? now - record.renewedAt : null, expired: record ? leaseExpired(record, now) : null };
217
217
  }
218
+ if (action === 'release' && ((opts.expectedHolder === undefined) !== (opts.expectedGeneration === undefined)
219
+ || (opts.expectedGeneration !== undefined && (!Number.isSafeInteger(opts.expectedGeneration) || opts.expectedGeneration < 1))
220
+ || (opts.expectedHolder !== undefined && !opts.expectedHolder)))
221
+ throw new Error('hq lease release: expected holder and positive generation must be supplied together');
218
222
  const local = readLocal(r.localPath);
223
+ const expected = opts.expectedHolder !== undefined && opts.expectedGeneration !== undefined
224
+ ? { holder: opts.expectedHolder, generation: opts.expectedGeneration } : undefined;
219
225
  const result = mutate(r, (record, now) => action === 'acquire' ? decideAcquire(record, r.me, now, r.ttl)
220
- : action === 'renew' ? decideRenew(record, r.me, local.generation, now) : decideRelease(record, r.me));
226
+ : action === 'renew' ? decideRenew(record, r.me, local.generation, now) : decideRelease(record, r.me, expected));
221
227
  noteSeen(r, result.record);
222
228
  if (result.ok && action !== 'release') writeLocal(r.localPath, { holder: result.record.holder, generation: result.record.generation, confirmedAt: r.now(), ttlSeconds: result.record.ttlSeconds });
223
229
  if (result.ok && action === 'release') writeLocal(r.localPath, { ...local, holder: undefined, confirmedAt: undefined });
package/src/hq/lease.ts CHANGED
@@ -39,8 +39,8 @@ export const leaseExpired = (record: LeaseRecord, now: number): boolean => recor
39
39
  /** acquire: only when there is no lease or it has expired; a new holder gets the next generation. */
40
40
  export function decideAcquire(record: LeaseRecord | null, me: string, now: number, ttlSeconds = DEFAULT_TTL_SECONDS):
41
41
  { ok: true; next: LeaseRecord } | { ok: false; reason: string } {
42
- if (record && record.holder === me && !leaseExpired(record, now)) return { ok: true, next: { ...record, renewedAt: now, ttlSeconds } };
43
- if (record && !leaseExpired(record, now)) return { ok: false, reason: `held by ${record.holder} (generation ${record.generation})` };
42
+ if (record && record.renewedAt !== 0 && record.holder === me && !leaseExpired(record, now)) return { ok: true, next: { ...record, renewedAt: now, ttlSeconds } };
43
+ if (record && record.renewedAt !== 0 && !leaseExpired(record, now)) return { ok: false, reason: `held by ${record.holder} (generation ${record.generation})` };
44
44
  return { ok: true, next: { holder: me, generation: (record?.generation ?? 0) + 1, acquiredAt: now, renewedAt: now, ttlSeconds, views: {}, dualUnreachableStreak: 0 } };
45
45
  }
46
46
 
@@ -52,8 +52,10 @@ export function decideRenew(record: LeaseRecord | null, me: string, generation:
52
52
  return { ok: true, next: { ...record, renewedAt: now, dualUnreachableStreak: 0 } };
53
53
  }
54
54
 
55
- export function decideRelease(record: LeaseRecord | null, me: string): { ok: true; next: LeaseRecord } | { ok: false; reason: string } {
55
+ export function decideRelease(record: LeaseRecord | null, me: string, expected?: { holder: string; generation: number }): { ok: true; next: LeaseRecord } | { ok: false; reason: string } {
56
56
  if (!record || record.holder !== me) return { ok: false, reason: record ? `held by ${record.holder}` : 'no lease' };
57
+ if (expected && (record.holder !== expected.holder || record.generation !== expected.generation))
58
+ return { ok: false, reason: `lease changed: expected ${expected.holder} generation ${expected.generation}, observed ${record.holder} generation ${record.generation}` };
57
59
  return { ok: true, next: { ...record, renewedAt: 0 } };
58
60
  }
59
61
 
@@ -0,0 +1,185 @@
1
+ import { appendFileSync, mkdirSync, readSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
5
+ import { getUserConfig } from '../user-config.js';
6
+ import { defaultSshRunner, localShellRunner, parseLease, sshLeaseStore, type LeaseStore, type SshRunner } from './lease.js';
7
+ import { promoteHq, type PromoteResult } from './promote.js';
8
+
9
+ type Direction = 'promote' | 'handback';
10
+ export interface TransferStep { step: string; status: 'done' | 'failed' | 'ready'; measurement: string }
11
+ export interface TransferResult { ok: boolean; direction: Direction; from: string; to: string; runId: string; journal: string | null; steps: TransferStep[] }
12
+ export interface TransferDeps {
13
+ store: LeaseStore;
14
+ run: SshRunner;
15
+ confirm: (question: string) => boolean;
16
+ promote: (host: string) => PromoteResult;
17
+ journalDir: string;
18
+ }
19
+
20
+ const aliases: Record<string, string[]> = { mbp: ['mbp', 'MacBookProM5'], node-b: ['node-b', 'MacStudioB1'] };
21
+ const valid = (alias: string, holder: string) => aliases[alias]?.includes(holder) ?? false;
22
+ const leaseCommand = (action: 'release' | 'acquire', expected?: { holder: string; generation: number }) =>
23
+ `elanous hq lease ${action}${expected ? ` --expected-holder '${expected.holder}' --expected-generation ${expected.generation}` : ''} --json`;
24
+ const identityCommand = `python3 - <<'PY'
25
+ import json, pathlib
26
+ home=pathlib.Path.home()
27
+ local=home/'.elanous-hq/host'
28
+ config=home/'.elanous-hqcfg/config.json'
29
+ name=local.read_text().strip() if local.exists() else json.loads(config.read_text())['hq']['hostName']
30
+ print(name)
31
+ PY`;
32
+ const cronCommand = `crontab -l && printf '\\n---HQ-AUDIT---\\n' && elanous hq fence-audit --json`;
33
+ const fenceCommand = `elanous hq fence --role cron -- sh -c 'printf "HQ_FENCE_ALLOWED:%s\\n" "$ELANOUS_HQ_GENERATION"'`;
34
+
35
+ export function defaultTransferDeps(): TransferDeps {
36
+ const arbiter = getUserConfig().hq?.arbiter ?? 'cloud-vm';
37
+ return {
38
+ store: sshLeaseStore(arbiter, arbiter === 'local' ? localShellRunner : defaultSshRunner),
39
+ run: defaultSshRunner,
40
+ confirm: (question) => {
41
+ if (!process.stdin.isTTY) return false;
42
+ process.stderr.write(`${question}\nType TRANSFER to continue: `);
43
+ const buffer = Buffer.alloc(128);
44
+ const count = requireRead(buffer);
45
+ return buffer.toString('utf8', 0, count).trim() === 'TRANSFER';
46
+ },
47
+ promote: (host) => promoteHq(host, true),
48
+ journalDir: join(getElanousConfigDir(), 'hq', 'transfers'),
49
+ };
50
+ }
51
+
52
+ function requireRead(buffer: Buffer): number {
53
+ // Read one terminal line without treating EOF or a non-interactive caller as consent.
54
+ let size = 0;
55
+ while (size < buffer.length) {
56
+ const n = requireReadByte(buffer, size);
57
+ if (n === 0) break;
58
+ size += n;
59
+ if (buffer[size - 1] === 10) break;
60
+ }
61
+ return size;
62
+ }
63
+
64
+ function requireReadByte(buffer: Buffer, offset: number): number {
65
+ return readSync(0, buffer, offset, 1, null);
66
+ }
67
+
68
+ export function transferHq(direction: Direction, apply = false, deps: TransferDeps = defaultTransferDeps()): TransferResult {
69
+ const from = direction === 'promote' ? 'mbp' : 'node-b';
70
+ const to = direction === 'promote' ? 'node-b' : 'mbp';
71
+ const runId = randomUUID();
72
+ const journal = apply ? join(deps.journalDir, `${runId}.jsonl`) : null;
73
+ const steps: TransferStep[] = [];
74
+ const result = (ok: boolean): TransferResult => ({ ok, direction, from, to, runId, journal, steps });
75
+ const record = (step: string, status: TransferStep['status'], measurement: string) => {
76
+ const row = { step, status, measurement };
77
+ steps.push(row);
78
+ if (journal) {
79
+ mkdirSync(deps.journalDir, { recursive: true, mode: 0o700 });
80
+ appendFileSync(journal, `${JSON.stringify({ at: new Date().toISOString(), runId, direction, from, to, ...row })}\n`, { mode: 0o600 });
81
+ }
82
+ return status !== 'failed';
83
+ };
84
+ const lease = () => {
85
+ const { now, raw } = deps.store.read();
86
+ const current = parseLease(raw);
87
+ return { current, now };
88
+ };
89
+ const remote = (host: string, command: string) => {
90
+ const r = deps.run(host, command);
91
+ if (r.status !== 0) throw new Error(`${host}: ${r.stderr.trim() || r.stdout.trim() || `exit=${r.status}`}`);
92
+ return r.stdout.trim();
93
+ };
94
+ try {
95
+ const before = lease();
96
+ if (!before.current || !valid(from, before.current.holder) || before.current.renewedAt + before.current.ttlSeconds < before.now) {
97
+ record('① current lease', 'failed', `holder=${before.current?.holder ?? 'none'} generation=${before.current?.generation ?? 'none'} expired=${before.current ? before.current.renewedAt + before.current.ttlSeconds < before.now : true}`);
98
+ return result(false);
99
+ }
100
+ const source = remote(from, identityCommand);
101
+ const target = remote(to, identityCommand);
102
+ if (source !== before.current.holder || !valid(to, target) || source === target) {
103
+ record('② host identities', 'failed', `source=${source} holder=${before.current.holder} target=${target}`);
104
+ return result(false);
105
+ }
106
+ record('① current lease', 'done', `holder=${source} generation=${before.current.generation}`);
107
+ record('② host identities', 'done', `source=${source} target=${target}`);
108
+ for (const host of [from, to]) {
109
+ const inventory = remote(host, cronCommand);
110
+ const marker = '\n---HQ-AUDIT---\n';
111
+ const index = inventory.lastIndexOf(marker);
112
+ if (index < 0) throw new Error(`${host}: missing cron audit`);
113
+ const cron = inventory.slice(0, index);
114
+ const audit = JSON.parse(inventory.slice(index + marker.length)) as unknown;
115
+ const live = cron.split('\n').filter(line => line.trim() && !line.trimStart().startsWith('#'));
116
+ if (!Array.isArray(audit) || audit.length > 0 || !live.some(line => /hq heartbeat\b/.test(line)) || !live.some(line => /hq fence --role cron\b/.test(line))) {
117
+ record(`③ cron fence ${host}`, 'failed', `unfenced=${Array.isArray(audit) ? audit.length : 'invalid'} heartbeat=${live.some(line => /hq heartbeat\b/.test(line))} fenced=${live.some(line => /hq fence --role cron\b/.test(line))}`);
118
+ return result(false);
119
+ }
120
+ record(`③ cron fence ${host}`, 'done', `unfenced=0 heartbeat=true fenced=true`);
121
+ }
122
+ const oldFence = remote(from, fenceCommand);
123
+ const newFence = remote(to, fenceCommand);
124
+ if (oldFence !== `HQ_FENCE_ALLOWED:${before.current.generation}` || newFence.includes('HQ_FENCE_ALLOWED:')) {
125
+ record('④ pre-transfer fence', 'failed', `source=${oldFence} target=${newFence}`);
126
+ return result(false);
127
+ }
128
+ record('④ pre-transfer fence', 'done', `source=allowed generation=${before.current.generation} target=skipped`);
129
+ if (!apply) {
130
+ record('⑤ human confirmation', 'ready', 'dry-run: no writes; --apply requires interactive TRANSFER confirmation');
131
+ return result(true);
132
+ }
133
+ if (!deps.confirm(`Release ${source} generation ${before.current.generation}, acquire ${target}, and switch cron fence? This cannot undo work already sent.`)) {
134
+ record('⑤ human confirmation', 'failed', 'confirmation declined');
135
+ return result(false);
136
+ }
137
+ record('⑤ human confirmation', 'done', 'operator typed TRANSFER');
138
+ const still = lease().current;
139
+ if (!still || still.holder !== source || still.generation !== before.current.generation || still.renewedAt + still.ttlSeconds < deps.store.read().now) {
140
+ record('⑥ release old lease', 'failed', 'lease changed before release');
141
+ return result(false);
142
+ }
143
+ let released: { ok?: boolean; record?: { holder?: string; generation?: number } };
144
+ try { released = JSON.parse(remote(from, leaseCommand('release', { holder: source, generation: before.current.generation }))); }
145
+ catch (error) {
146
+ record('⑥ release old lease', 'failed', error instanceof Error ? error.message : String(error));
147
+ return result(false);
148
+ }
149
+ if (released.ok !== true || released.record?.holder !== source || released.record.generation !== before.current.generation) {
150
+ record('⑥ release old lease', 'failed', 'release not confirmed');
151
+ return result(false);
152
+ }
153
+ record('⑥ release old lease', 'done', `holder=${source} generation=${released.record.generation} released=true`);
154
+ const acquired = JSON.parse(remote(to, leaseCommand('acquire'))) as { ok?: boolean; record?: { holder?: string; generation?: number } };
155
+ const current = lease().current;
156
+ if (acquired.ok !== true || acquired.record?.holder !== target || acquired.record.generation !== before.current.generation + 1 || current?.holder !== target || current.generation !== acquired.record.generation) {
157
+ record('⑦ acquire new lease', 'failed', `expected=${target} generation=${before.current.generation + 1} observed=${current?.holder ?? 'none'} generation=${current?.generation ?? 'none'}; keep old host fenced, recover manually`);
158
+ return result(false);
159
+ }
160
+ record('⑦ acquire new lease', 'done', `holder=${target} generation=${current.generation}`);
161
+ if (direction === 'promote') {
162
+ const promoted = deps.promote(to);
163
+ for (const line of promoted.lines) record(`⑧ promotion ${line.step}`, line.status, line.measurement);
164
+ if (!promoted.ok) return result(false);
165
+ }
166
+ const sourceAfter = remote(from, fenceCommand);
167
+ const targetAfter = remote(to, fenceCommand);
168
+ const observed = lease().current;
169
+ if (sourceAfter.includes('HQ_FENCE_ALLOWED:') || targetAfter !== `HQ_FENCE_ALLOWED:${current.generation}` || observed?.holder !== target || observed.generation !== current.generation) {
170
+ record('⑨ final fence', 'failed', `source=${sourceAfter} target=${targetAfter} lease=${observed?.holder ?? 'none'} generation=${observed?.generation ?? 'none'}`);
171
+ return result(false);
172
+ }
173
+ record('⑨ final fence', 'done', `source=skipped target=allowed generation=${current.generation}`);
174
+ return result(true);
175
+ } catch (error) {
176
+ record('halt', 'failed', error instanceof Error ? error.message : String(error));
177
+ return result(false);
178
+ }
179
+ }
180
+
181
+ export function formatTransfer(result: TransferResult): string {
182
+ return [`hq ${result.direction}: ${result.ok ? 'ok' : 'failed'} · ${result.from} → ${result.to} · run=${result.runId}`,
183
+ ...result.steps.map(s => `${s.step}: ${s.status} · ${s.measurement}`),
184
+ ...(result.journal ? [`journal=${result.journal}`] : [])].join('\n');
185
+ }
package/src/index.ts CHANGED
@@ -27,10 +27,12 @@ import { installDevCompletionGuard } from './self-dev/dev-completion-guard.js';
27
27
  import { buildOrchestrateDecomposePrepareArgs, normalizeOrchestrateRequest, splitOrchestrateGoalTexts } from './self-dev/self-orchestrate-runtime.js';
28
28
  import { CLI_ENTRANCE_BASELINE, evaluateCommandEntranceBaseline } from './self-dev/entrance-baseline.js';
29
29
  import { CLI_HARNESS_DOGFOOD_ENTRANCE, CLI_HARNESS_ORCHESTRATE_ENTRANCE, describeEntranceCommand, listEntrancesWithModelExposure, renderLaunchEntrances, summarizeEntrances, type EntranceId } from './self-dev/entrance-registry.js';
30
+ import { isOldDoorInternalCall, refuseOldDoor } from './self-dev/old-door.js';
30
31
  import { collectCommandEntrances, renderCommandEntrances } from './self-dev/entrance-inventory.js';
31
32
  import { isGoalAuthorFileName } from './self-implement/goal-document.js';
32
33
  import { DEV_PIPELINE_SINK_SURFACE } from './self-implement/self-cli-sink-surface.js';
33
34
  import { registerPrivateLedgerCommands } from './cli/private-ledger-commands.js';
35
+ import { registerKnowCommand } from './cli/know-cli.js';
34
36
  import { registerSeatRequestsCommands } from './cli/seat-requests-cli.js';
35
37
  import { applyTestStateDirFlagFromArgv } from './cli/test-state-dir-flag.js';
36
38
  import { applyTestFlagFromArgv, observeTestFlagOwnership, uncoveredTestFlagPaths, staleTestFlagPaths } from './cli/test-flag.js';
@@ -53,6 +55,7 @@ export { runSchedule, scheduleCreatePlan } from './cli/schedule-cli.js';
53
55
  export type { ScheduleDispatch } from './cli/schedule-cli.js';
54
56
  import { registerReviewLoopOptions, buildReviewLoopOpts } from './agent-mission/review-loop-cli.js';
55
57
  import { registerCapabilitiesCommand } from './agent-mission/capabilities-cli.js';
58
+ import { registerSupervisorMetricsCommand } from './agent-mission/supervisor-metrics-cli.js';
56
59
  import { agentBackendNames } from './agent-mission/driver.js';
57
60
  import { registerWhereCommand } from './cli/where-cli.js';
58
61
  import { registerShadowCommand } from './cli/shadow-cli.js';
@@ -70,6 +73,7 @@ import { registerFlowCommands } from './cli/flow-cli.js';
70
73
  import { registerModelWatchCommand } from './cli/model-watch-cli.js';
71
74
  import { registerIntakeCommands } from './cli/intake-cli.js';
72
75
  import { registerTasksCommands } from './cli/tasks-cli.js';
76
+ import { registerBriefCommands } from './cli/brief-cli.js';
73
77
  import { registerLabelsCommands } from './cli/labels-cli.js';
74
78
  import { registerMsgCommands } from './cli/msg-cli.js';
75
79
  import { registerDoctorCommand } from './cli/doctor-cli.js';
@@ -875,6 +879,7 @@ registerUsageCommand(program);
875
879
  registerLiveCommands(program);
876
880
  registerResearchCommand(program);
877
881
  registerReleaseCommands(program);
882
+ registerKnowCommand(program);
878
883
  registerFreezeCommands(program);
879
884
  registerFlowCommands(program);
880
885
  // HQ-HB · HQ-FENCE (10-04): one writer across mbp · node-b · cloud-vm — lease on the arbiter, 2-of-3 quorum, generation fencing.
@@ -893,24 +898,42 @@ hqCmd.command('host').description('이 머신의 HQ 호스트 이름 설정')
893
898
  });
894
899
  hqCmd.command('lease').argument('<action>', 'acquire|renew|status|release')
895
900
  .option('--host <name>', '이 호스트 이름(기본 ~/.elanous-hq/host · hq.hostName · os.hostname)')
901
+ .option('--expected-holder <name>', 'release CAS: 해제할 임대의 예상 소유자')
902
+ .option('--expected-generation <number>', 'release CAS: 해제할 임대의 예상 세대')
896
903
  .option('--json', 'JSON 출력')
897
- .action(async (action: string, opts: { host?: string; json?: boolean }) => {
904
+ .action(async (action: string, opts: { host?: string; expectedHolder?: string; expectedGeneration?: string; json?: boolean }) => {
898
905
  try {
899
906
  if (!['acquire', 'renew', 'status', 'release'].includes(action)) throw new CliUserError(`hq lease: unknown action ${action}`, 'acquire|renew|status|release');
907
+ if ((opts.expectedHolder !== undefined || opts.expectedGeneration !== undefined) && action !== 'release')
908
+ throw new CliUserError('hq lease: expected holder/generation are only valid for release');
909
+ if ((opts.expectedHolder === undefined) !== (opts.expectedGeneration === undefined)
910
+ || (opts.expectedGeneration !== undefined && (!/^[1-9]\d*$/.test(opts.expectedGeneration) || !Number.isSafeInteger(Number(opts.expectedGeneration)))))
911
+ throw new CliUserError('hq lease release: supply both --expected-holder and --expected-generation (positive integer)');
900
912
  await hqSink();
901
913
  const { hqLease } = await import('./hq/hq.js');
902
- const result = hqLease(action as 'acquire', {}, opts.host ? { host: opts.host } : {});
914
+ const result = hqLease(action as 'acquire', {}, {
915
+ ...(opts.host ? { host: opts.host } : {}),
916
+ ...(opts.expectedHolder !== undefined ? { expectedHolder: opts.expectedHolder, expectedGeneration: Number(opts.expectedGeneration) } : {}),
917
+ });
903
918
  if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
904
919
  else console.log(result.ok ? `hq lease ${action}: ok${'record' in result && result.record ? ` · holder ${result.record.holder} · generation ${result.record.generation}` : ''}` : `hq lease ${action}: refused — ${'reason' in result ? result.reason : ''}`);
905
920
  if (!result.ok) process.exitCode = 2;
906
921
  } catch (error) { if (error instanceof CliUserError) throw error; console.error(`hq lease: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
907
922
  });
908
923
  hqCmd.command('promote').description('HQ 승격 런북 (기본 dry-run · --apply 일 때만 변경)')
909
- .requiredOption('--to <host>', '승격 대상 호스트')
910
- .option('--apply', '대기 사본을 승격 우주에 반영하고 넥서스·rescue 실행')
924
+ .option('--to <host>', '기존 임대 취득 후 승격 대상 호스트 (생략하면 mbp → node-b 전환 포함)')
925
+ .option('--apply', '임대 이전 및 승격 실행 · 터미널 확인 필수 (기존 --to 경로는 임대 보유 필요)')
911
926
  .option('--json', 'JSON 출력')
912
927
  .action(async (opts: { to: string; apply?: boolean; json?: boolean }) => {
913
928
  try {
929
+ if (!opts.to) {
930
+ const { transferHq, formatTransfer } = await import('./hq/transfer.js');
931
+ const result = transferHq('promote', opts.apply === true);
932
+ if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
933
+ else console.log(formatTransfer(result));
934
+ if (!result.ok) process.exitCode = 1;
935
+ return;
936
+ }
914
937
  const { promoteHq, formatPromote } = await import('./hq/promote.js');
915
938
  const result = promoteHq(opts.to, opts.apply === true);
916
939
  if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
@@ -918,6 +941,18 @@ hqCmd.command('promote').description('HQ 승격 런북 (기본 dry-run · --appl
918
941
  if (!result.ok) process.exitCode = 1;
919
942
  } catch (error) { console.error(`hq promote: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
920
943
  });
944
+ hqCmd.command('handback').description('node-b → mbp 임대·크론 울타리 복귀 (기본 읽기 전용)')
945
+ .option('--apply', '각 검사를 통과한 뒤 터미널 확인을 받고 이전 실행')
946
+ .option('--json', '단계별 결과·저널 경로 JSON 출력')
947
+ .action(async (opts: { apply?: boolean; json?: boolean }) => {
948
+ try {
949
+ const { transferHq, formatTransfer } = await import('./hq/transfer.js');
950
+ const result = transferHq('handback', opts.apply === true);
951
+ if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
952
+ else console.log(formatTransfer(result));
953
+ if (!result.ok) process.exitCode = 1;
954
+ } catch (error) { console.error(`hq handback: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
955
+ });
921
956
  hqCmd.command('heartbeat').description('보유자는 갱신 · 아니면 대기(보유자에 닿는지 중재자에 보고)').option('--json', 'JSON 출력')
922
957
  .action(async (opts: { json?: boolean }) => {
923
958
  try {
@@ -967,9 +1002,59 @@ registerSeatRequestsCommands(seatCmd);
967
1002
  seatCmd.command('answer <askId> <answer>').description('자리 질문에 답을 남기고 원래 표면으로 회신 예약')
968
1003
  .action(async (askId: string, answer: string) => {
969
1004
  try {
970
- const { answerSeatAsk } = await import('./seat-dispatch/seat-ask.js');
1005
+ try { await (await import('./domains/standalone-log-sink.js')).registerStandaloneLogSink('seat-answer'); } catch { /* file trail remains available */ }
1006
+ const { debug } = await import('./debug/log.js');
1007
+ const { answerSeatAsk, deliverSeatAnswers, seatAskDeliveryInProgress, seatAskReplyInfo } = await import('./seat-dispatch/seat-ask.js');
971
1008
  answerSeatAsk(askId, answer);
1009
+ const { origin, askedAt, answeredAt } = seatAskReplyInfo(askId);
972
1010
  console.log(`자리 답변 기록: ${askId}`);
1011
+ if (origin.channel !== 'telegram') {
1012
+ debug.log('seat.ask', 'reply-no-target', { id: askId, via: origin.channel });
1013
+ console.log(`전달 대상 없음: ${askId} (${origin.channel})`);
1014
+ return;
1015
+ }
1016
+ const token = process.env.TELEGRAM_BOT_TOKEN?.trim();
1017
+ const elapsedMs = askedAt === null ? null : Math.max(0, answeredAt - askedAt);
1018
+ if (!token) {
1019
+ debug.log('seat.ask', 'reply-failed', { id: askId, via: 'telegram', reason: 'token-missing', elapsedMs });
1020
+ console.log(`텔레그램 토큰 없음 (TELEGRAM_BOT_TOKEN): ${askId} · 회신 대기`);
1021
+ return;
1022
+ }
1023
+ const botId = token.split(':')[0];
1024
+ if (origin.botId && origin.botId !== botId) {
1025
+ debug.log('seat.ask', 'reply-no-target', { id: askId, via: 'telegram', reason: 'bot-mismatch' });
1026
+ console.log(`전달 대상 없음: ${askId} (수신 봇 불일치) · 회신 대기`);
1027
+ return;
1028
+ }
1029
+ try {
1030
+ let sent = false;
1031
+ await deliverSeatAnswers({ askId, channel: 'telegram', botId, send: async (target, text) => {
1032
+ if (target.channel !== 'telegram') throw new Error('seat ask belongs to another surface');
1033
+ const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1034
+ method: 'POST', headers: { 'content-type': 'application/json' },
1035
+ body: JSON.stringify({ chat_id: target.chatId, text, reply_to_message_id: target.messageId,
1036
+ ...(target.threadId === undefined ? {} : { message_thread_id: target.threadId }) }),
1037
+ });
1038
+ const result = await response.json() as { ok?: boolean; description?: string };
1039
+ if (!response.ok || result.ok !== true) throw new Error(result.description ?? `Telegram HTTP ${response.status}`);
1040
+ sent = true;
1041
+ } });
1042
+ if (!sent) {
1043
+ if (seatAskDeliveryInProgress(askId)) {
1044
+ debug.log('seat.ask', 'reply-in-progress', { id: askId, via: 'telegram', reason: 'claimed-by-other-worker' });
1045
+ console.log(`다른 작업자가 전달 중: ${askId}`);
1046
+ } else {
1047
+ debug.log('seat.ask', 'reply-no-target', { id: askId, via: 'telegram', reason: 'not-claimed' });
1048
+ console.log(`전달 대상 없음: ${askId} (회신 대기)`);
1049
+ }
1050
+ return;
1051
+ }
1052
+ debug.log('seat.ask', 'reply-sent', { id: askId, via: 'telegram', elapsedMs });
1053
+ console.log(`텔레그램 회신 전달: ${askId} · 지연 ${elapsedMs === null ? '미상' : `${elapsedMs}ms`}`);
1054
+ } catch (error) {
1055
+ debug.log('seat.ask', 'reply-failed', { id: askId, via: 'telegram', reason: error instanceof Error ? error.message : String(error), elapsedMs });
1056
+ console.log(`텔레그램 회신 실패: ${askId} · 회신 대기`);
1057
+ }
973
1058
  } catch (error) { console.error(`seat answer: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
974
1059
  });
975
1060
  seatCmd.command('loop').requiredOption('--seat <SEAT>', 'MK|OP|TC|UX')
@@ -1178,8 +1263,10 @@ program.command('self-update')
1178
1263
  .option('--keep <n>', '설치 뒤 남길 최근 판 수(설치본은 current·직전 판, 체크아웃은 current·데몬 판 보호 · 0 이면 정리 안 함)', '3')
1179
1264
  .option('--alert', '실패(exit≠0)를 알림으로도 보냄 — 크론(무인) 실행용')
1180
1265
  .option('--skip-pwa-build', '체크아웃 설치 전 PWA 빌드를 명시적으로 건너뜀')
1266
+ .option('--dev', '판 사이 dev 빌드 설치 — versions/<판>-dev.<sha12> · install.json channel=dev · 이전 versionDir 보존 (사람 승인 · 자동 아님)')
1267
+ .option('--rollback', 'install.json 의 이전 versionDir 로 current 를 되돌린다 (이전이 없으면 거부)')
1181
1268
  .option('--auto <on|off|status>', '자동 갱신 — macOS launchd · Linux systemd 타이머가 매일 04:17 에 `self-update --restart --alert` (크론이 이미 부르면 켜지 않는다)')
1182
- .action(async (opts: { from?: string; version?: string; restart?: boolean; json?: boolean; keep?: string; alert?: boolean; skipPwaBuild?: boolean; auto?: string }) => {
1269
+ .action(async (opts: { from?: string; version?: string; restart?: boolean; json?: boolean; keep?: string; alert?: boolean; skipPwaBuild?: boolean; dev?: boolean; rollback?: boolean; auto?: string }) => {
1183
1270
  if (opts.auto !== undefined) {
1184
1271
  if (!['on', 'off', 'status'].includes(opts.auto)) { console.error(`--auto 는 on · off · status 중 하나: ${opts.auto}`); process.exitCode = 2; return; }
1185
1272
  const { runAutoUpdate } = await import('./cli/update-auto.js');
@@ -4259,6 +4346,12 @@ selfCmd
4259
4346
  .option('--supervise-rounds <n>', '슈퍼바이저 재개 라운드 상한 (기본 3 · 끄려면 --no-supervise)')
4260
4347
  .option('--json', '구조화 출력 {stage, ok, branch, worktree, prUrl?, merged?}')
4261
4348
  .action(async (parts: string[], opts: { featureFile?: string; base?: string; draft?: boolean; openPr?: boolean; autoMerge?: boolean; autoReview?: boolean; maxWait?: string; json?: boolean; plan?: boolean; enhance?: boolean; ground?: boolean; observeOnly?: boolean }) => {
4349
+ if (!isOldDoorInternalCall('self-implement')) {
4350
+ const refused = refuseOldDoor('self-implement');
4351
+ debug.log('harness.entrance', 'old-door-refused', { door: refused.door, caller: refused.caller });
4352
+ ui.error(refused.message);
4353
+ process.exit(refused.exitCode);
4354
+ }
4262
4355
  if (opts.featureFile && parts.length > 0) { ui.error('--feature-file 과 위치 인자 feature 는 함께 줄 수 없다'); process.exit(2); }
4263
4356
  let feature: string;
4264
4357
  try { feature = (opts.featureFile ? readFileSync(opts.featureFile, 'utf8') : parts.join(' ')).trim(); }
@@ -4418,6 +4511,12 @@ const selfOrchestrateCmd = selfCmd
4418
4511
  .option('--help-all', '모든 orchestrate 옵션 표시')
4419
4512
  .action(function (this: Command, parts: string[], opts: { goalFile?: string[]; concurrency?: string; autoMerge?: boolean; autoReview?: boolean; openPr?: boolean; base?: string; teardown?: boolean; resume?: string; board?: boolean; decompose?: boolean; fabricDecompose?: boolean; maxTasks?: string; supervise?: boolean; superviseRounds?: string; json?: boolean }) {
4420
4513
  return (async () => {
4514
+ if (!isOldDoorInternalCall('self-orchestrate')) {
4515
+ const refused = refuseOldDoor('self-orchestrate');
4516
+ debug.log('harness.entrance', 'old-door-refused', { door: refused.door, caller: refused.caller });
4517
+ ui.error(refused.message);
4518
+ process.exit(refused.exitCode);
4519
+ }
4421
4520
  // 위치 인자는 종전처럼 분리하고 파일 입력은 파일당 한 골이다.
4422
4521
  let inputGoals: ReturnType<typeof readSelfOrchestrateGoals>;
4423
4522
  try { inputGoals = readSelfOrchestrateGoals(parts, opts.goalFile); }
@@ -5943,6 +6042,7 @@ agentCmd.hook('preAction', async (_thisCommand, actionCommand) => {
5943
6042
  });
5944
6043
 
5945
6044
  registerCapabilitiesCommand(agentCmd);
6045
+ registerSupervisorMetricsCommand(agentCmd);
5946
6046
 
5947
6047
  agentCmd.command('install-from-docs <tool>')
5948
6048
  .description('공식 설치 문서를 읽고 안전한 설치 줄을 선택한다 — sudo는 사람에게 넘긴다')
@@ -6814,6 +6914,11 @@ const selfDevCmd = program
6814
6914
  // ⭐ `--ask`/`--say` — 발사 절차 넷을 «한 로직»으로. 저작 → 전제 검사 → 발사.
6815
6915
  // ⛔ 여기서 저작한 골 파일을 아래 `--file` 자리에 그대로 넘긴다(새 실행 경로를 만들지 않는다).
6816
6916
  let askAuthoredFile: string | undefined;
6917
+ if (opts.ask !== undefined && !isOldDoorInternalCall('dev-ask')) {
6918
+ const refused = refuseOldDoor('dev-ask');
6919
+ debug.log('harness.entrance', 'old-door-refused', { door: refused.door, caller: refused.caller });
6920
+ throw new DevPipelineError(refused.message);
6921
+ }
6817
6922
  const retirementInput = opts.ask !== undefined
6818
6923
  ? { kind: 'ask' as const, value: opts.ask }
6819
6924
  : opts.say !== undefined
@@ -7538,6 +7643,7 @@ missionCmd
7538
7643
  });
7539
7644
 
7540
7645
  registerTasksCommands(program);
7646
+ registerBriefCommands(program);
7541
7647
  registerLabelsCommands(program);
7542
7648
  registerMsgCommands(program);
7543
7649