elanous 0.2.16 → 0.2.17

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 (228) 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-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +13 -13
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +13 -13
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +13 -13
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +13 -13
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +13 -13
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +13 -13
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +13 -13
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +13 -13
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +13 -13
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +13 -13
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +13 -13
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +13 -13
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +13 -13
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +13 -13
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +13 -13
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +14 -14
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +13 -13
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +13 -13
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +13 -13
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +13 -13
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +13 -13
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +13 -13
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +13 -13
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +13 -13
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +13 -13
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +13 -13
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +13 -13
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +13 -13
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +13 -13
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +13 -13
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +13 -13
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +13 -13
  143. package/graphs/intake/intake-daily.yaml +9 -2
  144. package/graphs/intake/news-sources.yaml +4 -0
  145. package/graphs/intake/recipes.yaml +6 -1
  146. package/package.json +1 -1
  147. package/scripts/__fixtures__/intake-news.xml +7 -0
  148. package/scripts/intake-news.ts +169 -0
  149. package/scripts/release-loop/docs-land-node.ts +69 -10
  150. package/scripts/release-loop/publish-node.ts +2 -2
  151. package/scripts/release-loop/release-docs.ts +134 -0
  152. package/scripts/release-loop/version-node.ts +7 -4
  153. package/scripts/release-story/draft.ts +42 -14
  154. package/src/agent-mission/driver.ts +53 -10
  155. package/src/autopilot/track-agent.ts +5 -3
  156. package/src/cli/autopilot-cli.ts +66 -0
  157. package/src/cli/decisions-cli.ts +4 -1
  158. package/src/cli/intake-cli.ts +1 -1
  159. package/src/cli/log-event-names.ts +1 -0
  160. package/src/cli/pr-cli.ts +12 -1
  161. package/src/cli/provider-cli.ts +14 -0
  162. package/src/cli/self-update-canary.ts +46 -0
  163. package/src/cli/self-update.ts +200 -1
  164. package/src/context-card/index.ts +45 -0
  165. package/src/context-card/task.ts +76 -0
  166. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  167. package/src/decisions/decision-ledger.ts +10 -1
  168. package/src/harness/harness-cli-command.ts +142 -10
  169. package/src/harness/harness-pod-dispatch.ts +11 -0
  170. package/src/harness/harness-queue.ts +86 -7
  171. package/src/harness/nested-elanous-policy.ts +23 -4
  172. package/src/hq/fence-wrapper.ts +123 -0
  173. package/src/hq/hq.ts +8 -0
  174. package/src/index.ts +57 -8
  175. package/src/intake-plane/digest.ts +39 -4
  176. package/src/nexus/api/grid.ts +61 -0
  177. package/src/nexus/api/http-server.ts +19 -1
  178. package/src/nexus/api/task-cards-api.ts +19 -0
  179. package/src/oauth/codex.ts +82 -2
  180. package/src/release-loop/landing-freeze.ts +18 -10
  181. package/src/seat-loop/seat-loop.ts +95 -31
  182. package/src/self-dev/ask-launch-flow.ts +26 -7
  183. package/src/self-dev/run-store.ts +5 -1
  184. package/src/self-dev/run-supervisor.ts +7 -1
  185. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  186. package/src/self-implement/frozen-merges.ts +25 -20
  187. package/src/self-implement/goal-author-clarification.ts +81 -0
  188. package/src/self-implement/goal-author-cli.ts +33 -5
  189. package/src/self-implement/goal-author.ts +88 -12
  190. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  191. package/src/self-implement/goal-document-write.ts +28 -0
  192. package/src/self-implement/heal-intake.ts +4 -0
  193. package/src/self-implement/host-regate.ts +3 -1
  194. package/src/self-implement/orchestrator.ts +19 -1
  195. package/src/task-agent/actions.ts +118 -0
  196. package/src/task-agent/judge.ts +96 -0
  197. package/src/task-cards/card-store.ts +4 -1
  198. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  199. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  200. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  201. package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
  202. package/src/telegram-commands.ts +1 -1
  203. package/src/telegram-loops-command.ts +21 -14
  204. package/src/user-config.ts +25 -3
  205. package/src/version/packed-revision.json +1 -1
  206. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  207. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  208. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  209. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  210. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  211. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
  212. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  213. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
  214. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  215. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  216. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  217. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  218. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  219. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  220. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  221. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
  227. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  228. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -8,8 +8,8 @@
8
8
  // - 증거 게이트(doc 존재 / tsc 0 / test pass)로 완료 판정 후 commit
