elanous 0.2.11 → 0.2.12

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 (246) 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/1147-466ec15afacc1a65.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-ee3795b112907122.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-0259994816604c49.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-cefe8169e36cf62a.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/board/{page-cb95a7432c9ee744.js → page-d2e35228bc497119.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12b6d2f8d2d1aea.js → page-5c05b0da4bc1392c.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b9efe868bfd36255.js → page-e0ccdb576812b523.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-813c4a52c9d05ca8.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-a022b257950f39e6.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-a2ffc717e04b8b0a.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-b59ee4e70fc18fcf.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-d5dc7ff69a37b4cf.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/{page-2a9932ddb62d76bd.js → page-1d6a533ce5a6efcb.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-dfca9b7d2de0b135.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6dd3bfbbde2a5c2c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-e7a529e0507ac86c.js → not-found-490653e8d4cc8852.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-77447356baad75d2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-6d604227e1664b71.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-0be8d4b2ced75ba0.js → page-4048543b4b7ad5e0.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-4c3a3126abed551a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-64b0aecb1ca75e75.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-53f80edc6b8dc07c.js → page-db14d0be7f1c460c.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-9547b5cec7cf76bf.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-2cddeb9e2aeedc55.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-e297866051595d81.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-baba2e047872fb0f.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-e895da4ec9cac345.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
  54. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +14 -14
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +14 -14
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +14 -14
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +14 -14
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +14 -14
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +14 -14
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +14 -14
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +14 -14
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +14 -14
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +14 -14
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +14 -14
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +14 -14
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +14 -14
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +14 -14
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +14 -14
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +14 -14
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +14 -14
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +15 -15
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +14 -14
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +14 -14
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +14 -14
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +14 -14
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +14 -14
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +14 -14
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +14 -14
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +14 -14
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +14 -14
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +14 -14
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +14 -14
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +14 -14
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +14 -14
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +14 -14
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +14 -14
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +14 -14
  140. package/graphs/context/nightly-condense.yaml +21 -0
  141. package/graphs/context/recipes.yaml +12 -0
  142. package/graphs/demo/inside-seed.yaml +20 -0
  143. package/graphs/demo/recipes.yaml +12 -0
  144. package/package.json +1 -1
  145. package/plugins/card-followup/examples/sample-card.png +0 -0
  146. package/scripts/lib/telegram-inject-args.ts +31 -0
  147. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  148. package/scripts/release-loop/auto-approve-node.ts +1 -1
  149. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  150. package/scripts/release-loop/gate-node.ts +8 -2
  151. package/scripts/release-loop/ops-upgrade-node.ts +47 -6
  152. package/scripts/release-loop/publish-node.ts +4 -1
  153. package/scripts/release-loop/version-node.ts +13 -3
  154. package/scripts/telegram-inject.ts +14 -6
  155. package/scripts/test-diet/remeasure-list.ts +112 -0
  156. package/scripts/ux/demo-rehearsal.ts +17 -13
  157. package/scripts/ux/demo-seed.ts +108 -0
  158. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  159. package/src/agent-mission/driver.ts +138 -16
  160. package/src/agent-mission/pty-decision.ts +1 -1
  161. package/src/autopilot/pty-control-loop.ts +4 -1
  162. package/src/chat/index.ts +1 -0
  163. package/src/cli/persona-cli.ts +17 -14
  164. package/src/cli/pod-cli.ts +17 -0
  165. package/src/cli/pod-memory-advice.ts +35 -0
  166. package/src/cli/release-cli.ts +46 -6
  167. package/src/context-bus/context-now-surfaces.ts +17 -0
  168. package/src/context-bus/context-now.ts +2 -2
  169. package/src/context-bus/coord-events.ts +2 -2
  170. package/src/context-bus/nightly-condense.ts +153 -0
  171. package/src/context-bus/session-events.ts +35 -0
  172. package/src/dashboard/dashboard-status-lines.ts +2 -0
  173. package/src/dashboard/index.ts +8 -0
  174. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  175. package/src/domains/outbound-alert.ts +99 -31
  176. package/src/domains/outbound-log-probe.ts +8 -0
  177. package/src/execution-loop/launch-gate.ts +17 -8
  178. package/src/field-note/telegram.ts +142 -0
  179. package/src/graph-runner/graph-grow-llm.ts +23 -13
  180. package/src/graph-runner/graph-grow.ts +5 -2
  181. package/src/graph-runner/runner.ts +12 -8
  182. package/src/harness/harness-cli-command.ts +138 -16
  183. package/src/hooks/consult-intake.ts +153 -0
  184. package/src/hooks/receiver.ts +78 -1
  185. package/src/index.ts +33 -9
  186. package/src/intake-plane/discord-seat-work.ts +28 -2
  187. package/src/intake-plane/wish-card.ts +33 -0
  188. package/src/llm/difficulty.ts +1 -1
  189. package/src/maturity/feature-maturity.ts +2 -1
  190. package/src/nexus/api/http-server.ts +31 -8
  191. package/src/nexus/api/outbound-report.ts +40 -7
  192. package/src/nexus/api/personas.ts +95 -53
  193. package/src/nexus/api/task-cards-api.ts +19 -0
  194. package/src/nexus/api/workflow-history.ts +88 -0
  195. package/src/nexus/api/workflows.ts +52 -0
  196. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  197. package/src/nexus/outbound/router.ts +8 -3
  198. package/src/persona/presets.ts +8 -1
  199. package/src/plugins/maker/market-bundle.ts +2 -1
  200. package/src/release-loop/checklist.ts +15 -3
  201. package/src/release-loop/feature-store.ts +24 -10
  202. package/src/release-loop/landed-but-yellow.ts +48 -0
  203. package/src/seat-loop/seat-loop.ts +168 -7
  204. package/src/self-dev/draft-sweep.ts +44 -10
  205. package/src/self-dev/draft-triage-rules.ts +54 -2
  206. package/src/self-implement/budget-gate.ts +29 -12
  207. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  208. package/src/self-implement/gate-scope.ts +9 -8
  209. package/src/self-implement/headless-elanous-driver.ts +7 -2
  210. package/src/self-implement/orchestrator.ts +15 -0
  211. package/src/self-implement/test-seams.ts +1 -0
  212. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  213. package/src/task-orchestrator/surfaces/self-implement-pod.ts +8 -0
  214. package/src/telegram-commands.ts +21 -0
  215. package/src/telegram-project-command.ts +78 -4
  216. package/src/telegram.ts +32 -5
  217. package/src/user-config.ts +8 -4
  218. package/src/version/packed-revision.json +1 -1
  219. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  220. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  221. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  244. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  245. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_buildManifest.js +0 -0
  246. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bun
2
+ /** Seven-day read-only measurement of self-implement Pod memory by goal kind. */
3
+ import { existsSync } from 'node:fs';
4
+ import { LogStore, logsDbPath, type LogStoreRow } from '../src/mss/logging/log-store.js';
5
+
6
+ export const GOAL_TYPES = ['test', 'code', 'pwa-build', 'docs'] as const;
7
+ export type GoalType = typeof GOAL_TYPES[number];
8
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const MIB = 1024 * 1024;
10
+
11
+ type Observation = Pick<LogStoreRow, 'ts_ms' | 'event' | 'data' | 'category'>;
12
+ type Run = { type: GoalType | null; limit: string | null; peakMiB: number | null; samples: number; terminated: boolean; oomKilled: boolean };
13
+ type MemorySample = { cgroupBytes?: unknown };
14
+ export type MemoryMeasurement = {
15
+ window: { since: string; until: string };
16
+ sources: { logs: string; observations: number; samples: number; unclassifiedRuns: number; classification: 'goal-fields-or-space-id'; peakCoverage: 'recorded-samples-only' };
17
+ byGoalType: Array<{ goalType: GoalType; runs: number; measured: number; missingPeak: number; samples: number; memoryLimits: Record<string, number>; terminations: number; oomKilled: number; peakMiB: { p50: number | null; p95: number | null; max: number | null } }>;
18
+ };
19
+
20
+ function payload(raw: string | null): Record<string, unknown> {
21
+ try {
22
+ const parsed: unknown = JSON.parse(raw ?? 'null');
23
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
24
+ } catch { return {}; }
25
+ }
26
+
27
+ /** Infer only unambiguous kinds from free text; never turn an unknown title into code. */
28
+ export function goalTypeOf(description: string): GoalType | null {
29
+ const kinds: GoalType[] = [];
30
+ if (/\b(?:pwa|next\.js|frontend|front-end|web app)[-\s]+(?:build|compile|typecheck|bundle)\b/i.test(description)) kinds.push('pwa-build');
31
+ if (/\b(?:test|tests|testing|spec|coverage|gate)\b/i.test(description) || /시험|테스트|검증/.test(description)) kinds.push('test');
32
+ if (/\b(?:docs?|documentation|readme|manual|report)\b/i.test(description) || /문서|보고서/.test(description)) kinds.push('docs');
33
+ if (/\b(?:implement|implementation|coding|code|cli)\b/i.test(description) || /구현|코드/.test(description)) kinds.push('code');
34
+ return kinds.length === 1 ? kinds[0]! : null;
35
+ }
36
+
37
+ function recordedGoalType(data: Record<string, unknown>, spaceId: string): GoalType | null {
38
+ if ('goalType' in data) return GOAL_TYPES.find((type) => type === data.goalType) ?? null;
39
+ if ('goalKind' in data) return GOAL_TYPES.find((type) => type === data.goalKind) ?? null;
40
+ const descriptions = [data.goalTitle, data.goalDescription, data.feature].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
41
+ return descriptions.length ? goalTypeOf(descriptions.join(' ')) : goalTypeOf(spaceId);
42
+ }
43
+
44
+ function percentile(sorted: readonly number[], fraction: number): number | null {
45
+ if (!sorted.length) return null;
46
+ const index = (sorted.length - 1) * fraction;
47
+ const low = Math.floor(index);
48
+ return Math.round((sorted[low]! + (sorted[Math.ceil(index)]! - sorted[low]!) * (index - low)) * 100) / 100;
49
+ }
50
+
51
+ /** All rows are read before aggregation: log query's default 100-row limit is not a seven-day census. */
52
+ export function measurePodMemoryByGoal(rows: readonly Observation[], nowMs: number = Date.now(), logs = logsDbPath()): MemoryMeasurement {
53
+ const sinceMs = nowMs - WEEK_MS;
54
+ const windowRows = rows.filter((row) => row.ts_ms >= sinceMs && row.ts_ms <= nowMs && row.category === 'self-implement.pod');
55
+ const runs = new Map<string, Run>();
56
+ const jobToSpace = new Map<string, string>();
57
+ let samples = 0;
58
+ const ordered = [...windowRows].sort((a, b) => a.ts_ms - b.ts_ms);
59
+ for (const row of ordered) {
60
+ if (row.event !== 'memory-limit') continue;
61
+ const data = payload(row.data);
62
+ const id = typeof data.spaceId === 'string' ? data.spaceId : '';
63
+ if (!id) continue;
64
+ runs.set(id, { type: recordedGoalType(data, id), limit: typeof data.memoryLimit === 'string' ? data.memoryLimit : null,
65
+ peakMiB: null, samples: 0, terminated: false, oomKilled: false });
66
+ }
67
+ for (const row of ordered) {
68
+ const data = payload(row.data);
69
+ const job = typeof data.job === 'string' ? data.job : '';
70
+ const directId = typeof data.spaceId === 'string' ? data.spaceId : '';
71
+ if (row.event === 'job-applied' && job && runs.has(directId)) jobToSpace.set(job, directId);
72
+ const run = runs.get(directId || jobToSpace.get(job) || '');
73
+ if (!run) continue;
74
+ if (row.event === 'job-finished') {
75
+ run.terminated = true;
76
+ run.oomKilled ||= data.containerReason === 'OOMKilled';
77
+ }
78
+ if (row.event === 'oom-evidence') { run.terminated = true; run.oomKilled = true; }
79
+ if (data.reason === 'OOMKilled') { run.terminated = true; run.oomKilled = true; }
80
+ if (row.event !== 'oom-evidence' && row.event !== 'memory-last') continue;
81
+ const rawSamples: unknown[] = row.event === 'oom-evidence' && Array.isArray(data.samples) ? data.samples : [data.sample];
82
+ for (const raw of rawSamples) {
83
+ const record = raw && typeof raw === 'object' ? raw as MemorySample : {};
84
+ const bytes = record.cgroupBytes;
85
+ if (typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 0) continue;
86
+ const reading = bytes / MIB;
87
+ run.peakMiB = Math.max(run.peakMiB ?? 0, reading);
88
+ run.samples++;
89
+ samples++;
90
+ }
91
+ }
92
+ return {
93
+ window: { since: new Date(sinceMs).toISOString(), until: new Date(nowMs).toISOString() },
94
+ sources: { logs, observations: windowRows.length, samples, unclassifiedRuns: [...runs.values()].filter((run) => run.type === null).length,
95
+ classification: 'goal-fields-or-space-id', peakCoverage: 'recorded-samples-only' },
96
+ byGoalType: GOAL_TYPES.map((goalType) => {
97
+ const group = [...runs.values()].filter((run) => run.type === goalType);
98
+ const peaks = group.flatMap((run) => run.peakMiB === null ? [] : [run.peakMiB]).sort((a, b) => a - b);
99
+ const memoryLimits: Record<string, number> = {};
100
+ for (const run of group) if (run.limit) memoryLimits[run.limit] = (memoryLimits[run.limit] ?? 0) + 1;
101
+ return { goalType, runs: group.length, measured: peaks.length, missingPeak: group.length - peaks.length,
102
+ samples: group.reduce((count, run) => count + run.samples, 0), memoryLimits,
103
+ terminations: group.filter((run) => run.terminated).length, oomKilled: group.filter((run) => run.oomKilled).length,
104
+ peakMiB: { p50: percentile(peaks, 0.5), p95: percentile(peaks, 0.95), max: peaks.at(-1) ?? null } };
105
+ }),
106
+ };
107
+ }
108
+
109
+ export function readPodMemoryMeasurement(path: string = logsDbPath(), nowMs: number = Date.now()): MemoryMeasurement {
110
+ if (!existsSync(path)) throw new Error(`log store not found: ${path}`);
111
+ const store = LogStore.openReadOnly(path);
112
+ try {
113
+ return measurePodMemoryByGoal(store.queryAll({ exactCategories: ['self-implement.pod'], sinceMs: nowMs - WEEK_MS, untilMs: nowMs }), nowMs, path);
114
+ } finally { store.close(); }
115
+ }
116
+
117
+ if (import.meta.main) {
118
+ const args = process.argv.slice(2);
119
+ const usage = 'Usage: bun scripts/measure-pod-memory-by-goal.ts [--json] [--logs-db <existing-logs.db>] [--help]';
120
+ const dbArg = args.indexOf('--logs-db');
121
+ const db = dbArg === -1 ? logsDbPath() : args[dbArg + 1];
122
+ if (!db || (dbArg !== -1 && (db.startsWith('--') || args.lastIndexOf('--logs-db') !== dbArg))
123
+ || args.some((arg, index) => !['--json', '--help', '--logs-db'].includes(arg) && index !== dbArg + 1)) throw new Error(usage);
124
+ if (args.includes('--help')) console.log(usage);
125
+ else {
126
+ const result = readPodMemoryMeasurement(db);
127
+ if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
128
+ else for (const group of result.byGoalType) console.log(`${group.goalType}: runs=${group.runs} measured=${group.measured} peak p50=${group.peakMiB.p50 ?? 'n/a'} MiB p95=${group.peakMiB.p95 ?? 'n/a'} MiB OOMKilled=${group.oomKilled}`);
129
+ }
130
+ }
@@ -15,7 +15,7 @@ const outcomeMetric = (name: string, node: Record<string, unknown> | undefined):
15
15
  });
