elanous 0.2.6 → 0.2.7

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 (450) hide show
  1. package/.env.example +10 -7
  2. package/README.md +4 -0
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/page-55d50e9b0006a91d.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
  96. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/index.html +1 -1
  123. package/apps/pwa/out/index.txt +21 -21
  124. package/apps/pwa/out/intake/index.html +1 -1
  125. package/apps/pwa/out/intake/index.txt +22 -22
  126. package/apps/pwa/out/live/index.html +1 -1
  127. package/apps/pwa/out/live/index.txt +22 -22
  128. package/apps/pwa/out/market/index.html +1 -1
  129. package/apps/pwa/out/market/index.txt +21 -21
  130. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  131. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  132. package/apps/pwa/out/morning/index.html +1 -1
  133. package/apps/pwa/out/morning/index.txt +22 -22
  134. package/apps/pwa/out/observatory/index.html +1 -1
  135. package/apps/pwa/out/observatory/index.txt +22 -22
  136. package/apps/pwa/out/reflection/index.html +1 -1
  137. package/apps/pwa/out/reflection/index.txt +22 -22
  138. package/apps/pwa/out/scheduler/index.html +1 -1
  139. package/apps/pwa/out/scheduler/index.txt +22 -22
  140. package/apps/pwa/out/sessions/index.html +1 -1
  141. package/apps/pwa/out/sessions/index.txt +21 -21
  142. package/apps/pwa/out/settings/devices/index.html +1 -1
  143. package/apps/pwa/out/settings/devices/index.txt +22 -22
  144. package/apps/pwa/out/settings/index.html +1 -1
  145. package/apps/pwa/out/settings/index.txt +22 -22
  146. package/apps/pwa/out/setup/done/index.html +1 -1
  147. package/apps/pwa/out/setup/done/index.txt +22 -22
  148. package/apps/pwa/out/setup/index.html +1 -1
  149. package/apps/pwa/out/setup/index.txt +22 -22
  150. package/apps/pwa/out/share/index.html +1 -1
  151. package/apps/pwa/out/share/index.txt +22 -22
  152. package/apps/pwa/out/showroom/index.html +1 -1
  153. package/apps/pwa/out/showroom/index.txt +21 -21
  154. package/apps/pwa/out/tasks/index.html +1 -1
  155. package/apps/pwa/out/tasks/index.txt +22 -22
  156. package/apps/pwa/out/term/index.html +1 -1
  157. package/apps/pwa/out/term/index.txt +22 -22
  158. package/apps/pwa/out/trace/index.html +1 -1
  159. package/apps/pwa/out/trace/index.txt +22 -22
  160. package/apps/pwa/out/vault/index.html +1 -1
  161. package/apps/pwa/out/vault/index.txt +22 -22
  162. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  163. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  164. package/apps/pwa/out/workflows/index.html +1 -1
  165. package/apps/pwa/out/workflows/index.txt +22 -22
  166. package/apps/pwa/out/workspace/index.html +1 -1
  167. package/apps/pwa/out/workspace/index.txt +22 -22
  168. package/apps/pwa/out/worktrees/index.html +1 -1
  169. package/apps/pwa/out/worktrees/index.txt +22 -22
  170. package/bin/elanous.cjs +107 -0
  171. package/graphs/field/field-feed.yaml +24 -0
  172. package/graphs/field/recipes.yaml +12 -0
  173. package/graphs/landing/landing-heal.yaml +3 -1
  174. package/graphs/landing/recipes.yaml +3 -0
  175. package/graphs/release/recipes.yaml +10 -0
  176. package/graphs/release/release-loop.yaml +10 -3
  177. package/package.json +5 -5
  178. package/plugins/card-followup/README.md +13 -4
  179. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  180. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  181. package/plugins/card-followup/graphs/run-step.ts +122 -18
  182. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  183. package/plugins/card-followup/plugin.json +1 -1
  184. package/plugins/doc-draft/README.md +13 -0
  185. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  186. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  187. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  188. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  189. package/plugins/doc-draft/plugin.json +24 -0
  190. package/plugins/geo-check/README.md +3 -1
  191. package/plugins/geo-check/graphs/run-step.ts +24 -3
  192. package/plugins/geo-check/plugin.json +1 -1
  193. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  194. package/plugins/job-coach/README.md +1 -1
  195. package/plugins/job-coach/graphs/run-step.ts +2 -2
  196. package/plugins/job-coach/plugin.json +1 -1
  197. package/plugins/job-coach/plugin.ts +1 -1
  198. package/scripts/affinity/endcard.sh +3 -2
  199. package/scripts/affinity/reframe.sh +3 -1
  200. package/scripts/ci-public-export-leak-gate.ts +48 -0
  201. package/scripts/codex-quota-alert.ts +122 -258
  202. package/scripts/coord-tracks.json +4 -4
  203. package/scripts/field-feed/deliver-node.ts +52 -0
  204. package/scripts/field-feed/feed-node.ts +67 -0
  205. package/scripts/field-feed/lib.ts +162 -0
  206. package/scripts/field-feed/reel-node.ts +24 -0
  207. package/scripts/field-feed/start.ts +62 -0
  208. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  209. package/scripts/install.sh +92 -13
  210. package/scripts/landing-heal/clean-checkout.ts +114 -0
  211. package/scripts/landing-heal/run-mission.ts +38 -3
  212. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  213. package/scripts/public-export.ts +8 -4
  214. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  215. package/scripts/release-loop/env-known-failures.ts +31 -0
  216. package/scripts/release-loop/gate-node.ts +81 -19
  217. package/scripts/release-loop/known-issues-node.ts +159 -0
  218. package/scripts/release-loop/npm-publish-node.ts +118 -0
  219. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  220. package/scripts/release-loop/release-readiness.ts +58 -0
  221. package/scripts/release-loop/unattended-release.ts +93 -0
  222. package/scripts/ux/measure-first-text.ts +102 -0
  223. package/scripts/vflow-collect.ts +20 -7
  224. package/scripts/vflow-search.ts +73 -0
  225. package/scripts/video-free-line-test-walker.ts +36 -0
  226. package/scripts/video-pipeline.ts +2 -2
  227. package/skills/explainer-video/SKILL.md +12 -9
  228. package/skills/explainer-video/engine/build.mjs +1 -1
  229. package/skills/explainer-video/engine/reel.mjs +138 -31
  230. package/skills/explainer-video/engine/reel.sh +2 -1
  231. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  232. package/skills/grill-me/SKILL.md +1 -1
  233. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  234. package/src/a2a/server.ts +2 -0
  235. package/src/acp/elanous-aware-peers.ts +30 -0
  236. package/src/acp/server.ts +91 -15
  237. package/src/agent/monad-agent-turn.ts +25 -6
  238. package/src/agent/shared-app-tools.ts +5 -3
  239. package/src/agent-substrate/review-intent.ts +6 -2
  240. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  241. package/src/auth/github-app-token.ts +4 -1
  242. package/src/autopilot/track-agent-shadow.ts +151 -0
  243. package/src/autopilot/track-agent.ts +23 -3
  244. package/src/boot/acp-server-help.ts +16 -0
  245. package/src/boot/daemon-prompt-turn.ts +31 -10
  246. package/src/boot/daemon-tools/index.ts +7 -1
  247. package/src/budget/codex-credit-pace.ts +84 -0
  248. package/src/budget/codex-credit-plan.ts +154 -0
  249. package/src/budget/codex-reset-credit-state.ts +2 -0
  250. package/src/chat/chat-cli.ts +4 -1
  251. package/src/chat/short-question.ts +40 -0
  252. package/src/cli/agent-cli.ts +24 -15
  253. package/src/cli/ask-bare.ts +42 -0
  254. package/src/cli/cli-user-error.ts +25 -0
  255. package/src/cli/codex-credit-plan-view.ts +25 -0
  256. package/src/cli/dependence-gauge.ts +237 -0
  257. package/src/cli/doctor-cli.ts +5 -0
  258. package/src/cli/doctor-modes.ts +48 -0
  259. package/src/cli/intake-cli.ts +12 -2
  260. package/src/cli/intake-source-cli.ts +58 -0
  261. package/src/cli/machine-cli.ts +23 -0
  262. package/src/cli/msg-cli.ts +137 -0
  263. package/src/cli/plugin-cli.ts +25 -0
  264. package/src/cli/pr-cli.ts +36 -0
  265. package/src/cli/provider-cli.ts +5 -0
  266. package/src/cli/release-cli.ts +42 -1
  267. package/src/cli/release-run-if-ready.ts +48 -0
  268. package/src/cli/setup-cli.ts +27 -6
  269. package/src/cli/setup-recommend.ts +8 -0
  270. package/src/cli/usage-cli.ts +11 -1
  271. package/src/codex/setup.ts +27 -8
  272. package/src/dashboard/index.ts +1 -1
  273. package/src/discord.ts +56 -6
  274. package/src/domains/core-tools.ts +37 -14
  275. package/src/domains/outbound-alert.ts +50 -11
  276. package/src/exec-requests/planner.ts +140 -0
  277. package/src/exec-requests/runner.ts +338 -0
  278. package/src/exec-requests/store.ts +76 -0
  279. package/src/field/field-media.ts +8 -3
  280. package/src/field/field-reel.ts +196 -0
  281. package/src/grok/credential.ts +7 -5
  282. package/src/harness/aside-run-status.ts +33 -0
  283. package/src/harness/browser-verify.ts +48 -17
  284. package/src/harness/harness-cli-command.ts +25 -3
  285. package/src/harness/image-load-state.ts +21 -0
  286. package/src/harness/process-garbage.ts +45 -0
  287. package/src/index.ts +86 -14
  288. package/src/instance/leader.ts +13 -1
  289. package/src/intake-plane/collect-github.ts +5 -3
  290. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  291. package/src/intake-plane/discord-seat-work.ts +56 -0
  292. package/src/intake-plane/intake-sources.ts +202 -0
  293. package/src/intake-plane/items.ts +28 -12
  294. package/src/intake-plane/seat-doc-route.ts +207 -0
  295. package/src/intake-plane/submit-intake-work.ts +9 -1
  296. package/src/intake-plane/telegram-seat-work.ts +72 -0
  297. package/src/llm/model-defaults.ts +24 -1
  298. package/src/llm.ts +5 -4
  299. package/src/machines/machine-ledger.ts +127 -0
  300. package/src/market/pack-internal-refs.ts +28 -0
  301. package/src/market/publish.ts +31 -4
  302. package/src/maturity/feature-maturity.ts +65 -0
  303. package/src/model-tier/turn-tier.ts +24 -0
  304. package/src/msg/msg-store.ts +149 -0
  305. package/src/native-tool-catalog.ts +2 -1
  306. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  307. package/src/nexus/api/consult-requests.ts +117 -0
  308. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  309. package/src/nexus/api/exec-requests.ts +51 -0
  310. package/src/nexus/api/field-uploads.ts +40 -3
  311. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  312. package/src/nexus/api/graph-approvals.ts +71 -4
  313. package/src/nexus/api/http-server.ts +61 -7
  314. package/src/nexus/api/seat-requests.ts +254 -0
  315. package/src/nexus/api/trace-run-scan.ts +37 -0
  316. package/src/nexus/api/trace.ts +19 -3
  317. package/src/nexus/index.ts +3 -0
  318. package/src/nexus/setup-status.ts +4 -5
  319. package/src/notify/llm-budget-alert.ts +16 -10
  320. package/src/oauth/codex-account-rotation.ts +26 -4
  321. package/src/oauth/codex-account-store.ts +20 -2
  322. package/src/oauth/codex-browser-login.ts +173 -0
  323. package/src/oauth/codex.ts +4 -1
  324. package/src/oauth/fallback-chain.ts +5 -5
  325. package/src/oauth/store.ts +42 -5
  326. package/src/onboarding/continue-here.ts +29 -0
  327. package/src/onboarding/entry-hints.ts +8 -0
  328. package/src/onboarding/full-screen-io.ts +35 -3
  329. package/src/onboarding/help/skills.md +1 -1
  330. package/src/onboarding/non-interactive.ts +1 -0
  331. package/src/onboarding/subscription-hint.ts +27 -0
  332. package/src/onboarding/web-first.ts +83 -0
  333. package/src/onboarding.ts +230 -23
  334. package/src/plugins/maker/node-install.ts +92 -0
  335. package/src/plugins/maker/node-maker.ts +139 -0
  336. package/src/plugins/maker/plugin-maker.ts +19 -7
  337. package/src/policy/llm-policy.ts +268 -0
  338. package/src/prompt-library/session-guidance.ts +7 -1
  339. package/src/prompt-library/universal-preamble.ts +2 -2
  340. package/src/relay/relay-cli.ts +20 -0
  341. package/src/relay/relay-server.ts +220 -0
  342. package/src/release-loop/checklist.ts +9 -8
  343. package/src/self-dev/decompose.ts +1 -0
  344. package/src/self-dev/dev-pipeline.ts +6 -3
  345. package/src/self-dev/run-record-mirror.ts +44 -0
  346. package/src/self-dev/run-store.ts +12 -3
  347. package/src/self-implement/harness-policy-gates.ts +4 -1
  348. package/src/self-implement/report-origin.ts +12 -1
  349. package/src/session/chat.ts +17 -5
  350. package/src/skills/shared-agent-skill-roots.ts +25 -0
  351. package/src/steward/failure-streak.ts +78 -0
  352. package/src/steward/steward-cards.ts +24 -0
  353. package/src/steward/triage-budget.ts +127 -0
  354. package/src/steward/triage-plan.ts +75 -0
  355. package/src/steward/triage.ts +244 -42
  356. package/src/task-cards/card-store.ts +1 -1
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  358. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  359. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  360. package/src/telegram.ts +38 -2
  361. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  362. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  363. package/src/user-config.ts +80 -4
  364. package/src/version/packed-revision.json +1 -1
  365. package/src/video-pipeline/recipes/character.ts +42 -5
  366. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  367. package/src/video-pipeline/recipes/free-line.ts +16 -3
  368. package/src/video-pipeline/recipes/remote.ts +7 -1
  369. package/src/video-pipeline/vflow-reference.ts +109 -0
  370. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  379. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  448. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  449. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  450. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