9
9
  // 재사용: startPty(Bun 네이티브 PTY)·createWorktree·streamLLM·worktreeHasChanges·commitWorktree.
10
10
  import { execFileSync, spawnSync, spawn as spawnChild } from 'node:child_process';
11
- import { homedir } from 'node:os';
12
- import { existsSync, writeFileSync, mkdirSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync, unlinkSync } from 'node:fs';
11
+ import { homedir, tmpdir } from 'node:os';
12
+ import { existsSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync, unlinkSync } from 'node:fs';
13
13
  import { createHash } from 'node:crypto';
14
14
  import { join, relative, resolve, isAbsolute, sep } from 'node:path';
15
15
  import { startPty, mintPtyId, onPtyEvent, type PtyHandle, type StartOpts } from '../pty-shell/registry.js';
@@ -18,7 +18,7 @@ import { NESTED_AGENT_ENV_BLOCKLIST } from '../agent/nested-agent-env.js';
18
18
  import { getNestDepth } from '../agent/nest-depth.js';
19
19
  import { gateNestedElanousLaunch, nestedDepthCodexArgs, readNestedElanousDepth } from '../harness/nested-elanous-policy.js';
20
20
  import { createWorktree, resolveMainRepoRoot, type CreateWorktreeOpts } from '../git-fs/worktree.js';
21
- import { configuredWorktreeRoot } from '../user-config.js';
21
+ import { configuredWorktreeRoot, getUserConfig } from '../user-config.js';
22
22
  import { worktreeHasChanges, commitWorktree, changedFiles } from '../self-implement/seams.js';
23
23
  import { streamLLM, type LLMMessage } from '../llm.js';
24
24
  import { debug } from '../debug/log.js';
@@ -330,6 +330,27 @@ export function resolveBackendSpawn(
330
330
  return { cmd: backend.cmd, args, env, unsetEnv, nestedEnvRemovedCount, forcedEnv };
331
331
  }
332
332
 
333
+ /** Claude's strict MCP override is per-child; never edit the user's Claude settings. */
334
+ export function prepareClaudeChildSpawn(
335
+ spawn: ReturnType<typeof resolveBackendSpawn>,
336
+ options: { readonly backendName: string; readonly depth: number; readonly allowNestedElanous: boolean; readonly tempRoot?: string },
337
+ ): { readonly args: string[]; readonly cleanup: () => void; readonly configPath?: string } {
338
+ // Backend identity, not the command string — an absolute-path claude binary is still the Claude backend.
339
+ if (options.backendName !== 'claude' || (options.depth === 0 && options.allowNestedElanous)) {
340
+ return { args: [...spawn.args], cleanup: () => {} };
341
+ }
342
+ const dir = mkdtempSync(join(options.tempRoot ?? tmpdir(), 'elanous-claude-mcp-'));
343
+ try {
344
+ const configPath = join(dir, 'mcp.json');
345
+ writeFileSync(configPath, JSON.stringify({ mcpServers: {} }), { mode: 0o600 });
346
+ return { args: [...spawn.args, '--strict-mcp-config', '--mcp-config', configPath], configPath,
347
+ cleanup: () => rmSync(dir, { recursive: true, force: true }) };
348
+ } catch (error) {
349
+ rmSync(dir, { recursive: true, force: true });
350
+ throw error;
351
+ }
352
+ }
353
+
333
354
  /**
334
355
  * Refuse a requested nested elanous launch when this backend cannot carry the depth marker.
335
356
  * Unset depth is 0. A normal mission spawn (`nestedLaunchRequested: false`) is never refused here.
@@ -528,6 +549,8 @@ export interface AgentMissionSpec {
528
549
  * Absent means an ordinary mission spawn — never refused for a missing marker.
529
550
  */
530
551
  nestedElanousLaunch?: boolean;
