elanous 0.2.14 → 0.2.15

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 (293) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -0,0 +1,126 @@
1
+ // Read-only inventory of work tied to the current HQ host before moving it.
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { homedir, hostname } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { inventoryCrontab, listSchedules, openSchedulesDb, parseCronLine } from '../domains/schedule-registry.js';
7
+ import { cronLaunchedActionsWithFence, listAllLoops, type AllLoopEntry, type LoopRegistryOptions } from '../loops/registry.js';
8
+ import { defaultSshRunner, type SshRunner } from './lease.js';
9
+ import { FENCE_ROLES } from './hq.js';
10
+
11
+ type Measurement<T> = { status: 'measured'; value: T } | { status: '못 잼'; reason: string };
12
+ interface MovePlan {
13
+ to: string;
14
+ from: string | null;
15
+ unfencedCron: Measurement<Array<{ raw: string; cron: string; command: string }>>;
16
+ hostLoops: Measurement<AllLoopEntry[]>;
17
+ targetHost: Measurement<{ exists: boolean; name: string | null }>;
18
+ nexusLaunchd: Measurement<{ present: boolean; measuredOn: 'local' }>;
19
+ }
20
+
21
+ type CommandResult = { status: number | null; stdout: string; stderr: string };
22
+ interface MovePlanDeps {
23
+ crontab?: () => CommandResult;
24
+ loops?: typeof listAllLoops;
25
+ loopOptions?: LoopRegistryOptions;
26
+ ssh?: SshRunner;
27
+ launchctl?: () => CommandResult;
28
+ localHost?: () => string;
29
+ hostFile?: string;
30
+ }
31
+
32
+ const measured = <T>(value: T): Measurement<T> => ({ status: 'measured', value });
33
+ const unknown = <T>(reason: string): Measurement<T> => ({ status: '못 잼', reason });
34
+ const run = (command: string, args: string[]): CommandResult => {
35
+ const r = spawnSync(command, args, { encoding: 'utf8', timeout: 15_000, env: process.env });
36
+ return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? String(r.error ?? '') };
37
+ };
38
+ const message = (error: unknown) => error instanceof Error ? error.message : String(error);
39
+
40
+ // Only known HQ jobs qualify by script name; a random scripts/*.ts is not HQ work.
41
+ const HQ_SCRIPTS = new Set([
42
+ 'steward.ts', 'draft-cleanup.ts', 'test-diet.ts', 'contract-coordinator.ts',
43
+ 'market-posture.ts', 'mission-request-judge.ts',
44
+ ]);
45
+ function isHqAction(script: string): boolean {
46
+ if (/(?:^|\/)(?:eln|elanous|elanous\.mjs)$/i.test(script)) return true;
47
+ if (/(?:^|\/)\.elanous(?:\/|$)/i.test(script)) return true;
48
+ const match = script.match(/(?:^|\/)scripts\/([^/\s]+\.ts)$/);
49
+ return !!match && HQ_SCRIPTS.has(match[1]!);
50
+ }
51
+
52
+ export function hqMovePlan(to: string, deps: MovePlanDeps = {}): MovePlan {
53
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(to) || to.includes('..')) throw new Error('hq move-plan: invalid host');
54
+ let unfencedCron: MovePlan['unfencedCron'];
55
+ let hostLoops: MovePlan['hostLoops'];
56
+ let localName: string | undefined;
57
+ let localHostError: string | undefined;
58
+ try {
59
+ const value = readFileSync(deps.hostFile ?? join(homedir(), '.elanous-hq', 'host'), 'utf8');
60
+ const name = value.endsWith('\n') ? value.slice(0, -1) : value;
61
+ if (!name || name !== name.trim() || /[\r\n]/.test(name)) throw new Error('invalid local HQ host file');
62
+ localName = name;
63
+ } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') localHostError = message(error); }
64
+ const from = deps.localHost?.() ?? localName ?? null;
65
+ let text = '';
66
+ try {
67
+ const r = (deps.crontab ?? (() => run('crontab', ['-l'])))();
68
+ if (r.status !== 0 && !(r.status === 1 && /no crontab for /i.test(r.stderr))) {
69
+ throw new Error(r.stderr.trim() || `crontab -l exit=${r.status}`);
70
+ }
71
+ text = r.status === 0 ? r.stdout : '';
72
+ const rows = text.split('\n').map(raw => ({ raw, parsed: parseCronLine(raw) }))
73
+ .filter((row): row is { raw: string; parsed: { cron: string; command: string } } => row.parsed !== null);
74
+ unfencedCron = measured(rows.filter(({ parsed }) =>
75
+ cronLaunchedActionsWithFence(parsed.command).some(action =>
76
+ !!action.script && !(FENCE_ROLES as readonly string[]).includes(action.fenceRole ?? '')
77
+ && isHqAction(action.script)))
78
+ .map(({ raw, parsed }) => ({ raw, cron: parsed.cron, command: parsed.command })));
79
+ } catch (error) { unfencedCron = unknown(message(error)); }
80
+ try {
81
+ if (unfencedCron.status !== 'measured') throw new Error(unfencedCron.reason);
82
+ // The registry accepts an already-inventoried schedule set. Never open the production DB.
83
+ const db = openSchedulesDb(':memory:');
84
+ let schedules: ReturnType<typeof listSchedules>;
85
+ try { inventoryCrontab(db, { crontab: text }); schedules = listSchedules(db); }
86
+ finally { db.close(); }
87
+ if (localHostError) throw new Error(localHostError);
88
+ if (!from) throw new Error('local HQ host identity not confirmed (~/.elanous-hq/host missing)');
89
+ // Cron rows use os.hostname(); match that label only after confirming this machine's HQ identity.
90
+ const names = new Set([from, hostname()]);
91
+ hostLoops = measured((deps.loops ?? listAllLoops)({ ...deps.loopOptions, schedules })
92
+ .filter(loop => !!loop.host && names.has(loop.host)));
93
+ } catch (error) { hostLoops = unknown(message(error)); }
94
+ let targetHost: MovePlan['targetHost'];
95
+ try {
96
+ const r = (deps.ssh ?? defaultSshRunner)(to,
97
+ 'if [ -f "$HOME/.elanous-hq/host" ]; then printf "present\\n"; cat "$HOME/.elanous-hq/host"; elif [ ! -e "$HOME/.elanous-hq/host" ]; then printf "absent\\n"; else exit 2; fi');
98
+ if (r.status !== 0) throw new Error(r.stderr.trim() || `ssh exit=${r.status}`);
99
+ const [presence, ...lines] = r.stdout.split('\n');
100
+ if (presence === 'absent') targetHost = measured({ exists: false, name: null });
101
+ else if (presence === 'present' && lines.join('\n').trim() && !/[\r\n]/.test(lines.join('\n').trim()))
102
+ targetHost = measured({ exists: true, name: lines.join('\n').trim() });
103
+ else throw new Error('target host file unreadable or empty');
104
+ } catch (error) { targetHost = unknown(message(error)); }
105
+ let nexusLaunchd: MovePlan['nexusLaunchd'];
106
+ try {
107
+ const r = (deps.launchctl ?? (() => run('launchctl', ['list', 'com.elanous.nexus'])))();
108
+ if (r.status === 0) nexusLaunchd = measured({ present: true, measuredOn: 'local' });
109
+ else if (r.status !== null && /Could not find service|service not found|not found/i.test(r.stderr)) nexusLaunchd = measured({ present: false, measuredOn: 'local' });
110
+ else throw new Error(r.stderr.trim() || `launchctl exit=${r.status}`);
111
+ } catch (error) { nexusLaunchd = unknown(message(error)); }
112
+ return { to, from, unfencedCron, hostLoops, targetHost, nexusLaunchd };
113
+ }
114
+
115
+ export function formatMovePlan(plan: MovePlan): string {
116
+ const lines = [`hq move-plan: ${plan.from ?? '못 잼'} → ${plan.to} (read-only)`];
117
+ const section = <T>(name: string, result: Measurement<T>, render: (value: T) => string[]) => {
118
+ lines.push(`${name}: ${result.status === 'measured' ? '' : `못 잼 (${result.reason})`}`.trimEnd());
119
+ if (result.status === 'measured') lines.push(...render(result.value));
120
+ };
121
+ section('① unfenced elanous crontab', plan.unfencedCron, rows => rows.length ? rows.map(row => ` ${row.raw}`) : [' none']);
122
+ section('② local host loops (loop status --all)', plan.hostLoops, rows => rows.length ? rows.map(row => ` ${row.id} · ${row.host} · ${row.title}`) : [' none']);
123
+ section('③ target ~/.elanous-hq/host', plan.targetHost, host => [` ${host.exists ? host.name : 'absent'}`]);
124
+ section('④ local launchd com.elanous.nexus', plan.nexusLaunchd, service => [` ${service.present ? 'present' : 'absent'} (measured on local machine)`]);
125
+ return lines.join('\n');
126
+ }
package/src/hq/promote.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  // HQ promotion runbook: read-only preview by default; all remote mutations are behind --apply.
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { getUserConfig } from '../user-config.js';
4
+ import { debug } from '../debug/log.js';
5
+ import { join } from 'node:path';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
4
8
  import { parseLease, sshLeaseStore, defaultSshRunner, localShellRunner } from './lease.js';