1
+ import { closeSync, existsSync, openSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fetchLinearIssues } from '../connectors/linear.js';
4
4
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
@@ -9,6 +9,9 @@ import { effectiveInstanceRoot } from '../instance/resolve.js';
9
9
  import { redactSecrets } from '../task-cards/card-store.js';
10
10
  import { directiveHash } from './directive.js';
11
11
  import { recordTriageOnCards, type StewardCardDeps } from './steward-cards.js';
12
+ import { triageWithinBudget } from './triage-budget.js';
13
+ import { askSteward, classifyIssue, planIssues, ruleJudgment, unsafeReason, type ClassifiedIssue, type StewardAsk } from './triage-plan.js';
14
+ import { failureAlertText, markStageStart, markStageEnd, readFailureStreak, shouldAlert, writeFailureStreak, type FailureStreak, type StewardStage } from './failure-streak.js';
12
15
 
13
16
  export type Rung = 0 | 1 | 2 | 3 | 4 | 5 | 'hitl';
14
17
  /** `other` = the judge chose human review for a reason outside the four named ones (kept as HITL, never downgraded). */
@@ -19,6 +22,8 @@ export interface TriageDecision {
19
22
  hitlReason?: HitlReason; why: string; role?: string; cost?: number;
20
23
  /** Owner track from `loops.steward.tracks` (absent when no table is configured or the judge picked none of its keys). */
21
24
  owner?: string;
25
+ /** Carried from a previous triage without a current judgment; scheduling must not route it. */
26
+ deferred?: true;
22
27
  }