552
+ /** Explicit depth-0 allow from the caller (e.g. `--nested-elanous allow`). Ignored at depth >= 1. Config `harness.nestedElanous` also counts at depth 0. */
553
+ nestedElanousAllow?: boolean;
531
554
  /** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
532
555
  decisionMissionId?: string;
533
556
  /** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
@@ -1574,6 +1597,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1574
1597
  const nestedRefusal = refuseNestedElanousOnBackend({
1575
1598
  backendName: backend.name,
1576
1599
  nestedLaunchRequested: spec.nestedElanousLaunch === true,
1600
+ ...(spec.nestedElanousAllow === true ? { allow: true } : {}),
1601
+ ...(getUserConfig().harness?.nestedElanous === 'allow' ? { configAllow: true } : {}),
1577
1602
  env: process.env,
1578
1603
  });
1579
1604
  if (nestedRefusal.refused) {
@@ -1691,12 +1716,16 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1691
1716
  evidencePath: finalEv.path, evidenceSatisfied: true, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
1692
1717
  }
1693
1718
  env.TERM = 'xterm-256color';
1719
+ // Claude's per-child MCP override is held until the PTY exits, not until the control loop returns.
1720
+ const claudeChild = backend.name === 'claude'
1721
+ ? prepareClaudeChildSpawn(spawnParams, { backendName: backend.name, depth: readNestedElanousDepth(process.env), allowNestedElanous: spec.nestedElanousAllow === true || getUserConfig().harness?.nestedElanous === 'allow' })
1722
+ : undefined;
1694
1723
  // ⭐run-identity — runId was resolved before either headless or PTY spawn.
1695
- debug.log('agent-mission', 'spawn', { agent: backend.name, cmd: `${spawnParams.cmd} ${spawnParams.args.join(' ')}`, scrubbed: backend.scrubEnv ?? [], nestedEnvRemovedCount: spawnParams.nestedEnvRemovedCount, runId });
1724
+ debug.log('agent-mission', 'spawn', { agent: backend.name, cmd: `${spawnParams.cmd} ${(claudeChild?.args ?? spawnParams.args).join(' ')}`, scrubbed: backend.scrubEnv ?? [], nestedEnvRemovedCount: spawnParams.nestedEnvRemovedCount, runId });
1696
1725
  // PTY 정체성 — kind(=backend)·nickname(goto 로 나중 접근)·accessMode='auto'(헤드리스 자율·brain 이 write 소유).
1697
1726
  const ptyOpts = buildAgentMissionPtySpawnOptions({
1698
1727
  backend,
1699
- spawn: { cmd: spawnParams.cmd, args: spawnParams.args, env, unsetEnv: spawnParams.unsetEnv },
1728
+ spawn: { cmd: spawnParams.cmd, args: claudeChild?.args ?? spawnParams.args, env, unsetEnv: spawnParams.unsetEnv },
1700
1729
  workdir: wt.path,
1701
1730
  nickname: spec.nickname ?? spec.branch ?? backend.name,
1702
1731
  });
@@ -1706,6 +1735,11 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1706
1735
  let usageReemitted = false;
1707
1736
  let childSessionId: string | undefined;
1708
1737
  let sessionOutput = '';
1738
+ let claudeCleanupTimer: ReturnType<typeof setInterval> | undefined;
1739
+ const cleanupClaude = (): void => {
1740
+ if (claudeCleanupTimer) clearInterval(claudeCleanupTimer);
1741
+ claudeChild?.cleanup();
1742
+ };
1709
1743
  const offLive = onPtyEvent((ev) => {
1710
1744
  if (ev.id !== ptyOpts.id) return;
1711
1745
  if (ev.type === 'output') {
@@ -1718,6 +1752,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1718
1752
  if (ev.type === 'exit') {
1719
1753
  offLive();
1720
1754
  if (liveTimer) clearInterval(liveTimer);
1755
+ cleanupClaude();
1721
1756
  }
1722
1757
  if (ev.type === 'exit' && backend.name === 'codex' && !usageReemitted) {
1723
1758
  usageReemitted = true;
@@ -1740,9 +1775,17 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1740
1775
  }
1741
1776
  });
1742
1777
  let h: PtyHandle;
1743
- if (deps.isMissionAborted?.()) { offLive(); throw new Error('MISSION_ABORTED'); }
1778
+ if (deps.isMissionAborted?.()) { offLive(); cleanupClaude(); throw new Error('MISSION_ABORTED'); }
1744
1779
  try { h = spawnPty(ptyOpts); }
1745
- catch (error) { offLive(); throw error; }
1780
+ catch (error) { offLive(); cleanupClaude(); throw error; }
1781
+ if (!h.isAlive()) cleanupClaude();
1782
+ else if (claudeChild) {
1783
+ // A test-injected PTY may not emit registry events; keep the config until its handle exits.
1784
+ claudeCleanupTimer = setInterval(() => {
1785
+ if (!h.isAlive()) cleanupClaude();
1786
+ }, 1000);
1787
+ claudeCleanupTimer.unref();
1788
+ }
1746
1789
  if (backend.name === 'codex') observeExternalEvent({ origin: 'codex-agent-mission', kind: 'started',
1747
1790
  summary: 'Codex agent mission started', source: `elanous://agent-mission/${encodeURIComponent(runId)}/${encodeURIComponent(h.id)}` });
1748
1791
  let contextFinished = false;
@@ -1753,8 +1796,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1753
1796
  summary: `Codex agent mission ${ok ? 'completed' : 'failed'}`,
1754
1797
  source: `elanous://agent-mission/${encodeURIComponent(runId)}/${encodeURIComponent(h.id)}` });
1755
1798
  };
1756
- deps.onPtyStarted?.(h, () => { if (liveTimer) clearInterval(liveTimer); offLive(); finishContext(false); });
1757
- if (deps.isMissionAborted?.()) { finishContext(false); return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
1799
+ deps.onPtyStarted?.(h, () => { if (liveTimer) clearInterval(liveTimer); offLive(); finishContext(false); cleanupClaude(); });
1800
+ if (deps.isMissionAborted?.()) { finishContext(false); cleanupClaude(); return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
1758
1801
  rounds: 0, evidencePath: null, committed: false, usedOmniCrawl: false, detail: 'aborted' }; }
1759
1802
  const executorRef = buildExecutorPtyRef({
1760
1803
  ptyId: h.id,
@@ -2275,7 +2318,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2275
2318
  ptyId: h.id, webUrl: webAddress.webUrl, driveVerdict: driveVerdict.kind,
2276
2319
  detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : driveVerdict.kind === 'success-unverified' && finalEv.ok ? '완료(증거)' : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2277
2320
  };
2278
- } finally { stopLive(); finishContext(false); }
2321
+ } finally { stopLive(); finishContext(false); if (!h.isAlive() || deps.isMissionAborted?.()) cleanupClaude(); }
2279
2322
  }
2280
2323
 
2281
2324
  /** @deprecated codex 특정 이름 — runAgentMission 을 쓰라(codex 는 기본 backend). */