5
9
 
6
10
  export interface PromoteRunner {
@@ -295,6 +299,33 @@ export function promoteHq(host: string, apply = false, runner: PromoteRunner = d
295
299
  }
296
300
  }
297
301
 
302
+ /** One OP seat request (never a CEO decision card · 대표 10-05 10:03); idempotent by key. */
303
+ export type HqOpRequest = { key: string; text: string; [field: string]: unknown };
304
+ export type HqOpRequestWriter = (request: HqOpRequest) => { key: string; created: boolean };
305
+
306
+ export function writeOpSeatRequest(request: HqOpRequest, root = effectiveInstanceRoot()): { key: string; created: boolean } {
307
+ const path = join(root, 'seat-requests', 'requests.jsonl');
308
+ return withSeatRequestLedgerLock(path, () => {
309
+ if (listSeatRequests(root).some((row) => row.key === request.key)) return { key: request.key, created: false };
310
+ appendSeatRequestRows(path, [{ ...request, seat: 'OP', status: 'pending' as const, queuedAt: new Date().toISOString(), source: 'hq-arbiter' }]);
311
+ return { key: request.key, created: true };
312
+ });
313
+ }
314
+
315
+ /** Propose promotion to the OP seat for a silent lease generation; the runbook runs only under explicit opt-in (see hqArbiterCheck). */
316
+ export function proposeHqPromotion(
317
+ input: { holder: string; standby: string; generation: number; streak: number },
318
+ deps: { request?: HqOpRequestWriter; log?: typeof debug.log } = {},
319
+ ): { promoted: false; requestKey: string; created: boolean } {
320
+ const key = `hq:promote:${input.holder}:${input.generation}`;
321
+ const written = (deps.request ?? writeOpSeatRequest)({ key,
322
+ text: `본부 ${input.holder}(임대 세대 ${input.generation}) 연속 무응답 ${input.streak}회 — ${input.standby} 승격을 OP 가 판단한다(자동 승격 꺼짐). 승격하면 \`eln hq promote --to ${input.standby} --apply\`.`,
323
+ holder: input.holder, standby: input.standby, generation: input.generation, streak: input.streak });
324
+ try { (deps.log ?? debug.log.bind(debug))('hq.arbiter', 'promote-proposed', { ...input, ref: key, mode: 'op-request', created: written.created }); }
325
+ catch { /* observation cannot change the proposal */ }
326
+ return { promoted: false, requestKey: key, created: written.created };
327
+ }
328
+
298
329
  export function formatPromote(result: PromoteResult): string {
299
330
  return [`hq promote --to ${result.host} ${result.apply ? '--apply' : '(dry-run)'}`,
300
331
  ...result.lines.map(line => `${line.step}: ${line.status} · ${line.measurement}${line.status === 'ready' ? ` · ${line.command}` : ''}`)].join('\n');
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import { collectCommandEntrances, renderCommandEntrances } from './self-dev/entr
31
31
  import { isGoalAuthorFileName } from './self-implement/goal-document.js';
32
32
  import { DEV_PIPELINE_SINK_SURFACE } from './self-implement/self-cli-sink-surface.js';
33
33
  import { registerPrivateLedgerCommands } from './cli/private-ledger-commands.js';
34
+ import { registerSeatRequestsCommands } from './cli/seat-requests-cli.js';
34
35
  import { applyTestStateDirFlagFromArgv } from './cli/test-state-dir-flag.js';
35
36
  import { applyTestFlagFromArgv, observeTestFlagOwnership, uncoveredTestFlagPaths, staleTestFlagPaths } from './cli/test-flag.js';
36
37
  import { registerPtyTakeoverCommands } from './cli/pty-takeover-cli.js';
@@ -46,6 +47,7 @@ import { registerPublishCommands } from './cli/publish-cli.js';
46
47
  import { registerAutopilotCommands } from './cli/autopilot-cli.js';
47
48
  import { registerRoleCommands } from './cli/role-cli.js';
48
49
  import { registerMachineCommands } from './cli/machine-cli.js';
50
+ import { registerAgentEnvCommands } from './cli/agent-env-cli.js';
49
51
  import { registerScheduleCommands } from './cli/schedule-cli.js';
50
52
  export { runSchedule, scheduleCreatePlan } from './cli/schedule-cli.js';
51
53
  export type { ScheduleDispatch } from './cli/schedule-cli.js';
@@ -80,6 +82,7 @@ import { registerStartCommand } from './cli/start-cli.js';
80
82
  import { registerGroundingSourcesCli } from './grounding/sources-cli.js';
81
83
  import { registerGraphCommands } from './graph-runner/graph-cli.js';
82
84
  import { registerLoopCommands } from './loops/loop-cli.js';
85
+ import { registerHqMovePlanCommand } from './hq/hq.js';
83
86
  import { registerCardCommand } from './task-cards/card-cli.js';
84
87
  import { registerLaunchHeadCommands } from './launch-head/launch-head-cli.js';
85
88
  import { acpServerHelpText } from './boot/acp-server-help.js';
@@ -856,6 +859,7 @@ registerPrCommands(program);
856
859
  registerRepoCommands(program);
857
860
  registerRoleCommands(program);
858
861
  registerMachineCommands(program);
862
+ registerAgentEnvCommands(program);
859
863
  // CLI entry: where action → resolveCurrentInstance → resolveInstance (instance universe).
860
864
  registerWhereCommand(program);
861
865
  registerShadowCommand(program);
@@ -937,6 +941,18 @@ hqCmd.command('seen').description('이 호스트가 본 가장 높은 임대 세
937
941
  else console.log(result.seen ? `hq seen: generation ${result.generation}` : 'hq seen: never');
938
942
  process.exitCode = result.seen ? 0 : 3;
939
943
  });
