elanous 0.2.4 → 0.2.5

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 (412) hide show
  1. package/README.md +3 -3
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
  12. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
  43. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +8 -1
  172. package/graphs/release/release-loop.yaml +6 -1
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  177. package/plugins/job-coach/README.md +12 -0
  178. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  179. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  180. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  181. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  182. package/plugins/job-coach/graphs/run-step.ts +211 -17
  183. package/plugins/job-coach/plugin.json +16 -3
  184. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  185. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  186. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  187. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  188. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  189. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  190. package/scripts/backup/elanous-backup.sh +4 -1
  191. package/scripts/build-docs-site.ts +4 -3
  192. package/scripts/coord-channel-watch.sh +23 -4
  193. package/scripts/coord-post.sh +49 -12
  194. package/scripts/coord-tracks.json +5 -5
  195. package/scripts/eval-bot-vs-grok.ts +79 -0
  196. package/scripts/eval-grok-live-baseline.ts +36 -0
  197. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  198. package/scripts/fleet/node-setup.sh +46 -7
  199. package/scripts/landing-heal/must-fix-match.ts +26 -0
  200. package/scripts/landing-heal/run-mission.ts +114 -20
  201. package/scripts/lib/front-route-measurement.ts +131 -0
  202. package/scripts/lib/tui-sim-keys.ts +42 -0
  203. package/scripts/measure-front-route-classifier.ts +56 -0
  204. package/scripts/media/ocr-leak-scan.sh +21 -0
  205. package/scripts/public-export.ts +2 -3
  206. package/scripts/public-import-pr.ts +80 -6
  207. package/scripts/publish-internal-dist.ts +2 -3
  208. package/scripts/red-sweep-diff.ts +1 -1
  209. package/scripts/release-loop/auto-approve-node.ts +72 -0
  210. package/scripts/release-loop/gate-node.ts +332 -44
  211. package/scripts/release-loop/publish-node.ts +7 -3
  212. package/scripts/release-loop/release-docs.ts +20 -13
  213. package/scripts/release-loop/test-durations.ts +63 -0
  214. package/scripts/release-loop/upgrade-node.ts +3 -2
  215. package/scripts/test-audit-coverage-per-file.sh +16 -0
  216. package/scripts/test-audit-meaningless.ts +88 -0
  217. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  218. package/scripts/test-deterministic.ts +27 -5
  219. package/scripts/tui-sim.ts +59 -14
  220. package/scripts/ux/bump-installed.sh +20 -0
  221. package/scripts/ux/lib/cdp.ts +72 -0
  222. package/scripts/ux/record-page.ts +50 -0
  223. package/skills/explainer-video/SKILL.md +99 -0
  224. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  225. package/skills/explainer-video/engine/build.mjs +431 -0
  226. package/skills/explainer-video/engine/vo.ts +37 -0
  227. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  228. package/src/agent/nested-agent-env.ts +1 -0
  229. package/src/agent-mission/driver.ts +95 -16
  230. package/src/agent-mission/mission-cli.ts +6 -0
  231. package/src/agent-mission/resource-ladder.ts +73 -25
  232. package/src/auth/acp-token.ts +16 -0
  233. package/src/auth/github-app-token.ts +112 -0
  234. package/src/autopilot/gh-rest-fallback.ts +177 -0
  235. package/src/autopilot/pr-manager.ts +51 -4
  236. package/src/boot/acp-server.ts +2 -16
  237. package/src/boot/daemon-tools/web-search.ts +2 -1
  238. package/src/boot/ws-bridge.ts +11 -0
  239. package/src/chat/index.ts +37 -8
  240. package/src/chat/log-scroll-key.ts +12 -0
  241. package/src/chat/submit-trace.ts +30 -0
  242. package/src/cli/agent-cli.ts +7 -2
  243. package/src/cli/decisions-cli.ts +94 -0
  244. package/src/cli/doctor-cli.ts +40 -54
  245. package/src/cli/doctor-readiness.ts +16 -24
  246. package/src/cli/intake-cli.ts +1 -1
  247. package/src/cli/mcp-call.ts +9 -0
  248. package/src/cli/mcp-gateway-cli.ts +7 -3
  249. package/src/cli/plugin-cli.ts +109 -11
  250. package/src/cli/pr-lineage.ts +2 -1
  251. package/src/cli/pwa-start.ts +3 -0
  252. package/src/cli/release-cli.ts +94 -7
  253. package/src/cli/self-update.ts +7 -4
  254. package/src/cli/start-cli.ts +33 -14
  255. package/src/dashboard/acp-boot.ts +3 -0
  256. package/src/dashboard/index.ts +62 -13
  257. package/src/dashboard/input/no-provider-reply.ts +13 -0
  258. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  259. package/src/dashboard/stale-install-check.ts +77 -0
  260. package/src/decisions/decision-ledger.ts +404 -0
  261. package/src/domains/outbound-alert.ts +8 -2
  262. package/src/domains/telegram-kind-route.ts +21 -0
  263. package/src/execution-loop/dispatch-task.ts +139 -0
  264. package/src/execution-loop/gate-decision-summary.ts +29 -0
  265. package/src/execution-loop/gates.ts +197 -0
  266. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  267. package/src/graph-kinds/plugin-nodes.ts +10 -2
  268. package/src/graph-runner/runner.ts +35 -5
  269. package/src/grounding/token.ts +12 -6
  270. package/src/harness/harness-cli-command.ts +68 -9
  271. package/src/harness/harness-pod-dispatch.ts +8 -2
  272. package/src/harness/harness-stop.ts +147 -18
  273. package/src/harness/self-send-decide.ts +26 -17
  274. package/src/harness/self-send-target.ts +22 -1
  275. package/src/harness/worktree-branch-prefix.ts +4 -3
  276. package/src/index.ts +56 -13
  277. package/src/instance/resolve.ts +6 -0
  278. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  279. package/src/intake-plane/digest.ts +11 -2
  280. package/src/live/detail-switch.ts +8 -3
  281. package/src/llm/usable-llm.ts +32 -0
  282. package/src/loops/registry.ts +27 -5
  283. package/src/market/publish.ts +59 -3
  284. package/src/native-tool-catalog.ts +2 -2
  285. package/src/nexus/api/graph-approvals.ts +125 -0
  286. package/src/nexus/api/http-server.ts +60 -6
  287. package/src/nexus/api/log-fabric.ts +10 -1
  288. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  289. package/src/nexus/api/merge-approvals.ts +2 -1
  290. package/src/nexus/api/meta-api.ts +14 -0
  291. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  292. package/src/nexus/api/plugins-market.ts +87 -3
  293. package/src/nexus/api/pod-credential-api.ts +68 -14
  294. package/src/nexus/api/public-routes.ts +7 -0
  295. package/src/nexus/api/trace.ts +57 -6
  296. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  297. package/src/nexus/index.ts +33 -32
  298. package/src/nexus/outbound/router.ts +1 -1
  299. package/src/onboarding.ts +25 -2
  300. package/src/plugins/core/host.ts +14 -0
  301. package/src/plugins/install/market-fetch.ts +4 -3
  302. package/src/plugins/install/plugin-credentials.ts +106 -0
  303. package/src/plugins/install/plugin-install.ts +163 -32
  304. package/src/prompt-library/universal-preamble.ts +8 -2
  305. package/src/release-loop/checklist.ts +191 -0
  306. package/src/release-loop/release-note.ts +6 -3
  307. package/src/self-dev/dev-cli.ts +40 -20
  308. package/src/self-dev/dev-pipeline.ts +77 -1
  309. package/src/self-implement/gate-baseline.ts +3 -1
  310. package/src/self-implement/goal-author.ts +18 -1
  311. package/src/self-implement/host-regate.ts +6 -2
  312. package/src/self-implement/orchestrator.ts +7 -2
  313. package/src/self-implement/seams.ts +4 -2
  314. package/src/skills/tools/dev-harness.ts +42 -0
  315. package/src/steward/steward-cards.ts +45 -0
  316. package/src/steward/triage.ts +50 -14
  317. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  318. package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
  319. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  320. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  321. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  322. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  323. package/src/telegram-report.ts +12 -6
  324. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  325. package/src/tui.ts +3 -2
  326. package/src/user-config.ts +28 -2
  327. package/src/util/spawn-sync-output.ts +15 -0
  328. package/src/version/code-revision.ts +1 -1
  329. package/src/version/packed-revision.json +1 -1
  330. package/src/web-search/free.ts +77 -0
  331. package/src/web-search/index.ts +2 -0
  332. package/src/workflow-runtime/deps-bridge.ts +4 -3
  333. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  334. package/src/workflow-runtime/types.ts +2 -1
  335. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  346. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  348. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  411. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  412. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