16
16
 
17
17
  /** Rows shown in auto-approval.md but never counted toward the 8 blocking metrics. */
18
- const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
18
+ export const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
19
19
 
20
20
  /** `ok` · `warn` (a failure) · `unmeasured` (error or no output) — the macOS smoke never blocks publishing. */
21
21
  function macSmokeLevel(node: Record<string, unknown> | undefined): 'ok' | 'warn' | 'unmeasured' {
@@ -11,10 +11,11 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
11
11
  if (gate.ok && gate.moved.length) {
12
12
  const [major, minor, patch] = version.split('.').map(Number);
13
13
  const next = `${major}.${minor}.${patch! + 1}`;
14
+ // 이월은 «같은 칸»을 다음 판으로 옮기는 것 — 다른 판 id 충돌 검사(CKID #23407)의 대상이 아니다.
14
15
  for (const id of gate.moved) {
15
16
  const item = data.items.find((candidate) => candidate.id === id)!;
16
17
  if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
17
- try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
18
+ try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }, { allowDuplicateId: true }); }
18
19
  catch (error) {
19
20
  if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
20
21
  }
@@ -124,7 +124,8 @@ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-20
124
124
  /** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
125
125
  export const POD_SHARD_FILE_CAP = 27;
126
126
 
127
- export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
127
+ // review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
128
+ export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
128
129
  /** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
129
130
  export const GATE_NIGHTLY_AUDITS: readonly string[] = [
130
131
  // Replaces F12's real-repo bucket B apps/pwa scope check and its whole-repo export partition check.
@@ -267,7 +268,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
267
268
  }, 0);
268
269
  const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
269
270
  const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
270
- const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
271
+ const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
271
272
  const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
272
273
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
273
274
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
@@ -354,6 +355,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
354
355
  }
355
356
  }
356
357
  }
358
+ if (reason === 'no-output' && !retriedNoOutput) {
359
+ const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
360
+ debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
361
+ return retry;
362
+ }
357
363
  if (reason) {
358
364
  // 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
359
365
  // 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
@@ -1,12 +1,43 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { homedir } from 'node:os';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import { join, isAbsolute } from 'node:path';
5
5
  import { debug } from '../../src/debug/log.js';
6
6
  import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
7
7
 
8
8
  interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
9
- interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null }
9
+ /** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
10
+ export interface FeedSource { checkout: string; commit: string; cleanup: () => void }
11
+ interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null; feedSource?: (version: string) => FeedSource }
12
+
13
+ /** Detached worktree at refs/tags/v<version> with the PWA built — the feed ships what was released (10-04 0.2.11:
14
+ * packing process.cwd() shipped main HEAD ee689ad2 as «0.2.11» to node-b·cloud-vm instead of the cut 31129c91). */
15
+ export function tagFeedSource(run: CommandRunner, repo: string = process.cwd()): (version: string) => FeedSource {
16
+ return (version) => {
17
+ const tag = run('git', ['rev-parse', '--verify', `refs/tags/v${version}^{commit}`], repo);
18
+ const commit = tag.stdout.trim();
19
+ if (tag.status !== 0 || !/^[0-9a-f]{40,64}$/.test(commit)) throw new Error(`태그 없음: v${version}`);
20
+ const dir = mkdtempSync(join(tmpdir(), 'ops-feed-'));
21
+ const tree = join(dir, 'tree');
22
+ const cleanup = () => {
23
+ run('git', ['worktree', 'remove', '--force', tree], repo);
24
+ rmSync(dir, { recursive: true, force: true });
25
+ };
26
+ try {
27
+ const added = run('git', ['worktree', 'add', '--detach', tree, commit], repo);
28
+ if (added.status !== 0) throw new Error(`태그 체크아웃 실패: ${reason(added)}`);
29
+ symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
30
+ if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
31
+ // The feed carries the PWA build (apps/pwa/out is ignored, so a fresh tag checkout has none until built).
32
+ const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], tree, 900_000);
33
+ if (built.status !== 0) throw new Error(`태그 PWA 빌드 실패: ${reason(built)}`);
34
+ } catch (error) {
35
+ cleanup();
36
+ throw error;
37
+ }
38
+ return { checkout: tree, commit, cleanup };
39
+ };
40
+ }
10
41
 