@@ -47,7 +47,9 @@ const runCli = async (args: string[]): Promise<{ stdout: string }> => {
47
47
  const { argv, env } = universeLaunch(args);
48
48
  return exec('bun', argv, { cwd: repoDir(), env, timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' });
49
49
  };
50
- const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|force|pay|purchase|secret|credential|sudo|rm\s+-rf|config\s+set)\b|(?:^|\s)--prod\b|삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|자격|비밀|강제/i;
50
+ // A work topic containing a verb is not necessarily an instruction; the guard matches executable requests.
51
+ // Security words (secret · credential · force · 자격 · 비밀 · 강제) stay blocked anywhere — topic or request (UX harvest).
52
+ const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|pay|purchase|sudo)\b(?=\s+(?:the\s+(?:branch|site|app|release|file|record)\b|(?:this\s+)?branch\b|(?:to|into)\s+main\b|main\b|now\b|(?:site|app)\b))|\bcut\s+a\s+release\b|\brm\s+-rf\b|\bconfig\s+set\b|\b(?:secret|credential|force|sudo)\b|자격|비밀|강제|(?:^|\s)--prod\b|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|강제)(?:해라|하세요|해줘|하라|해\s*주세요|하시오)(?!\S)|(?:^\s*|후\s*)(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*$|\s*후\s*)|(?:^\s*운영\s*(?:배포|재시작)(?=\s*$))|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*(?:해|실행|진행)(?:\s|$))/iu;
51
53
  export const TRACK_AGENT_FORBIDDEN_ACTION_REGEX = new RegExp(forbiddenActionRegex.source, forbiddenActionRegex.flags);
52
54
  let running = 0;
53
55
 
@@ -101,7 +103,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
101
103
  const values = decision as Record<string, unknown>;
102
104
  if (!['say', 'hold'].includes(String(values.action)) || (values.reason !== undefined && typeof values.reason !== 'string')
103
105
  || Object.keys(values).some((key) => !['action', 'reason'].includes(key))) throw new Error('invalid decision');