23
28
  export interface ScheduledDecision extends TriageDecision { disposition: 'now' | 'wait' | 'hitl' }
24
29
  export interface StewardSettings {
@@ -26,6 +31,7 @@ export interface StewardSettings {
26
31
  roles?: Record<string, { maxConcurrent?: number }>;
27
32
  budget?: number;
28
33
  tracks?: Record<string, string>;
34
+ alertAfterFailures?: number;
29
35
  }
30
36
  export interface StewardDeps {
31
37
  fetch?: typeof fetch;
@@ -37,18 +43,15 @@ export interface StewardDeps {
37
43
  root?: string;
38
44
  cardStore?: StewardCardDeps['store'];
39
45
  warn?: (message: string) => void;
46
+ ask?: StewardAsk;
47
+ /** Internal command boundary: the current sync has already claimed this pid. */
48
+ stageOwnedByCommand?: boolean;
40
49
  }
41
50
 
42
51
  export function stewardSettings(): StewardSettings {
43
52
  return getUserConfig().loops?.steward ?? {};
44
53
  }
45
54
 
46
- const unsafe: Array<[HitlReason, RegExp]> = [
47
- ['money', /돈|유료|결제|구매|판매|송금|환불|크레딧\s*(?:추가|구매)|paid|payment|purchase/i],
48
- ['public', /공개|발행|외부\s*게시|publish|public\s*release/i],
49
- ['security', /보안|(?:키|자격|권한|secret|credential|permission)\s*(?:변경|교체|발급|삭제|공유|노출)|키체인/i],
50
- ['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
51
- ];
52
55
 
53
56
  function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string, string>): TriageDecision {
54
57
  const value = typeof raw === 'string' ? JSON.parse(raw) as unknown : raw;
@@ -59,8 +62,8 @@ function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string,
59
62
  typeof d.priority !== 'number' || !Number.isFinite(d.priority) || typeof d.why !== 'string' || !d.why.trim()) {
60
63
  throw new Error(`Invalid triage judgment: ${issue.identifier}`);
61
64
  }
62
- const forced = unsafe.find(([, pattern]) => pattern.test(`${issue.title}\n${issue.body}`))?.[0];
63
- const named = forced ?? (unsafe.some(([reason]) => reason === d.hitlReason) ? d.hitlReason as HitlReason : undefined);
65
+ const forced = unsafeReason(issue);
66
+ const named = forced ?? (['money', 'public', 'security', 'irreversible'].includes(d.hitlReason as string) ? d.hitlReason as HitlReason : undefined);
64
67
  // A HITL judgment without one of the four named reasons stays HITL (`other`) — one issue must not stop the stage.
65
68
  const hitlReason = named ?? (d.rung === 'hitl' ? 'other' as const : undefined);
66
69
  return {
@@ -81,30 +84,68 @@ export function trackTablePrompt(tracks?: Record<string, string>): string {
81
84
  return `\n담당 트랙(owner) — 아래 중 하나(${keys.map(k => JSON.stringify(k)).join('|')}) · 이슈가 어느 트랙 일인지:\n${keys.map(k => `- ${k}: ${tracks[k]}`).join('\n')}`;
82
85
  }
83
86
 
84
- async function judgeStewardIssue(issue: TriageIssue, issues: TriageIssue[], tracks?: Record<string, string>): Promise<unknown> {
85
- const ownerField = tracks && Object.keys(tracks).length ? `,"owner":${JSON.stringify(Object.keys(tracks)[0])}` : '';
86
- const prompt = `스튜어드 트리아지. JSON 객체만 출력: {"rung":0|1|2|3|4|5|"hitl","dependsOn":[],"priority":1,"duplicateOf":null,"hitlReason":null,"why":"한 문장","role":"builder","cost":0${ownerField}}.\n0=이미됨/중복, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl(hitlReason = "money"|"public"|"security"|"irreversible" · 그 밖의 이유로 사람 확인이 필요하면 "other"). 확인되지 않은 완료/중복은 추측하지 마라. 의존은 제공된 이슈 키만 써라.${trackTablePrompt(tracks)}\n이슈 목록: ${JSON.stringify(issues.map(i => ({ key: i.identifier, title: i.title })))}\n판정 대상: ${JSON.stringify(issue)}`;
87
- const { streamLLM } = await import('../llm.js');
88
- const { tierModel } = await import('../llm/model-defaults.js');
89
- const result = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
90
- return JSON.parse(result.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) as unknown;
91
- }
92
-
93
- export async function triageIssues(issues: TriageIssue[], judge: NonNullable<StewardDeps['judge']> = judgeStewardIssue, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>): Promise<TriageDecision[]> {
94
- const decisions: TriageDecision[] = [];
95
- for (const issue of issues) {
96
- let result: TriageDecision;
87
+ export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<StewardDeps['judge']>, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>, previous: Array<TriageDecision & { inputHash?: string }> = [], ask: StewardAsk = askSteward): Promise<TriageDecision[]> {
88
+ const started = judge ? 0 : Date.now();
89
+ const classified: ClassifiedIssue[] = [];
90
+ const classifiedIds = new Set<string>();
91
+ const counts = { rule: 0, classify: 0, planning: 0 };
92
+ const signal = (result: TriageDecision): void => {
93
+ decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: result.issue, reason: result.why,
94
+ purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: result.issue } });
95
+ };
96
+ const { decisions } = await triageWithinBudget({ issues, previous, tracks, judge: async (issue, observedIssues) => {
97
97
  try {
98
- result = parseJudgment(issue, await judge(issue, issues, tracks), tracks);
98
+ if (judge) return parseJudgment(issue, await judge(issue, observedIssues ?? issues, tracks), tracks);
99
+ const byRule = ruleJudgment(issue, observedIssues ?? issues);
100
+ if (byRule) { counts.rule++; return { ...byRule, dependsOn: [], priority: 0 }; }
101
+ counts.classify++;
102
+ const row = await classifyIssue(issue, ask);
103
+ return { ...row, dependsOn: [], priority: 0 };
99
104
  } catch (error) {
100
- // A malformed or failed judgment for one issue goes to a human; the other issues are still triaged.
101
105
  try { debug.log('steward.stage', 'judgment-invalid', { issue: issue.identifier, reason: redactSecrets(String(error)) }); } catch { /* fail-soft */ }
102
- result = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
106
+ return { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
107
+ }
108
+ }, onJudged: result => {
109
+ if (!judge && !result.deferred && !result.duplicateOf &&
110
+ result.why !== 'triage judgment unavailable — human review' &&
111
+ result.why !== 'triage judgment timed out — human review' &&
112
+ result.why !== 'triage deferred — deadline') {
113
+ classified.push({ issue: result.issue, rung: result.rung, why: result.why,
114
+ ...(result.hitlReason ? { hitlReason: result.hitlReason } : {}) });
115
+ classifiedIds.add(result.issue);
116
+ } else signal(result);
117
+ } });
118
+ const planningBatch = classified.slice();
119
+ if (!judge && planningBatch.length) {
120
+ const remaining = 240_000 - (Date.now() - started);
121
+ try {
122
+ if (remaining <= 0) throw new Error('planning deadline');
123
+ counts.planning++;
124
+ let timer: ReturnType<typeof setTimeout> | undefined;
125
+ let plans;
126
+ try {
127
+ plans = await Promise.race([planIssues(planningBatch, ask, tracks, issues), new Promise<never>((_, reject) => {
128
+ timer = setTimeout(() => reject(new Error('planning deadline')), remaining);
129
+ })]);
130
+ } finally { if (timer) clearTimeout(timer); }
131
+ const byIssue = new Map(plans.map(plan => [plan.issue, plan]));
132
+ for (const row of decisions) {
133
+ const plan = byIssue.get(row.issue);
134
+ if (plan && !row.deferred) {
135
+ row.priority = plan.priority;
136
+ row.dependsOn = plan.dependsOn;
137
+ if (plan.owner) row.owner = plan.owner;
138
+ }
139
+ }
140
+ } catch (error) {
141
+ debug.log('steward.stage', 'planning-invalid', { reason: redactSecrets(String(error)) });
142
+ for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred && !row.duplicateOf) {
143
+ row.rung = 'hitl'; row.hitlReason = 'other'; row.why = 'triage judgment unavailable — human review';
144
+ }
103
145
  }
104
- decisions.push(result);
105
- decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: issue.identifier, reason: result.why,
106
- purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: issue.identifier } });
146
+ for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred) signal(row);
107
147
  }