944
+ registerHqMovePlanCommand(hqCmd);
945
+ hqCmd.command('fence-audit').description('운영 crontab 의 HQ 울타리 밖 쓰기 잡 목록과 감싼 줄 제안(읽기 전용)')
946
+ .option('--json', 'JSON 출력')
947
+ .action(async (opts: { json?: boolean }) => {
948
+ try {
949
+ await hqSink();
950
+ const rows = (await import('./hq/fence-audit.js')).hqFenceAudit();
951
+ if (opts.json) await writeStdoutJson(`${JSON.stringify(rows)}\n`);
952
+ else if (!rows.length) console.log('hq fence-audit: unfenced 0');
953
+ else for (const row of rows) console.log(`${row.raw}\n role: ${row.recommendedRole}\n suggested: ${row.suggestedLine}`);
954
+ } catch (error) { console.error(`hq fence-audit: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
955
+ });
940
956
  hqCmd.command('fence').description('이 호스트가 임대를 쥐었을 때만 명령 실행(아니면 건너뜀 · exit 0)')
941
957
  .requiredOption('--role <role>', 'telegram-poller|cron|seat-loop|release-run|conatus|git-push')
942
958
  .argument('<command...>', '-- 뒤의 명령')
@@ -947,6 +963,7 @@ hqCmd.command('fence').description('이 호스트가 임대를 쥐었을 때만
947
963
  process.exitCode = hqFenceRun(opts.role as 'cron', command);
948
964
  });
949
965
  const seatCmd = program.command('seat').description('분배된 자리의 하루 루프와 보고');
966
+ registerSeatRequestsCommands(seatCmd);
950
967
  seatCmd.command('answer <askId> <answer>').description('자리 질문에 답을 남기고 원래 표면으로 회신 예약')
951
968
  .action(async (askId: string, answer: string) => {
952
969
  try {
@@ -962,10 +979,10 @@ seatCmd.command('loop').requiredOption('--seat <SEAT>', 'MK|OP|TC|UX')
962
979
  try {
963
980
  // V3 F5: without the sink, the seat loop's judgment lines (seat.loop) never reach the log store when cron runs it.
964
981
  await (await import('./domains/standalone-log-sink.js')).registerStandaloneLogSink('seat-loop');
965
- const { runSeatLoopTurn } = await import('./seat-loop/seat-loop.js');
982
+ const { runSeatLoopTurn, seatLoopTickLine } = await import('./seat-loop/seat-loop.js');
966
983
  const result = await runSeatLoopTurn(opts.seat);
967
984
  if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
968
- else console.log(`seat loop ${opts.seat}: ${result.status}${'runId' in result && result.runId ? ` ${result.runId}` : ''}`);
985
+ else console.log(seatLoopTickLine(result));
969
986
  } catch (error) { console.error(`seat loop: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
970
987
  });
971
988
  seatCmd.command('report').requiredOption('--seat <SEAT>', 'MK|OP|TC|UX')
@@ -3457,6 +3474,22 @@ selfCmd
3457
3474
  }
3458
3475
  });