104
- if (typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
106
+ if (values.action === 'say' && typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
105
107
  return decision as TrackAgentDecision;
106
108
  } catch (error) {
107
109
  log('decision-failed', { track: input.track, taskId: input.taskId, error: redactSecretText(String(error)) });
@@ -111,7 +113,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
111
113
 
112
114
  export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
113
115
  if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
114
- if (forbiddenActionRegex.test(`${input.title}\n${input.prompt}`)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
116
+ if (forbiddenActionRegex.test(input.prompt)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
115
117
  if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
116
118
  try {
117
119
  const checkBudget = deps.checkBudget ?? (async () => {
@@ -7,9 +7,75 @@ import { writeStdoutJson } from './stdout-json.js';
7
7
  // AutopilotLoopDriver against the mission, prints agent deltas on
8
8
  // stdout + envelope/termination summary on stderr.
9
9
  export function registerAutopilotCommands(program: Command): void {
10
+ // Explicit runtime entry for a selected action; the task chooser can invoke this entry.
10
11
  const autopilotCmd = program
11
12
  .command('autopilot')
12
13
  .description('Mission-driven autopilot — runs an ACP agent against a single mission with safety + budget guards');
14
+ autopilotCmd.command('task-agent-action <actionJson>')
15
+ .description('Execute a selected task-agent action; defaults to shadow')
16
+ .action(async (actionJson: string) => {
17
+ const { executeNextAction } = await import('../task-agent/actions.js');
18
+ const { getUserConfig } = await import('../user-config.js');
19
+ const { spawnSync } = await import('node:child_process');
20
+ const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs');
21
+ const { join, dirname } = await import('node:path');
22
+ const { debug } = await import('../debug/log.js');
23
+ const { withFileLockSync } = await import('../storage/file-lock.js');
24
+ const config = getUserConfig() as ReturnType<typeof getUserConfig> & { taskAgent?: { mode?: string } };
25
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
26
+ const statePath = join(effectiveInstanceRoot(), 'task-agent-actions.json');
27
+ let state: { landingDay?: string; landingsToday?: number; failureCounts?: Record<string, number> } = {};
28
+ try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first run */ }
29
+ const action = JSON.parse(actionJson) as import('../task-agent/actions.js').NextAction;
30
+ const deps: import('../task-agent/actions.js').ActionDeps = {
31
+ ...state,
32
+ failureCounts: state.failureCounts ?? {},
33
+ saveState: () => {
34
+ mkdirSync(dirname(statePath), { recursive: true });
35
+ // 실패 횟수는 incrementFailure 가 잠금 안에서 키별로 더한다 — 여기선 덮어쓰지 않는다.
36
+ },
37
+ mode: config.taskAgent?.mode,
38
+ // 하루 착지 칸은 상태 파일 잠금 안에서 확보·반환한다 — 두 CLI 프로세스가 같은 잔여 칸을 쓰지 않게.
39
+ reserveLanding: (day, limit) => withFileLockSync(`${statePath}.lock`, () => {
40
+ let disk: typeof state = {};
41
+ try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
42
+ const used = disk.landingDay === day ? disk.landingsToday ?? 0 : 0;
43
+ if (used >= limit) return false;
44
+ mkdirSync(dirname(statePath), { recursive: true });
45
+ writeFileSync(statePath, JSON.stringify({ ...disk, landingDay: day, landingsToday: used + 1 }));
46
+ deps.landingDay = day; deps.landingsToday = used + 1;
47
+ return true;
48
+ }),
49
+ incrementFailure: (key) => withFileLockSync(`${statePath}.lock`, () => {
50
+ let disk: typeof state = {};
51
+ try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
52
+ const counts = { ...(disk.failureCounts ?? {}) };
53
+ counts[key] = (counts[key] ?? 0) + 1;
54
+ mkdirSync(dirname(statePath), { recursive: true });
55
+ writeFileSync(statePath, JSON.stringify({ ...disk, failureCounts: counts }));
56
+ return counts[key]!;
57
+ }),
58
+ releaseLanding: (day) => withFileLockSync(`${statePath}.lock`, () => {
59
+ let disk: typeof state = {};
60
+ try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { return; }
61
+ if (disk.landingDay !== day || !disk.landingsToday) return;
62
+ writeFileSync(statePath, JSON.stringify({ ...disk, landingsToday: disk.landingsToday - 1 }));
63
+ deps.landingsToday = disk.landingsToday - 1;
64
+ }),
65
+ observe: (_event, data) => debug.log('task-agent', 'action', data),
66
+ command: async args => {
67
+ const p = args[0] === 'gh'
68
+ // PR 조회는 착지할 작업 트리에서 — 다른 저장소에서 불러도 엉뚱한 PR 을 읽지 않게.
69
+ ? spawnSync('gh', args.slice(1), { encoding: 'utf8', ...(action.cwd ? { cwd: action.cwd } : {}) })
70
+ // 지금 돌고 있는 진입점으로 부른다 — 저장소 밖(설치본)에서 불러도 같은 elanous 를 쓴다.
71
+ : spawnSync(process.execPath, [process.argv[1]!, ...(process.argv.includes('--test') ? ['--test'] : []), ...args], { encoding: 'utf8' });
72
+ return { status: p.status ?? 1, stdout: p.stdout ?? '', stderr: p.stderr ?? '' };
73
+ },
74
+ };
75
+ const result = await executeNextAction(action, deps);
76
+ if (config.taskAgent?.mode === 'live') deps.saveState?.();
77
+ process.stdout.write(`${result}\n`);
78
+ });
13
79
 
14
80
  autopilotCmd
15
81
  .command('run <mission...>')
@@ -53,6 +53,7 @@ export function formatDecisionDetail(e: DecisionEntry): string {
53
53
  ...(e.note ? [`메모: ${e.note}`] : [])] : []),
54
54
  ...(e.withdrawnAt ? [`철회: ${kst(e.withdrawnAt)} · ${e.withdrawReason}`] : []),
55
55
  '이력:', ...e.history.map(h => ` ${h.type} · ${h.at ? kst(h.at) : '시각 미상'} · ${h.by}${h.choice ? ` · ${h.choice}` : ''}${h.reason ? ` · ${h.reason}` : ''} · ${h.version ? version(h.version) : '판 미상'}`),
56
+ ...(e.pendingQuestion ? [`판단 재료:\n${e.pendingQuestion}`] : []),
56
57
  ...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
57
58
  }
58
59
 
@@ -86,6 +87,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
86
87
  root.command('raise').description('SCQA·선택지와 권고로 결정 항목을 올린다')
87
88
  .requiredOption('--title <text>').requiredOption('--category <category>')
88
89
  .requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
90
+ .option('--pending-question <text>', '판단 재료 전문 (무엇·지금까지·재측·선택지별 결과·권고·확신·그냥 두면·근거)')
89
91
  .requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
90
92
  .option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
91
93
  .option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
@@ -96,7 +98,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
96
98
  .option('--ref <url>', '참조 (반복)', repeat, [] as string[])
97
99
  .option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
98
100
  .option('--json').option('--hq-override')
99
- .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
101
+ .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; pendingQuestion?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
100
102
  if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
101
103
  if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
102
104
  if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
@@ -111,6 +113,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
111
113
  const who = agent(o.agent);
112
114
  if (!mayWrite('raise', o.hqOverride)) return;
113
115
  const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
116
+ ...(o.pendingQuestion ? { pendingQuestion: o.pendingQuestion } : {}),
114
117
  ...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
115
118
  ...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
116
119
  recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
@@ -102,7 +102,7 @@ export async function runIntakeDigestCli(
102
102
  }
103
103
  const d = (deps.buildDigest ?? buildIntakeDigest)(root, day);
104
104
  if (opts.telegram) {
105
- if (!d.absorbed.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
105
+ if (!d.absorbed.length && !d.news?.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
106
106
  const text = renderDigestTelegram(d, { ...(opts.vault ? { vaultRoot: opts.vault } : {}), ...(opts.note ? { notePath: opts.note } : {}) });
107
107
  const sent = deps.sendTelegram
108
108
  ? await deps.sendTelegram(text)
@@ -1064,6 +1064,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
1064
1064
  "redecompose-opus-tap",
1065
1065
  "redecompose-respawn",
1066
1066
  "redecompose-tap",
1067
+ "reemit-target",
1067
1068
  "reflect.done",
1068
1069
  "refresh-attempt",
1069
1070
  "refused",
package/src/cli/pr-cli.ts CHANGED
@@ -75,6 +75,8 @@ export interface PrLandOpts {
75
75
  excludeActiveRunFiles?: boolean;
76
76
  excludeActiveRunSources?: boolean;
77
77
  landReason?: string;
78
+ /** Explicit reason for bypassing the landing freeze. */
79
+ forceFreeze?: string;
78
80
  /** Land even though changed files would leak into the public export (the reason goes in --land-reason). */
79
81
  allowPublicLeak?: boolean;
80
82
  }
@@ -130,6 +132,8 @@ export interface PrLandDeps {
130
132
  overlapConfirmChannels?: ConfirmChannel[];
131
133
  overlapConfirmTimeoutMs?: number;
132
134
  decideOverlapLanding?: () => OverlapLandingDecision | Promise<OverlapLandingDecision>;
135
+ /** Operational freeze authority (isolated in tests). */
136
+ prodFreezeRoot?: string;
133
137
  }
134
138
 
135
139
  export const OVERLAP_DECISION_PROMPT = '겹친 파일이 있습니다. 이번 착지를 그대로 병합할까요?';
@@ -1132,6 +1136,10 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1132
1136
  out.error('✗ body: --body와 --body-file은 함께 사용할 수 없습니다.');
1133
1137
  return 1;
1134
1138
  }
1139
+ if (opts.forceFreeze !== undefined && !opts.forceFreeze.trim()) {
1140
+ out.error('✗ freeze: --force-freeze 에는 이유가 필요합니다.');
1141
+ return 1;
1142
+ }
1135
1143
  if (opts.allowPublicLeak && !opts.landReason?.trim()) {
1136
1144
  out.error('✗ public-export-leak: --allow-public-leak 에는 --land-reason "<이유>" 가 필요합니다.');
1137
1145
  return 1;
@@ -1649,7 +1657,8 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1649
1657
  const headCommit = landedHead.ok && /^[a-f0-9]{40}$/i.test(landedHead.out.trim()) ? landedHead.out.trim() : undefined;
1650
1658
  const validPr = Number.isSafeInteger(prNumber) && prNumber > 0;
1651
1659
  const heldEntry = validPr && headCommit ? { prNumber, headCommit, repoRoot: cwd } : null;
1652
- const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined);
1660
+ const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined,
1661
+ { prodFreezeRoot: deps.prodFreezeRoot, forceReason: opts.forceFreeze });
1653
1662
  if (admission.kind === 'held' && !heldEntry) {
1654
1663
  out.error(`✗ freeze: ready PR의 검증 HEAD/번호를 보존하지 못했습니다: ${upsert.url}`);
1655
1664
  return 1;
@@ -1712,6 +1721,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1712
1721
  }
1713
1722
  return 1;
1714
1723
  }
1724
+ if (opts.forceFreeze) debug.log('release-loop.freeze', 'force-merge', { pr: prNumber, reason: opts.forceFreeze.trim() });
1715
1725
  record('merge', true, { result: merged.kind, ...(merged.remoteBranchDeletion ? { remoteBranchDeletion: merged.remoteBranchDeletion.detail } : {}) });
1716
1726
  if (merged.remoteBranchDeletion) {
1717
1727
  out.log(`⚠ remote branch deletion: ${merged.remoteBranchDeletion.detail}`);
@@ -1763,6 +1773,7 @@ export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queu
1763
1773
  .option('--exclude-active-run-files', '도는 런과 겹친 파일을 이번 착지에서 제외')
1764
1774
  .option('--exclude-active-run-sources', '⚠ 위험: --exclude-active-run-files 가 골 문서가 아닌 파일(소스)을 빼도 착지를 강행')
1765
1775
  .option('--land-reason <text>', '권고를 보고도 지금 내는 이유(예: 남이 기다리는 차단 해제)')
1776
+ .option('--force-freeze <reason>', '동결을 넘겨 병합할 명시적 이유(필수)')
1766
1777
  .option('--allow-public-leak', '바뀐 파일이 공개본에 사적 흔적을 실어도 착지(이유는 --land-reason)')
1767
1778
  .action(async (opts: PrLandOpts) => {
1768
1779
  // ⛔⭐⭐⭐ **관측 sink 를 먼저 건다** — 라이브 도그푸드가 잡은 결함(2026-08-03).
@@ -392,6 +392,20 @@ export function registerProviderCommands(program: Command): void {
392
392
  console.log(' 확인: bun bin/elanous.mjs provider codex account list · bun bin/elanous.mjs usage');
393
393
  });
394
394
 
395
+ // POD-TOKEN-PREREFRESH (10-06): 사람이 두 번 손으로 한 «백업 → refresh → 원자적 0600 쓰기 → import» 를 한 명령으로.
396
+ // ⛔ 토큰 값은 안 찍는다 — 남은 시간 전→후만. ⛔ default(~/.codex · 대표 기본 계정)는 거부.
397
+ accountCmd
398
+ .command('refresh <name>')
399
+ .description('그 계정의 codex 토큰을 호스트에서 갱신해 정본 스토어로 반영한다 (⛔ default 거부 · 토큰 값은 안 찍는다 · ⚠️ refresh 토큰은 갱신마다 회전 — 본부 한 곳에서만)')
400
+ .action(async (name: string) => {
401
+ const { refreshCodexAccountHome } = await import('../oauth/codex.js');
402
+ const r = await refreshCodexAccountHome(name);
403
+ if (!r.ok) { console.error(`갱신 실패(${r.kind}): ${r.message}`); process.exitCode = 1; return; }
404
+ const h = (v: number | null) => (v === null ? '?' : `${v}h`);
405
+ console.log(`✅ ${name} 갱신 — 남은 시간 ${h(r.beforeH)} → ${h(r.afterH)} · storeKey=${r.storeKey}`);
406
+ console.log(` 백업: ${r.backupPath}`);
407
+ });
408
+
395
409
  const resetCreditsCmd = codexCmd.command('reset-credits').description('리셋 크레딧 — 조회 · 관측 · 사용(⛔ 사용은 되돌릴 수 없다)');
396
410
 
397
411
  resetCreditsCmd
@@ -0,0 +1,46 @@
1
+ export type EdgeRailDecision = 'hold' | 'canary-install' | 'promote-all' | 'rollback' | 'wait-quiet';
2
+ export interface EdgeRailPlan { decision: EdgeRailDecision; reason: string }
3
+ export interface EdgeRailInput {
4
+ now: Date;
5
+ mainCommit: string;
6
+ installed: string;
7
+ seat: string;
8
+ canary: { seat: string; okRuns: number; startedAt: string } | null;
9
+ failureRate: { before: number; after: number; samples: number } | null;
10
+ quietWindow: boolean;
11
+ canaryOkRuns?: number;
12
+ failureMultiplier?: number;
13
+ minSamples?: number;
14
+ promoted?: boolean;
15
+ }
16
+
17
+ /** No I/O: unknown health, insufficient samples and unknown revision never trigger a cutover. */
18
+ export function planEdgeRail(input: EdgeRailInput): EdgeRailPlan {
19
+ const { mainCommit, installed, seat, canary, failureRate, quietWindow } = input;
20
+ const okRuns = input.canaryOkRuns ?? 3;
21
+ const multiplier = input.failureMultiplier ?? 2;
22
+ const minSamples = input.minSamples ?? 30;
23
+ if (!Number.isFinite(input.now.getTime()) || !/^[a-f0-9]{12,40}$/i.test(mainCommit) || !/^[a-f0-9]{12,40}$/i.test(installed)
24
+ || !Number.isSafeInteger(okRuns) || okRuns < 1 || !Number.isFinite(multiplier) || multiplier <= 1
25
+ || !Number.isSafeInteger(minSamples) || minSamples < 1) return { decision: 'hold', reason: 'invalid or unknown rollout input' };
26
+ const same = mainCommit.startsWith(installed) || installed.startsWith(mainCommit);
27
+ if (canary && Date.parse(canary.startedAt) > input.now.getTime()) return { decision: 'hold', reason: 'canary started in the future' };
28
+ if (canary && (canary.seat !== 'OP' || !Number.isSafeInteger(canary.okRuns) || canary.okRuns < 0
29
+ || !Number.isFinite(Date.parse(canary.startedAt)))) return { decision: 'hold', reason: 'invalid canary state' };
30
+ if (failureRate) {
31
+ if (![failureRate.before, failureRate.after, failureRate.samples].every(Number.isFinite)
32
+ || failureRate.before < 0 || failureRate.after < 0 || failureRate.before > 1 || failureRate.after > 1
33
+ || !Number.isSafeInteger(failureRate.samples) || failureRate.samples < 0) return { decision: 'hold', reason: 'invalid failure-rate measurement' };
34
+ if (failureRate.samples < minSamples) return { decision: 'hold', reason: `failure-rate samples ${failureRate.samples} < ${minSamples}` };
35
+ if (same && canary && failureRate.after > 0 && failureRate.after >= failureRate.before * multiplier) return quietWindow
36
+ ? { decision: 'rollback', reason: `failure rate ${failureRate.after} >= ${multiplier}x ${failureRate.before}` }
37
+ : { decision: 'wait-quiet', reason: 'rollback requires quiet window for resident restart' };
38
+ }
39
+ if (canary && canary.okRuns < okRuns) return { decision: 'hold', reason: `OP canary ${canary.okRuns}/${okRuns} successful runs` };
40
+ if (same) return { decision: 'hold', reason: 'installed commit already matches main' };
41
+ if (input.promoted && canary && seat !== 'OP') return { decision: 'hold', reason: 'fleet promotion already completed' };
42
+ if (!canary && seat !== 'OP') return { decision: 'hold', reason: 'OP canary has not installed' };
43
+ if (!quietWindow) return { decision: 'wait-quiet', reason: 'resident restart requires quiet window' };
44
+ if (!canary) return { decision: 'canary-install', reason: 'install OP canary first' };
45
+ return { decision: 'promote-all', reason: `OP canary passed ${canary.okRuns}/${okRuns} successful runs` };
46
+ }