11
42
  function localInstallSource(): string | null {
12
43
  try {
@@ -54,11 +85,21 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
54
85
  : (deps.exists ?? existsSync)(defaultFeed) ? defaultFeed : undefined;
55
86
  let feedError = '';
56
87
  if (feedPath !== undefined) {
88
+ let source: FeedSource | undefined;
57
89
  try {
58
- const feed = run('bun', ['scripts/publish-internal-dist.ts', '--checkout', process.cwd(), '--out', feedPath]);
59
- const data = lastResult(feed);
60
- if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
90
+ source = (deps.feedSource ?? tagFeedSource(run))(version);
91
+ const released = context.outputs['version-release']?.commit;
92
+ if (typeof released === 'string' && released && released !== source.commit) {
93
+ feedError = `내부 피드 발행 실패: 태그 v${version}(${source.commit.slice(0, 12)})가 컷(${released.slice(0, 12)})과 다르다`;
94
+ } else {
95
+ const feed = run('bun', ['scripts/publish-internal-dist.ts', '--checkout', source.checkout, '--out', feedPath]);
96
+ const data = lastResult(feed);
97
+ if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
98
+ else if (data.commit !== source.commit) feedError = `내부 피드 발행 실패: 묶은 커밋 ${String(data.commit).slice(0, 12)} ≠ 태그 ${source.commit.slice(0, 12)}`;
99
+ debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, packed: data?.commit, pwaBuild: data?.pwaBuild });
100
+ }
61
101
  } catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
102
+ finally { source?.cleanup(); }
62
103
  }