@@ -20,9 +20,13 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
20
20
  export function runPublish(run: CommandRunner = runCommand) {
21
21
  const context = readGraphContext();
22
22
  const version = context.input.version;
23
- // ⛔ 승인 노드가 «approved» 를 남기지 않았으면 발행하지 않는다(그래프 밖에서 부르거나 간선이 바뀌어도).
24
- if (context.outputs['approve-publish']?.outcome !== 'approved') {
25
- return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: approve-publish is not approved' };
23
+ // A measured automatic decision or the existing human approval must authorize publication.
24
+ const auto = context.outputs['auto-approve'];
25
+ if (context.outputs['approve-publish']?.outcome !== 'approved' &&
26
+ !(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
27
+ && Array.isArray(auto.metrics) && auto.metrics.length === 8
28
+ && auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
29
+ return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
26
30
  }
27
31
  for (const node of ['gate', 'pwa', 'upgrade', 'tui', 'prepare', 'docs']) {
28
32
  if (context.outputs[node]?.outcome !== 'ok') return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `publish blocked: ${node} outcome is not ok` };
@@ -1,26 +1,27 @@
1
+ import { debug } from '../../src/debug/log.js';
1
2
  import type { ReleaseManifest } from '../../src/release-loop/manifest.js';
2
3
 
3
- /** Render the cutoff's IN set, never the deferred or escalation lists. */
4
+ /** Render public IN lines only; keep private titles and PR numbers out of release notes. */
4
5
  export function renderReleaseNotes(manifest: ReleaseManifest, version: string): string {
5
6
  const sections = [
6
7
  { heading: 'Behavior changes', kinds: ['feat', 'fix'] },
7
8
  { heading: 'Security', kinds: ['security'] },
8
- { heading: 'Internal', kinds: ['internal'] },
9
- { heading: 'Other changes', kinds: ['unknown'] },
10
9
  ] as const;
11
10
  const lines = [`# ${version}`, ''];
11
+ let shown = 0;
12
12
  for (const section of sections) {
13
- const entries = manifest.in.filter((entry) => section.kinds.some((kind) => kind === (entry.kind ?? 'unknown')));
13
+ const entries = manifest.in.filter((entry) => section.kinds.some((kind) => kind === entry.kind) && entry.line);
14
14
  if (!entries.length) continue;
15
15
  lines.push(`## ${section.heading}`, '');
16
16
  for (const entry of entries) {
17
- const label = (entry.line ?? entry.title).replace(/\r?\n/g, ' ').trim();
18
- lines.push(entry.prNumber === undefined
19
- ? `- ${label}`
20
- : `- ${label} ([#${entry.prNumber}](https://github.com/ElanvitalAI/elanous/pull/${entry.prNumber}))`);
17
+ lines.push(`- ${entry.line!.replace(/\r?\n/g, ' ').trim()}`);
18
+ shown++;
21
19
  }
22
20
  lines.push('');
23
21
  }
22
+ const omitted = manifest.in.length - shown;
23
+ if (omitted) lines.push(`Plus ${omitted} internal ${omitted === 1 ? 'change' : 'changes'}.`);
24
+ debug.log('release-loop.docs', 'notes-rendered', { version, shown, omitted });
24
25
  return `${lines.join('\n').trimEnd()}\n`;
25
26
  }
26
27
 
@@ -33,8 +34,8 @@ export function flipNextReleaseMarkers(text: string, version: string): string {
33
34
  * Fold notes that landed before the release loop (a PR appending its own line, a headline paragraph)
34
35
  * into the rendered notes instead of refusing or overwriting them. Paragraphs before any section stay
35
36
  * under the title (the headline slot); list lines the cutoff did not render keep their own `##` section
36
- * (lines outside any section go to «Also in this release»). A line that differs from a rendered one only
37
- * by the trailing PR link is the same line. Folding its own output again gives the same bytes.
37
+ * (lines outside any section go to «Also in this release»). A hand-written link cannot be identified
38
+ * as generator output from its shape alone. Folding its own output again gives the same bytes.
38
39
  */
39
40
  export function foldPreLandedNotes(existing: string, rendered: string): { text: string; folded: number } {
40
41
  const key = (line: string) => line.trim().replace(/\s*\(\[#\d+\]\([^)]*\)\)\s*$/, '').trim();
@@ -46,12 +47,14 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
46
47
  const alreadyRendered = (line: string, section: string | null) => section === null ? renderedAll.has(key(line)) : (renderedBySection.get(section)?.has(key(line)) ?? false);
47
48
  const paragraphs: string[] = [];
48
49
  const sections = new Map<string, string[]>();
50
+ const [title, ...rest] = rendered.trimEnd().split('\n');
51
+ const footer = rest.find((line) => /^Plus \d+ internal changes?\.$/.test(line));
49
52
  const DEFAULT = '## Also in this release';
50
53
  let heading: string | null = null;
51
54
  for (const raw of existing.split(/\r?\n/)) {
52
55
  const line = raw.trimEnd();
53
56
  const trimmed = line.trim();
54
- if (/^#\s/.test(trimmed)) continue;
57
+ if (/^#\s/.test(trimmed) || /^Plus \d+ internal changes?\.$/.test(trimmed)) continue;
55
58
  if (!trimmed) {
56
59
  // Keep paragraph breaks (collapse runs of blank lines to one) in the headline block and in sections.
57
60
  if (heading === null) { if (paragraphs.length && paragraphs.at(-1) !== '') paragraphs.push(''); }
@@ -75,9 +78,9 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
75
78
  }
76
79
  }
77
80
  // Rendered notes = title ⊕ ordered `##` sections. A pre-landed section with the same heading merges into it.
78
- const [title, ...rest] = rendered.trimEnd().split('\n');
79
81
  const renderedSections: Array<{ heading: string; lines: string[] }> = [];
80
82
  for (const line of rest) {
83
+ if (line === footer) continue;
81
84
  if (/^##\s/.test(line.trim())) renderedSections.push({ heading: line.trim(), lines: [] });
82
85
  else if (renderedSections.length && line.trim()) renderedSections.at(-1)!.lines.push(line);
83
86
  }
@@ -97,6 +100,10 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
97
100
  while (paragraphs.at(-1) === '') paragraphs.pop();
98
101
  if (paragraphs.length) parts.push(paragraphs.join('\n'), '');
99
102
  for (const section of renderedSections) parts.push(section.heading, '', ...section.lines, '');
100
- for (const [name, lines] of sections) { parts.push(name, '', ...lines, ''); folded += Math.max(lines.filter(Boolean).length, 1); }
103
+ for (const [name, lines] of sections) {
104
+ parts.push(name, '', ...lines, '');
105
+ folded += Math.max(lines.filter(Boolean).length, 1);
106
+ }
107
+ if (footer) parts.push(footer);
101
108
  return { text: `${parts.join('\n').trimEnd()}\n`, folded };
102
109
  }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * 게이트 조각 junit(`gate-logs/<cut|baseline>/pod-*.junit.xml`)에서 파일별 소요를 모은다 — K10 D4 «느린 시험 목록»의 자.
4
+ * 같은 파일이 여러 번 돌았으면(반쪼개기·격리) 가장 적은 파일과 함께 돈 기록을 쓴다 — 혼자 돈 값이 그 파일의 값에 가장 가깝다.
5
+ *
6
+ * bun scripts/release-loop/test-durations.ts <gate-logs/cut 디렉터리> [--top 5] [--json]
7
+ */
8
+ import { readdirSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ export type FileDuration = { file: string; seconds: number; tests: number; runFiles: number };
12
+
13
+ /** junit 한 벌에서 `<testsuite file=… time=… tests=…>` 를 읽는다. */
14
+ export function parseJunitSuites(xml: string): Array<{ file: string; seconds: number; tests: number }> {
15
+ const suites: Array<{ file: string; seconds: number; tests: number }> = [];
16
+ for (const match of xml.matchAll(/<testsuite\b([^>]*)>/g)) {
17
+ const attrs = match[1]!;
18
+ const file = /\bfile="([^"]+)"/.exec(attrs)?.[1];
19
+ const time = Number(/\btime="([^"]+)"/.exec(attrs)?.[1]);
20
+ const tests = Number(/\btests="([^"]+)"/.exec(attrs)?.[1] ?? '0');
21
+ if (file && Number.isFinite(time)) suites.push({ file, seconds: time, tests: Number.isFinite(tests) ? tests : 0 });
22
+ }
23
+ return suites;
24
+ }
25
+
26
+ /** 여러 조각의 junit 을 파일별 하나로 접는다. */
27
+ export function collectDurations(reports: string[]): FileDuration[] {
28
+ const best = new Map<string, FileDuration>();
29
+ for (const xml of reports) {
30
+ const suites = parseJunitSuites(xml);
31
+ for (const suite of suites) {
32
+ const current = best.get(suite.file);
33
+ if (!current || suites.length < current.runFiles) best.set(suite.file, { ...suite, runFiles: suites.length });
34
+ }
35
+ }
36
+ return [...best.values()].sort((a, b) => b.seconds - a.seconds || a.file.localeCompare(b.file));
37
+ }
38
+
39
+ /** 상위 `percent`% 파일과 그 몫. */
40
+ export function topShare(durations: FileDuration[], percent: number): { files: FileDuration[]; seconds: number; totalSeconds: number; share: number } {
41
+ const count = Math.max(1, Math.ceil(durations.length * percent / 100));
42
+ const files = durations.slice(0, count);
43
+ const seconds = files.reduce((sum, item) => sum + item.seconds, 0);
44
+ const totalSeconds = durations.reduce((sum, item) => sum + item.seconds, 0);
45
+ return { files, seconds, totalSeconds, share: totalSeconds ? seconds / totalSeconds : 0 };
46
+ }
47
+
48
+ if (import.meta.main) {
49
+ const args = process.argv.slice(2);
50
+ const dir = args.find((arg) => !arg.startsWith('--'));
51
+ if (!dir) { console.error('usage: test-durations.ts <gate-logs/cut> [--top 5] [--json]'); process.exit(2); }
52
+ const topIndex = args.indexOf('--top');
53
+ const percent = topIndex >= 0 ? Number(args[topIndex + 1]) : 5;
54
+ const names = readdirSync(dir).filter((name) => name.endsWith('.junit.xml'));
55
+ if (!names.length) { console.error(`no *.junit.xml under ${dir} — was this sweep run before junit reports were kept?`); process.exit(1); }
56
+ const durations = collectDurations(names.map((name) => readFileSync(join(dir, name), 'utf8')));
57
+ const top = topShare(durations, percent);
58
+ if (args.includes('--json')) console.log(JSON.stringify({ reports: names.length, files: durations.length, totalSeconds: top.totalSeconds, topPercent: percent, topSeconds: top.seconds, topShare: top.share, top: top.files }, null, 2));
59
+ else {
60
+ console.log(`reports ${names.length} · files ${durations.length} · total ${top.totalSeconds.toFixed(0)}s · top ${percent}% = ${top.files.length} files · ${top.seconds.toFixed(0)}s (${(top.share * 100).toFixed(1)}%)`);
61
+ for (const item of top.files) console.log(`${item.seconds.toFixed(1)}\t${item.tests}\t${item.file}`);
62
+ }
63
+ }
@@ -11,8 +11,9 @@ export function runUpgrade(run: CommandRunner = runCommand) {
11
11
  const output = run('bash', ['scripts/release-upgrade-check.sh', '--from', context.input.previousVersion, '--candidate', candidate]);
12
12
  if (output.stderr) process.stderr.write(output.stderr);
13
13
  const verdicts = output.stdout.split('\n').filter((line) => /\[upgrade\] verdict\b/.test(line));
14
- const passed = verdicts.length === 2 && verdicts.some((line) => /^ubuntu:24\.04\s+\[upgrade\] verdict ok\s*$/.test(line))
15
- && verdicts.some((line) => /^debian:12\s+\[upgrade\] verdict ok\s*$/.test(line));
14
+ // `release-upgrade-check.sh` appends `from=<v> to=«<v> <sha>»` after the verdict (0.2.4) — the verdict word is what counts.
15
+ const passed = verdicts.length === 2 && verdicts.some((line) => /^ubuntu:24\.04\s+\[upgrade\] verdict ok(?:\s|$)/.test(line))
16
+ && verdicts.some((line) => /^debian:12\s+\[upgrade\] verdict ok(?:\s|$)/.test(line));
16
17
  const outcome: NodeOutcome = output.status === 0 && passed ? 'ok' : verdicts.length === 0 || output.status === null || output.status === 2 ? 'error' : 'fail';
17
18
  return { outcome, verdict: outcome === 'ok' ? 'pass' as const : 'fail' as const,
18
19
  summary: outcome === 'ok' ? `upgrade ${context.input.previousVersion} → ${context.input.version} · ${verdicts.length} images` : `upgrade failed (rc=${output.status}): ${output.stdout.trim().split('\n').at(-1) || output.stderr.trim() || 'no upgrade verdict'}` };
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # K10 D5b ① — test/ 파일별 커버리지(node-b). 산출: ~/f-audit/cov/<n>/lcov.info ⊕ index.tsv(file n rc secs)
3
+ set -u
4
+ A=~/f-audit; rm -rf $A && mkdir -p $A/cov $A/logs && cd $A
5
+ git clone -q --depth 1 file://$HOME/mirror/elanous-agent.git repo || exit 2
6
+ cd repo && git log --oneline -1 > $A/COMMIT && ~/.local/share/elanous/bin/bun install --frozen-lockfile >/dev/null 2>&1 || echo "install rc=$?" >> $A/COMMIT
7
+ git ls-files 'test/*.test.ts' 'test/**/*.test.ts' | grep -v '^test/integration/' | awk '{print NR"\t"$0}' > $A/list.tsv
8
+ wc -l < $A/list.tsv > $A/TOTAL
9
+ run_one() {
10
+ n="$1"; f="$2"; s=$(date +%s)
11
+ ( ulimit -v 8000000 2>/dev/null; timeout 120 ~/.local/share/elanous/bin/bun test "./$f" --coverage --coverage-reporter=lcov --coverage-dir "$HOME/f-audit/cov/$n" > "$HOME/f-audit/logs/$n.log" 2>&1 ); rc=$?
12
+ printf '%s\t%s\t%s\t%s\n' "$f" "$n" "$rc" "$(( $(date +%s) - s ))" >> "$HOME/f-audit/index.tsv"
13
+ }
14
+ export -f run_one
15
+ cd $A/repo && awk -F'\t' '{print $1" "$2}' $A/list.tsv | xargs -P 8 -n 2 bash -c 'run_one "$0" "$1"'
16
+ echo DONE > $A/DONE
@@ -0,0 +1,88 @@
1
+ // K10 D5 — 무의미 시험 전수조사(읽기 전용 · 정적). 산출 = TSV(파일 · 분류 · 근거) ⊕ 요약 JSON.
2
+ // 분류: A 대상 없음(상대 import 가 가리키는 파일이 없다 · 또는 저장소 코드를 하나도 안 부른다)
3
+ // B 상수=상수(expect(<리터럴>)의 리터럴이 기대값과 같다 · expect(x).toBe(x) 같은 식)
4
+ // C 중복(파일 본문 해시 동일 · 또는 같은 대상 모듈 ⊕ 같은 시험 제목 집합)
5
+ // D 문면 고정(소스·문서를 readFileSync 로 읽어 toContain/toMatch 만 한다 — 동작을 안 부른다)
6
+ // E 영구 skip(skip/todo/xit · skipIf(true) · 전부 skip)
7
+ import { execFileSync } from 'node:child_process';
8
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
9
+ import { createHash } from 'node:crypto';
10
+ import { dirname, join, resolve } from 'node:path';
11
+
12
+ const root = process.argv[2]; const out = process.argv[3];
13
+ const files = execFileSync('git', ['ls-files', '*.test.*'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\n').filter(Boolean);
14
+
15
+ const EXT = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs', '.mts', '/index.ts', '/index.tsx', '/index.js'];
16
+ function resolveRel(fromFile: string, spec: string): boolean {
17
+ const base = resolve(root, dirname(fromFile), spec);
18
+ const cands = [base, base.replace(/\.js$/, '.ts'), base.replace(/\.js$/, '.tsx'), base.replace(/\.mjs$/, '.mts')];
19
+ for (const c of cands) for (const e of EXT) if (existsSync(c + e)) return true;
20
+ return false;
21
+ }
22
+ function resolveAlias(fromFile: string, spec: string): boolean {
23
+ // PWA `@/x` → apps/pwa/src/x
24
+ if (!spec.startsWith('@/')) return true;
25
+ const base = join(root, 'apps/pwa/src', spec.slice(2));
26
+ for (const e of EXT) if (existsSync(base + e)) return true;
27
+ return false;
28
+ }
29
+
30
+ type Row = { file: string; cls: string[]; evidence: string[]; lines: number; tests: number };
31
+ const rows: Row[] = [];
32
+ const hashes = new Map<string, string[]>();
33
+ const titleSig = new Map<string, string[]>();
34
+
35
+ for (const f of files) {
36
+ const text = readFileSync(join(root, f), 'utf8');
37
+ const lines = text.split('\n').length;
38
+ const cls: string[] = []; const ev: string[] = [];
39
+ const tests = (text.match(/\b(?:test|it)(?:\.(?:only|each\([^)]*\)))?\s*\(/g) ?? []).length;
40
+ // imports
41
+ // 실제 import 문만(줄 맨 앞 import/export … from · 줄 맨 앞 await/const … import('…')) — 픽스처 문자열·주석 속 모양은 뺀다.
42
+ const specs = [
43
+ ...[...text.matchAll(/^\s*(?:import|export)\b[^'";`]*?\bfrom\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]),
44
+ ...[...text.matchAll(/^\s*import\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]),
45
+ ...[...text.matchAll(/^[^'"`\n/]*\bawait\s+import\(\s*['"]([^'"]+)['"]\s*\)/gm)].map((m) => m[1]),
46
+ ...[...text.matchAll(/^[^'"`\n/]*\bimport\(\s*`(\.{1,2}\/[^`$?]+)/gm)].map((m) => m[1]),
47
+ ];
48
+ const rel = specs.filter((s) => s.startsWith('.') || s.startsWith('@/'));
49
+ const missing = rel.filter((s) => (s.startsWith('@/') ? !resolveAlias(f, s) : !resolveRel(f, s)));
50
+ if (missing.length) { cls.push('A'); ev.push(`missing-import:${missing.slice(0, 3).join(',')}`); }
51
+ const repoCode = rel.filter((s) => !/\.(json|md|txt|css)$/.test(s));
52
+ // E: skip
53
+ const skips = (text.match(/\b(?:test|it|describe)\.(?:skip|todo)\s*\(|\bx(?:it|describe|test)\s*\(|\.skipIf\(\s*true\s*\)/g) ?? []).length;
54
+ if (skips > 0 && (tests === 0 || skips >= tests)) { cls.push('E'); ev.push(`all-skipped:${skips}/${tests}`); }
55
+ else if (skips > 0) ev.push(`some-skipped:${skips}/${tests}`);
56
+ // B: constant = constant
57
+ const constEq = [...text.matchAll(/^[^'"`\n]*?expect\(\s*(['"`][^'"`\n]{0,80}['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\(\s*(['"`][^'"`\n]{0,80}['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)/gm)]
58
+ .filter((m) => m[1] === m[2]);
59
+ const selfEq = [...text.matchAll(/^[^'"`\n]*?expect\(\s*([A-Za-z_$][\w$.]*)\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\(\s*([A-Za-z_$][\w$.]*)\s*\)/gm)].filter((m) => m[1] === m[2]);
60
+ const litExpect = (text.match(/expect\(\s*(?:['"`][^'"`\n]*['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)/g) ?? []).length;
61
+ if (constEq.length + selfEq.length > 0) { cls.push('B'); ev.push(`const-eq:${constEq.length + selfEq.length}`); }
62
+ else if (litExpect >= 3) ev.push(`literal-subject-expects:${litExpect}`);
63
+ // D: text pinning — readFileSync of source/docs ⊕ toContain/toMatch only, no calls into imported code
64
+ const readsText = /readFileSync\(|Bun\.file\([^)]*\)\.text\(/.test(text);
65
+ const matchers = (text.match(/\.\s*(?:toContain|toMatch|not\.toContain|not\.toMatch)\(/g) ?? []).length;
66
+ const allExpects = (text.match(/\bexpect\(/g) ?? []).length;
67
+ if (readsText && allExpects > 0 && matchers / allExpects >= 0.8 && repoCode.length === 0) { cls.push('D'); ev.push(`text-pin:${matchers}/${allExpects}`); }
68
+ else if (readsText && allExpects > 0 && matchers / allExpects >= 0.8) ev.push(`text-pin-with-imports:${matchers}/${allExpects}`);
69
+ // A(2): no repo code at all and no file reads — tests only itself
70
+ if (repoCode.length === 0 && !readsText && !/spawn|execFile|Bun\.spawn|fetch\(/.test(text)) { if (!cls.includes('A')) { cls.push('A'); ev.push('no-repo-code'); } }
71
+ // C: duplicates
72
+ const h = createHash('sha256').update(text).digest('hex').slice(0, 16);
73
+ hashes.set(h, [...(hashes.get(h) ?? []), f]);
74
+ const titles = [...text.matchAll(/\b(?:test|it)\s*\(\s*['"`]([^'"`]{4,})['"`]/g)].map((m) => m[1]).sort();
75
+ if (titles.length >= 2) { const sig = createHash('sha256').update(repoCode.sort().join('|') + '#' + titles.join('|')).digest('hex').slice(0, 16); titleSig.set(sig, [...(titleSig.get(sig) ?? []), f]); }
76
+ rows.push({ file: f, cls, evidence: ev, lines, tests });
77
+ }
78
+ const byFile = new Map(rows.map((r) => [r.file, r]));
79
+ for (const [h, fs] of hashes) if (fs.length > 1) for (const f of fs) { const r = byFile.get(f)!; if (!r.cls.includes('C')) r.cls.push('C'); r.evidence.push(`same-bytes:${fs.filter((x) => x !== f).slice(0, 2).join(',')}`); }
80
+ for (const [s, fs] of titleSig) if (fs.length > 1) for (const f of fs) { const r = byFile.get(f)!; if (!r.cls.includes('C')) r.cls.push('C'); r.evidence.push(`same-target+titles:${fs.filter((x) => x !== f).slice(0, 2).join(',')}`); }
81
+
82
+ const tsv = ['file\tclasses\ttests\tlines\tevidence', ...rows.filter((r) => r.cls.length).map((r) => `${r.file}\t${r.cls.join(',')}\t${r.tests}\t${r.lines}\t${r.evidence.join(' ; ')}`)].join('\n');
83
+ writeFileSync(join(out, 'test-audit.tsv'), tsv + '\n');
84
+ const count = (c: string) => rows.filter((r) => r.cls.includes(c)).length;
85
+ const summary = { total: rows.length, flagged: rows.filter((r) => r.cls.length).length, A: count('A'), B: count('B'), C: count('C'), D: count('D'), E: count('E'),
86
+ A_missing: rows.filter((r) => r.evidence.some((e) => e.startsWith('missing-import'))).length, A_noRepo: rows.filter((r) => r.evidence.includes('no-repo-code')).length };
87
+ writeFileSync(join(out, 'test-audit-summary.json'), JSON.stringify(summary, null, 2));
88
+ console.log(JSON.stringify(summary));
@@ -0,0 +1,73 @@
1
+ // K10 D5b — ① 커버리지 겹침(고유 기여) ② 은퇴 기능 시험 ③ 약한 단언. 읽기 전용.
2
+ // 사용: bun test-audit-b.ts <repo> <cov-root|-> <out-dir>
3
+ // cov-root = 파일별 lcov 폴더(<n>/lcov.info) ⊕ index.tsv(file n rc secs). '-' 면 ①을 건너뛴다.
4
+ import { execFileSync } from 'node:child_process';
5
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ const [repo, covRoot, out] = process.argv.slice(2);
9
+ const files = execFileSync('git', ['ls-files', '*.test.ts', '*.test.tsx'], { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\n').filter(Boolean);
10
+
11
+ // ③ 약한 단언 — 파일마다 매처 종류를 센다(줄이 아니라 출현 수).
12
+ const WEAK = new Set(['toBeDefined', 'toBeTruthy', 'toBeFalsy', 'not.toThrow', 'toBeInstanceOf', 'not.toBeNull', 'not.toBeUndefined']);
13
+ const weakRows: string[] = ['file\texpects\tweak\tkinds'];
14
+ let weakOnly = 0;
15
+ // ② 은퇴 기능 — 제목에 은퇴·제거·옛 낱말 ⊕ 부정 단언(없음을 지킴)이 아닌 것
16
+ const RETIRED = /\b(retired|legacy|deprecated|obsolete)\b|은퇴|폐기|옛 (?:문|경로|명령|이름|동작)/i;
17
+ const retiredRows: string[] = ['file\ttitles\tnegative\tpositive\tverdict'];
18
+ for (const f of files) {
19
+ const text = readFileSync(join(repo, f), 'utf8');
20
+ const kinds = new Map<string, number>();
21
+ for (const m of text.matchAll(/\)\s*\.\s*((?:not\s*\.\s*)?[a-zA-Z]+)\s*\(/g)) {
22
+ const k = m[1].replace(/\s+/g, '');
23
+ if (!/^(not\.)?to[A-Z]/.test(k)) continue;
24
+ kinds.set(k, (kinds.get(k) ?? 0) + 1);
25
+ }
26
+ const total = [...kinds.values()].reduce((a, b) => a + b, 0);
27
+ const weak = [...kinds].filter(([k]) => WEAK.has(k)).reduce((a, [, v]) => a + v, 0);
28
+ if (total > 0 && weak === total) { weakOnly++; weakRows.push(`${f}\t${total}\t${weak}\t${[...kinds].map(([k, v]) => `${k}:${v}`).join(',')}`); }
29
+ const titles = [...text.matchAll(/\b(?:test|it|describe)\s*\(\s*['"`]([^'"`]{4,})['"`]/g)].map((m) => m[1]).filter((t) => RETIRED.test(t));
30
+ if (titles.length) {
31
+ const neg = [...kinds].filter(([k]) => k.startsWith('not.') || ['toThrow','toBeUndefined','toBeNull','toBeFalsy'].includes(k)).reduce((a, [, v]) => a + v, 0)
32
+ + (text.match(/toHaveLength\(0\)|toEqual\(\[\]\)|toBe\(false\)|toBe\(0\)/g) ?? []).length;
33
+ const pos = total - neg;
34
+ const verdict = neg >= pos ? '가드(없음을 지킨다) — 유지' : '옛 동작 검사 후보 — 열어 보기';
35
+ retiredRows.push(`${f}\t${titles.slice(0, 3).join(' | ')}\t${neg}\t${pos}\t${verdict}`);
36
+ }
37
+ }
38
+ writeFileSync(join(out, 'weak-assertions.tsv'), weakRows.join('\n') + '\n');
39
+ writeFileSync(join(out, 'retired-feature-tests.tsv'), retiredRows.join('\n') + '\n');
40
+
41
+ // ① 커버리지 겹침
42
+ let covSummary: Record<string, unknown> = { skipped: true };
43
+ if (covRoot !== '-') {
44
+ const index = readFileSync(join(covRoot, 'index.tsv'), 'utf8').split('\n').filter(Boolean).map((l) => l.split('\t'));
45
+ const lineOwners = new Map<string, number>(); // "src:line" → 몇 파일이 닿나
46
+ const perTest = new Map<string, Set<string>>();
47
+ for (const [file, n, rc] of index) {
48
+ if (rc !== '0') continue;
49
+ const p = join(covRoot, 'cov', n, 'lcov.info');
50
+ if (!existsSync(p)) continue;
51
+ const set = new Set<string>(); let sf = '';
52
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
53
+ if (line.startsWith('SF:')) sf = line.slice(3);
54
+ else if (line.startsWith('DA:') && sf.startsWith('src/') && !/\.test\.tsx?$/.test(sf)) {
55
+ const [ln, hits] = line.slice(3).split(',');
56
+ if (Number(hits) > 0) set.add(`${sf}:${ln}`);
57
+ }
58
+ }
59
+ perTest.set(file, set);
60
+ for (const k of set) lineOwners.set(k, (lineOwners.get(k) ?? 0) + 1);
61
+ }
62
+ const rows = [...perTest].map(([file, set]) => {
63
+ let unique = 0; for (const k of set) if (lineOwners.get(k) === 1) unique++;
64
+ return { file, lines: set.size, unique };
65
+ }).sort((a, b) => a.unique - b.unique || b.lines - a.lines);
66
+ writeFileSync(join(out, 'coverage-unique.tsv'), ['file\tcovered_src_lines\tunique_src_lines', ...rows.map((r) => `${r.file}\t${r.lines}\t${r.unique}`)].join('\n') + '\n');
67
+ const zero = rows.filter((r) => r.unique === 0 && r.lines > 0).length;
68
+ const noSrc = rows.filter((r) => r.lines === 0).length;
69
+ covSummary = { measured: rows.length, failedOrSkipped: index.filter((r) => r[2] !== '0').length, zeroUnique: zero, touchesNoSrc: noSrc, uniqueLe5: rows.filter((r) => r.lines > 0 && r.unique > 0 && r.unique <= 5).length, distinctSrcLines: lineOwners.size };
70
+ }
71
+ const summary = { files: files.length, weakOnly, retiredTitled: retiredRows.length - 1, retiredOldBehaviour: retiredRows.filter((r) => r.includes('옛 동작')).length, coverage: covSummary };
72
+ writeFileSync(join(out, 'test-audit-b-summary.json'), JSON.stringify(summary, null, 2));
73
+ console.log(JSON.stringify(summary));
@@ -51,7 +51,24 @@ const HARNESS_TEST_ENV_KEYS = [
51
51
  'ELANOUS_HARNESS_BOUNDARY',
52
52
  'ELANOUS_HARNESS_ROLE',
53
53
  'ELANOUS_HARNESS_DETACHED',
54
+ ] as const;
55
+
56
+ // Discovery: rg -n "process\.env\.ELANOUS_(HOST|RUN|ORIGIN|SUPERVISOR|PARENT)" src
57
+ // These are ambient execution identities, not inputs to a deterministic test.
58
+ // debug.log also attributes SUBSTRATE and ARM_ID to every plain-object record.
59
+ const EXECUTION_ORIGIN_ENV_KEYS = [
60
+ 'ELANOUS_HOST_ID',
54
61
  'ELANOUS_RUN_ID',
62
+ 'ELANOUS_RUN_CONTEXT',
63
+ 'ELANOUS_PARENT_RUN_ID',
64
+ 'ELANOUS_PARENT_PTY_ID',
65
+ 'ELANOUS_PARENT_SELF_DEV_RUNS_DIR',
66
+ 'ELANOUS_SUBSTRATE',
67
+ 'ELANOUS_ARM_ID',
68
+ // Parent account selection changes CLI rotation assertions in the child.
69
+ 'ELANOUS_CODEX_ACCOUNT',
70
+ // A parent's derived-root label must not reclassify a test's explicit state dir.
71
+ 'ELANOUS_STATE_DIR_SOURCE',
55
72
  ] as const;
56
73
 
57
74
  export type ShutdownSignal = 'SIGINT' | 'SIGTERM';
@@ -254,10 +271,16 @@ export async function terminateDirectChild(
254
271
 
255
272
  export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: string): NodeJS.ProcessEnv {
256
273
  const env = Object.fromEntries(
257
- Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key) && !HARNESS_TEST_ENV_KEYS.includes(key as typeof HARNESS_TEST_ENV_KEYS[number])),
274
+ Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key)
275
+ && !HARNESS_TEST_ENV_KEYS.includes(key as typeof HARNESS_TEST_ENV_KEYS[number])
276
+ && !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])),
258
277
  );
259
278
  env.HOME = testRoot;
260
- env.XDG_CONFIG_HOME = join(testRoot, '.config');
279
+ // userConfigPath() warns whenever XDG_CONFIG_HOME is set, including when
280
+ // config.json is absent. Use its non-XDG path; migrateLegacyXdgUserConfig()
281
+ // resolves the old path through ELANOUS_TEST_HOME rather than the real home.
282
+ delete env.XDG_CONFIG_HOME;
283
+ env.ELANOUS_TEST_HOME = testRoot;
261
284
  // ⚠️ Redirect elanous's OWN roots explicitly — do not merely inherit them.
262
285
  // `ELANOUS_STATE_DIR` / `ELANOUS_CONFIG_DIR` are absolute paths that win over
263
286
  // HOME, so a developer who exports either one keeps pointing the "isolated"
@@ -267,9 +290,8 @@ export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: s
267
290
  // isolation legible in the child's own environment rather than implied.
268
291
  env.ELANOUS_STATE_DIR = join(testRoot, 'state');
269
292
  env.ELANOUS_CONFIG_DIR = join(testRoot, 'config');
270
- // A deterministic test child must not inherit the harness run identity:
271
- // logger API-shape tests use its absence to validate legacy records.
272
- delete env.ELANOUS_RUN_ID;
293
+ // All ambient execution identities are removed by EXECUTION_ORIGIN_ENV_KEYS
294
+ // above, before the child is spawned; logger API-shape tests need their absence.
273
295
  // ⚠️ No `ELANOUS_TEST_DETERMINISTIC` marker is exported. Nothing reads it yet,
274
296
  // and an env var with no consumer is a surface that looks like a contract
275
297
  // while guaranteeing nothing — a later test could branch on it believing it
@@ -45,6 +45,10 @@ import {
45
45
  writeFileSync, appendFileSync, readFileSync, mkdirSync, readdirSync, existsSync, unlinkSync, rmSync, renameSync,
46
46
  } from 'node:fs';
47
47
  import { execFileSync } from 'node:child_process';
48
+ import { resolve } from 'node:path';
49
+ import { getCapturedEnv } from '../src/shell-env-bootstrap.js';
50
+ import { identityEnvKeys } from '../src/agent/identity-env.js';
51
+ import { resolveKeyBytes, resolveKeyInput, writeKeyInput } from './lib/tui-sim-keys.js';
48
52
 
49
53
  const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
50
54
 
@@ -91,14 +95,18 @@ function writeMeta(id: string, m: Meta): void {
91
95
  renameSync(tmp, metaPath(id));
92
96
  }
93
97
 
94
- // ── named keys → escape sequences ──
95
- const KEYS: Record<string, string> = {
96
- enter: '\r', return: '\r', tab: '\t', 'shift-tab': '\x1b[Z', esc: '\x1b', escape: '\x1b',
97
- space: ' ', backspace: '\x7f', delete: '\x1b[3~',
98
- up: '\x1b[A', down: '\x1b[B', right: '\x1b[C', left: '\x1b[D',
99
- pageup: '\x1b[5~', pagedown: '\x1b[6~', home: '\x1b[H', end: '\x1b[F',
100
- 'ctrl-c': '\x03', 'ctrl-d': '\x04', 'ctrl-u': '\x15', 'ctrl-a': '\x01', 'ctrl-e': '\x05',
101
- };
98
+ // ── optional child credential isolation ──
99
+ const CLEAN_ENV_KEYS = new Set([
100
+ 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'COLORTERM', 'TZ', 'TMPDIR',
101
+ 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_COLLATE', 'LC_NUMERIC', 'LC_TIME', 'LC_MONETARY',
102
+ 'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION',
103
+ ]);
104
+
105
+ /** Block everything except basic execution/locale variables after PTY env synthesis. */
106
+ export function cleanPtyUnsetEnv(captured: Record<string, string>, parent: NodeJS.ProcessEnv): string[] {
107
+ return [...new Set([...Object.keys(captured), ...Object.keys(parent), ...identityEnvKeys()])]
108
+ .filter((key) => !CLEAN_ENV_KEYS.has(key));
109
+ }
102
110
 
103
111
  // ── client: drop a command into inbox, optionally await result ──
104
112
  function argVal(args: string[], flag: string): string | null {
@@ -136,6 +144,10 @@ async function runDaemon(args: string[]): Promise<void> {
136
144
  const testMode = args.includes('--test');
137
145
  const autoWorktree = args.includes('--worktree');
138
146
  const explicitWorkdir = argVal(args, '--workdir');
147
+ const home = argVal(args, '--home');
148
+ const cleanEnv = args.includes('--clean-env');
149
+ if (args.includes('--home') && (!home || home.startsWith('--'))) { console.error('[tui-sim] --home requires a directory'); process.exit(1); }
150
+ const childHome = home ? resolve(home) : null;
139
151
  const cols = Number(argVal(args, '--cols')) || 220;
140
152
  const rows = Number(argVal(args, '--rows')) || 60;
141
153
  const bootSec = Number(argVal(args, '--boot')) || 8;
@@ -176,9 +188,12 @@ async function runDaemon(args: string[]): Promise<void> {
176
188
  + `# follow live: tail -f ${forwardPath}\n\n`);
177
189
  console.error(`[tui-sim] start id=${id} workdir=${workdir} test=${testMode}`);
178
190
  console.error(`[tui-sim] 📡 live timeline → ${forwardPath} (follow: tail -f ${forwardPath})`);
191
+ if (childHome) mkdirSync(childHome, { recursive: true });
192
+ const unsetEnv = cleanEnv ? cleanPtyUnsetEnv(getCapturedEnv(), process.env) : undefined;
179
193
  const h: PtyHandle = startPty({
180
194
  cmd: 'bun', args: [binPath, ...isoArgs], cols, rows, workdir,
181
- env: { ELANOUS_DRIVE_TUI: '1', ...(testMode ? { ELANOUS_STATE_DIR: testDir } : {}) },
195
+ env: { ELANOUS_DRIVE_TUI: '1', ...(testMode ? { ELANOUS_STATE_DIR: testDir } : {}), ...(childHome ? { HOME: childHome } : {}) },
196
+ ...(unsetEnv ? { unsetEnv } : {}),
182
197
  });
183
198
 
184
199
  const meta: Meta = {
@@ -296,9 +311,9 @@ async function runDaemon(args: string[]): Promise<void> {
296
311
  feed(text, verb === 'send');
297
312
  res = { ok: true, sent: text.length };
298
313
  } else if (verb === 'key') {
299
- const seq = KEYS[String(cmd.name).toLowerCase()];
300
- if (!seq) { res = { ok: false, error: `unknown key '${cmd.name}'` }; }
301
- else { const n = Number(cmd.repeat) || 1; for (let i = 0; i < n; i++) h.write(seq); res = { ok: true }; }
314
+ const seq = resolveKeyInput(String(cmd.name));
315
+ if (seq === null) { res = { ok: false, error: `unknown key '${cmd.name}'` }; }
316
+ else { const n = Number(cmd.repeat) || 1; for (let i = 0; i < n; i++) writeKeyInput(h, seq); res = { ok: true }; }
302
317
  } else if (verb === 'mouse') {
303
318
  const x = Number(cmd.x), y = Number(cmd.y);
304
319
  const btn = cmd.button === 'right' ? 2 : cmd.button === 'middle' ? 1 : 0;
@@ -369,6 +384,23 @@ async function main(): Promise<void> {
369
384
  const verb = argv[0];
370
385
  const rest = argv.slice(1);
371
386
 
387
+ if (verb === '--help' || verb === 'help') {
388
+ console.log(`usage: tui-sim <verb> [args]
389
+ start [--id id] [--test] [--worktree] [--workdir dir] [--cols n] [--rows n]
390
+ start --home <dir> Set the child TUI HOME (default: inherited).
391
+ start --clean-env Allow only basic execution/locale env into the child (default: off).
392
+ send|type <id> <text> Send text with|without Enter.
393
+ key <id> <name> [--repeat n] Send a named key; unknown keys exit 1.
394
+ key names: enter/return/tab/shift-tab/esc/escape/space/backspace/delete/up/down/right/left/pageup/pagedown/home/end.
395
+ key ctrl-<a..z> Send control byte (ctrl-j sends newline).
396
+ key ctrl-\\ | ctrl-] Send 0x1c | 0x1d.
397
+ key alt-<key> Send ESC followed by the named key or one character.
398
+ key shift-enter Send ESC [ 13 ; 2 u.
399
+ key hex:<bytes> Send raw hex bytes (e.g. hex:1b5b41).
400
+ resize <id> <cols> <rows> Resize the running PTY.
401
+ mouse|dump|text|transcript|status|list|stop|promote|clean`);
402
+ return;
403
+ }
372
404
  if (verb === 'start') { await runDaemon(rest); return; }
373
405
 
374
406
  if (verb === 'list') {
@@ -394,9 +426,22 @@ async function main(): Promise<void> {
394
426
  }
395
427
  case 'key': {
396
428
  const name = rest[1];
429
+ if (!name || resolveKeyBytes(name) === null) { console.error(`알 수 없는 키 '${name}'`); process.exit(1); }
397
430
  const repeat = argVal(rest, '--repeat');
398
431
  const res = await sendCommand(id, 'key', { name, ...(repeat ? { repeat: Number(repeat) } : {}) }, true);
399
432
  console.log(res?.ok ? `✓ key ${name}` : `✗ ${res?.error}`);
433
+ if (!res?.ok) process.exitCode = 1;
434
+ break;
435
+ }
436
+ case 'resize': {
437
+ const cols = Number(rest[1]), rows = Number(rest[2]);
438
+ if (!Number.isSafeInteger(cols) || cols < 2 || !Number.isSafeInteger(rows) || rows < 2) {
439
+ console.error('usage: tui-sim resize <id> <cols> <rows> (positive integers >= 2)');
440
+ process.exit(1);
441
+ }
442
+ const res = await sendCommand(id, 'resize', { cols, rows }, true);
443
+ console.log(res?.ok ? `✓ resize ${cols}x${rows}` : `✗ ${res?.error ?? 'failed'}`);
444
+ if (!res?.ok) process.exitCode = 1;
400
445
  break;
401
446
  }
402
447
  case 'mouse': {
@@ -508,9 +553,9 @@ async function main(): Promise<void> {
508
553
  break;
509
554
  }
510
555
  default:
511
- console.error(`unknown verb '${verb}'. verbs: start|send|type|key|mouse|dump|text|transcript|status|list|promote|clean|stop`);
556
+ console.error(`unknown verb '${verb}'. verbs: start|send|type|key|resize|mouse|dump|text|transcript|status|list|promote|clean|stop`);
512
557
  process.exit(1);
513
558
  }
514
559
  }
515
560
 
516
- main().catch((e) => { console.error(e); process.exit(1); });
561
+ if (import.meta.main) main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env zsh
2
+ # 판올림(설치본 갱신) — UX 자리 · 내부 문서 `UX` §3a.
3
+ # origin/main 을 깨끗한 체크아웃(D)에 떼어 놓고, 설치본을 그 체크아웃으로 갱신하고 넥서스를 재시작한 뒤, 서빙 화면이 새 판인지 잰다.
4
+ # 사용: zsh scripts/ux/bump-installed.sh [<ref>=origin/main]
5
+ # ⛔ zsh 에서 set -e 는 아무것도 안 한다 — 단계는 전부 && 로 잇는다. ⛔ 채널 공지(전·후)는 이 스크립트 밖에서 사람이/자리가 한다.
6
+ REF=${1:-origin/main}
7
+ D=${UX_DEPLOY_DIR:-$TMPDIR/deploy-main}
8
+ [[ -d $D/.git || -f $D/.git ]] || { echo "deploy checkout missing: $D (git worktree add --detach $D origin/main 로 한 번 만든다)"; exit 64; }
9
+ cd "$D" && git fetch -q origin && git checkout -q --detach "$REF" && bun install --silent && (cd apps/pwa && bun install --silent) \
10
+ && echo "[bump] checkout $(git rev-parse --short HEAD) — $(git log -1 --format=%s | cut -c1-80)" \
11
+ && cd ~ && elanous update --from "$D" --restart \
12
+ || { echo "[bump] FAILED — PWA 빌드가 깨졌으면 도구의 처방(cd $D/apps/pwa && bun install --force)을 먼저 본다"; exit 1; }
13
+ # 재는 칸 셋: 데몬 pid · 설치본 판 · 서빙 화면 = 설치본 out/index.html
14
+ sleep 15
15
+ launchctl list | rg com.elanous.nexus
16
+ elanous --version
17
+ CUR=~/.local/share/elanous/current
18
+ WANT=$(rg -o 'page-[a-f0-9]+\.js' -m1 "$CUR/node_modules/elanous/apps/pwa/out/index.html" | head -1)
19
+ GOT=$(curl -s --max-time 15 http://127.0.0.1:31415/app/ | rg -o 'page-[a-f0-9]+\.js' -m1 | head -1)
20
+ [[ -n $WANT && $WANT == $GOT ]] && echo "[bump] ok — serving $GOT" || { echo "[bump] MISMATCH installed=$WANT serving=$GOT"; exit 1; }