3459
3476
 
3477
+ selfCmd
3478
+ .command('speed-claims')
3479
+ .description('런 원장 발사→종결 시간의 자리별·결과별 분포를 읽기 전용으로 출력한다')
3480
+ .option('--json', '구조화된 JSON으로 출력한다')
3481
+ .action(async (opts: { json?: boolean }) => {
3482
+ try {
3483
+ const { measureSpeedClaims, renderSpeedClaims } = await import('./self-implement/speed-claims.js');
3484
+ const result = measureSpeedClaims();
3485
+ if (opts.json) await writeStdoutJson(JSON.stringify(result) + '\n');
3486
+ else console.log(renderSpeedClaims(result));
3487
+ } catch (error) {
3488
+ console.error(`speed-claims measurement failed: ${error instanceof Error ? error.message : String(error)}`);
3489
+ process.exitCode = 1;
3490
+ }
3491
+ });
3492
+
3460
3493
  selfCmd
3461
3494
  .command('goal-source-distribution')
3462
3495
  .description('self-implement 런 원장의 start goalSource 분포를 읽기 전용으로 출력한다')
@@ -4379,6 +4412,9 @@ const selfOrchestrateCmd = selfCmd
4379
4412
  .option('--no-pod-rebuild', 'pod: 이미지 판(elanous.commit)이 HEAD 와 달라도 다시 굽지 않는다 — 측정은 «이미지 판»을 잰다')
4380
4413
  .option('--pod-pass-env <keys>', 'pod: 호스트 env 에서 Pod 로 넘길 키(쉼표) — 예 OPENROUTER_API_KEY,ANTHROPIC_API_KEY(벤치마크 과금 경로)')
4381
4414
  .option('--bench-arms <spec>', 'pod 벤치마크: 골 1개를 팔마다 «라벨 한 줄만 다르게» 복제해 동시에 — "id=provider[:model][@KEY+KEY];…" (예 codex=openai-codex;or-kimi=openrouter:openrouter/moonshotai/kimi-k3@OPENROUTER_API_KEY) · --auto-merge 거부 · RFC fleet 슈퍼바이저 §A3')
4415
+ .addOption(new Option('--child-llm-provider <id>', 'pod: 구현 자식 LLM provider(openai-codex|grok) — 하니스 --child-llm-provider 가 이 자리로 온다(10-05)').hideHelp())
4416
+ .addOption(new Option('--child-llm-model <id>', 'pod: 구현 자식 LLM model(--child-llm-provider 와 함께)').hideHelp())
4417
+ .addOption(new Option('--child-llm-effort <level>', 'pod: 구현 자식 추론 노력(--child-llm-provider 와 함께)').hideHelp())
4382
4418
  .option('--help-all', '모든 orchestrate 옵션 표시')