63
104
  const results: HostResult[] = [];
64
105
  for (const host of hosts as string[]) {
@@ -2,6 +2,7 @@
2
2
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { WARNING_ONLY } from './auto-approve-node.js';
5
6
  import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
6
7
 
7
8
  export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
@@ -40,7 +41,9 @@ export function runPublish(run: CommandRunner = runCommand) {
40
41
  const auto = context.outputs['auto-approve'];
41
42
  if (context.outputs['approve-publish']?.outcome !== 'approved' &&
42
43
  !(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
43
- && Array.isArray(auto.metrics) && auto.metrics.length === 8
44
+ // The 8 blocking metrics; warning-only rows (tui-regress · mac-smoke) are shown but never counted (V1g · MAC1).
45
+ && Array.isArray(auto.metrics)
46
+ && auto.metrics.filter((metric: unknown) => !(metric && typeof metric === 'object' && 'name' in metric && WARNING_ONLY.has(String(metric.name)))).length === 8
44
47
  && auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
45
48
  return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
46
49
  }
@@ -7,7 +7,7 @@ import { emitNodeResult } from './node-verdict.js';
7
7
  import { debug } from '../../src/debug/log.js';
8
8
 
9
9
  type Kind = 'release' | 'dev-bump';
10
- type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; worktree?: string; error?: string };
10
+ type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; files?: string[]; worktree?: string; error?: string };
11
11
 
12
12
  export function nextDevVersion(version: string): string {
13
13
  const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
@@ -120,12 +120,22 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
120
120
  const old = `"version": "${current}"`;
121
121
  if (!text.includes(old)) throw new Error(`package.json version field not found: ${current}`);
122
122
  writeFileSync(path, text.replace(old, `"version": "${target}"`));
123
+ const files = ['package.json'];
123
124
  const serverPath = join(worktree, 'server.json');
124
125
  if (existsSync(serverPath)) {
125
126
  const server = JSON.parse(readFileSync(serverPath, 'utf8')) as { version: string; packages: Array<{ version: string }> };
126
127
  server.version = target;
127
128
  server.packages[0]!.version = target;
128
129
  writeFileSync(serverPath, `${JSON.stringify(server, null, 2)}\n`);
130
+ files.push('server.json');
131
+ }
132
+ const agentPath = join(worktree, 'integrations', 'acp-registry', 'elanous', 'agent.json');
133
+ if (existsSync(agentPath)) {
134
+ const agent = JSON.parse(readFileSync(agentPath, 'utf8')) as { version: string; distribution: { npx: { package: string } } };
135
+ agent.version = target;
136
+ agent.distribution.npx.package = `elanous@${target}`;
137
+ writeFileSync(agentPath, `${JSON.stringify(agent, null, 2)}\n`);
138
+ files.push('agent.json');
129
139
  }
130
140
  const lockPath = join(worktree, 'bun.lock');
131
141
  const lock = readFileSync(lockPath, 'utf8');
@@ -185,7 +195,7 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
185
195
  fetchMain(repo);
186
196
  const commit = git(repo, 'rev-parse', 'origin/main');
187
197
  if (versionAt(repo) !== target || commit === before) throw new Error(`origin/main did not advance to ${target} after merge`);
188
- const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null };
198
+ const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null, files };
189
199
  if (kind === 'release') debug.log('release-loop.cut', 'chosen', { version: releaseVersion, source: 'main', commit });