148
+ debug.log('steward.triage', 'llm-calls', counts);
108
149
  return decisions;
109
150
  }
110
151
 
@@ -128,7 +169,7 @@ export function scheduleTriage(decisions: TriageDecision[], settings: StewardSet
128
169
  const role = item.owner ?? item.role ?? String(item.rung);
129
170
  const max = settings.roles?.[role]?.maxConcurrent ?? Infinity;
130
171
  const blocked = item.dependsOn.some(dep => !completed.has(dep));
131
- const status: ScheduledDecision['disposition'] = item.rung === 'hitl' || item.hitlReason ? 'hitl' :
172
+ const status: ScheduledDecision['disposition'] = item.deferred || item.rung === 'hitl' || item.hitlReason ? 'hitl' :
132
173
  item.rung === 0 || item.duplicateOf || blocked || (slots[role] ?? 0) >= max || (item.cost ?? 0) > remaining ? 'wait' : 'now';
133
174
  if (status === 'now') { slots[role] = (slots[role] ?? 0) + 1; remaining -= item.cost ?? 0; }
134
175
  return { ...item, disposition: status };
@@ -147,6 +188,44 @@ async function linear<T>(key: string, query: string, variables: Record<string, u
147
188
  return body.data;
148
189
  }
149
190
 
191
+ function graphRunId(): string | undefined {
192
+ const path = process.env.ELANOUS_GRAPH_CONTEXT;
193
+ if (!path) return undefined;
194
+ try {
195
+ const context = JSON.parse(readFileSync(path, 'utf8')) as { graphId?: string; runId?: string };
196
+ return context.graphId === 'steward' && /^[a-zA-Z0-9._-]+$/.test(context.runId ?? '') ? context.runId : undefined;
197
+ } catch { return undefined; }
198
+ }
199
+
200
+ function isProcessAlive(pid: number): boolean {
201
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
202
+ try { process.kill(pid, 0); return true; }
203
+ catch (error) { return (error as NodeJS.ErrnoException).code === 'EPERM'; }
204
+ }
205
+
206
+ function skippedPath(root: string): string {
207
+ // A graph run has a stable id across command nodes. Without a graph context,
208
+ // keep the skip until the next sync: stage subprocess parents may differ.
209
+ const runId = graphRunId();
210
+ return join(root, 'steward', runId ? `skipped-${runId}.json` : 'skipped-no-context.json');
211
+ }
212
+
213
+ function currentOpen(root: string, stage: StewardStage): FailureStreak['open'] & { pid?: number; runId?: string } {
214
+ const open = readFailureStreak(root).open;
215
+ return open ?? { stage, startedAt: new Date().toISOString() };
216
+ }
217
+
218
+ function skipOverlappingRun(root: string, open: FailureStreak['open'] & { pid?: number; runId?: string }): void {
219
+ // The skip marker is shared by every stage of one graph run. A loser inside the SAME run must not write it,
220
+ // or the winner's own triage·schedule·report would skip too — it just exits.
221
+ if (open.runId && open.runId === graphRunId()) {
222
+ debug.log('steward.stage', 'skipped-overlap-same-run', { openStage: open.stage, pid: open.pid });
223
+ return;
224
+ }
225
+ debug.log('steward.stage', 'skipped-overlap', { openStage: open.stage, pid: open.pid });
226
+ writeFileSync(skippedPath(root), JSON.stringify({ openStage: open.stage, pid: open.pid }));
227
+ }
228
+
150
229
  function reportPath(root: string): string { return join(root, 'steward', 'observe.json'); }
151
230
  function readState(path: string): { issues: Record<string, string>; digestDay?: string } {
152
231
  try { return JSON.parse(readFileSync(path, 'utf8')) as ReturnType<typeof readState>; }
@@ -157,30 +236,66 @@ function readState(path: string): { issues: Record<string, string>; digestDay?:
157
236
  export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'report', deps: StewardDeps = {}): Promise<void> {
158
237
  const settings = stewardSettings();
159
238
  if ((settings.mode ?? 'observe') !== 'observe') throw new Error('steward act mode is out of scope');
160
- const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
161
- if (!key) throw new Error('connector.linear.apiKey missing');
162
239
  const root = deps.root ?? effectiveInstanceRoot();
240
+ if (stage === 'sync' && !deps.stageOwnedByCommand) {
241
+ return withStageSlot(root, async () => {
242
+ const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number }) | undefined;
243
+ if (open?.pid && isProcessAlive(open.pid)) { skipOverlappingRun(root, open); return; }
244
+ rmSync(skippedPath(root), { force: true });
245
+ await runStewardStage(stage, { ...deps, stageOwnedByCommand: true });
246
+ }, () => { skipOverlappingRun(root, currentOpen(root, stage)); });
247
+ }
163
248
  const dir = join(root, 'steward');
164
249
  mkdirSync(dir, { recursive: true });
165
250
  const snapshot = join(dir, 'issues.json');
166
251
  const judgments = join(dir, 'triage.json');
167
252
  const schedule = join(dir, 'schedule.json');
168
253
  const fetchFn = deps.fetch ?? fetch;
254
+ const skip = skippedPath(root);
255
+ if (stage !== 'sync' && existsSync(skip)) return;
256
+ const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
257
+ if (!key) throw new Error('connector.linear.apiKey missing');
169
258
  if (stage === 'sync') {
170
259
  const events = await fetchLinearIssues({ apiKey: key, teamKey: settings.linearTeam ?? 'ELA', fetch: fetchFn });
171
260
  writeFileSync(snapshot, JSON.stringify(events.map(e => ({ identifier: e.identifier, ref: e.ref, title: e.title, body: e.body }))));
172
261
  } else if (stage === 'triage') {
173
262
  const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
174
- const judge = deps.judge ?? judgeStewardIssue;
175
- writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks)));
263
+ const judge = deps.judge;
264
+ let previous: Array<TriageDecision & { inputHash?: string }> = [];
265
+ try {
266
+ const parsed: unknown = JSON.parse(readFileSync(judgments, 'utf8'));
267
+ if (Array.isArray(parsed) && parsed.every((item): item is TriageDecision & { inputHash?: string } =>
268
+ item !== null && typeof item === 'object' && typeof item.issue === 'string' &&
269
+ (item.inputHash === undefined || typeof item.inputHash === 'string') &&
270
+ (item.deferred === undefined || item.deferred === true) &&
271
+ (item.contextInputs === undefined || (Array.isArray(item.contextInputs) &&
272
+ item.contextInputs.every((id: unknown) => typeof id === 'string'))) &&
273
+ [0, 1, 2, 3, 4, 5, 'hitl'].includes(item.rung) &&
274
+ Array.isArray(item.dependsOn) && item.dependsOn.every((dep: unknown) => typeof dep === 'string') &&
275
+ typeof item.priority === 'number' && Number.isFinite(item.priority) && typeof item.why === 'string')) previous = parsed;
276
+ } catch { /* Missing or corrupt previous judgments are an empty cache. */ }
277
+ writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks, previous, deps.ask)));
176
278
  } else if (stage === 'schedule') {
177
279
  const decisions = JSON.parse(readFileSync(judgments, 'utf8')) as TriageDecision[];
178
280
  const pending = new Set(decisions.flatMap(decision => decision.dependsOn));
179
281
  const completed = new Set<string>();
180
- for (const identifier of pending) {
181
- const data = await linear<{ issue: { identifier: string; state?: { type: string } } | null }>(key,
182
- 'query($id:String!){issue(id:$id){identifier state{type}}}', { id: identifier }, fetchFn);
183
- if (data.issue?.identifier === identifier && data.issue.state?.type === 'completed') completed.add(identifier);
282
+ if (pending.size) {
283
+ const ids = [...pending];
284
+ for (let offset = 0; offset < ids.length; offset += 250) {
285
+ const batch = ids.slice(offset, offset + 250);
286
+ let cursor: string | null = null;
287
+ do {
288
+ const data: { issues: { nodes: Array<{ identifier: string; state?: { type: string } }>; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } = await linear(key,
289
+ 'query($ids:[String!]!,$after:String){issues(filter:{identifier:{in:$ids}},first:250,after:$after){nodes{identifier state{type}} pageInfo{hasNextPage endCursor}}}',
290
+ { ids: batch, after: cursor }, fetchFn);
291
+ if (!Array.isArray(data.issues?.nodes) || !data.issues.pageInfo ||
292
+ (data.issues.pageInfo.hasNextPage && (!data.issues.pageInfo.endCursor || data.issues.pageInfo.endCursor === cursor))) {
293
+ throw new Error('Linear dependencies incomplete');
294
+ }
295
+ for (const item of data.issues.nodes) if (pending.has(item.identifier) && item.state?.type === 'completed') completed.add(item.identifier);
296
+ cursor = data.issues.pageInfo.hasNextPage ? data.issues.pageInfo.endCursor : null;
297
+ } while (cursor);
298
+ }
184
299
  }
185
300
  writeFileSync(schedule, JSON.stringify(scheduleTriage(decisions, settings, {}, completed)));
186
301
  } else {
@@ -222,17 +337,104 @@ async function sendStewardDigest(text: string): Promise<void> {
222
337
  if (!deliver(text, 'digest')) throw new Error('steward digest delivery failed');
223
338
  }
224
339
 
340
+ export interface StewardCommandDeps extends StewardDeps {
341
+ runStage?: typeof runStewardStage;
342
+ deliverAlert?: (text: string, kind: 'alert') => boolean | Promise<boolean>;
343
+ }
344
+
345
+ // One stage at a time per instance: an exclusive flock(2) on a stable file, taken in-process through libc
346
+ // (as graph-tick does). The kernel drops it when the process dies, so a killed stage never leaves a stale lock.
347
+ // The `flock` command-line tool is Linux-only — on macOS, where the steward cron runs, it does not exist (10-01).
348
+ const STAGE_LOCK_WAIT_MS = 15 * 60_000;
349
+
350
+ async function withStageSlot<T>(root: string, run: () => Promise<T>, onBusy?: () => T): Promise<T> {
351
+ const dir = join(root, 'steward');
352
+ mkdirSync(dir, { recursive: true });
353
+ const library = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
354
+ if (!library) throw new Error(`steward stage lock unavailable on ${process.platform}`);
355
+ const { dlopen, FFIType } = await import('bun:ffi');
356
+ const lib = dlopen(library, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
357
+ const flock = (fd: number, operation: number): number => lib.symbols.flock(fd, operation) as number;
358
+ const fd = openSync(join(dir, 'stage-slot.lock'), 'a');
359
+ // LOCK_EX|LOCK_NB (2|4), retried on a timer: a blocking FFI call would freeze the event loop, and a holder in the
360
+ // same process could then never release (the lock is per open file, so a second open in one process contends too).
361
+ // With onBusy (sync), one try only: a busy slot means another run is in flight — skip instead of waiting.
362
+ if (onBusy && flock(fd, 2 | 4) !== 0) { closeSync(fd); lib.close(); return onBusy(); }
363
+ const deadline = Date.now() + STAGE_LOCK_WAIT_MS;
364
+ while (flock(fd, 2 | 4) !== 0) {
365
+ if (Date.now() >= deadline) { closeSync(fd); lib.close(); throw new Error('steward stage lock unavailable: another stage is still running'); }
366
+ await new Promise(resolve => setTimeout(resolve, 200));
367
+ }
368
+ try { return await run(); }
369
+ finally { try { flock(fd, 8); } finally { closeSync(fd); lib.close(); } }
370
+ }
371
+
372
+ /** The command-node boundary records both normal exits and an unclosed stage from a killed process. */
373
+ export async function runStewardStageCommand(stage: StewardStage, deps: StewardCommandDeps = {}): Promise<0 | 1> {
374
+ const root = deps.root ?? effectiveInstanceRoot();
375
+ if (stage !== 'sync' && existsSync(skippedPath(root))) return 0;
376
+ return withStageSlot(root, async () => {
377
+ const now = deps.now ?? (() => new Date());
378
+ const threshold = stewardSettings().alertAfterFailures ?? 3;
379
+ const alertIfNeeded = async (state: FailureStreak): Promise<void> => {
380
+ if (!shouldAlert(state, threshold)) return;
381
+ try {
382
+ const send = deps.deliverAlert ?? (async (text: string, kind: 'alert') => {
383
+ const { deliver } = await import('../domains/outbound-alert.js');
384
+ return deliver(text, kind) !== false;
385
+ });
386
+ if (!await send(failureAlertText(state), 'alert')) throw new Error('steward alert delivery failed');
387
+ state.alertedAt = now().toISOString();
388
+ writeFailureStreak(root, state);
389
+ debug.log('steward.streak', 'alerted', { consecutive: state.consecutive, stage: state.lastStage });
390
+ } catch (error) {
391
+ try { debug.log('steward.streak', 'alert-failed', { consecutive: state.consecutive, stage: state.lastStage,
392
+ reason: redactSecretText(error instanceof Error ? error.message : String(error)) }); } catch { /* alert observability must not change the stage result */ }
393
+ }
394
+ };
395
+ const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number; runId?: string }) | undefined;
396
+ if (stage === 'sync') {
397
+ if (open?.pid && isProcessAlive(open.pid)) {
398
+ skipOverlappingRun(root, open);
399
+ return 0;
400
+ }
401
+ rmSync(skippedPath(root), { force: true });
402
+ } else if (existsSync(skippedPath(root))) return 0;
403
+ const hadOpen = open !== undefined;
404
+ const started = markStageStart(root, stage, now());
405
+ if (started.open) {
406
+ Object.assign(started.open, { pid: process.pid, ...(graphRunId() ? { runId: graphRunId() } : {}) });
407
+ writeFailureStreak(root, started);
408
+ }
409
+ if (hadOpen) {
410
+ debug.log('steward.streak', 'recorded', { consecutive: started.consecutive, stage: started.lastStage });
411
+ await alertIfNeeded(started);
412
+ }
413
+ let error: unknown;
414
+ let ok = false;
415
+ try { await (deps.runStage ?? runStewardStage)(stage, { ...deps, stageOwnedByCommand: true }); ok = true; }
416
+ catch (caught) { error = caught; }
417
+ const reason = ok ? '' : redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
418
+ const state = markStageEnd(root, stage, ok, reason, now());
419
+ debug.log('steward.streak', 'recorded', { consecutive: state.consecutive, stage });
420
+ await alertIfNeeded(state);
421
+ if (!ok) {
422
+ console.error(`steward ${stage}: stage failed — ${reason}`);
423
+ debug.log('steward.stage', 'failed', { stage, reason });
424
+ return 1;
425
+ }
426
+ return 0;
427
+ }, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
428
+ }
429
+
225
430
  if (import.meta.main) {
226
431
  const stage = process.argv[2];
227
432
  if (!['sync', 'triage', 'schedule', 'report'].includes(stage ?? '')) {
228
433
  console.error('steward: expected sync|triage|schedule|report');
229
434
  process.exitCode = 2;
230
435
  } else {
231
- runStewardStage(stage as 'sync' | 'triage' | 'schedule' | 'report').catch((error: unknown) => {
232
- // Say why (secrets masked) — 09-29 the first resident run failed as a bare «stage failed» on a missing Linear key.
233
- const reason = redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
234
- console.error(`steward ${stage}: stage failed — ${reason}`);
235
- debug.log('steward.stage', 'failed', { stage, reason });
436
+ runStewardStageCommand(stage as StewardStage).then(code => { process.exitCode = code; }).catch((error: unknown) => {
437
+ console.error(`steward ${stage}: ${redactSecretText(error instanceof Error ? error.message : String(error))}`);
236
438
  process.exitCode = 1;
237
439
  });
238
440
  }
@@ -38,7 +38,7 @@ type CardEvent =
38
38
  * A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
39
39
  * task's life (e.g. workspace diff updates), and the same full key is written once. `incidents` is open to any loop. */
40
40
  export const SECTION_OWNERS: Readonly<Record<string, string | null>> = {
41
- intake: 'steward', triage: 'steward',
41
+ intake: 'steward', triage: 'steward', launch: 'steward', outcome: 'steward',
42
42
  gates: 'execution-loop', relations: 'execution-loop', memory: 'execution-loop',
43
43
  workspace: 'executor', run: 'executor',
44
44
  landing: 'landing-loop', release: 'release-loop',
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
2
2
  import { gunzipSync } from 'node:zlib';
3
3
  import { debug } from '../../debug/log.js';
4
4
  import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
5
+ import { ledgerLineToLogEvent } from './pod-ledger-events.js';
5
6
 
6
7
  type Ledger = { runId: string; jsonl: string };
7
8
  type Incomplete = { runId: string; reason: string };
@@ -124,7 +125,7 @@ export function createPodLedgerFollower(opts: {
124
125
  let owned = false;
125
126
  let disabled = false;
126
127
  let lastProgressAt = now();
127
- let lastProgressEvent: string | null = null;
128
+ let visibleProgressEvent: string | null = null;
128
129
  let lastActivityMeasuredAt = lastProgressAt;
129
130
  let lastActivityLogAt = -Infinity;
130
131
  let nextStallMinute = stallMinutes;
@@ -132,11 +133,11 @@ export function createPodLedgerFollower(opts: {
132
133
  function checkStall(ledgerRead: boolean, activityMeasured: boolean) {
133
134
  const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
134
135
  if (idleMinutes >= nextStallMinute) {
135
- log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent, idleMinutes });
136
+ log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent: visibleProgressEvent, idleMinutes });
136
137
  const observation = ledgerRead
137
138
  ? (activityMeasured ? '원장·작업 트리 모두 조용함' : '원장 조용함')
138
139
  : '원장 못 잼';
139
- onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${lastProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
140
+ onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${visibleProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
140
141
  stalled = true;
141
142
  nextStallMinute = (Math.floor(idleMinutes / stallMinutes) + 1) * stallMinutes;
142
143
  }
@@ -169,7 +170,7 @@ export function createPodLedgerFollower(opts: {
169
170
  if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
170
171
  stalled = false;
171
172
  lastProgressAt = now();
172
- lastProgressEvent = 'worktree-activity';
173
+ visibleProgressEvent = 'worktree-activity';
173
174
  nextStallMinute = stallMinutes;
174
175
  if (now() - lastActivityLogAt >= 5 * 60_000) {
175
176
  log('self-implement.pod', 'worktree-activity', { runId: opts.runId, files });
@@ -190,6 +191,11 @@ export function createPodLedgerFollower(opts: {
190
191
  log('self-implement.pod', 'ledger-live-appended', { runId: opts.runId, lines: complete.split('\n').filter(Boolean).length, bytes: Buffer.byteLength(complete), offset });
191
192
  for (const line of complete.split('\n')) {
192
193
  if (!line) continue;
194
+ let logEvent: ReturnType<typeof ledgerLineToLogEvent>;
195
+ try {
196
+ logEvent = ledgerLineToLogEvent(line, opts.runId);
197
+ if (logEvent) log(logEvent.category, logEvent.event, logEvent.data);
198
+ } catch { /* A failed host log write must not interrupt ledger following or stall accounting. */ }
193
199
  let entry: unknown;
194
200
  try { entry = JSON.parse(line); } catch { continue; }
195
201
  const event = (entry && typeof entry === 'object' && 'event' in entry) ? entry.event : undefined;
@@ -198,7 +204,7 @@ export function createPodLedgerFollower(opts: {
198
204
  if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
199
205
  stalled = false;
200
206
  lastProgressAt = now();
201
- lastProgressEvent = event;
207
+ visibleProgressEvent = logEvent?.event ?? '[redacted]';
202
208
  nextStallMinute = stallMinutes;
203
209
  }
204
210
  checkStall(true, activityMeasured);
@@ -0,0 +1,68 @@
1
+ const SECRET_FIELD = /token|secret|password|authorization|key/i;
2
+ const SAFE_FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
3
+ const SECRET_VALUE = /(?:\b(?:bearer|basic)\s+\S+|\b(?:token|secret|password|authorization|api[-_]?key|access[-_]?key|private[-_]?key)\b\s*(?:is|[=:])\s*["']?\S+|\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}))\b/i;
4
+ const MAX_FIELDS = 32;
5
+ // Review R2: only lifecycle events the host needs to see, and no free text. Event names must start with a known
6
+ // lifecycle prefix; string fields survive only when they are short code-like values in a fixed set of keys.
7
+ const EVENT_ALLOWED = /^(?:gate|gated|review|reviewed|repair|repaired|rework|merge|merged|pr|land|landed|stage|round|supervisor|child|plan|planned|decompose|decomposed|shard|stall|harvest|decision|run|pipeline|progress|human|start|started|finish|finished|done|implement|implemented|commit|test|typecheck)(?:-(?:started|finished|round|opened|closed|failed|passed|blocked|done|entry|node|outcome|delivery|decision|stop|merged|ready|requested|resolved|result|fix|must-fix|verdict|skipped|origin|cleared))*$/;
8
+ const CODE_FIELDS = new Set(['stage', 'outcome', 'verdict', 'status', 'kind', 'phase', 'role', 'result']);
9
+ const CODE_VALUE = /^[a-z0-9_.:-]{1,40}$/i;
10
+
11
+ function secretValues(root: Record<string, unknown>): string[] {
12
+ const secrets: string[] = [];
13
+ const pending: unknown[] = [root];
14
+ while (pending.length) {
15
+ const current = pending.pop();
16
+ if (!current || typeof current !== 'object') continue;
17
+ if (Array.isArray(current)) {
18
+ pending.push(...current);
19
+ continue;
20
+ }
21
+ for (const [name, value] of Object.entries(current)) {
22
+ if (SECRET_FIELD.test(name) && typeof value === 'string' && value.length >= 3) secrets.push(value);
23
+ if (value && typeof value === 'object') pending.push(value);
24
+ }
25
+ }
26
+ return secrets;
27
+ }
28
+
29
+ /** Project bounded ledger metadata, excluding named secrets and recognizable credential values. */
30
+ export function ledgerLineToLogEvent(line: string, runId: string): {
31
+ category: string;
32
+ event: string;
33
+ data: Record<string, unknown>;
34
+ } | undefined {
35
+ let entry: unknown;
36
+ try { entry = JSON.parse(line); } catch { return undefined; }
37
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !('event' in entry) || typeof entry.event !== 'string') return undefined;
38
+ // The real run-ledger writer wraps observer fields in `data`; older/synthetic lines can be flat.
39
+ const details = 'data' in entry && entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
40
+ ? entry.data as Record<string, unknown> : undefined;
41
+ const sources = [entry, details].filter((source): source is Record<string, unknown> => source !== undefined);
42
+ const knownSecrets = secretValues(entry);
43
+ const containsCredential = (value: string) => SECRET_VALUE.test(value) || knownSecrets.some((secret) => value.includes(secret));
44
+ if (containsCredential(entry.event) || !EVENT_ALLOWED.test(entry.event)) return undefined;
45
+
46
+ const data: Record<string, unknown> = { runId };
47
+ let fields = 0;
48
+ for (const source of sources) {
49
+ for (const [name, value] of Object.entries(source)) {
50
+ if (name === 'event' || name === 'runId' || name === 'data' || !SAFE_FIELD.test(name) || SECRET_FIELD.test(name)) continue;
51
+ if (fields >= MAX_FIELDS) break;
52
+ const countName = `${name}Count`;
53
+ const outputName = Array.isArray(value) || value && typeof value === 'object' ? countName : name;
54
+ if (!SAFE_FIELD.test(outputName) || SECRET_FIELD.test(outputName) || Object.hasOwn(data, outputName)) continue;
55
+ if (typeof value === 'string') {
56
+ // Free text (reasons, messages, patches) never leaves the pod ledger; only short code values in known keys.
57
+ if (!CODE_FIELDS.has(name) || !CODE_VALUE.test(value) || containsCredential(value)) continue;
58
+ data[name] = value;
59
+ } else if (typeof value === 'number' && Number.isFinite(value) || typeof value === 'boolean' || value === null) {
60
+ data[name] = value;
61
+ } else if (Array.isArray(value) || value && typeof value === 'object') {
62
+ data[countName] = Array.isArray(value) ? value.length : Object.keys(value).length;
63
+ } else continue;
64
+ fields += 1;
65
+ }
66
+ }
67
+ return { category: 'self-implement.pod.ledger', event: entry.event, data };
68
+ }