4383
4419
  .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 }) {
4384
4420
  return (async () => {
@@ -4613,6 +4649,19 @@ const selfOrchestrateCmd = selfCmd
4613
4649
  let rotationAccounts: readonly string[] | undefined;
4614
4650
  let podProvider: 'openai-codex' | 'grok' = 'openai-codex';
4615
4651
  let grokApiKeyOptIn = false;
4652
+ {
4653
+ // 10-05 PODPROVIDER: refuse child choices this Pod launch cannot honour instead of dropping them.
4654
+ const child = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
4655
+ const named = child.childLlmProvider?.trim();
4656
+ const refuse = !named && (child.childLlmModel?.trim() || child.childLlmEffort?.trim())
4657
+ ? 'pod: --child-llm-model/--child-llm-effort 는 --child-llm-provider 와 함께'
4658
+ : named && benchArms
4659
+ ? 'pod: --child-llm-provider 는 --bench-arms 와 함께 못 쓴다(팔마다 모델이 하나다)'
4660
+ : named === 'grok' && explicitPodAccount
4661
+ ? 'pod: --child-llm-provider grok 은 --pod-account 와 함께 못 쓴다(그것은 codex 계정을 고정한다)'
4662
+ : undefined;
4663
+ if (refuse) { ui.error(refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: named ?? null, reason: refuse }); process.exit(2); }
4664
+ }
4616
4665
  if (!explicitPodAccount && !benchArms) {
4617
4666
  const { inspectCodexRotation } = await import('./oauth/codex-account-store.js');
4618
4667
  const { planPodProvider, makePodAccountBroker } = await import('./task-orchestrator/surfaces/pod-account-broker.js');
@@ -4651,17 +4700,31 @@ const selfOrchestrateCmd = selfCmd
4651
4700
  if (!opts.json) ui.warn(podGrokSkippedLine(grokCheck));
4652
4701
  }
4653
4702
  }
4654
- const plan = planPodProvider({
4703
+ // 10-05 PODPROVIDER: a launch that named the child provider gets it, or is refused — never a silent codex.
4704
+ const namedChild = (opts as { childLlmProvider?: string }).childLlmProvider?.trim();
4705
+ const { podNamedChildProvider, podFallbackCredentials, podGrokSubscriptionUsable: namedGrokUsable } = await import('./task-orchestrator/surfaces/self-implement-pod.js');
4706
+ const namedChildDecision = podNamedChildProvider(namedChild, {
4707
+ grokSubscription: namedChild === 'grok' && credential?.kind === 'subscription' && (grokSubscriptionUsable || namedGrokUsable().usable),
4708
+ grokApiKey: credential?.kind === 'api_key' && grokApiKeyOptIn,
4709
+ });
4710
+ if (namedChildDecision.refuse) { ui.error(namedChildDecision.refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: namedChild, reason: namedChildDecision.refuse }); process.exit(2); }
4711
+ // A named codex child must stay codex — the plan may not fall back to grok (post-review must-fix).
4712
+ const namedCodex = namedChildDecision.provider === 'openai-codex';
4713
+ const plan: import('./task-orchestrator/surfaces/pod-account-broker.js').PodProviderPlan = namedChildDecision.provider === 'grok'
4714
+ ? { provider: 'grok', excluded: [], grokSubscriptionEligible: credential?.kind === 'subscription' }
4715
+ : planPodProvider({
4655
4716
  codexCandidates: inspectCodexRotation().candidates,
4656
4717
  // Per-account caps (대표 default:97) override the Pod default cap — without them default was dropped at 60%.
4657
4718
  thresholdPercentByAccount: getUserConfig().llm?.codexAccountRotationThresholdPercentByAccount,
4658
4719
  // 대표 한도 정책 한 값(`llm.codexQuotaPolicy`) — 크레딧·폴백을 같이 정한다.
4659
4720
  creditsAllowed: codexPolicyAllowsCredits(quotaPolicy),
4660
- grokSubscription: codexPolicyAllowsFallback(quotaPolicy) && grokSubscriptionUsable,
4661
- grokApiKey: codexPolicyAllowsFallback(quotaPolicy) && credential?.kind === 'api_key',
4721
+ ...podFallbackCredentials(namedChildDecision.provider, {
4722
+ grokSubscription: codexPolicyAllowsFallback(quotaPolicy) && grokSubscriptionUsable,
4723
+ grokApiKey: codexPolicyAllowsFallback(quotaPolicy) && credential?.kind === 'api_key',
4724
+ }),
4662
4725
  grokApiKeyOptIn,
4663
4726
  });
4664
- if (plan.provider === null) { ui.error(`pod: 쓸 codex 계정이 없다 — ${plan.reasons.join(' · ')} · 계정을 명시하려면 --pod-account <이름>`); process.exit(2); }
4727
+ if (plan.provider === null) { ui.error(`pod: 쓸 codex 계정이 없다 — ${plan.reasons.join(' · ')}${namedCodex ? ' · child provider 를 openai-codex 로 지정해 grok 으로 넘기지 않는다' : ''} · 계정을 명시하려면 --pod-account <이름>`); process.exit(2); }
4665
4728
  podProvider = plan.provider;
4666
4729
  if (plan.provider === 'openai-codex') {
4667
4730
  accountBroker = makePodAccountBroker({ usable: plan.accounts, excluded: plan.excluded });
@@ -4670,6 +4733,10 @@ const selfOrchestrateCmd = selfCmd
4670
4733
  { const { emitDecision } = await import('./live/detail-switch.js');
4671
4734
  emitDecision({ kind: 'ROUTE', what: `Pod 계정 배분 ${plan.accounts.join(' → ')}`, reason: plan.excluded.length ? `뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : '모든 계정 여유', purpose: plan.creditAccounts ? '구독 잔량 0 — 정책상 크레딧으로 병렬 조각을 돌린다' : '병렬 조각이 한 계정에 몰리지 않게 잔량 순으로 돌려 준다', phase: 'dispatch', target: `Pod ${plan.accounts.length}계정`, paths: plan.accounts.length + plan.excluded.length }); }
4672
4735
  if (!opts.json) ui.info(`[pod] 계정 배분(잔량 순 · 돌려 가며): ${plan.accounts.join(' → ')}${plan.creditAccounts ? ' · 💳 크레딧(구독 잔량 0 · 허가됨)' : ''}${plan.excluded.length ? ` · 뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : ''}`);
4736
+ } else if (namedChildDecision.provider === 'grok') {
4737
+ const model = (opts as { childLlmModel?: string }).childLlmModel?.trim() || defaultGrokModel().id;
4738
+ debug.log('self-implement.pod', 'provider-named', { provider: 'grok', model, credential: credential?.kind ?? null });
4739
+ if (!opts.json) ui.info(`[pod] child provider = grok(모델 ${model}) — 발사 인자로 지정됨`);
4673
4740
  } else {
4674
4741
  const model = defaultGrokModel().id;
4675
4742
  debug.log('self-implement.pod', 'provider-fallback', { from: 'openai-codex', to: 'grok', excluded: plan.excluded, model });
@@ -4697,7 +4764,10 @@ const selfOrchestrateCmd = selfCmd
4697
4764
  ...(podSource.kind === 'bundle' ? { headCommit: podSource.headCommit, sizeBytes: podSource.sizeBytes } : {}),
4698
4765
  });
4699
4766
  }
4700
- const podBase = { hostSupervised: opts.supervise === true, ...(remoteOnlyPool ? { imageCommit: image.imageCommit } : {}), account: podProvider === 'grok' ? 'grok' : explicitPodAccount ?? 'team', ...(accountBroker ? { accountBroker, rotationAccounts } : {}), ...(podProvider === 'grok' ? { provider: 'grok' as const, grokApiKeyOptIn } : {}), passEnv, ...(pool ? { pool } : {}), ...((opts as { podSkillEnv?: boolean }).podSkillEnv ? { skillEnv: true } : {}), ...(podSource ? { source: podSource } : {}) };
4767
+ const childFlags = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
4768
+ const podChildLlm = childFlags.childLlmProvider?.trim() ? { childProviderExplicit: true, ...(childFlags.childLlmModel?.trim() ? { childModel: childFlags.childLlmModel.trim() } : {}), ...(childFlags.childLlmEffort?.trim() ? { childEffort: childFlags.childLlmEffort.trim() } : {}) } : {};
4769
+ if (childFlags.childLlmProvider?.trim()) debug.log('self-implement.pod', 'child-provider-named', { provider: childFlags.childLlmProvider.trim(), model: childFlags.childLlmModel ?? null, podProvider });
4770
+ const podBase = { hostSupervised: opts.supervise === true, ...podChildLlm, ...(remoteOnlyPool ? { imageCommit: image.imageCommit } : {}), account: podProvider === 'grok' ? 'grok' : explicitPodAccount ?? 'team', ...(accountBroker ? { accountBroker, rotationAccounts } : {}), ...(podProvider === 'grok' ? { provider: 'grok' as const, grokApiKeyOptIn } : {}), passEnv, ...(pool ? { pool } : {}), ...((opts as { podSkillEnv?: boolean }).podSkillEnv ? { skillEnv: true } : {}), ...(podSource ? { source: podSource } : {}) };
4701
4771
  if (benchArms) {
4702
4772
  const { benchPodSpawn } = await import('./task-orchestrator/surfaces/self-implement-pod.js');
4703
4773
  podSpawn = benchPodSpawn(benchArms, podBase);
@@ -61,8 +61,22 @@ export interface IntakeCheckEvidence {
61
61
  readonly contrary?: boolean;
62
62
  }
63
63
 
64
+ export interface IntakeReplacementCandidate {
65
+ readonly tool: string;
66
+ readonly paidDependency: string;
67
+ readonly status: '현장 시험 필요';
68
+ /** A comparison claim is not evidence that any of these checks have passed. */
69
+ readonly trial: {
70
+ readonly installation: '미검증';
71
+ readonly sampleRun: '미검증';
72
+ readonly license: '미검증';
73
+ readonly maintenanceStatus: '미검증';
74
+ };
75
+ }
76
+
64
77
  export interface IntakeCheckItem {
65
78
  readonly fact: string;
79
+ readonly replacementCandidate?: IntakeReplacementCandidate;
66
80
  /** Source identity preserved from the supplied fact for card-backed goal evidence. */
67
81
  readonly sourceRef?: string;
68
82
  readonly originalClaims?: readonly string[];
@@ -1053,6 +1067,40 @@ function factKey(text: string): string {
1053
1067
  return text.replace(/\s+/g, ' ').trim().toLowerCase();
1054
1068
  }
1055
1069
 
1070
+ /** A stated possibility is a trial candidate, not proof of a working replacement. */
1071
+ function replacementCandidate(fact: string): IntakeReplacementCandidate | undefined {
1072
+ if (!/(?:대체(?:하|한|할| 가능| 후보|재)|replace|replacement|alternative to)/i.test(fact)
1073
+ || /(?:대체하지\s*(?:는|도|조차|를)?\s*(?:않|못)|대체\s*못|대체할\s*수\s*(?:는|도|가)?\s*없|대체(?:가|는| 후보가)?\s*(?:아니|불가)|cannot replace|can't replace|does not replace|not a replacement)/i.test(fact)) return undefined;
1074
+ const paid = fact.match(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?`?([a-z][\w.-]+)`?/i);
1075
+ // firecrawl is a named paid-dependency example even when the claim omits the word "paid".
1076
+ const paidDependency = paid?.[1] ?? fact.match(/(?:^|[^\w])`?(firecrawl)`?(?=$|[^\w])/i)?.[1];
1077
+ if (!paidDependency) return undefined;
1078
+ const names = [...fact.matchAll(/`([a-z][\w.-]+)`/gi)].map((match) => match[1]!);
1079
+ const explicit = names.filter((name) => name.toLowerCase() !== paidDependency.toLowerCase());
1080
+ const plain = fact.match(/^\s*([a-z][\w.-]+)\s*(?:가|이|는|은|으로|로|can\b|could\b|may\b)/i)?.[1];
1081
+ const tool = explicit.length === 1 ? explicit[0] : explicit.length === 0 ? plain : undefined;
1082
+ if (!tool || tool.toLowerCase() === paidDependency.toLowerCase() || tool.toLowerCase() === 'elanous') return undefined;
1083
+ // Names appearing together do not establish the direction of replacement.
1084
+ const clause = fact.replace(/`/g, '').replace(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?/gi, '');
1085
+ const escape = (name: string): string => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1086
+ const name = (value: string): string => `(?<![\\w.-])${escape(value)}(?![\\w.-])`;
1087
+ const toolName = name(tool);
1088
+ const dependencyName = name(paidDependency);
1089
+ const replaces = (subject: string, object: string): boolean => [
1090
+ new RegExp(`${subject}\\s*(?:이|가|은|는)?\\s+${object}\\s*(?:을|를)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
1091
+ new RegExp(`${object}\\s*(?:을|를)\\s+${subject}\\s*(?:이|가|은|는)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
1092
+ new RegExp(`${subject}\\s+(?:can |could |may )?replace(?:s|d)?\\s+${object}`, 'i'),
1093
+ new RegExp(`${subject}\\s+(?:is |could be |may be )?(?:an? )?(?:replacement|alternative)\\s+to\\s+${object}`, 'i'),
1094
+ ].some((pattern) => pattern.test(clause));
1095
+ if (!replaces(toolName, dependencyName) || replaces(dependencyName, toolName)) return undefined;
1096
+ return {
1097
+ tool,
1098
+ paidDependency,
1099
+ status: '현장 시험 필요',
1100
+ trial: { installation: '미검증', sampleRun: '미검증', license: '미검증', maintenanceStatus: '미검증' },
1101
+ };
1102
+ }
1103
+
1056
1104
  function isLens(value: string): value is PreprocessLens {
1057
1105
  return (PREPROCESS_LENSES as readonly string[]).includes(value);
1058
1106
  }
@@ -1414,8 +1462,17 @@ export function runIntakeCheck(
1414
1462
  }
1415
1463
  // src/index.ts intake check .action → runIntakeCheck / runIntakeCheckDocument → runIntakeCheck → judgeFact.
1416
1464
  const judged = judgeFact(fact, ruler, deps);
1465
+ const candidate = replacementCandidate(fact.text);
1466
+ const verdict = candidate && judged.verdict !== '못 쟀다' ? '판단 필요' : judged.verdict;
1467
+ const current = candidate && judged.verdict !== '못 쟀다'
1468
+ ? `${judged.current} · 대체 후보 ${candidate.tool} → ${candidate.paidDependency}: 설치·샘플 실행·라이선스·유지보수 상태 현장 시험 필요`
1469
+ : judged.current;
1417
1470
  const item: IntakeCheckItem = {
1418
1471
  ...judged,
1472
+ verdict,
1473
+ current,
1474
+ line: `${fact.text} → ${current} → ${verdict}`,
1475
+ ...(candidate ? { replacementCandidate: candidate } : {}),
1419
1476
  quotes: [quote],
1420
1477
  ...(fact.sourceRef ? { sourceRef: fact.sourceRef } : {}),
1421
1478
  ...(fact.originalClaim ? { originalClaims: [fact.originalClaim] } : {}),
@@ -1711,6 +1768,11 @@ export function renderIntakeCheckReport(report: IntakeCheckReport): string {
1711
1768
  for (const item of report.items) {
1712
1769
  lines.push(item.line);
1713
1770
  lines.push(` 판정: ${item.verdict}`);
1771
+ if (item.replacementCandidate) {
1772
+ const candidate = item.replacementCandidate;
1773
+ lines.push(` 대체 후보: ${candidate.tool} → 유료 의존성 ${candidate.paidDependency} (${candidate.status})`);
1774
+ lines.push(' 현장 시험: 설치 미검증 · 샘플 실행 미검증 · 라이선스 미검증 · 유지보수 상태 미검증');
1775
+ }
1714
1776
  if (item.goalDraftPath) lines.push(` 골 초안: ${item.goalDraftPath}`);
1715
1777
  for (const quote of item.quotes) lines.push(` 인용: ${quote}`);
1716
1778
  for (const original of item.originalClaims ?? []) lines.push(` 원 주장: ${original}`);
@@ -1746,6 +1808,7 @@ export function intakeCheckReportJson(report: IntakeCheckReport): Record<string,
1746
1808
  current: item.current,
1747
1809
  verdict: item.verdict,
1748
1810
  line: item.line,
1811
+ ...(item.replacementCandidate ? { replacementCandidate: item.replacementCandidate } : {}),
1749
1812
  quotes: item.quotes,
1750
1813
  evidence: item.evidence,
1751
1814
  patterns: item.patterns,
@@ -1,12 +1,12 @@
1
1
  // 흡수 하루 다이제스트 — 그날 흡수한 것을 사람이 «발견»하는 자리. 노트 절(마크다운) ⊕ 텔레그램 짧은 판.
2
2
  // 결정론만: 요약 한 줄은 각 노트가 이미 쓴 「한 줄 결론」을 뽑는다(LLM 을 다시 부르지 않는다).
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
- import { basename, join, relative } from 'node:path';
4
+ import { basename, join } from 'node:path';
5
5
  import { loadIntakeLedger, type IntakeItem } from './items.js';
6
6
  import { readSavedCursorAt } from './collect-telegram-saved.js';
7
7
  import { intakeOutboxDir, kstDay } from './route.js';
8
8
 
9
- export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string }
9
+ export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string; impact?: { fact: string; current: string; action: string } }
10
10
  export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
11
11
 
12
12
  const AXIS_LABEL: Record<string, string> = {
@@ -43,17 +43,38 @@ const SAVED_SILENCE_MS = 48 * 3600_000;
43
43
  export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date()): IntakeDigest {
44
44
  const items = [...loadIntakeLedger(root).items.values()]
45
45
  .filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
46
+ const out = intakeOutboxDir(root);
47
+ const relevant = new Map<string, NonNullable<DigestEntry['impact']>>();
48
+ for (const [folder, action] of [
49
+ ['goals', '없는 기능의 골 후보를 세운다'],
50
+ ['manual', '문서·약속을 현재 동작에 맞게 보강한다'],
51
+ ['review', '대조 근거를 추가로 확인한다'],
52
+ ['release', '이미 착지한 기능의 바깥 맥락을 릴리스 노트에 반영한다'],
53
+ ] as const) {
54
+ for (const row of readJsonl(join(out, folder, `${day}.jsonl`))) {
55
+ if (typeof row.id !== 'string' || relevant.has(row.id)) continue;
56
+ const fact = folder === 'release' ? row.title : row.fact;
57
+ if (typeof fact !== 'string' || !fact.trim()) continue;
58
+ relevant.set(row.id, {
59
+ fact,
60
+ current: typeof (folder === 'release' ? row.summary : row.current) === 'string'
61
+ ? String(folder === 'release' ? row.summary : row.current) : '대조 근거 없음',
62
+ action,
63
+ });
64
+ }
65
+ }
46
66
  const absorbed = items.map((i: IntakeItem): DigestEntry => {
47
67
  const note = [...i.outputs].reverse().find((o) => o.kind === 'note')?.ref;
48
68
  const md = note ? readFile(note) : undefined;
69
+ const impact = relevant.get(i.id);
49
70
  return {
50
71
  id: i.id, sources: i.sources, ...(i.url ? { url: i.url } : {}),
51
72
  ...(note ? { note, noteName: basename(note, '.md') } : {}),
52
73
  ...(md ? { oneLiner: noteOneLiner(md) } : {}),
53
74
  axis: AXIS_LABEL[i.judgement?.axis ?? i.axis ?? ''] ?? (i.sources.includes('telegram-saved') ? '내가 저장한 것' : '그 밖'),
75
+ ...(impact ? { impact } : {}),
54
76
  };
55
77
  });
56
- const out = intakeOutboxDir(root);
57
78
  const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
58
79
  const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
59
80
  const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
@@ -94,18 +115,19 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
94
115
  return L.join('\n');
95
116
  }
96
117
 
97
- /** 텔레그램 짧은 판 — 수 · 한 줄 요약 다섯 · 골 후보 · 노트 자리. */
98
- export function renderDigestTelegram(d: IntakeDigest, opts: { vaultRoot?: string; notePath?: string } = {}): string {
99
- const L = [`📰 *${d.day} 흡수 ${d.absorbed.length}편*`];
100
- if (d.savedSilence) L.push(`⚠️ 저장된 메시지 새 글 ${d.savedSilence.days}일째 0 — 저장한 곳 확인`);
101
- for (const e of d.absorbed.slice(0, 5)) L.push(`• ${e.oneLiner ?? e.noteName ?? e.url ?? e.id}`);
102
- if (d.absorbed.length > 5) L.push(`… 외 ${d.absorbed.length - 5}편`);
103
- if (d.goals.length) L.push('', `🔴 엘라누스에 없는 것 ${d.goals.length}건 — 골 후보`);
104
- if (d.review?.length) L.push(`${d.goals.length ? '' : '\n'}🟡 사람이 가를 것 ${d.review.length}건 — 노트 🧭 절`);
105
- if (opts.vaultRoot && opts.notePath) {
106
- const vault = basename(opts.vaultRoot);
107
- const file = relative(opts.vaultRoot, opts.notePath).replace(/\.md$/, '');
108
- L.push('', `📂 obsidian://open?vault=${encodeURIComponent(vault)}&file=${encodeURIComponent(file)}`);
118
+ /** 텔레그램 — 흡수 렌즈가 대조한 항목만 SCQA 짧은 판으로 낸다. */
119
+ export function renderDigestTelegram(d: IntakeDigest, _opts: { vaultRoot?: string; notePath?: string } = {}): string {
120
+ const touching = d.absorbed.filter((e) => e.impact);
121
+ const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
122
+ if (!touching.length) return L[0];
123
+ for (const e of touching.slice(0, 3)) {
124
+ L.push('',
125
+ `S 무엇: ${e.impact!.fact}`,
126
+ `C 우리에게 왜: ${e.impact!.current}`,
127
+ `A 그래서 무엇을 하나: ${e.impact!.action}`,
128
+ `🔗 원문 링크: ${e.url?.trim() || '링크 없음'}`,
129
+ );
109
130
  }
131
+ if (d.absorbed.length > touching.length) L.push('', `그 밖 ${d.absorbed.length - touching.length}건 · 닿지 않음`);
110
132
  return L.join('\n');
111
133
  }
@@ -18,6 +18,12 @@ export interface FrontClassifierDecision extends IntakeFrontRouteDecision {
18
18
  urlRoute: UrlRouteDecision | null;
19
19
  }
20
20
 
21
+ /** Only Telegram links explicitly routed to saved/absorbed work enter the event lane. */
22
+ export function telegramAbsorbUrls(input: FrontClassifierInput, decision: FrontClassifierDecision): string[] {
23
+ if (input.surface !== 'telegram' || decision.track !== 'absorb' || !decision.urlRoute) return [];
24
+ return decision.urlRoute.urls;
25
+ }
26
+
21
27
  const QUESTION_WORD_RE = new RegExp(
22
28
  `(?:${INTAKE_FRONT_QUESTION_WORDS.map((word) => /^[a-z]+$/i.test(word) ? `(?<![\\p{L}\\p{N}_])${word}(?![\\p{L}\\p{N}_])` : word).join('|')})[.!?。?!]*$`,
23
29
  'iu',
@@ -5,6 +5,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { debug } from '../debug/log.js';
7
7
  import { findTrack } from '../autopilot/mission-phase-track.js';
8
+ import { wasIntakeEventScheduled } from './route.js';
8
9
 
9
10
  export const INTAKE_SOURCES = ['x', 'youtube', 'github', 'telegram-saved', 'telegram-bot', 'memo', 'pwa'] as const;
10
11
  export type RegisteredIntakeSourceKind = 'rss' | 'command';
@@ -243,13 +244,13 @@ export function parseRawIntakeJsonl(text: string): { raws: RawIntakeItem[]; bad:
243
244
  */
244
245
  const STALE_QUEUED_MS = 24 * 3600_000;
245
246
 
246
- export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString()): IntakeItem[] {
247
+ export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString(), taskStatus?: Parameters<typeof wasIntakeEventScheduled>[3]): IntakeItem[] {
247
248
  const userLeft = (i: IntakeItem) => i.sources.some((s) => s === 'telegram-saved' || s === 'memo');
248
249
  const score = (i: IntakeItem) => Math.max(0, ...Object.entries(i.signals).filter(([k]) => k.endsWith('.score')).map(([, v]) => v));
249
250
  const eligible = [...loadIntakeLedger(instanceRoot).items.values()]
250
251
  // queued 로 옮긴 뒤 흡수·표시가 없이 하루가 지난 것(흡수 도중 죽은 판)은 다시 고른다.
251
252
  .filter((i) => (i.status === 'new' || (i.status === 'queued' && Date.parse(now) - Date.parse(i.lastSeenAt) > STALE_QUEUED_MS))
252
- && !!i.url && (!opts.kind || i.kind === opts.kind))
253
+ && !!i.url && !wasIntakeEventScheduled(instanceRoot, i.url, now, taskStatus) && (!opts.kind || i.kind === opts.kind))
253
254
  .sort((a, b) => Number(userLeft(b)) - Number(userLeft(a)) || b.sources.length - a.sources.length || score(b) - score(a) || b.lastSeenAt.localeCompare(a.lastSeenAt));
254
255
  const laneMax = opts.laneMax ?? 30;
255
256
  const lane = eligible.filter((i) => i.sources.includes('telegram-saved')).slice(0, Math.max(0, laneMax));