190
200
  git(repo, 'worktree', 'remove', '--force', worktree);
191
201
  rmSync(temp, { recursive: true, force: true });
@@ -240,7 +250,7 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
240
250
 
241
251
  if (import.meta.main) {
242
252
  const output = main();
243
- debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome });
253
+ debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome, files: output.files ?? [] });
244
254
  emitNodeResult({ ...output, verdict: output.outcome === 'ok' ? 'pass' : 'fail', summary: output.outcome === 'ok' ? `${output.kind} ${output.version} · ${output.commit}` : `${output.kind ?? 'version'}: ${output.error ?? 'failed'}` });
245
255
  if (output.outcome === 'error') process.exitCode = 1;
246
256
  }
@@ -16,6 +16,7 @@
16
16
  import { TelegramClient, Api } from 'telegram';
17
17
  import { StringSession } from 'telegram/sessions';
18
18
  import { createInterface } from 'node:readline/promises';
19
+ import { parseInjectArgs } from './lib/telegram-inject-args.js';
19
20
 
20
21
  const args = process.argv.slice(2);
21
22
  const has = (f: string): boolean => args.includes(f);
@@ -201,15 +202,22 @@ if (has('--buttons') || has('--click') || has('--react')) {
201
202
 
202
203
  // 무인 주입: SESSION STRING 으로 로그인 → 봇에게 메시지.
203
204
  const sessionStr = process.env.TELEGRAM_USER_SESSION ?? val('--session');
204
- const to = val('--to') ?? process.env.TELEGRAM_TEST_BOT ?? configBot;
205
- const text = val('--text');
206
205
  if (!sessionStr) { console.error('❌ TELEGRAM_USER_SESSION 필요 — 먼저 `--login` 으로 발급.'); process.exit(2); }
207
- if (!to || !text) { console.error('사용: telegram-inject.ts --to @monad_test_bot --text "<objective>"'); process.exit(2); }
208
-
206
+ let inject: ReturnType<typeof parseInjectArgs>;
207
+ try {
208
+ inject = parseInjectArgs(args, { TELEGRAM_TEST_BOT: process.env.TELEGRAM_TEST_BOT ?? configBot });
209
+ } catch (error) {
210
+ console.error((error as Error).message);
211
+ process.exit(2);
212
+ }
213
+ const { to, text, photos, caption } = inject;
209
214
  const client = new TelegramClient(new StringSession(sessionStr), apiId, apiHash, { connectionRetries: 5 });
210
215
  await client.connect();
211
- const msg = await client.sendMessage(to, { message: text });
212
- console.log(`✅ 주입 완료 → ${to} (msgId=${(msg as { id?: number }).id ?? '?'}). 이 계정의 텔레그램 앱에도 대화가 보입니다.`);
216
+ const msg = photos.length > 0
217
+ ? await client.sendFile(to, { file: photos.length === 1 ? photos[0]! : photos, caption })
218
+ : await client.sendMessage(to, { message: text! });
219
+ const sent = Array.isArray(msg) ? msg[0] : msg;
220
+ console.log(`✅ 주입 완료 → ${to} (msgId=${(sent as { id?: number } | undefined)?.id ?? '?'}). 이 계정의 텔레그램 앱에도 대화가 보입니다.`);
213
221
  console.log(' → 데몬 처리 후 watchdog 검증: log/watchdog-stall.log (stall 없으면 A subprocess 격리 정상).');
214
222
  await client.disconnect();
215
223
  process.exit(0);
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { release } from 'node:os';
5
+
6
+ export const FILE_TIMEOUT_MS = 300_000;
7
+ export type Execution = { rc: number | null; stdout: string; stderr: string; secs: number; timedOut: boolean };
8
+ export type Executor = (file: string, timeoutMs: number) => Promise<Execution>;
9
+ export type Measurement = {
10
+ file: string; rc: number | null; pass: number | null; fail: number | null; skip: number | null;
11
+ secs: number; platform: string; release: string; verdict: 'pass' | 'fail' | 'skip' | '못 잼';
12
+ };
13
+
14
+ export const TIMEOUT_EXIT_GRACE_MS = 5_000;
15
+
16
+ // The wrapper handles SIGTERM by stopping its detached test group. Do not
17
+ // start another file until the wrapper closes, even after SIGKILL.
18
+ export function execute(
19
+ file: string,
20
+ timeoutMs: number,
21
+ deps: {
22
+ spawnChild?: typeof spawn;
23
+ signalGroup?: (pid: number, signal: NodeJS.Signals) => void;
24
+ graceMs?: number;
25
+ } = {},
26
+ ): Promise<Execution> {
27
+ return new Promise((resolve, reject) => {
28
+ const started = performance.now();
29
+ const child = (deps.spawnChild ?? spawn)('bun', ['run', 'test:deterministic', file], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
30
+ let stdout = '';
31
+ let stderr = '';
32
+ let timedOut = false;
33
+ let closed = false;
34
+ let escalation: ReturnType<typeof setTimeout> | undefined;
35
+ let terminationCheck: ReturnType<typeof setTimeout> | undefined;
36
+ const seconds = () => Math.round((performance.now() - started) / 1000 * 100) / 100;
37
+ const signal = (name: NodeJS.Signals) => {
38
+ if (child.pid === undefined) return;
39
+ try { (deps.signalGroup ?? ((pid, sig) => process.kill(-pid, sig)))(child.pid, name); }
40
+ catch (groupError) {
41
+ try { child.kill(name); }
42
+ catch (directError) { stderr += `\n${name} failed: ${String(groupError)}; ${String(directError)}`; }
43
+ }
44
+ };
45
+ // Bun's summary is at the end; bounded capture avoids an unbounded memory cost.
46
+ const collect = (previous: string, chunk: Buffer) => (previous + chunk.toString('utf8')).slice(-256_000);
47
+ child.stdout.on('data', (chunk: Buffer) => { stdout = collect(stdout, chunk); });
48
+ child.stderr.on('data', (chunk: Buffer) => { stderr = collect(stderr, chunk); });
49
+ const graceMs = deps.graceMs ?? TIMEOUT_EXIT_GRACE_MS;
50
+ const timer = setTimeout(() => {
51
+ timedOut = true;
52
+ signal('SIGTERM');
53
+ escalation = setTimeout(() => {
54
+ if (closed) return;
55
+ signal('SIGKILL');
56
+ terminationCheck = setTimeout(() => {
57
+ if (!closed) reject(new Error(`Cannot confirm termination of timed-out test: ${file}; stopping measurements`));
58
+ }, graceMs);
59
+ }, graceMs);
60
+ }, timeoutMs);
61
+ child.on('error', (error) => { stderr += `\n${error.message}`; });
62
+ child.on('close', (code) => {
63
+ closed = true;
64
+ clearTimeout(timer);
65
+ if (escalation) clearTimeout(escalation);
66
+ if (terminationCheck) clearTimeout(terminationCheck);
67
+ resolve({ rc: timedOut ? null : code, stdout, stderr, secs: seconds(), timedOut });
68
+ });
69
+ });
70
+ }
71
+
72
+ export async function measureList(files: readonly string[], runner: Executor = execute): Promise<Measurement[]> {
73
+ const measurements: Measurement[] = [];
74
+ for (const file of files) {
75
+ const result = await runner(file, FILE_TIMEOUT_MS);
76
+ const output = `${result.stdout}\n${result.stderr}`;
77
+ const count = (kind: 'pass' | 'fail' | 'skip'): number | null => {
78
+ const matches = [...output.matchAll(new RegExp(`^\\s*(\\d+) ${kind}\\b`, 'gm'))];
79
+ return matches.length ? Number(matches.at(-1)![1]) : null;
80
+ };
81
+ const pass = result.timedOut ? null : count('pass');
82
+ const fail = result.timedOut ? null : count('fail');
83
+ const skip = result.timedOut ? null : count('skip');
84
+ const measured = pass !== null || fail !== null || skip !== null;
85
+ const verdict = result.timedOut || !measured || result.rc === null ? '못 잼'
86
+ : fail !== null && fail > 0 ? 'fail'
87
+ : result.rc !== 0 ? '못 잼'
88
+ : skip !== null && skip > 0 ? 'skip'
89
+ : pass !== null && pass > 0 ? 'pass' : '못 잼';
90
+ measurements.push({ file, rc: result.rc, pass, fail, skip, secs: result.secs, platform: process.platform, release: release(), verdict });
91
+ }
92
+ return measurements;
93
+ }
94
+
95
+ if (import.meta.main) {
96
+ const args = process.argv.slice(2);
97
+ if (args.length < 1 || args.length > 2 || (args.length === 2 && args[1] !== '--json')) {
98
+ console.error('usage: bun scripts/test-diet/remeasure-list.ts <목록 파일> [--json]');
99
+ process.exitCode = 2;
100
+ } else {
101
+ try {
102
+ const files = readFileSync(args[0]!, 'utf8').split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
103
+ if (!files.length) throw new Error('empty test list');
104
+ const results = await measureList(files);
105
+ if (args[1] === '--json') console.log(JSON.stringify(results, null, 2));
106
+ else for (const row of results) console.log([row.file, row.rc ?? 'null', row.pass ?? '?', row.fail ?? '?', row.skip ?? '?', row.secs, row.platform, row.release, row.verdict].join('\t'));
107
+ } catch (error) {
108
+ console.error(error);
109
+ process.exitCode = 2;
110
+ }
111
+ }
112
+ }
@@ -5,6 +5,11 @@ import { Frames, launch, PUBLIC_LEAK_CHECK, sleep } from './lib/cdp.js';
5
5
  import { failedApiResponse, judgeRun, judgeScene, worstSceneState, type SceneObservation, type SceneState } from './lib/rehearsal-verdict.js';
6
6
 
7
7
  const TITLES = ['문서 아키텍처', '라이브 트레이스', '루프 에이전트', '그래프 편집기', '마법사 → 마켓', 'PTY 인텔리전스'];
8
+ export const EMPTY_STATE_PHRASES = ['지금 도는 런이 없습니다', '마법사가 아직 돌지 않았습니다', '터미널을 기다리는 중', '판단을 기다리는 중', '못 읽음'] as const;
9
+
10
+ export function sceneSnapshotScript(): string {
11
+ return `(()=>{const sections=[...document.querySelectorAll('[data-inside-scene]')]; const visible=sections.filter(s=>!s.hidden && s.getClientRects().length>0); const text=visible.length===1?visible[0].innerText:''; return {sectionsInDom:sections.length,visibleScene:visible.length===1?Number(visible[0].getAttribute('data-inside-scene')):null,textLength:text.length,emptyStates:${JSON.stringify(EMPTY_STATE_PHRASES)}.filter(phrase=>text.includes(phrase))}})()`;
12
+ }
8
13
 
9
14
  type Args = { url: string; out: string; secs: number; width: number; height: number; demo: boolean; tokenStdin: boolean };
10
15
 
@@ -168,23 +173,20 @@ export async function run(args: Args): Promise<number> {
168
173
  const initialEvents = observer.take();
169
174
  const scenes = [];
170
175
  for (let scene = 1; scene <= 6; scene++) {
171
- const hiddenByDemo = args.demo && scene === 5;
172
- if (hiddenByDemo) {
173
- const result = judgeScene({ scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: 0, frames: 0, exceptions: [], failedRequests: [], textLength: 0, leaks: [], sectionsInDom: 0, visibleScene: null });
174
- scenes.push(result);
175
- console.log(`scene ${scene} ${result.verdict} 0s frames=0`);
176
- continue;
177
- }
176
+ const hiddenByDemo = false;
178
177
  if (scene !== 1) {
179
178
  await cdp.ev<boolean>(`(()=>{const n=${scene}; const nav=document.querySelector('nav[aria-label="장면 선택"]'); const button=[...(nav?.querySelectorAll('button')??[])].find(b=>b.textContent?.trim().startsWith(n+' ')); button?.click(); return !!button})()`);
180
179
  await sleep(300);
181
180
  }
182
181
  const frames = new Frames(join(args.out, `scene-${scene}`));
183
182
  const states: SceneState[] = [];
183
+ const emptyStates = new Set<string>();
184
184
  const sceneRect = await cdp.ev<FrameRect | null>(`(()=>{const s=document.querySelector('[data-inside-scene="${scene}"]'); if (!s || s.hidden) return null; const r=s.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height}})()`);
185
185
  let blankFrames = 0;
186
186
  const inspect = async () => {
187
- states.push(await cdp.ev<SceneState>(`(()=>{const sections=[...document.querySelectorAll('[data-inside-scene]')]; const visible=sections.filter(s=>!s.hidden && s.getClientRects().length>0); return {sectionsInDom:sections.length,visibleScene:visible.length===1?Number(visible[0].getAttribute('data-inside-scene')):null,textLength:visible.length===1?visible[0].innerText.length:0}})()`));
187
+ const { emptyStates: observedEmpty, ...state } = await cdp.ev<SceneState & { emptyStates: string[] }>(sceneSnapshotScript());
188
+ states.push(state);
189
+ for (const phrase of observedEmpty) emptyStates.add(phrase);
188
190
  };
189
191
  const until = Date.now() + args.secs * 1000;
190
192
  await inspect();
@@ -203,7 +205,7 @@ export async function run(args: Args): Promise<number> {
203
205
  failedRequests: [...initialEvents.failedRequests, ...observed.failedRequests],
204
206
  } : observed;
205
207
  frames.toMp4(join(args.out, `scene-${scene}.mp4`), args.width, args.height);
206
- const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames };
208
+ const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames, emptyStates: [...emptyStates] };
207
209
  const result = judgeScene(obs);
208
210
  scenes.push(result);
209
211
  console.log(`scene ${scene} ${result.verdict} ${args.secs}s frames=${result.frames}`);
@@ -222,13 +224,15 @@ export async function run(args: Args): Promise<number> {
222
224
  '|---|---|---:|---:|---|',
223
225
  ...scenes.map((s) => `| ${s.scene} ${s.title} | ${s.verdict} | ${s.secs} | ${s.frames} | ${s.reasons.join('; ') || '없음'} |`),
224
226
  '',
225
- `ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified}`,
227
+ `ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified} · 실데이터 없음 ${result.noData}`,
228
+ '',
229
+ '종료 코드: ok=0 · broken=1 · unverified=2 · no-data=3',
226
230
  '',
227
231
  ].join('\n'));
228
232
  for (const line of result.banner) console.log(`⚠️ ${line}`);
229
- console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified}`);
230
- // 0 = all seen and ok · 1 = broken · 2 = nothing broken but a scene was not seen (not a pass).
231
- return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : 0;
233
+ console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified} no-data=${result.noData}`);
234
+ // 0 = ok · 1 = broken · 2 = unverified · 3 = no-data.
235
+ return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : result.verdict === 'no-data' ? 3 : 0;
232
236
  } finally { observer?.close(); cdp.close(); }
233
237
  }
234
238