elanous 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
5
+ import { listSchedules } from '../../src/release-loop/release-schedule.js';
6
+ import { listChecklist } from '../../src/release-loop/checklist.js';
7
+
8
+ export type MonthlyDeps = {
9
+ now?: () => Date;
10
+ root?: string;
11
+ landings?: (window: { from: Date; to: Date }) => Promise<unknown[]>;
12
+ decisions?: () => Promise<unknown[]>;
13
+ schedules?: () => unknown[];
14
+ weeklyDir?: string;
15
+ };
16
+
17
+ export async function runMonthly(deps: MonthlyDeps = {}) {
18
+ if (process.env.NODE_ENV === 'test' && (!deps.root || !deps.landings || !deps.decisions || !deps.schedules)) {
19
+ throw new Error('시험에서 월간 원장 수집에는 root·landings·decisions·schedules 주입 필요');
20
+ }
21
+ const now = (deps.now ?? (() => new Date()))();
22
+ const kst = new Date(now.getTime() + 9 * 3_600_000);
23
+ const year = kst.getUTCFullYear();
24
+ const month = kst.getUTCMonth();
25
+ const from = new Date(Date.UTC(year, month - 1, 1) - 9 * 3_600_000);
26
+ const to = new Date(Date.UTC(year, month, 1) - 9 * 3_600_000);
27
+ const key = new Date(from.getTime() + 9 * 3_600_000).toISOString().slice(0, 7);
28
+ const root = deps.root ?? effectiveInstanceRoot();
29
+ const file = join(root, 'rhythm', 'monthly', `${key}.md`);
30
+ if (existsSync(file)) return { file, status: '이미 있음' as const };
31
+
32
+ const safe = async <T>(read: () => T | Promise<T>): Promise<{ value: T; error?: never } | { error: string; value?: never }> => {
33
+ try { return { value: await read() }; }
34
+ catch (error) { return { error: (error instanceof Error ? error.message : String(error)).split(/\r?\n/, 1)[0]!.slice(0, 200) }; }
35
+ };
36
+ const landings = await safe(async () => (await (deps.landings ?? (async window => (await import('./daily.js')).collectLandings(now, window)))({ from, to })).length);
37
+ const releases = await safe(() => {
38
+ const schedules = deps.schedules ? deps.schedules() : listSchedules(root);
39
+ return schedules.map(value => {
40
+ if (!value || typeof value !== 'object' || !('cutAt' in value) || typeof value.cutAt !== 'string'
41
+ || !('version' in value) || typeof value.version !== 'string' || !Number.isFinite(Date.parse(value.cutAt))) {
42
+ throw new Error('판 일정 형식 오류');
43
+ }
44
+ return { version: value.version as string, cutAt: value.cutAt as string };
45
+ }).filter(schedule => Date.parse(schedule.cutAt) >= from.getTime() && Date.parse(schedule.cutAt) < to.getTime());
46
+ });
47
+ const checklist = await safe(() => {
48
+ if (deps.schedules || releases.error !== undefined) return [];
49
+ return releases.value.map(schedule => ({ version: schedule.version, items: listChecklist(schedule.version, root).items }));
50
+ });
51
+ const decisions = await safe(async () => (await (deps.decisions ?? (async () => (await import('./daily.js')).collectDecisions()))()).length);
52
+ const weeks = await safe(() => readdirSync(deps.weeklyDir ?? join(root, 'rhythm', 'weekly'), { withFileTypes: true })
53
+ .filter(entry => entry.isFile()).map(entry => entry.name).filter(name => {
54
+ const match = /^(\d{4})-W(\d{2})\.md$/.exec(name);
55
+ if (!match) return false;
56
+ const jan4 = Date.UTC(Number(match[1]), 0, 4);
57
+ const monday = jan4 - ((new Date(jan4).getUTCDay() + 6) % 7) * 86_400_000 + (Number(match[2]) - 1) * 7 * 86_400_000;
58
+ const start = Date.UTC(year, month - 1, 1);
59
+ const end = Date.UTC(year, month, 1);
60
+ return Number(match[2]) >= 1 && Number(match[2]) <= 53 && monday < end && monday + 7 * 86_400_000 > start;
61
+ }).map(name => name.slice(0, -3)).sort());
62
+ const show = <T>(section: { value: T; error?: never } | { error: string; value?: never }, format: (value: T) => string) =>
63
+ section.error !== undefined ? `못 읽음 · ${section.error}` : format(section.value as T);
64
+ const releaseText = show(releases, values => values.length ? `판 ${values.length}건\n${values.map(schedule => {
65
+ const items = checklist.error === undefined ? checklist.value.find(row => row.version === schedule.version)?.items : undefined;
66
+ const progress = items ? ` · green ${items.filter(item => item.status === 'green').length}/${items.length}` : '';
67
+ return `- ${schedule.version} · ${new Date(Date.parse(schedule.cutAt) + 9 * 3_600_000).toISOString().slice(0, 10)}${progress}`;
68
+ }).join('\n')}${checklist.error !== undefined ? `\n체크리스트 못 읽음 · ${checklist.error}` : ''}` : '판 0건');
69
+ const landingText = show(landings, count => `착지 ${count}건`);
70
+ const decisionText = show(decisions, count => `결정 대기 ${count}건`);
71
+ const weekText = show(weeks, keys => keys.length ? keys.map(week => `- ${week}`).join('\n') : '주간 리뷰 0건');
72
+ const questions = [
73
+ landings.error !== undefined || releases.error !== undefined
74
+ ? '착지와 판 이력을 확인하면 다음 달에 점검할 거리는 얼마인가?'
75
+ : `착지 ${landings.value}건과 판 ${releases.value.length}건을 바탕으로 다음 달에 점검할 거리는 얼마인가?`,
76
+ decisions.error !== undefined || weeks.error !== undefined
77
+ ? '결정 대기와 주간 리뷰를 확인하면 다음 달 첫 우선순위는 무엇인가?'
78
+ : `결정 대기 ${decisions.value}건과 주간 리뷰 ${weeks.value.length}개의 신호로 다음 달 첫 우선순위는 무엇인가?`,
79
+ ];
80
+ const markdown = [
81
+ `# 월간 미팅 — ${key} (KST)`,
82
+ `## S 지난달\n${landingText}\n${decisionText}\n주간 리뷰\n${weekText}`,
83
+ `## C 로드맵 거리\n기존 로드맵 기준(현행 목표 여부 미확인): 10-28 출시 · 11월 기업용\n판 이력\n${releaseText}`,
84
+ `## Q 다음 달 방향\n${questions.map((question, index) => `${index + 1}. ${question}`).join('\n')}`,
85
+ '## A 다음 달 첫 주\n- 판 이력과 주간 리뷰를 대조해 다음 달에 확인할 첫 칸을 고른다.\n- 열린 결정을 확인해 첫 주의 방향을 정한다.',
86
+ ].join('\n\n') + '\n';
87
+ mkdirSync(dirname(file), { recursive: true });
88
+ try { writeFileSync(file, markdown, { flag: 'wx' }); }
89
+ catch (error) {
90
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') return { file, status: '이미 있음' as const };
91
+ throw error;
92
+ }
93
+ return { file, status: 'created' as const, markdown };
94
+ }
95
+
96
+ if (import.meta.main) runMonthly().then(result => console.log(`${result.status}: ${result.file}`))
97
+ .catch(error => { console.error(`rhythm-monthly: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });
package/src/acp/client.ts CHANGED
@@ -302,7 +302,7 @@ const ACP_FORCED_BILLING_ENV_BY_BACKEND: Readonly<Record<string, Readonly<Record
302
302
  };
303
303
 
304
304
  function scrubAcpBillingEnv(spec: AcpBackendSpec, env: NodeJS.ProcessEnv): AcpBillingEnvScrubObservation {
305
- const billingEnvScrubEnabled = getUserConfig().acp.scrubBillingEnv !== false;
305
+ const billingEnvScrubEnabled = spec.id === 'claude' || getUserConfig().acp.scrubBillingEnv !== false;
306
306
  const scrubbedBillingEnv: string[] = [];
307
307
  const forcedBillingEnv: string[] = [];
308
308
  if (billingEnvScrubEnabled) {
@@ -378,6 +378,8 @@ export interface AcpAgentOpts {
378
378
  /** Extra env vars to pass to the subprocess. Inherits from
379
379
  * process.env unless overridden. */
380
380
  env?: Record<string, string>;
381
+ /** Test seam: replace the child process without launching Claude. */
382
+ spawnChild?: typeof spawn;
381
383
  /** Session-scoped Codex app-server configuration arguments. Ignored by
382
384
  * non-Codex ACP backends. */
383
385
  codexArgs?: readonly string[];
@@ -419,6 +421,7 @@ export class AcpAgent {
419
421
  private readonly spec: AcpBackendSpec;
420
422
  private readonly cwd: string;
421
423
  private readonly env: NodeJS.ProcessEnv;
424
+ private readonly spawnChild: typeof spawn;
422
425
  private readonly billingEnvScrub: AcpBillingEnvScrubObservation;
423
426
  private readonly log: (msg: string) => void;
424
427
  private readonly onCapabilities: ((capabilities: ElanousCapabilities) => void) | undefined;
@@ -461,6 +464,7 @@ export class AcpAgent {
461
464
  }
462
465
  this.billingEnvScrub = scrubAcpBillingEnv(this.spec, cleanEnv);
463
466
  this.env = cleanEnv;
467
+ this.spawnChild = opts.spawnChild ?? spawn;
464
468
  this.log = opts.log ?? ((m) => console.error(`[acp:${this.spec.id}] ${m}`));
465
469
  this.onCapabilities = opts.onCapabilities;
466
470
  this.permissionApprover = opts.permissionApprover ?? null;
@@ -501,7 +505,18 @@ export class AcpAgent {
501
505
  throw new Error(`acp spawn: cwd does not exist: ${this.cwd}`);
502
506
  }
503
507
 
504
- const child = spawn(resolvedBin.path, this.spec.args, {
508
+ if (this.spec.id === 'claude') {
509
+ const remaining = (claudeBackend.scrubEnv ?? []).filter(key => key in this.env);
510
+ debug.log('acp.claude', 'auth-source', {
511
+ apiKeyPresent: remaining.length > 0,
512
+ source: remaining.length > 0 ? 'billing-env' : 'subscription',
513
+ });
514
+ if (remaining.length > 0 && process.env.NODE_ENV !== 'test' && !process.env.ELANOUS_TEST_HOME) {
515
+ throw new Error('ACP Claude refused: billing authentication environment remains after scrub');
516
+ }
517
+ }
518
+
519
+ const child = this.spawnChild(resolvedBin.path, this.spec.args, {
505
520
  cwd: this.cwd,
506
521
  env: this.env,
507
522
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -15,6 +15,8 @@ import { join, relative, resolve, isAbsolute, sep } from 'node:path';
15
15
  import { startPty, mintPtyId, onPtyEvent, type PtyHandle, type StartOpts } from '../pty-shell/registry.js';
16
16
  import { withChildPtyIdentity } from '../agent/pty-identity.js';
17
17
  import { NESTED_AGENT_ENV_BLOCKLIST } from '../agent/nested-agent-env.js';
18
+ import { getNestDepth } from '../agent/nest-depth.js';
19
+ import { gateNestedElanousLaunch, nestedDepthCodexArgs, readNestedElanousDepth } from '../harness/nested-elanous-policy.js';
18
20
  import { createWorktree, resolveMainRepoRoot, type CreateWorktreeOpts } from '../git-fs/worktree.js';
19
21
  import { configuredWorktreeRoot } from '../user-config.js';
20
22
  import { worktreeHasChanges, commitWorktree, changedFiles } from '../self-implement/seams.js';
@@ -321,7 +323,36 @@ export function resolveBackendSpawn(
321
323
  // 지운 키 이름 — PTY 합성이 로그인 셸 캡처본에서 되살리지 않게 그대로 넘긴다(강제 env 로 다시 넣은 키는 뺀다).
322
324
  // ⚠️ 과금 스크럽 키만 — 중첩 표지는 정체성 allowlist 가 다시 싣는 키가 있어 이 PR 의 범위 밖이다.
323
325
  const unsetEnv = [...new Set(backend.scrubEnv ?? [])].filter((k) => !(k in env));
324
- return { cmd: backend.cmd, args: [...backend.args], env, unsetEnv, nestedEnvRemovedCount, forcedEnv };
326
+ // Codex drops parent env under shell_environment_policy inherit=core, so the nested-depth
327
+ // marker rides on argv (`-c shell_environment_policy.set.ELANOUS_NESTED_DEPTH=<child>`).
328
+ // Backends that cannot carry it are refused only when a nested elanous launch was requested.
329
+ const args = [...backend.args, ...nestedDepthCodexArgs(backend.name, getNestDepth() + 1)];
330
+ return { cmd: backend.cmd, args, env, unsetEnv, nestedEnvRemovedCount, forcedEnv };
331
+ }
332
+
333
+ /**
334
+ * Refuse a requested nested elanous launch when this backend cannot carry the depth marker.
335
+ * Unset depth is 0. A normal mission spawn (`nestedLaunchRequested: false`) is never refused here.
336
+ */
337
+ export function refuseNestedElanousOnBackend(input: {
338
+ readonly backendName: string;
339
+ readonly nestedLaunchRequested: boolean;
340
+ readonly allow?: boolean;
341
+ readonly configAllow?: boolean;
342
+ readonly env?: Record<string, string | undefined>;
343
+ }): { readonly refused: boolean; readonly reason?: string; readonly depth: number } {
344
+ const depth = readNestedElanousDepth(input.env);
345
+ const decision = gateNestedElanousLaunch({
346
+ backendName: input.backendName,
347
+ depth,
348
+ nestedLaunchRequested: input.nestedLaunchRequested,
349
+ ...(input.allow === true ? { allow: true } : {}),
350
+ ...(input.configAllow === true ? { configAllow: true } : {}),
351
+ });
352
+ if (input.nestedLaunchRequested && !decision.allowed) {
353
+ return { refused: true, reason: decision.reason, depth };
354
+ }
355
+ return { refused: false, depth };
325
356
  }
326
357
 
327
358
  /** Build the mission PTY options after identity is preallocated for child env propagation. */
@@ -491,6 +522,12 @@ export interface AgentMissionSpec {
491
522
  memory?: boolean;
492
523
  /** PTY 닉네임(휴먼 리더블·goto 로 나중 접근). 기본 branch. */
493
524
  nickname?: string;
525
+ /**
526
+ * True only when this mission is itself a nested elanous launch.
527
+ * A backend that cannot carry the depth marker is then refused.
528
+ * Absent means an ordinary mission spawn — never refused for a missing marker.
529
+ */
530
+ nestedElanousLaunch?: boolean;
494
531
  /** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
495
532
  decisionMissionId?: string;
496
533
  /** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
@@ -1534,6 +1571,19 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1534
1571
  // 구독 모드 — backend 지정 env 키 스크럽 후 강제 env까지 적용해 cmd·args·env를 순수 seam으로 구성한다.
1535
1572
  // forcedEnv 이름의 후속 로깅은 이 착지의 의도적 경계이며, spawn에는 같은 seam의 env가 그대로 흐른다.
1536
1573
  const spawnParams = resolveBackendSpawn(backend, process.env);
1574
+ const nestedRefusal = refuseNestedElanousOnBackend({
1575
+ backendName: backend.name,
1576
+ nestedLaunchRequested: spec.nestedElanousLaunch === true,
1577
+ env: process.env,
1578
+ });
1579
+ if (nestedRefusal.refused) {
1580
+ debug.log('agent-mission.nested', 'refused', { agent: backend.name, depth: nestedRefusal.depth, reason: nestedRefusal.reason });
1581
+ return {
1582
+ ok: false, worktree: spec.workdir ?? '', branch: spec.branch ?? null, rounds: 0,
1583
+ evidencePath: null, committed: false, usedOmniCrawl: false,
1584
+ detail: `nested elanous refused (${nestedRefusal.reason})`,
1585
+ };
1586
+ }
1537
1587
  const env = spawnParams.env;
1538
1588
  if (backend.name === 'claude') {
1539
1589
  const status = (deps.checkClaudeSubscription ?? checkClaudeSubscription)({ env });
@@ -0,0 +1,40 @@
1
+ import type { Command } from 'commander';
2
+ import { parseSince } from '../cli/logs-cli.js';
3
+ import {
4
+ SUPERVISOR_METRICS_DEFAULT_SINCE,
5
+ formatSupervisorMetrics,
6
+ loadSupervisorMetrics,
7
+ type ReadSupervisorLogs,
8
+ } from './supervisor-metrics.js';
9
+
10
+ export interface SupervisorMetricsCliDeps {
11
+ readonly read?: ReadSupervisorLogs;
12
+ readonly write?: (text: string) => void;
13
+ readonly nowMs?: number;
14
+ }
15
+
16
+ export function registerSupervisorMetricsCommand(parent: Command, deps: SupervisorMetricsCliDeps = {}): Command {
17
+ return parent.command('metrics')
18
+ .description('외부 에이전트 미션 슈퍼바이저 품질 지표 — 이미 남은 미션 기록만 읽는다')
19
+ .option('--since <window>', '조회 창 (30s/15m/2h/7d · 기본 7d)', SUPERVISOR_METRICS_DEFAULT_SINCE)
20
+ .option('--json', 'JSON 출력')
21
+ .action((opts: { since?: string; json?: boolean }) => {
22
+ const since = opts.since?.trim() || SUPERVISOR_METRICS_DEFAULT_SINCE;
23
+ const nowMs = deps.nowMs ?? Date.now();
24
+ const relative = /^(\d+)(s|m|h|d)$/.exec(since);
25
+ const unitMs = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 } as const;
26
+ // parseSince returns an absolute epoch (including for relative inputs); anchor relative windows to this invocation's clock.
27
+ const sinceMs = relative
28
+ ? nowMs - Number(relative[1]) * unitMs[relative[2] as keyof typeof unitMs]
29
+ : parseSince(since);
30
+ const write = deps.write ?? ((text: string) => { process.stdout.write(text); });
31
+ if (sinceMs === null || !Number.isFinite(sinceMs)) {
32
+ write(`--since 를 읽지 못함: ${since}\n`);
33
+ process.exitCode = 2;
34
+ return;
35
+ }
36
+ const report = loadSupervisorMetrics(since, sinceMs, nowMs, deps.read);
37
+ if (opts.json) write(`${JSON.stringify(report)}\n`);
38
+ else write(`${formatSupervisorMetrics(report)}\n`);
39
+ });
40
+ }
@@ -0,0 +1,218 @@
1
+ // X2 — 외부 에이전트 미션 슈퍼바이저 품질 지표. 읽기 전용.
2
+ // 새 기록을 만들지 않는다. 이미 남는 미션 로그(logs.db)만 집계한다.
3
+ // 기록이 없는 지표는 0 이 아니라 «못 잼 · 이유»다. 창 안 미션이 0 이면 «미션 없음».
4
+
5
+ import { existsSync } from 'node:fs';
6
+ import { LogStore, logsDbPath, type LogStoreRow } from '../mss/logging/log-store.js';
7
+
8
+ export const SUPERVISOR_METRICS_DEFAULT_SINCE = '7d';
9
+
10
+ const RESULT_CATEGORY = 'agent-mission';
11
+ const RESULT_EVENT = 'result';
12
+ const DECISION_CATEGORY = 'pty.decision';
13
+ const ANSWER_EVENT = 'answer';
14
+ const CONTROL_CATEGORY = 'autopilot.control';
15
+ const CONTROL_END_EVENT = 'end';
16
+ const STALL_CATEGORY = 'self-implement';
17
+ const STALL_EVENT = 'frame-stall';
18
+ const LADDER_CATEGORY = 'agent-mission.resources';
19
+
20
+ const LADDER_STEPS = ['needs', 'have', 'official-index', 'backend', 'discover'] as const;
21
+
22
+ export interface MetricRow {
23
+ readonly id: 'completion' | 'questionsAnswered' | 'unnecessaryInterventions' | 'stall' | 'ladderHits';
24
+ readonly label: string;
25
+ readonly status: 'measured' | 'unmeasured' | 'no-missions';
26
+ readonly reason?: string;
27
+ readonly value?: number | string;
28
+ readonly detail?: Record<string, number | string>;
29
+ }
30
+
31
+ export interface SupervisorMetricsReport {
32
+ readonly since: string;
33
+ readonly sinceMs: number;
34
+ readonly missions: number;
35
+ readonly status: 'measured' | 'no-missions';
36
+ readonly metrics: readonly MetricRow[];
37
+ }
38
+
39
+ export interface SupervisorLogRow {
40
+ readonly tsMs: number;
41
+ readonly category: string;
42
+ readonly event: string;
43
+ readonly data: Record<string, unknown> | null;
44
+ }
45
+
46
+ export interface ReadSupervisorLogs {
47
+ (sinceMs: number, nowMs: number): readonly SupervisorLogRow[];
48
+ }
49
+
50
+ function parseData(raw: string | null): Record<string, unknown> | null {
51
+ if (!raw) return null;
52
+ try {
53
+ const parsed: unknown = JSON.parse(raw);
54
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function missionKey(data: Record<string, unknown> | null): string | null {
61
+ const runId = data?.runId;
62
+ if (typeof runId === 'string' && runId.trim()) return runId.trim();
63
+ const missionId = data?.missionId;
64
+ if (typeof missionId === 'string' && missionId.trim()) return missionId.trim();
65
+ return null;
66
+ }
67
+
68
+ function linkedRows(rows: readonly SupervisorLogRow[], missionIds: ReadonlySet<string>): { linked: SupervisorLogRow[]; unlinked: boolean } {
69
+ const linked: SupervisorLogRow[] = [];
70
+ let unlinked = false;
71
+ for (const row of rows) {
72
+ const key = missionKey(row.data);
73
+ if (key && missionIds.has(key)) linked.push(row);
74
+ else unlinked = true;
75
+ }
76
+ return { linked, unlinked };
77
+ }
78
+
79
+ const UNLINKED_REASON = '못 잼 · 미션과 연결 불가';
80
+
81
+ function unmeasured(id: MetricRow['id'], label: string, reason: string): MetricRow {
82
+ return { id, label, status: 'unmeasured', reason };
83
+ }
84
+
85
+ /** 주입된 로그만으로 다섯 지표를 집계한다. 스토어를 열지 않는다. */
86
+ export function aggregateSupervisorMetrics(
87
+ rows: readonly SupervisorLogRow[],
88
+ opts: { since?: string; sinceMs: number },
89
+ ): SupervisorMetricsReport {
90
+ const since = opts.since ?? SUPERVISOR_METRICS_DEFAULT_SINCE;
91
+ const results = rows.filter((row) => row.category === RESULT_CATEGORY && row.event === RESULT_EVENT);
92
+ const missionIds = new Set(results.map((row) => missionKey(row.data)).filter((key): key is string => key !== null));
93
+ const missions = results.length;
94
+ if (missions === 0) {
95
+ const reason = '미션 없음';
96
+ return {
97
+ since,
98
+ sinceMs: opts.sinceMs,
99
+ missions: 0,
100
+ status: 'no-missions',
101
+ metrics: [
102
+ { id: 'completion', label: '완주율', status: 'no-missions', reason },
103
+ { id: 'questionsAnswered', label: '대화형 질문에 답해 넘긴 수', status: 'no-missions', reason },
104
+ { id: 'unnecessaryInterventions', label: '불필요했던 슈퍼바이저 개입', status: 'no-missions', reason },
105
+ { id: 'stall', label: '멈춤 감지 · 실제 멈춤', status: 'no-missions', reason },
106
+ { id: 'ladderHits', label: '사다리 단계별 적중', status: 'no-missions', reason },
107
+ ],
108
+ };
109
+ }
110
+
111
+ const completed = results.filter((row) => row.data?.ok === true).length;
112
+ const completion: MetricRow = {
113
+ id: 'completion',
114
+ label: '완주율',
115
+ status: 'measured',
116
+ value: `${completed}/${missions}`,
117
+ detail: { completed, missions, rate: completed / missions },
118
+ };
119
+
120
+ const answerCandidates = rows.filter((row) => row.category === DECISION_CATEGORY && row.event === ANSWER_EVENT);
121
+ const answers = linkedRows(answerCandidates, missionIds);
122
+ const questionsAnswered: MetricRow = answers.unlinked
123
+ ? unmeasured('questionsAnswered', '대화형 질문에 답해 넘긴 수', UNLINKED_REASON)
124
+ : answers.linked.length === 0
125
+ ? unmeasured('questionsAnswered', '대화형 질문에 답해 넘긴 수', '못 잼 · pty.decision answer 기록 없음')
126
+ : {
127
+ id: 'questionsAnswered',
128
+ label: '대화형 질문에 답해 넘긴 수',
129
+ status: 'measured',
130
+ value: answers.linked.length,
131
+ detail: { answered: answers.linked.length },
132
+ };
133
+
134
+ const endCandidates = rows.filter((row) => row.category === CONTROL_CATEGORY && row.event === CONTROL_END_EVENT && row.data?.interventionLevel !== undefined);
135
+ const ends = linkedRows(endCandidates, missionIds);
136
+ // 종료 상태와 verdict 는 개입이 불필요했다는 인과 근거가 아니다.
137
+ const unnecessaryInterventions: MetricRow = unmeasured('unnecessaryInterventions', '불필요했던 슈퍼바이저 개입', ends.unlinked
138
+ ? UNLINKED_REASON
139
+ : ends.linked.length === 0
140
+ ? '못 잼 · autopilot.control end 개입 기록 없음'
141
+ : '못 잼 · 개입 불필요성 근거 기록 없음');
142
+
143
+ const stallCandidates = rows.filter((row) => row.category === STALL_CATEGORY && row.event === STALL_EVENT);
144
+ const stalls = linkedRows(stallCandidates, missionIds);
145
+ // frame-stall is a detector stage, not a record that a mission actually stopped.
146
+ const stall: MetricRow = unmeasured('stall', '멈춤 감지 · 실제 멈춤', stalls.unlinked
147
+ ? UNLINKED_REASON
148
+ : stalls.linked.length === 0
149
+ ? '못 잼 · self-implement frame-stall 기록 없음'
150
+ : '못 잼 · 실제 멈춤 근거 기록 없음');
151
+
152
+ const ladderCandidates = rows.filter((row) => row.category === LADDER_CATEGORY && LADDER_STEPS.includes(row.event as typeof LADDER_STEPS[number]));
153
+ const ladderRows = linkedRows(ladderCandidates, missionIds);
154
+ // VERIFY/ROUTE are decisions. No outcome record connects a step to an observed success.
155
+ const ladderHits: MetricRow = unmeasured('ladderHits', '사다리 단계별 적중', ladderRows.unlinked
156
+ ? UNLINKED_REASON
157
+ : ladderRows.linked.length === 0
158
+ ? '못 잼 · agent-mission.resources 사다리 기록 없음'
159
+ : '못 잼 · 사다리 적중 근거 기록 없음');
160
+
161
+ return {
162
+ since,
163
+ sinceMs: opts.sinceMs,
164
+ missions,
165
+ status: 'measured',
166
+ metrics: [completion, questionsAnswered, unnecessaryInterventions, stall, ladderHits],
167
+ };
168
+ }
169
+
170
+ export function readSupervisorLogsFromStore(path: string = logsDbPath()): ReadSupervisorLogs {
171
+ return (sinceMs, nowMs) => {
172
+ if (!existsSync(path)) return [];
173
+ const store = LogStore.openReadOnly(path);
174
+ try {
175
+ const queried: LogStoreRow[] = store.queryAll({
176
+ exactCategories: [RESULT_CATEGORY, DECISION_CATEGORY, CONTROL_CATEGORY, STALL_CATEGORY, LADDER_CATEGORY],
177
+ sinceMs,
178
+ untilMs: nowMs,
179
+ });
180
+ return queried
181
+ .filter((row) => row.ts_ms >= sinceMs && row.ts_ms <= nowMs)
182
+ .filter((row) =>
183
+ (row.category === RESULT_CATEGORY && row.event === RESULT_EVENT)
184
+ || (row.category === DECISION_CATEGORY && row.event === ANSWER_EVENT)
185
+ || (row.category === CONTROL_CATEGORY && row.event === CONTROL_END_EVENT)
186
+ || (row.category === STALL_CATEGORY && row.event === STALL_EVENT)
187
+ || (row.category === LADDER_CATEGORY && (LADDER_STEPS as readonly string[]).includes(row.event)))
188
+ .map((row) => ({ tsMs: row.ts_ms, category: row.category, event: row.event, data: parseData(row.data) }));
189
+ } finally {
190
+ store.close();
191
+ }
192
+ };
193
+ }
194
+
195
+ export function loadSupervisorMetrics(
196
+ since: string,
197
+ sinceMs: number,
198
+ nowMs: number,
199
+ read: ReadSupervisorLogs = readSupervisorLogsFromStore(),
200
+ ): SupervisorMetricsReport {
201
+ return aggregateSupervisorMetrics(read(sinceMs, nowMs), { since, sinceMs });
202
+ }
203
+
204
+ export function formatSupervisorMetrics(report: SupervisorMetricsReport): string {
205
+ const lines = [`agent-mission metrics · since ${report.since} · 미션 ${report.missions}`];
206
+ if (report.status === 'no-missions') {
207
+ lines.push('미션 없음');
208
+ return lines.join('\n');
209
+ }
210
+ for (const metric of report.metrics) {
211
+ if (metric.status === 'unmeasured') {
212
+ lines.push(`${metric.label}: ${metric.reason}`);
213
+ continue;
214
+ }
215
+ lines.push(`${metric.label}: ${metric.value}`);
216
+ }
217
+ return lines.join('\n');
218
+ }
@@ -3,6 +3,7 @@ import { appendFileSync, mkdirSync, readFileSync, rmdirSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import { stateDirSourceForChild } from '../agent/identity-env.js';
6
+ import { oldDoorInternalEnv } from '../self-dev/old-door.js';
6
7
  import { effectiveInstanceRoot } from '../instance/resolve.js';
7
8
  import type { TrackAgentDecision, TrackAgentInput, TrackAgentResult } from './track-agent.js';
8
9
 
@@ -128,7 +129,7 @@ export async function launchTrackAgentShadow(input: TrackAgentInput, options: Sh
128
129
  const instanceRoot = effectiveInstanceRoot();
129
130
  const { stdout } = await exec('bun', ['bin/elanous.mjs', '--config-dir', instanceRoot, ...values], {
130
131
  cwd: process.env.ELANOUS_TOOL_CWD?.trim() || resolve(import.meta.dir, '../..'),
131
- env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot, ELANOUS_STATE_DIR_SOURCE: stateDirSourceForChild(instanceRoot) },
132
+ env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot, ELANOUS_STATE_DIR_SOURCE: stateDirSourceForChild(instanceRoot), ...oldDoorInternalEnv('self-orchestrate') },
132
133
  encoding: 'utf8', maxBuffer: 1024 * 1024,
133
134
  });
134
135
  return { stdout };