elanous 0.2.5 → 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 (479) hide show
  1. package/.env.example +10 -7
  2. package/README.md +9 -4
  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/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
  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/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  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/{8617.465ca22a319c95b3.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/{8918-482b30ce2a8ebf4c.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/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
  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/{f6f3850d-7f324c6566002113.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-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
  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-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
  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-2bf59c720fe9542c.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/icons/elanous-16.png +0 -0
  123. package/apps/pwa/out/icons/elanous-180.png +0 -0
  124. package/apps/pwa/out/icons/elanous-192.png +0 -0
  125. package/apps/pwa/out/icons/elanous-32.png +0 -0
  126. package/apps/pwa/out/icons/elanous-512.png +0 -0
  127. package/apps/pwa/out/index.html +1 -1
  128. package/apps/pwa/out/index.txt +21 -21
  129. package/apps/pwa/out/intake/index.html +1 -1
  130. package/apps/pwa/out/intake/index.txt +22 -22
  131. package/apps/pwa/out/live/index.html +1 -1
  132. package/apps/pwa/out/live/index.txt +22 -22
  133. package/apps/pwa/out/market/index.html +1 -1
  134. package/apps/pwa/out/market/index.txt +21 -21
  135. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  136. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  137. package/apps/pwa/out/morning/index.html +1 -1
  138. package/apps/pwa/out/morning/index.txt +22 -22
  139. package/apps/pwa/out/observatory/index.html +1 -1
  140. package/apps/pwa/out/observatory/index.txt +22 -22
  141. package/apps/pwa/out/reflection/index.html +1 -1
  142. package/apps/pwa/out/reflection/index.txt +22 -22
  143. package/apps/pwa/out/scheduler/index.html +1 -1
  144. package/apps/pwa/out/scheduler/index.txt +22 -22
  145. package/apps/pwa/out/sessions/index.html +1 -1
  146. package/apps/pwa/out/sessions/index.txt +21 -21
  147. package/apps/pwa/out/settings/devices/index.html +1 -1
  148. package/apps/pwa/out/settings/devices/index.txt +22 -22
  149. package/apps/pwa/out/settings/index.html +1 -1
  150. package/apps/pwa/out/settings/index.txt +22 -22
  151. package/apps/pwa/out/setup/done/index.html +1 -1
  152. package/apps/pwa/out/setup/done/index.txt +22 -22
  153. package/apps/pwa/out/setup/index.html +1 -1
  154. package/apps/pwa/out/setup/index.txt +22 -22
  155. package/apps/pwa/out/share/index.html +1 -1
  156. package/apps/pwa/out/share/index.txt +22 -22
  157. package/apps/pwa/out/showroom/index.html +1 -1
  158. package/apps/pwa/out/showroom/index.txt +21 -21
  159. package/apps/pwa/out/tasks/index.html +1 -1
  160. package/apps/pwa/out/tasks/index.txt +22 -22
  161. package/apps/pwa/out/term/index.html +1 -1
  162. package/apps/pwa/out/term/index.txt +22 -22
  163. package/apps/pwa/out/trace/index.html +1 -1
  164. package/apps/pwa/out/trace/index.txt +22 -22
  165. package/apps/pwa/out/vault/index.html +1 -1
  166. package/apps/pwa/out/vault/index.txt +22 -22
  167. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  168. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  169. package/apps/pwa/out/workflows/index.html +1 -1
  170. package/apps/pwa/out/workflows/index.txt +22 -22
  171. package/apps/pwa/out/workspace/index.html +1 -1
  172. package/apps/pwa/out/workspace/index.txt +22 -22
  173. package/apps/pwa/out/worktrees/index.html +1 -1
  174. package/apps/pwa/out/worktrees/index.txt +22 -22
  175. package/bin/elanous.cjs +107 -0
  176. package/graphs/field/field-feed.yaml +24 -0
  177. package/graphs/field/recipes.yaml +12 -0
  178. package/graphs/landing/landing-heal.yaml +3 -1
  179. package/graphs/landing/recipes.yaml +3 -0
  180. package/graphs/release/recipes.yaml +13 -0
  181. package/graphs/release/release-loop.yaml +13 -4
  182. package/package.json +5 -5
  183. package/plugins/card-followup/README.md +42 -0
  184. package/plugins/card-followup/examples/card-sample.json +7 -0
  185. package/plugins/card-followup/graphs/card-followup.yaml +18 -0
  186. package/plugins/card-followup/graphs/recipes.yaml +16 -0
  187. package/plugins/card-followup/graphs/run-step.ts +276 -0
  188. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  189. package/plugins/card-followup/plugin.json +25 -0
  190. package/plugins/doc-draft/README.md +13 -0
  191. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  192. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  193. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  194. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  195. package/plugins/doc-draft/plugin.json +24 -0
  196. package/plugins/geo-check/README.md +15 -0
  197. package/plugins/geo-check/examples/brand-sample.json +8 -0
  198. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  199. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  200. package/plugins/geo-check/graphs/run-step.ts +198 -0
  201. package/plugins/geo-check/plugin.json +24 -0
  202. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  203. package/plugins/job-coach/README.md +31 -1
  204. package/plugins/job-coach/graphs/run-step.ts +2 -2
  205. package/plugins/job-coach/plugin.json +1 -1
  206. package/plugins/job-coach/plugin.ts +1 -1
  207. package/scripts/affinity/endcard.sh +3 -2
  208. package/scripts/affinity/reframe.sh +3 -1
  209. package/scripts/ci-public-export-leak-gate.ts +48 -0
  210. package/scripts/codex-quota-alert.ts +122 -258
  211. package/scripts/coord-tracks.json +4 -4
  212. package/scripts/daemon-port-baseline.txt +1 -2
  213. package/scripts/field-feed/deliver-node.ts +52 -0
  214. package/scripts/field-feed/feed-node.ts +67 -0
  215. package/scripts/field-feed/lib.ts +162 -0
  216. package/scripts/field-feed/reel-node.ts +24 -0
  217. package/scripts/field-feed/start.ts +62 -0
  218. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  219. package/scripts/install.sh +92 -13
  220. package/scripts/landing-heal/clean-checkout.ts +114 -0
  221. package/scripts/landing-heal/run-mission.ts +38 -3
  222. package/scripts/lib/android-test-device.ts +32 -0
  223. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  224. package/scripts/public-export.ts +8 -4
  225. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  226. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  227. package/scripts/release-loop/env-known-failures.ts +31 -0
  228. package/scripts/release-loop/gate-diff.ts +29 -0
  229. package/scripts/release-loop/gate-node.ts +124 -29
  230. package/scripts/release-loop/known-issues-node.ts +159 -0
  231. package/scripts/release-loop/npm-publish-node.ts +118 -0
  232. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  233. package/scripts/release-loop/release-readiness.ts +58 -0
  234. package/scripts/release-loop/shard-plan.ts +47 -0
  235. package/scripts/release-loop/unattended-release.ts +93 -0
  236. package/scripts/ux/measure-first-text.ts +102 -0
  237. package/scripts/ux/record-page.ts +4 -1
  238. package/scripts/vflow-collect.ts +20 -7
  239. package/scripts/vflow-search.ts +73 -0
  240. package/scripts/video-free-line-test-walker.ts +36 -0
  241. package/scripts/video-pipeline.ts +2 -2
  242. package/skills/explainer-video/SKILL.md +24 -3
  243. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  244. package/skills/explainer-video/engine/build.mjs +2 -2
  245. package/skills/explainer-video/engine/reel.mjs +235 -0
  246. package/skills/explainer-video/engine/reel.sh +9 -0
  247. package/skills/explainer-video/engine/vo.ts +58 -16
  248. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  249. package/skills/grill-me/SKILL.md +1 -1
  250. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  251. package/src/a2a/server.ts +2 -0
  252. package/src/acp/elanous-aware-peers.ts +30 -0
  253. package/src/acp/server.ts +91 -15
  254. package/src/agent/monad-agent-turn.ts +25 -6
  255. package/src/agent/shared-app-tools.ts +5 -3
  256. package/src/agent-substrate/review-intent.ts +6 -2
  257. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  258. package/src/auth/github-app-token.ts +4 -1
  259. package/src/autopilot/mission-origin.ts +20 -4
  260. package/src/autopilot/mission-phase-track.ts +11 -1
  261. package/src/autopilot/track-agent-shadow.ts +151 -0
  262. package/src/autopilot/track-agent.ts +26 -6
  263. package/src/autopilot/track-courier.ts +2 -2
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/chat/chat-cli.ts +4 -1
  271. package/src/chat/short-question.ts +40 -0
  272. package/src/cli/agent-cli.ts +24 -15
  273. package/src/cli/ask-bare.ts +42 -0
  274. package/src/cli/cli-user-error.ts +25 -0
  275. package/src/cli/codex-credit-plan-view.ts +25 -0
  276. package/src/cli/dependence-gauge.ts +237 -0
  277. package/src/cli/doctor-cli.ts +5 -0
  278. package/src/cli/doctor-modes.ts +48 -0
  279. package/src/cli/intake-cli.ts +12 -2
  280. package/src/cli/intake-source-cli.ts +58 -0
  281. package/src/cli/machine-cli.ts +23 -0
  282. package/src/cli/msg-cli.ts +137 -0
  283. package/src/cli/nexus-ios-bind.ts +25 -8
  284. package/src/cli/phone-link.ts +117 -0
  285. package/src/cli/plugin-cli.ts +52 -0
  286. package/src/cli/pr-cli.ts +36 -0
  287. package/src/cli/private-ledger-commands.ts +31 -0
  288. package/src/cli/provider-cli.ts +5 -0
  289. package/src/cli/release-cli.ts +56 -12
  290. package/src/cli/release-run-if-ready.ts +48 -0
  291. package/src/cli/setup-cli.ts +27 -6
  292. package/src/cli/setup-recommend.ts +8 -0
  293. package/src/cli/usage-cli.ts +11 -1
  294. package/src/codex/setup.ts +27 -8
  295. package/src/dashboard/index.ts +1 -1
  296. package/src/discord.ts +56 -6
  297. package/src/domains/core-tools.ts +37 -14
  298. package/src/domains/outbound-alert.ts +55 -16
  299. package/src/exec-requests/planner.ts +140 -0
  300. package/src/exec-requests/runner.ts +338 -0
  301. package/src/exec-requests/store.ts +76 -0
  302. package/src/field/field-media.ts +250 -0
  303. package/src/field/field-reel.ts +196 -0
  304. package/src/grok/credential.ts +7 -5
  305. package/src/harness/aside-run-status.ts +33 -0
  306. package/src/harness/browser-verify.ts +48 -17
  307. package/src/harness/harness-cli-command.ts +25 -3
  308. package/src/harness/image-load-state.ts +21 -0
  309. package/src/harness/process-garbage.ts +45 -0
  310. package/src/index.ts +104 -38
  311. package/src/instance/leader.ts +13 -1
  312. package/src/intake-plane/collect-github.ts +5 -3
  313. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  314. package/src/intake-plane/discord-seat-work.ts +56 -0
  315. package/src/intake-plane/intake-sources.ts +202 -0
  316. package/src/intake-plane/items.ts +28 -12
  317. package/src/intake-plane/seat-doc-route.ts +207 -0
  318. package/src/intake-plane/submit-intake-work.ts +9 -1
  319. package/src/intake-plane/telegram-seat-work.ts +72 -0
  320. package/src/llm/model-defaults.ts +24 -1
  321. package/src/llm.ts +5 -4
  322. package/src/machines/machine-ledger.ts +127 -0
  323. package/src/market/pack-internal-refs.ts +28 -0
  324. package/src/market/publish.ts +31 -4
  325. package/src/maturity/feature-maturity.ts +65 -0
  326. package/src/model-tier/turn-tier.ts +24 -0
  327. package/src/msg/msg-store.ts +149 -0
  328. package/src/native-tool-catalog.ts +2 -1
  329. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  330. package/src/nexus/api/consult-requests.ts +117 -0
  331. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  332. package/src/nexus/api/exec-requests.ts +51 -0
  333. package/src/nexus/api/field-uploads.ts +180 -0
  334. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  335. package/src/nexus/api/graph-approvals.ts +120 -4
  336. package/src/nexus/api/harness-api.ts +219 -5
  337. package/src/nexus/api/http-server.ts +68 -5
  338. package/src/nexus/api/seat-requests.ts +254 -0
  339. package/src/nexus/api/trace-run-scan.ts +37 -0
  340. package/src/nexus/api/trace.ts +19 -3
  341. package/src/nexus/index.ts +3 -0
  342. package/src/nexus/setup-status.ts +4 -5
  343. package/src/notify/llm-budget-alert.ts +125 -0
  344. package/src/oauth/codex-account-rotation.ts +26 -4
  345. package/src/oauth/codex-account-store.ts +20 -2
  346. package/src/oauth/codex-browser-login.ts +173 -0
  347. package/src/oauth/codex.ts +4 -1
  348. package/src/oauth/fallback-chain.ts +5 -5
  349. package/src/oauth/store.ts +42 -5
  350. package/src/onboarding/continue-here.ts +29 -0
  351. package/src/onboarding/entry-hints.ts +8 -0
  352. package/src/onboarding/full-screen-io.ts +35 -3
  353. package/src/onboarding/help/skills.md +1 -1
  354. package/src/onboarding/non-interactive.ts +1 -0
  355. package/src/onboarding/subscription-hint.ts +27 -0
  356. package/src/onboarding/web-first.ts +83 -0
  357. package/src/onboarding.ts +230 -23
  358. package/src/plugins/maker/node-install.ts +92 -0
  359. package/src/plugins/maker/node-maker.ts +139 -0
  360. package/src/plugins/maker/plugin-maker.ts +220 -0
  361. package/src/policy/llm-policy.ts +268 -0
  362. package/src/prompt-library/session-guidance.ts +7 -1
  363. package/src/prompt-library/universal-preamble.ts +2 -2
  364. package/src/relay/channel.ts +184 -0
  365. package/src/relay/mux.ts +182 -0
  366. package/src/relay/relay-cli.ts +20 -0
  367. package/src/relay/relay-server.ts +220 -0
  368. package/src/relay/test-vectors.json +29 -0
  369. package/src/release-loop/checklist.ts +36 -9
  370. package/src/seat-address/seat-address.ts +34 -0
  371. package/src/self-dev/decompose.ts +1 -0
  372. package/src/self-dev/dev-pipeline.ts +6 -3
  373. package/src/self-dev/run-record-mirror.ts +44 -0
  374. package/src/self-dev/run-store.ts +12 -3
  375. package/src/self-implement/harness-policy-gates.ts +4 -1
  376. package/src/self-implement/report-origin.ts +12 -1
  377. package/src/self-implement/run-ledger-compact.ts +1 -1
  378. package/src/session/chat.ts +17 -5
  379. package/src/session/index.ts +23 -10
  380. package/src/skills/shared-agent-skill-roots.ts +25 -0
  381. package/src/steward/failure-streak.ts +78 -0
  382. package/src/steward/steward-cards.ts +24 -0
  383. package/src/steward/triage-budget.ts +127 -0
  384. package/src/steward/triage-plan.ts +75 -0
  385. package/src/steward/triage.ts +244 -42
  386. package/src/task-cards/card-store.ts +1 -1
  387. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  388. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  389. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  390. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  391. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  392. package/src/telegram.ts +145 -2
  393. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  394. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  395. package/src/user-config.ts +84 -4
  396. package/src/version/packed-revision.json +1 -1
  397. package/src/video-pipeline/recipes/character.ts +42 -5
  398. package/src/video-pipeline/recipes/explainer.ts +213 -0
  399. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  400. package/src/video-pipeline/recipes/free-line.ts +16 -3
  401. package/src/video-pipeline/recipes/remote.ts +7 -1
  402. package/src/video-pipeline/vflow-reference.ts +109 -0
  403. package/src/video-pipeline/walk-line.ts +3 -2
  404. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  411. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  419. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  477. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  478. package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
  479. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -1,4 +1,5 @@
1
1
  # Inputs gateRemote (SSH host, e.g. node-b) and gateRemoteMirror (bare mirror path, default ~/mirror/elanous-agent.git): run gate sweeps in temporary remote clones without a matching caller checkout; omit gateRemote for local execution.
2
+ # Optional opsHosts: ['local', '<ssh-host>']; omit for no operational upgrades. Optional internalDist publishes the release checkout to the internal feed first; opsRestart: true explicitly allows daemon restarts.
2
3
  graph_id: release-loop
3
4
  loop:
4
5
  title: 릴리스
@@ -12,34 +13,42 @@ terminal_nodes: [done, failed]
12
13
  nodes:
13
14
  - { node_id: version-release, kind: git, recipe: 'cmd:version-release', max_visits: 1 }
14
15
  - { node_id: cutoff, kind: judge, recipe: 'cmd:cutoff', max_visits: 1 }
16
+ - { node_id: checklist-gate, kind: gate, recipe: 'cmd:checklist-gate', max_visits: 1 }
15
17
  - { node_id: gate, kind: gate, recipe: 'cmd:gate', max_visits: 1, notify: start }
16
18
  - { node_id: pwa, kind: gate, recipe: 'cmd:pwa', max_visits: 1, notify: start }
17
19
  - { node_id: prepare, kind: agent, recipe: 'cmd:prepare', max_visits: 1, notify: start }
18
20
  - { node_id: upgrade, kind: gate, recipe: 'cmd:upgrade', max_visits: 1 }
19
21
  - { node_id: tui, kind: gate, recipe: 'cmd:tui', max_visits: 1 }
20
22
  - { node_id: docs, kind: agent, recipe: 'cmd:docs', max_visits: 1 }
23
+ - { node_id: known-issues, kind: agent, recipe: 'cmd:known-issues', max_visits: 1 }
21
24
  - { node_id: notes-check, kind: gate, recipe: 'cmd:notes-check', max_visits: 1 }
22
25
  - { node_id: auto-approve, kind: gate, recipe: 'cmd:auto-approve', max_visits: 1 }
23
26
  - { node_id: approve-publish, kind: hitl, recipe: 'approval:publish', max_visits: 1 }
24
27
  - { node_id: publish, kind: git, recipe: 'cmd:publish-command', max_visits: 1 }
28
+ - { node_id: npm-publish, kind: git, recipe: 'cmd:npm-publish', max_visits: 1 }
25
29
  - { node_id: docs-land, kind: git, recipe: 'cmd:docs-land', max_visits: 1 }
26
30
  - { node_id: verify, kind: gate, recipe: 'cmd:verify', max_visits: 1 }
31
+ - { node_id: ops-upgrade, kind: agent, recipe: 'cmd:ops-upgrade', max_visits: 1 }
27
32
  - { node_id: version-dev-bump, kind: git, recipe: 'cmd:version-dev-bump', max_visits: 1 }
28
33
  - { node_id: done, kind: gate, max_visits: 1 }
29
34
  - { node_id: failed, kind: gate, max_visits: 1 }
30
35
  edges:
31
36
  - { from: version-release, on: outcome, map: { ok: cutoff, fail: failed, error: failed } }
32
- - { from: cutoff, on: outcome, map: { ok: gate, fail: failed, error: failed } }
37
+ - { from: cutoff, on: outcome, map: { ok: checklist-gate, fail: failed, error: failed } }
38
+ - { from: checklist-gate, on: outcome, map: { ok: gate, fail: failed, error: failed } }
33
39
  - { from: gate, on: outcome, map: { ok: pwa, fail: failed, error: failed } }
34
40
  - { from: pwa, on: outcome, map: { ok: prepare, fail: failed, error: failed } }
35
41
  - { from: prepare, on: outcome, map: { ok: upgrade, fail: failed, error: failed } }
36
42
  - { from: upgrade, on: outcome, map: { ok: tui, fail: failed, error: failed } }
37
43
  - { from: tui, on: outcome, map: { ok: docs, fail: failed, error: failed } }
38
- - { from: docs, on: outcome, map: { ok: notes-check, fail: failed, error: failed } }
44
+ - { from: docs, on: outcome, map: { ok: known-issues, fail: failed, error: failed } }
45
+ - { from: known-issues, on: outcome, map: { ok: notes-check, fail: failed, error: failed } }
39
46
  - { from: notes-check, on: outcome, map: { ok: auto-approve, fail: failed, error: failed } }
40
47
  - { from: auto-approve, on: outcome, map: { ok: publish, fail: approve-publish, error: approve-publish } }
41
48
  - { from: approve-publish, on: outcome, map: { ok: publish, fail: failed } }
42
- - { from: publish, on: outcome, map: { ok: docs-land, fail: failed, error: failed } }
49
+ - { from: publish, on: outcome, map: { ok: npm-publish, fail: failed, error: failed } }
50
+ - { from: npm-publish, on: outcome, map: { ok: docs-land, fail: docs-land, error: docs-land } }
43
51
  - { from: docs-land, on: outcome, map: { ok: verify, fail: failed, error: failed } }
44
- - { from: verify, on: outcome, map: { ok: version-dev-bump, fail: failed, error: failed } }
52
+ - { from: verify, on: outcome, map: { ok: ops-upgrade, fail: failed, error: failed } }
53
+ - { from: ops-upgrade, on: outcome, map: { ok: version-dev-bump, fail: version-dev-bump, error: version-dev-bump } }
45
54
  - { from: version-dev-bump, on: outcome, map: { ok: done, fail: failed, error: failed } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elanous",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Self-healing coding harness where eyes, hands, and memory all turn on one sentence — it authors its own goals, implements them in isolated worktrees, and reviews its own work.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Elanvital AI",
@@ -11,8 +11,8 @@
11
11
  },
12
12
  "type": "module",
13
13
  "bin": {
14
- "elanous": "./bin/elanous.mjs",
15
- "eln": "./bin/elanous.mjs"
14
+ "elanous": "./bin/elanous.cjs",
15
+ "eln": "./bin/elanous.cjs"
16
16
  },
17
17
  "engines": {
18
18
  "bun": ">=1.3.5"
@@ -111,7 +111,6 @@
111
111
  "remark-rehype": "^11.1.2",
112
112
  "sharp": "^0.34.5",
113
113
  "shiki": "^4.0.2",
114
- "telegram": "^2.26.22",
115
114
  "typescript": "^5.7.0",
116
115
  "unified": "^11.0.5",
117
116
  "web-push": "^3.6.7",
@@ -119,7 +118,8 @@
119
118
  "yaml": "^2.8.3"
120
119
  },
121
120
  "optionalDependencies": {
122
- "node-pty": "^1.1.0"
121
+ "node-pty": "^1.1.0",
122
+ "telegram": "^2.26.22"
123
123
  },
124
124
  "devDependencies": {
125
125
  "@types/bun": "^1.2.0",
@@ -0,0 +1,42 @@
1
+ # card-followup
2
+
3
+ 명함 사진 한 장으로 사람·회사 조사, 타겟 판정, 접근 전략, 로컬 CRM 한 줄과 이에 맞는 팔로업 초안을 만듭니다. 메일과 LinkedIn 메시지는 보내지 않습니다.
4
+
5
+ ## 실행 명령
6
+
7
+ ```bash
8
+ elanous graph run plugins/card-followup/graphs/card-followup.yaml --input '<json>' --json
9
+ ```
10
+
11
+ ## 입력
12
+
13
+ - `image` (필수): 명함 사진의 절대 경로. `png`, `jpg`, `jpeg`, `heic`만 허용합니다. `~/`는 홈 경로로 펼칩니다.
14
+ - `context` (선택): 어디서 만났고 무슨 이야기를 했는지 한두 줄.
15
+ - `sender` (선택): 보내는 사람 이름·회사. 기본값은 비움입니다.
16
+ - `language` (선택): `ko` 또는 `en`. 생략하면 명함에서 읽은 언어를 사용합니다.
17
+ - `outDir` (선택): 보고서 출력 절대 경로. 생략하면 그래프 런 디렉터리를 사용합니다.
18
+ - `offer` (선택): 우리 제품과 대상 고객을 설명하는 한두 줄. 없으면 적합도 점수는 `null`, 라벨은 `unknown`이며 접근 전략과 초안은 계속 작성합니다.
19
+ - `crm` (선택): 로컬 CRM CSV의 절대 경로. 기본값은 `<outDir>/crm.csv`입니다.
20
+
21
+ ## 산출
22
+
23
+ - `followup.md`: 맨 위에 사람·회사 분석, CRM 한 줄, 타겟 판정(점수·라벨·근거), 접근 전략(대상·문제·제안·채널·시점), 메일·LinkedIn 초안을 순서대로 보여줍니다. 그 아래에 출처와 경고가 있습니다.
24
+ - `followup.json`: `fit`, `approach`, `nextAction`, `crm` 경로 및 초안의 구조화 데이터. `sent`는 항상 `false`입니다. 타겟 판정의 `S#` 이유는 번호뿐 아니라 해당 출처의 제목·본문 조각과 주장 내용도 재검증합니다. `assumption`은 적합성 근거로 세지 않고, 실제 명함·만남 맥락·제안·조사 출처에 뒷받침되는 이유만 남깁니다. 재작성 후에도 유효한 이유가 두 개 미만이면 점수는 `null`, 라벨은 `unknown`입니다. 접근 전략은 주장별 근거와 전체 출처 원문을 대조하고, 다음 행동의 확인되지 않은 고객 약속·확정 일정은 재작성 후에도 남으면 산출을 중단합니다. 다음 행동·기한은 제안 또는 내부 목표이지 고객 확약이 아닙니다.
25
+ - `crm.csv` (또는 지정한 `crm` 경로): `name,company,title,email,interest,met_context,fit_score,fit_label,next_action,due,updated_at` 헤더의 로컬 CSV. 같은 이메일(없으면 이름+회사)은 갱신하고 다른 연락처는 추가합니다. 적합도 점수 내림차순으로 저장하며 미판정 점수는 맨 뒤입니다. 쉼표·따옴표·줄바꿈이 포함된 값은 CSV 인용 규칙으로 저장합니다. 외부 CRM과 동기화하지 않습니다.
26
+
27
+ ## 개인정보 주의
28
+
29
+ 명함 사진과 연락처는 그래프 런 상태에 남습니다. 비공개 경로만 사용하고 공유하지 마세요.
30
+
31
+ 어디로 나가나:
32
+ - **명함 이미지** → `codex`(설치된 codex CLI 의 모델)가 읽는다.
33
+ - **이름·회사·사이트** → `elanous research` 가 웹 검색어로 쓴다.
34
+ - **명함 필드·만남 맥락·offer·조사 결과** → `elanous ask` 로 설정된 LLM 에 보내 판정·전략·초안을 만든다.
35
+ - **CRM(`crm.csv`)·보고서** → 이 기계의 파일에만 쓴다. 외부 CRM 과 동기화하지 않고, 메일·LinkedIn 을 보내지 않는다.
36
+
37
+ ## 릴리스 노트
38
+
39
+ - 한 줄: feat — card-followup turns one business card into a sales plan: a local CRM row sorted by fit, a target-fit score with three sourced reasons, an approach strategy and matching drafts, all on one report
40
+ - 종류: feat
41
+ - 문서: plugins/card-followup/README.md
42
+ - 대상: next
@@ -0,0 +1,7 @@
1
+ {
2
+ "image": "/absolute/path/to/fictional-card.png",
3
+ "context": "가상 행사에서 가상 제품 데모를 짧게 이야기했습니다.",
4
+ "sender": "가상 발신자 · 가상 발신 회사",
5
+ "language": "ko",
6
+ "outDir": "/absolute/path/to/private-output"
7
+ }
@@ -0,0 +1,18 @@
1
+ graph_id: card-followup
2
+ version: 1
3
+ entry_node: read-card
4
+ terminal_nodes: [done, failed]
5
+ nodes:
6
+ - { node_id: read-card, kind: agent, recipe: 'cmd:read-card', max_visits: 1 }
7
+ - { node_id: research, kind: agent, recipe: 'cmd:research', max_visits: 1 }
8
+ - { node_id: strategy, kind: agent, recipe: 'cmd:strategy', max_visits: 1 }
9
+ - { node_id: draft, kind: agent, recipe: 'cmd:draft', max_visits: 1 }
10
+ - { node_id: report, kind: agent, recipe: 'cmd:report', max_visits: 1 }
11
+ - { node_id: done, kind: gate, max_visits: 1 }
12
+ - { node_id: failed, kind: gate, max_visits: 1 }
13
+ edges:
14
+ - { from: read-card, on: outcome, map: { ok: research, fail: failed } }
15
+ - { from: research, on: outcome, map: { ok: strategy, fail: failed } }
16
+ - { from: strategy, on: outcome, map: { ok: draft, fail: failed } }
17
+ - { from: draft, on: outcome, map: { ok: report, fail: failed } }
18
+ - { from: report, on: outcome, map: { ok: done, fail: failed } }
@@ -0,0 +1,16 @@
1
+ read-card:
2
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" read-card'
3
+ timeout_ms: 120000
4
+ research:
5
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" research'
6
+ timeout_ms: 120000
7
+ # strategy·draft make up to four LLM calls (write ⊕ verify, one retry) — 61–80 s clean, a retry doubles it (10-01 live: 120 s timeout hit).
8
+ strategy:
9
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" strategy'
10
+ timeout_ms: 300000
11
+ draft:
12
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" draft'
13
+ timeout_ms: 300000
14
+ report:
15
+ command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" report'
16
+ timeout_ms: 30000
@@ -0,0 +1,276 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, extname, isAbsolute, join, basename } from 'node:path';
4
+ import { buildStrategy, crmRow, upsertCrm } from './sales-strategy.js';
5
+
6
+ type Data = Record<string, unknown>;
7
+ const object = (v: unknown): Data => v && typeof v === 'object' && !Array.isArray(v) ? v as Data : {};
8
+ const text = (v: unknown): string => typeof v === 'string' ? v.trim() : '';
9
+ const fields = ['name', 'title', 'company', 'email', 'phone', 'url', 'linkedin', 'language'] as const;
10
+ const clean = (v: unknown) => text(v) || null;
11
+ const line = (v: unknown) => text(v).replace(/[\r\n|]/g, ' ');
12
+
13
+ // Find the first balanced object, respecting JSON string escapes and braces in quoted text.
14
+ function firstObject(raw: string): Data {
15
+ for (let start = raw.indexOf('{'); start !== -1; start = raw.indexOf('{', start + 1)) {
16
+ let depth = 0, quoted = false, escaped = false;
17
+ for (let i = start; i < raw.length; i++) {
18
+ const ch = raw[i];
19
+ if (quoted) {
20
+ if (escaped) escaped = false;
21
+ else if (ch === '\\') escaped = true;
22
+ else if (ch === '"') quoted = false;
23
+ } else if (ch === '"') quoted = true;
24
+ else if (ch === '{') depth++;
25
+ else if (ch === '}' && --depth === 0) {
26
+ try {
27
+ const parsed = JSON.parse(raw.slice(start, i + 1));
28
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Data;
29
+ } catch { /* try the next object */ }
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ throw new Error(`JSON 객체 없음: ${raw.slice(0, 200)}`);
35
+ }
36
+
37
+ async function cli(bin: string, args: string[]): Promise<string> {
38
+ const child = Bun.spawn([bin, ...args], { stdout: 'pipe', stderr: 'pipe', env: process.env });
39
+ const [stdout, stderr, status] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
40
+ if (status !== 0) throw new Error(`${args[0]} 실패: ${(stderr || stdout).slice(0, 200)}`);
41
+ return stdout;
42
+ }
43
+
44
+ function absolute(path: string, label: string): string {
45
+ const expanded = path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
46
+ if (!isAbsolute(expanded)) throw new Error(`${label}: 절대 경로 필요`);
47
+ return expanded;
48
+ }
49
+
50
+ // The page's own <title> and meta/og description — the company's words about itself. 5 s cap; null on any failure.
51
+ async function readPage(url: string): Promise<{ title: string; description: string } | null> {
52
+ try {
53
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000), redirect: 'follow', headers: { 'user-agent': 'Mozilla/5.0 card-followup' } });
54
+ if (!res.ok || !(res.headers.get('content-type') ?? '').includes('html')) return null;
55
+ const html = (await res.text()).slice(0, 200_000);
56
+ const decode = (v: string) => v.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, ' ').trim();
57
+ const meta = (key: string) => {
58
+ for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
59
+ if (new RegExp(`(?:name|property)=["']${key}["']`, 'i').test(tag)) {
60
+ const content = /content=["']([^"']*)["']/i.exec(tag)?.[1];
61
+ if (content) return decode(content);
62
+ }
63
+ }
64
+ return '';
65
+ };
66
+ const title = decode(/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1] ?? '') || meta('og:title');
67
+ const description = meta('description') || meta('og:description');
68
+ return title && description ? { title, description } : null;
69
+ } catch { return null; }
70
+ }
71
+
72
+ function sources(raw: string): { title: string; url: string; snippet: string }[] {
73
+ const data = object(JSON.parse(raw));
74
+ // research --json exposes the human-readable result in `output`.
75
+ const output = text(data.output);
76
+ return output.split('\n').flatMap(row => {
77
+ const match = row.match(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/) ?? row.match(/(https?:\/\/[^\s)]+)/);
78
+ if (!match) return [];
79
+ // A logo or thumbnail row («[![](…)](…png)») is not a source.
80
+ if (/^\s*[-*]?\s*\[?!\[/.test(row) || /\.(?:png|jpe?g|gif|svg|webp)(?:[?#]|$)/i.test(match[2] ?? match[1]!)) return [];
81
+ return [{ title: match[2] ? match[1]! : row.replace(match[0], '').trim(), url: match[2] ?? match[1]!, snippet: row.replace(match[0], '').trim() }];
82
+ });
83
+ }
84
+
85
+ const contextFile = process.env.ELANOUS_GRAPH_CONTEXT;
86
+ if (!contextFile) throw new Error('ELANOUS_GRAPH_CONTEXT 필요');
87
+ const context = object(JSON.parse(readFileSync(contextFile, 'utf8')));
88
+ const input = object(context.input);
89
+ const outputs = object(context.outputs);
90
+ const step = process.argv[2];
91
+ const elanous = process.env.CARD_FOLLOWUP_ELANOUS_BIN || 'elanous';
92
+ const codex = process.env.CARD_FOLLOWUP_CODEX_BIN || 'codex';
93
+ const result = (data: Data) => console.log(JSON.stringify(data.outcome ? data : { outcome: 'ok', ...data }));
94
+
95
+ try {
96
+ if (step === 'read-card') {
97
+ const image = absolute(text(input.image), 'image');
98
+ if (!/\.(png|jpe?g|heic)$/i.test(extname(image))) throw new Error('image: png/jpg/heic 필요');
99
+ if (!existsSync(image)) throw new Error('image: 파일 없음');
100
+ // The prompt goes before `-i`: `--image` takes several files, so a prompt after it is read as another image
101
+ // and codex answers «No prompt provided» (10-01 live run). The graph may run outside a git checkout.
102
+ const raw = await cli(codex, ['exec', '--skip-git-repo-check', '명함 사진에서 읽을 수 있는 값만 JSON 객체로 출력하라: name, title, company, email, phone, url, linkedin, language(ko 또는 en). 빈 칸은 null. 추측 금지.', '-i', image]);
103
+ const card = firstObject(raw);
104
+ const normalized = Object.fromEntries(fields.map(field => [field, clean(card[field])]));
105
+ result({ card: normalized, image });
106
+ } else if (step === 'research') {
107
+ const read = object(outputs['read-card']);
108
+ const card = object(read.card ?? read);
109
+ const company = text(card.company), name = text(card.name);
110
+ if (!company && !name) throw new Error('조사할 이름·회사 없음');
111
+ // The card's own site is the strongest query: «Elanvital AI» alone ranks vital.ai and elanvital.co above elanvital.ai
112
+ // (10-01 dry run: 19 hits, none from the company), while «elanvital.ai» returns the site itself.
113
+ const siteQuery = text(card.url).replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0]!.toLowerCase();
114
+ const queries = [...new Set([...(siteQuery.includes('.') ? [siteQuery] : []), company || name, [name, company].filter(Boolean).join(' ')])];
115
+ const hits = (await Promise.all(queries.map(query => cli(elanous, ['research', '--json', '--limit', '5', query])))).flatMap(sources);
116
+ const unique = [...new Map(hits.map(hit => [hit.url, hit])).values()];
117
+ // Only the exact result text may be reported as a company fact, never a model-invented summary.
118
+ // Put results that name the card's company (or its site) first; if none does, say nothing as a summary —
119
+ // the first search hit was another company's page on the 10-01 live run («Vital AI» for Elanvital AI).
120
+ const site = text(card.url).replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0]!.toLowerCase();
121
+ const companyKey = company.toLowerCase().replace(/[^a-z0-9가-힣]/g, '');
122
+ const squash = (value: string) => value.toLowerCase().replace(/[^a-z0-9가-힣]/g, '');
123
+ const nameKey = squash(name);
124
+ const hostOf = (url: string) => { try { return new URL(url).hostname.toLowerCase().replace(/^www\./, ''); } catch { return ''; } };
125
+ // The site counts by host («vercel.com», «docs.vercel.com»), never by substring — «someone.vercel.app» is a stranger's page.
126
+ // The company counts by title only, and a title carrying the contact's name counts only on the company's own site:
127
+ // a search cannot tell this Alex Rivera from another.
128
+ const matches = (hit: { title: string; url: string }) => {
129
+ const host = hostOf(hit.url);
130
+ const onSite = !!site && (host === site || host.endsWith(`.${site}`));
131
+ if (onSite) return true;
132
+ // A bare-link row has the link as its «title» — that is a host, not a name.
133
+ const title = /^https?:\/\//i.test(hit.title.trim()) ? '' : squash(hit.title);
134
+ if (nameKey.length >= 3 && title.includes(nameKey)) return false;
135
+ return companyKey.length >= 3 && title.includes(companyKey);
136
+ };
137
+ // Only hits that name the card's company or site are kept: a name search also returns other people with the
138
+ // same name (10-01 live run: strangers' LinkedIn, Instagram and IMDb pages listed as this contact's sources).
139
+ // A site hit often comes back as a bare link with no title or snippet («https://elanvital.ai» · «-»): nothing a
140
+ // strategy can cite, and the verifier rejects claims resting on it (10-01 dry run: strategy failed twice on «who»).
141
+ // Read the page's own <title> and description once instead, and keep one row per host.
142
+ const thin = (hit: { title: string }) => /^https?:\/\//i.test(hit.title.trim()) || hostOf(`https://${hit.title.trim()}`) === hit.title.trim().toLowerCase();
143
+ const seenHost = new Set<string>();
144
+ const relevant: { title: string; url: string; snippet: string }[] = [];
145
+ for (const hit of unique.filter(matches)) {
146
+ const host = hostOf(hit.url);
147
+ if (!thin(hit)) { relevant.push(hit); continue; }
148
+ if (seenHost.has(host)) continue;
149
+ seenHost.add(host);
150
+ const page = await readPage(hit.url);
151
+ if (page) relevant.push({ title: page.title, url: hit.url, snippet: page.description });
152
+ }
153
+ result({ summary: relevant.length ? relevant[0]!.title : null, news: relevant.slice(1, 3).map(hit => hit.title), sources: relevant, dropped: unique.length - relevant.length });
154
+ } else if (step === 'strategy') {
155
+ const read = object(outputs['read-card']);
156
+ const card = object(read.card ?? read);
157
+ const research = object(outputs.research);
158
+ const ask = async (payload: Data): Promise<Data> => {
159
+ const response = JSON.parse(await cli(elanous, ['ask', '--json', JSON.stringify(payload)])) as Data;
160
+ return firstObject(typeof response.reply === 'string' ? response.reply : JSON.stringify(response));
161
+ };
162
+ const strategy = await buildStrategy({ card, research, context: text(input.context), offer: text(input.offer), ask });
163
+ const fallback = dirname(contextFile).endsWith('.json.contexts') ? dirname(contextFile).slice(0, -'.json.contexts'.length) : dirname(contextFile);
164
+ const outDir = input.outDir === undefined ? fallback : absolute(text(input.outDir), 'outDir');
165
+ const crm = input.crm === undefined ? join(outDir, 'crm.csv') : absolute(text(input.crm), 'crm');
166
+ const row = crmRow(card, strategy.fit, strategy.approach, strategy.nextAction, text(input.context), new Date());
167
+ upsertCrm(crm, row);
168
+ result({ ...strategy, crm, crmRow: row });
169
+ } else if (step === 'draft') {
170
+ const read = object(outputs['read-card']);
171
+ const card = object(read.card ?? read);
172
+ const research = object(outputs.research);
173
+ const approach = object(object(outputs.strategy).approach);
174
+ const evidence = (Array.isArray(research.sources) ? research.sources.map(object) : [])
175
+ .filter(source => /^https?:\/\//.test(text(source.url)));
176
+ // A card reader may answer «English»/«Korean»/«한국어» — fold them to the two codes the draft step writes in.
177
+ const languageCode = (value: string) => ({ english: 'en', en: 'en', korean: 'ko', ko: 'ko', '한국어': 'ko', '영어': 'en' } as Record<string, string>)[value.toLowerCase()] ?? value;
178
+ const language = languageCode(text(input.language) || text(card.language) || 'ko');
179
+ if (!['ko', 'en'].includes(language)) throw new Error('language: ko 또는 en 필요');
180
+ const contextLine = text(input.context);
181
+ const numbered = evidence.map((source, index) => ({ id: `S${index + 1}`, title: text(source.title), snippet: text(source.snippet) }));
182
+ const marker = /\s*\[S(\d+)\]/g;
183
+ const strip = (value: string) => value.replace(marker, '').trim();
184
+ const sentencesOf = (value: string) => value.split(/(?<=[.!?。!?])\s+|\n+/).map(s => s.trim()).filter(Boolean);
185
+ const ask = async (payload: Data): Promise<Data> => {
186
+ const response = JSON.parse(await cli(elanous, ['ask', '--json', JSON.stringify(payload)])) as Data;
187
+ return firstObject(typeof response.reply === 'string' ? response.reply : JSON.stringify(response));
188
+ };
189
+ // A Korean meeting note on an English card used to be pasted verbatim into the English mail
190
+ // («Hi Joosung, 마케터의 밤에서 인사.» · 10-01 dry run). Across languages the note is translated, not copied.
191
+ const crossLanguage = language === 'en' && /[가-힣]/.test(contextLine);
192
+ const contextRule = crossLanguage
193
+ ? 'context 는 한국어 메모다 — 원문을 그대로 넣지 말고 영어로 옮겨 body 에서 한 번 언급하라. subject·body·linkedin·question 에 한글을 쓰지 말 것. '
194
+ : 'context가 있으면 body에 그 문구를 그대로 한 번 포함. ';
195
+ const instruction = 'JSON 객체만 출력: subject, body (120~200자 · [S#] 표시는 글자 수에서 뺀다), linkedin (300자 이하), question. ' + contextRule
196
+ + '명함 필드(이름·직함·회사)와 context 는 출처 없이 써도 된다. 그 밖의 회사·사람·시장 사실은 evidence 에 있는 것만 쓰고, 그 문장 끝에 근거 번호를 [S1] 처럼 붙인다. '
197
+ + 'approach.problem·proposal·channel 에 맞춰 메일과 LinkedIn 초안을 작성하되, 전략 문구도 회사·사람·시장 사실이면 위의 문장별 근거 규칙을 반드시 따를 것. '
198
+ + 'evidence 에 없는 사실(매출·순위·규모·수상·최근 소식 등)은 쓰지 말 것. email이 없으면 메일 주소를 만들지 말 것. 어떤 메시지도 보내지 말 것.';
199
+ let feedback = '';
200
+ let accepted: { warnings: string[]; draft: Data; facts: { text: string; url: string }[] } | undefined;
201
+ let lastProblem = '';
202
+ for (let attempt = 0; attempt < 3 && !accepted; attempt++) {
203
+ const draft = await ask({ task: 'draft', instruction: instruction + feedback, card, context: contextLine, sender: text(input.sender), language, evidence: numbered, approach: { problem: text(approach.problem), proposal: text(approach.proposal), channel: text(approach.channel) } });
204
+ const parts = { subject: text(draft.subject), body: text(draft.body), linkedin: text(draft.linkedin), question: text(draft.question) };
205
+ if (!parts.subject || !parts.body || !parts.linkedin || !parts.question) { lastProblem = '초안 필수 필드 없음'; feedback = ' 이전 응답에 필드가 빠졌다.'; continue; }
206
+ const body = strip(parts.body), linkedin = strip(parts.linkedin);
207
+ const shape: string[] = [];
208
+ if (body.length < 120 || body.length > 200) shape.push(`메일 본문 120~200자 범위 밖(${body.length}자)`);
209
+ // LinkedIn 한도는 한 번 다시 부르고, 그래도 넘으면 막지 않고 보고서에 경고로 남긴다(보내기 전 사람이 고친다).
210
+ const linkedinOver = linkedin.length > 300;
211
+ if (linkedinOver && attempt === 0) shape.push(`LinkedIn 300자 초과(${linkedin.length}자)`);
212
+ if (contextLine && !crossLanguage && !body.includes(contextLine)) shape.push('메일 본문에 context 없음');
213
+ if (crossLanguage && /[가-힣]/.test([parts.subject, body, linkedin, parts.question].join(' '))) shape.push('영어 초안에 한글이 섞였다(context 는 영어로 옮길 것)');
214
+ const sentences = Object.values(parts).flatMap(sentencesOf).map(sentence => ({
215
+ raw: sentence, text: strip(sentence), cites: [...sentence.matchAll(marker)].map(match => Number(match[1])),
216
+ }));
217
+ const badCite = sentences.find(sentence => sentence.cites.some(n => n < 1 || n > evidence.length));
218
+ if (badCite) shape.push(`없는 출처 번호: ${badCite.raw}`);
219
+ if (shape.length) { lastProblem = shape.join(' · '); feedback = ` 이전 초안 문제: ${lastProblem}. 모두 고쳐 다시 작성하라.`; continue; }
220
+ // Every sentence is judged against the card, the context and only the evidence it cites — a company name alone is identity, not a claim.
221
+ const verdict = await ask({
222
+ task: 'verify',
223
+ instruction: '각 문장에 대해, 받는 사람·그 회사·시장에 관한 사실 주장이 명함 필드, context, 또는 그 문장이 인용한 evidence 로 뒷받침되는지 판정하라. 인사·호칭·명함에 있는 이름/회사/직함 언급·제안·질문·보내는 사람의 의도는 사실 주장이 아니다. JSON 객체만 출력: {"unsupported":[뒷받침 안 되는 문장 번호]}',
224
+ card, context: contextLine,
225
+ sentences: sentences.map((sentence, i) => ({ i, text: sentence.text, evidence: sentence.cites.map(n => numbered[n - 1]) })),
226
+ });
227
+ if (!Array.isArray(verdict.unsupported)) throw new Error('사실 검증 응답을 읽지 못했다');
228
+ const unsupported = verdict.unsupported.map(Number).filter(i => Number.isInteger(i) && sentences[i]).map(i => sentences[i]!.text);
229
+ if (unsupported.length) { lastProblem = `출처 없는 회사 사실: ${unsupported.join(' / ')}`; feedback = ` 이전 초안에서 근거 없는 문장: ${unsupported.join(' / ')}. 그 문장을 빼거나 evidence 로 뒷받침되는 문장으로 바꿔라.`; continue; }
230
+ const facts = sentences.flatMap(sentence => sentence.cites.map(n => ({ text: sentence.text, url: text(evidence[n - 1]!.url) })));
231
+ accepted = { warnings: linkedinOver ? ['LinkedIn 문구 300자 초과 — 전송 전 수정 필요'] : [], draft: { subject: strip(parts.subject), body, linkedin, question: strip(parts.question) }, facts: [...new Map(facts.map(f => [`${f.text}\n${f.url}`, f])).values()] };
232
+ }
233
+ if (!accepted) throw new Error(lastProblem || '초안 생성 실패');
234
+ result({ ...accepted.draft, companyFacts: accepted.facts, destination: card.email ? ['email', 'LinkedIn'] : ['LinkedIn'], warnings: accepted.warnings, language });
235
+ } else if (step === 'report') {
236
+ const read = object(outputs['read-card']);
237
+ const card = object(read.card ?? read);
238
+ const research = object(outputs.research), draft = object(outputs.draft), strategy = object(outputs.strategy);
239
+ const fit = object(strategy.fit), approach = object(strategy.approach), nextAction = object(strategy.nextAction);
240
+ if (!text(draft.subject) || !text(draft.linkedin) || !text(draft.question)) throw new Error('초안 없음');
241
+ const fallback = dirname(contextFile).endsWith('.json.contexts') ? dirname(contextFile).slice(0, -'.json.contexts'.length) : dirname(contextFile);
242
+ const outDir = input.outDir === undefined ? fallback : absolute(text(input.outDir), 'outDir');
243
+ const hits = Array.isArray(research.sources) ? research.sources.map(object) : [];
244
+ const warnings = Array.isArray(draft.warnings) ? draft.warnings.map(text).filter(Boolean) : [];
245
+ const crm = text(strategy.crm);
246
+ const row = object(strategy.crmRow);
247
+ if (!crm || !Array.isArray(fit.reasons) || !text(approach.who) || !text(approach.problem)
248
+ || !text(approach.proposal) || !text(approach.channel) || !text(approach.timing)
249
+ || !text(nextAction.what) || !text(nextAction.due) || !('fit_label' in row)) throw new Error('전략 없음');
250
+ const md = ['# 명함 영업 계획', '', '## ① 사람·회사 분석',
251
+ `${line(card.name) || '이름 없음'} · ${line(card.title) || '직함 없음'} · ${line(card.company) || '회사 없음'} · ${line(card.email) || '이메일 없음'}`,
252
+ `조사 요약: ${hits.length ? line(research.summary) : '조사 결과 없음'}`, '',
253
+ '## ② CRM 한 줄', `파일: ${basename(crm)}`, Object.entries(row).map(([key, value]) => `${key}: ${line(value)}`).join(' · '), '',
254
+ '## ③ 타겟 판정', `점수: ${fit.score === null ? 'null' : String(fit.score)} · 라벨: ${line(fit.label)}`,
255
+ ...(fit.reasons as unknown[]).map(v => `- ${line(object(v).text)} (${line(object(v).basis)})`), '',
256
+ '## ④ 접근 전략', `- 누구에게: ${line(approach.who)}`, `- 어떤 문제: ${line(approach.problem)}`,
257
+ `- 어떤 제안: ${line(approach.proposal)}`, `- 어느 채널: ${line(approach.channel)}`, `- 언제: ${line(approach.timing)}`,
258
+ `- 다음 행동: ${line(nextAction.what)} · 기한: ${line(nextAction.due)}`, '',
259
+ '## ⑤ 메일·LinkedIn 초안', '### 팔로업 메일', `제목: ${line(draft.subject)}`, '', text(draft.body), '',
260
+ '### LinkedIn 초대 문구', text(draft.linkedin), '', '### 대화 이어 갈 질문', text(draft.question), '',
261
+ '## 명함 전체 필드', '| 필드 | 값 |', '| --- | --- |', ...fields.map(field => `| ${field} | ${line(card[field]) || 'null'} |`), '',
262
+ '## 추가 조사 결과', ...((Array.isArray(research.news) ? research.news : []).map(v => `- ${line(v)}`)), '',
263
+ '## 출처', ...(hits.length ? hits.map(hit => `- ${line(hit.title)} — ${text(hit.url)}`) : ['- 조사 결과 없음']), '',
264
+ '## 보낼 곳', ...((Array.isArray(draft.destination) ? draft.destination : []).map(v => `- ${line(v)}`)), '',
265
+ '## 초안의 사실과 출처', ...((Array.isArray(draft.companyFacts) && draft.companyFacts.length ? draft.companyFacts : ['출처를 인용한 문장 없음']).map(v => typeof v === 'string' ? `- ${v}` : `- ${line(object(v).text)} — ${text(object(v).url)}`)),
266
+ ...(warnings.length ? ['## 경고', ...warnings.map(v => `- ${v}`)] : []), '', '※ 초안만 생성했습니다. 전송은 사람이 결정합니다.', ''].join('\n');
267
+ mkdirSync(outDir, { recursive: true, mode: 0o700 });
268
+ const report = { card, research, fit, approach, nextAction, crm, draft, warnings, sent: false };
269
+ const markdown = join(outDir, 'followup.md'), json = join(outDir, 'followup.json');
270
+ writeFileSync(markdown, md, { mode: 0o600 });
271
+ writeFileSync(json, JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
272
+ result({ markdown, json });
273
+ } else throw new Error(`unknown step: ${step}`);
274
+ } catch (error) {
275
+ result({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) });
276
+ }