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.
- package/.env.example +10 -7
- package/README.md +4 -0
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-55d50e9b0006a91d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +10 -0
- package/graphs/release/release-loop.yaml +10 -3
- package/package.json +5 -5
- package/plugins/card-followup/README.md +13 -4
- package/plugins/card-followup/graphs/card-followup.yaml +3 -1
- package/plugins/card-followup/graphs/recipes.yaml +5 -1
- package/plugins/card-followup/graphs/run-step.ts +122 -18
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +1 -1
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +3 -1
- package/plugins/geo-check/graphs/run-step.ts +24 -3
- package/plugins/geo-check/plugin.json +1 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +1 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/field-feed/deliver-node.ts +52 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +162 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +92 -13
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +81 -19
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +118 -0
- package/scripts/release-loop/ops-upgrade-node.ts +87 -0
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +12 -9
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +138 -31
- package/skills/explainer-video/engine/reel.sh +2 -1
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +91 -15
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +23 -3
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-cli.ts +5 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/plugin-cli.ts +25 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +42 -1
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +27 -8
- package/src/dashboard/index.ts +1 -1
- package/src/discord.ts +56 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +50 -11
- package/src/exec-requests/planner.ts +140 -0
- package/src/exec-requests/runner.ts +338 -0
- package/src/exec-requests/store.ts +76 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +196 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/index.ts +86 -14
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-doc-route.ts +207 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +117 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +40 -3
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +71 -4
- package/src/nexus/api/http-server.ts +61 -7
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +3 -0
- package/src/nexus/setup-status.ts +4 -5
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +29 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +83 -0
- package/src/onboarding.ts +230 -23
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +19 -7
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/release-loop/checklist.ts +9 -8
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/session/chat.ts +17 -5
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram.ts +38 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +80 -4
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elanous",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
15
|
-
"eln": "./bin/elanous.
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# card-followup
|
|
2
2
|
|
|
3
|
-
명함 사진 한 장으로 사람·회사
|
|
3
|
+
명함 사진 한 장으로 사람·회사 조사, 타겟 판정, 접근 전략, 로컬 CRM 한 줄과 이에 맞는 팔로업 초안을 만듭니다. 메일과 LinkedIn 메시지는 보내지 않습니다.
|
|
4
4
|
|
|
5
5
|
## 실행 명령
|
|
6
6
|
|
|
@@ -15,19 +15,28 @@ elanous graph run plugins/card-followup/graphs/card-followup.yaml --input '<json
|
|
|
15
15
|
- `sender` (선택): 보내는 사람 이름·회사. 기본값은 비움입니다.
|
|
16
16
|
- `language` (선택): `ko` 또는 `en`. 생략하면 명함에서 읽은 언어를 사용합니다.
|
|
17
17
|
- `outDir` (선택): 보고서 출력 절대 경로. 생략하면 그래프 런 디렉터리를 사용합니다.
|
|
18
|
+
- `offer` (선택): 우리 제품과 대상 고객을 설명하는 한두 줄. 없으면 적합도 점수는 `null`, 라벨은 `unknown`이며 접근 전략과 초안은 계속 작성합니다.
|
|
19
|
+
- `crm` (선택): 로컬 CRM CSV의 절대 경로. 기본값은 `<outDir>/crm.csv`입니다.
|
|
18
20
|
|
|
19
21
|
## 산출
|
|
20
22
|
|
|
21
|
-
- `followup.md`:
|
|
22
|
-
- `followup.json`:
|
|
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과 동기화하지 않습니다.
|
|
23
26
|
|
|
24
27
|
## 개인정보 주의
|
|
25
28
|
|
|
26
29
|
명함 사진과 연락처는 그래프 런 상태에 남습니다. 비공개 경로만 사용하고 공유하지 마세요.
|
|
27
30
|
|
|
31
|
+
어디로 나가나:
|
|
32
|
+
- **명함 이미지** → `codex`(설치된 codex CLI 의 모델)가 읽는다.
|
|
33
|
+
- **이름·회사·사이트** → `elanous research` 가 웹 검색어로 쓴다.
|
|
34
|
+
- **명함 필드·만남 맥락·offer·조사 결과** → `elanous ask` 로 설정된 LLM 에 보내 판정·전략·초안을 만든다.
|
|
35
|
+
- **CRM(`crm.csv`)·보고서** → 이 기계의 파일에만 쓴다. 외부 CRM 과 동기화하지 않고, 메일·LinkedIn 을 보내지 않는다.
|
|
36
|
+
|
|
28
37
|
## 릴리스 노트
|
|
29
38
|
|
|
30
|
-
- 한 줄: feat —
|
|
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
|
|
31
40
|
- 종류: feat
|
|
32
41
|
- 문서: plugins/card-followup/README.md
|
|
33
42
|
- 대상: next
|
|
@@ -5,12 +5,14 @@ terminal_nodes: [done, failed]
|
|
|
5
5
|
nodes:
|
|
6
6
|
- { node_id: read-card, kind: agent, recipe: 'cmd:read-card', max_visits: 1 }
|
|
7
7
|
- { node_id: research, kind: agent, recipe: 'cmd:research', max_visits: 1 }
|
|
8
|
+
- { node_id: strategy, kind: agent, recipe: 'cmd:strategy', max_visits: 1 }
|
|
8
9
|
- { node_id: draft, kind: agent, recipe: 'cmd:draft', max_visits: 1 }
|
|
9
10
|
- { node_id: report, kind: agent, recipe: 'cmd:report', max_visits: 1 }
|
|
10
11
|
- { node_id: done, kind: gate, max_visits: 1 }
|
|
11
12
|
- { node_id: failed, kind: gate, max_visits: 1 }
|
|
12
13
|
edges:
|
|
13
14
|
- { from: read-card, on: outcome, map: { ok: research, fail: failed } }
|
|
14
|
-
- { from: research, on: outcome, map: { ok:
|
|
15
|
+
- { from: research, on: outcome, map: { ok: strategy, fail: failed } }
|
|
16
|
+
- { from: strategy, on: outcome, map: { ok: draft, fail: failed } }
|
|
15
17
|
- { from: draft, on: outcome, map: { ok: report, fail: failed } }
|
|
16
18
|
- { from: report, on: outcome, map: { ok: done, fail: failed } }
|
|
@@ -4,9 +4,13 @@ read-card:
|
|
|
4
4
|
research:
|
|
5
5
|
command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" research'
|
|
6
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
|
|
7
11
|
draft:
|
|
8
12
|
command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" draft'
|
|
9
|
-
timeout_ms:
|
|
13
|
+
timeout_ms: 300000
|
|
10
14
|
report:
|
|
11
15
|
command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" report'
|
|
12
16
|
timeout_ms: 30000
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { dirname, extname, isAbsolute, join, basename } from 'node:path';
|
|
4
|
+
import { buildStrategy, crmRow, upsertCrm } from './sales-strategy.js';
|
|
4
5
|
|
|
5
6
|
type Data = Record<string, unknown>;
|
|
6
7
|
const object = (v: unknown): Data => v && typeof v === 'object' && !Array.isArray(v) ? v as Data : {};
|
|
@@ -46,6 +47,28 @@ function absolute(path: string, label: string): string {
|
|
|
46
47
|
return expanded;
|
|
47
48
|
}
|
|
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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/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
|
+
|
|
49
72
|
function sources(raw: string): { title: string; url: string; snippet: string }[] {
|
|
50
73
|
const data = object(JSON.parse(raw));
|
|
51
74
|
// research --json exposes the human-readable result in `output`.
|
|
@@ -53,6 +76,8 @@ function sources(raw: string): { title: string; url: string; snippet: string }[]
|
|
|
53
76
|
return output.split('\n').flatMap(row => {
|
|
54
77
|
const match = row.match(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/) ?? row.match(/(https?:\/\/[^\s)]+)/);
|
|
55
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 [];
|
|
56
81
|
return [{ title: match[2] ? match[1]! : row.replace(match[0], '').trim(), url: match[2] ?? match[1]!, snippet: row.replace(match[0], '').trim() }];
|
|
57
82
|
});
|
|
58
83
|
}
|
|
@@ -72,7 +97,9 @@ try {
|
|
|
72
97
|
const image = absolute(text(input.image), 'image');
|
|
73
98
|
if (!/\.(png|jpe?g|heic)$/i.test(extname(image))) throw new Error('image: png/jpg/heic 필요');
|
|
74
99
|
if (!existsSync(image)) throw new Error('image: 파일 없음');
|
|
75
|
-
|
|
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]);
|
|
76
103
|
const card = firstObject(raw);
|
|
77
104
|
const normalized = Object.fromEntries(fields.map(field => [field, clean(card[field])]));
|
|
78
105
|
result({ card: normalized, image });
|
|
@@ -81,18 +108,74 @@ try {
|
|
|
81
108
|
const card = object(read.card ?? read);
|
|
82
109
|
const company = text(card.company), name = text(card.name);
|
|
83
110
|
if (!company && !name) throw new Error('조사할 이름·회사 없음');
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
116
|
const unique = [...new Map(hits.map(hit => [hit.url, hit])).values()];
|
|
87
117
|
// Only the exact result text may be reported as a company fact, never a model-invented summary.
|
|
88
|
-
|
|
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 });
|
|
89
169
|
} else if (step === 'draft') {
|
|
90
170
|
const read = object(outputs['read-card']);
|
|
91
171
|
const card = object(read.card ?? read);
|
|
92
172
|
const research = object(outputs.research);
|
|
173
|
+
const approach = object(object(outputs.strategy).approach);
|
|
93
174
|
const evidence = (Array.isArray(research.sources) ? research.sources.map(object) : [])
|
|
94
175
|
.filter(source => /^https?:\/\//.test(text(source.url)));
|
|
95
|
-
|
|
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');
|
|
96
179
|
if (!['ko', 'en'].includes(language)) throw new Error('language: ko 또는 en 필요');
|
|
97
180
|
const contextLine = text(input.context);
|
|
98
181
|
const numbered = evidence.map((source, index) => ({ id: `S${index + 1}`, title: text(source.title), snippet: text(source.snippet) }));
|
|
@@ -100,17 +183,24 @@ try {
|
|
|
100
183
|
const strip = (value: string) => value.replace(marker, '').trim();
|
|
101
184
|
const sentencesOf = (value: string) => value.split(/(?<=[.!?。!?])\s+|\n+/).map(s => s.trim()).filter(Boolean);
|
|
102
185
|
const ask = async (payload: Data): Promise<Data> => {
|
|
103
|
-
const response = JSON.parse(await cli(elanous, ['
|
|
186
|
+
const response = JSON.parse(await cli(elanous, ['ask', '--json', JSON.stringify(payload)])) as Data;
|
|
104
187
|
return firstObject(typeof response.reply === 'string' ? response.reply : JSON.stringify(response));
|
|
105
188
|
};
|
|
106
|
-
|
|
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
|
|
107
196
|
+ '명함 필드(이름·직함·회사)와 context 는 출처 없이 써도 된다. 그 밖의 회사·사람·시장 사실은 evidence 에 있는 것만 쓰고, 그 문장 끝에 근거 번호를 [S1] 처럼 붙인다. '
|
|
197
|
+
+ 'approach.problem·proposal·channel 에 맞춰 메일과 LinkedIn 초안을 작성하되, 전략 문구도 회사·사람·시장 사실이면 위의 문장별 근거 규칙을 반드시 따를 것. '
|
|
108
198
|
+ 'evidence 에 없는 사실(매출·순위·규모·수상·최근 소식 등)은 쓰지 말 것. email이 없으면 메일 주소를 만들지 말 것. 어떤 메시지도 보내지 말 것.';
|
|
109
199
|
let feedback = '';
|
|
110
200
|
let accepted: { warnings: string[]; draft: Data; facts: { text: string; url: string }[] } | undefined;
|
|
111
201
|
let lastProblem = '';
|
|
112
202
|
for (let attempt = 0; attempt < 3 && !accepted; attempt++) {
|
|
113
|
-
const draft = await ask({ task: 'draft', instruction: instruction + feedback, card, context: contextLine, sender: text(input.sender), language, evidence: numbered });
|
|
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) } });
|
|
114
204
|
const parts = { subject: text(draft.subject), body: text(draft.body), linkedin: text(draft.linkedin), question: text(draft.question) };
|
|
115
205
|
if (!parts.subject || !parts.body || !parts.linkedin || !parts.question) { lastProblem = '초안 필수 필드 없음'; feedback = ' 이전 응답에 필드가 빠졌다.'; continue; }
|
|
116
206
|
const body = strip(parts.body), linkedin = strip(parts.linkedin);
|
|
@@ -119,7 +209,8 @@ try {
|
|
|
119
209
|
// LinkedIn 한도는 한 번 다시 부르고, 그래도 넘으면 막지 않고 보고서에 경고로 남긴다(보내기 전 사람이 고친다).
|
|
120
210
|
const linkedinOver = linkedin.length > 300;
|
|
121
211
|
if (linkedinOver && attempt === 0) shape.push(`LinkedIn 300자 초과(${linkedin.length}자)`);
|
|
122
|
-
if (contextLine && !body.includes(contextLine)) shape.push('메일 본문에 context 없음');
|
|
212
|
+
if (contextLine && !crossLanguage && !body.includes(contextLine)) shape.push('메일 본문에 context 없음');
|
|
213
|
+
if (crossLanguage && /[가-힣]/.test([parts.subject, body, linkedin, parts.question].join(' '))) shape.push('영어 초안에 한글이 섞였다(context 는 영어로 옮길 것)');
|
|
123
214
|
const sentences = Object.values(parts).flatMap(sentencesOf).map(sentence => ({
|
|
124
215
|
raw: sentence, text: strip(sentence), cites: [...sentence.matchAll(marker)].map(match => Number(match[1])),
|
|
125
216
|
}));
|
|
@@ -144,24 +235,37 @@ try {
|
|
|
144
235
|
} else if (step === 'report') {
|
|
145
236
|
const read = object(outputs['read-card']);
|
|
146
237
|
const card = object(read.card ?? read);
|
|
147
|
-
const research = object(outputs.research), draft = object(outputs.draft);
|
|
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);
|
|
148
240
|
if (!text(draft.subject) || !text(draft.linkedin) || !text(draft.question)) throw new Error('초안 없음');
|
|
149
241
|
const fallback = dirname(contextFile).endsWith('.json.contexts') ? dirname(contextFile).slice(0, -'.json.contexts'.length) : dirname(contextFile);
|
|
150
242
|
const outDir = input.outDir === undefined ? fallback : absolute(text(input.outDir), 'outDir');
|
|
151
243
|
const hits = Array.isArray(research.sources) ? research.sources.map(object) : [];
|
|
152
244
|
const warnings = Array.isArray(draft.warnings) ? draft.warnings.map(text).filter(Boolean) : [];
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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)}`)), '',
|
|
157
263
|
'## 출처', ...(hits.length ? hits.map(hit => `- ${line(hit.title)} — ${text(hit.url)}`) : ['- 조사 결과 없음']), '',
|
|
158
264
|
'## 보낼 곳', ...((Array.isArray(draft.destination) ? draft.destination : []).map(v => `- ${line(v)}`)), '',
|
|
159
|
-
'## 팔로업 메일', `제목: ${line(draft.subject)}`, '', text(draft.body), '',
|
|
160
|
-
'## LinkedIn 초대 문구', text(draft.linkedin), '', '## 대화 이어 갈 질문', text(draft.question), '',
|
|
161
265
|
'## 초안의 사실과 출처', ...((Array.isArray(draft.companyFacts) && draft.companyFacts.length ? draft.companyFacts : ['출처를 인용한 문장 없음']).map(v => typeof v === 'string' ? `- ${v}` : `- ${line(object(v).text)} — ${text(object(v).url)}`)),
|
|
162
266
|
...(warnings.length ? ['## 경고', ...warnings.map(v => `- ${v}`)] : []), '', '※ 초안만 생성했습니다. 전송은 사람이 결정합니다.', ''].join('\n');
|
|
163
267
|
mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
164
|
-
const report = { card, research, draft, warnings, sent: false };
|
|
268
|
+
const report = { card, research, fit, approach, nextAction, crm, draft, warnings, sent: false };
|
|
165
269
|
const markdown = join(outDir, 'followup.md'), json = join(outDir, 'followup.json');
|
|
166
270
|
writeFileSync(markdown, md, { mode: 0o600 });
|
|
167
271
|
writeFileSync(json, JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
type Data = Record<string, unknown>;
|
|
5
|
+
const object = (value: unknown): Data => value && typeof value === 'object' && !Array.isArray(value) ? value as Data : {};
|
|
6
|
+
const text = (value: unknown): string => typeof value === 'string' ? value.trim() : '';
|
|
7
|
+
|
|
8
|
+
type Fit = {
|
|
9
|
+
score: number | null;
|
|
10
|
+
label: 'high' | 'medium' | 'low' | 'unknown';
|
|
11
|
+
reasons: { text: string; basis: string }[];
|
|
12
|
+
};
|
|
13
|
+
type Approach = { who: string; problem: string; proposal: string; channel: string; timing: string };
|
|
14
|
+
type NextAction = { what: string; due: string };
|
|
15
|
+
type Strategy = { fit: Fit; approach: Approach; nextAction: NextAction };
|
|
16
|
+
type CrmRow = Record<(typeof columns)[number], string>;
|
|
17
|
+
const columns = ['name', 'company', 'title', 'email', 'interest', 'met_context', 'fit_score', 'fit_label', 'next_action', 'due', 'updated_at'] as const;
|
|
18
|
+
|
|
19
|
+
function parseAnswer(raw: unknown): Data {
|
|
20
|
+
const response = object(raw);
|
|
21
|
+
if (typeof response.reply === 'string') {
|
|
22
|
+
const reply = response.reply;
|
|
23
|
+
const start = reply.indexOf('{'), end = reply.lastIndexOf('}');
|
|
24
|
+
if (start < 0 || end < start) throw new Error('strategy 응답 JSON 객체 없음');
|
|
25
|
+
return object(JSON.parse(reply.slice(start, end + 1)));
|
|
26
|
+
}
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function defaultAsk(payload: Data): Promise<unknown> {
|
|
31
|
+
const child = Bun.spawn(['elanous', 'ask', '--json', JSON.stringify(payload)], { stdout: 'pipe', stderr: 'pipe', env: process.env });
|
|
32
|
+
const [stdout, stderr, status] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
33
|
+
if (status !== 0) throw new Error(`ask 실패: ${(stderr || stdout).slice(0, 200)}`);
|
|
34
|
+
return JSON.parse(stdout);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function buildStrategy({ card, research, context = '', offer = '', ask = defaultAsk }: {
|
|
38
|
+
card: Data; research: Data; context?: string; offer?: string; ask?: (payload: Data) => Promise<unknown>;
|
|
39
|
+
}): Promise<Strategy> {
|
|
40
|
+
const sources = Array.isArray(research.sources) ? research.sources.map(object).filter(source => /^https?:\/\//.test(text(source.url))) : [];
|
|
41
|
+
const evidence = sources.map((source, i) => ({ id: `S${i + 1}`, title: text(source.title), snippet: text(source.snippet), url: text(source.url) }));
|
|
42
|
+
const hasOffer = !!text(offer);
|
|
43
|
+
const sourceBasis = (basis: string) => /^S[1-9]\d*$/.test(basis) && Number(basis.slice(1)) <= evidence.length
|
|
44
|
+
&& !!(evidence[Number(basis.slice(1)) - 1]?.title || evidence[Number(basis.slice(1)) - 1]?.snippet);
|
|
45
|
+
const fitBasis = (basis: string) => basis === 'card' && Object.values(card).some(value => !!text(value))
|
|
46
|
+
|| basis === 'context' && !!text(context) || basis === 'offer' && hasOffer || sourceBasis(basis);
|
|
47
|
+
const validBasis = (basis: string) => basis === 'assumption' || fitBasis(basis);
|
|
48
|
+
const citedEvidence = (basis: string) => /^S\d+$/.test(basis) ? evidence[Number(basis.slice(1)) - 1] : undefined;
|
|
49
|
+
const instruction = 'JSON 객체만 출력: {"fit":{"score":0~100,"label":"high|medium|low","reasons":[{"text":"이유","basis":"S1|card|context|offer"} 3개]},"approach":{"who":"누구에게","problem":"어떤 문제","proposal":"어떤 제안","channel":"어느 채널","timing":"언제"},"approachBasis":{"who":["card"],"problem":["S1"],"proposal":["offer"],"channel":["assumption"],"timing":["assumption"]},"nextAction":{"what":"할 일","due":"기한"},"nextActionBasis":{"what":["assumption"],"due":["assumption"]}}. approachBasis와 nextActionBasis에는 각 필드의 주장에 실제로 사용한 근거 번호/입력을 적을 것. S#는 제공된 evidence 번호만 사용. nextAction은 고객의 확정 약속이나 확정 일정을 주장하지 말고 우리가 제안할 내부 행동과 목표 기한으로만 작성할 것. 출처에 없는 사실을 꾸며내지 말 것. offer가 없으면 fit 평가는 하지 말고 명함과 만남 맥락만으로 접근 전략을 작성할 것. 메시지는 보내지 말 것. 전략은 구체적이어야 한다(일반론 «AI 로 효율화» 금지): who = 이름·직함 ⊕ 결정권 추정(추정이면 basis 에 assumption); problem = 그 회사가 지금 겪을 법한 문제 하나 — 근거는 출처 S# 또는 만남 맥락(context)이어야 하고 없으면 만남 맥락에서 나온 관심사로 좁힌다; proposal = 우리 제안 하나(수치 약속 없음 · 작은 시범·시연·파일럿처럼 바로 해 볼 수 있는 것); channel = 메일/LinkedIn/전화 중 하나 ⊕ 그 이유 한 구; timing = 날짜나 요일로(예: 행사 다음 날 오전 · 이번 주 금요일 전). fit 이유는 서로 다른 근거로 셋을 채워라(card·context·offer·S#).';
|
|
50
|
+
let answer: Data = {};
|
|
51
|
+
let reasons: Fit['reasons'] = [];
|
|
52
|
+
let feedback = '';
|
|
53
|
+
// Three tries: with company-site evidence the writer still over-cites about one run in three (10-01 dry runs 0.2.6–0.2.8);
|
|
54
|
+
// the third try gets the rejected fields by name. The verdict rule itself is unchanged.
|
|
55
|
+
const LAST = 2;
|
|
56
|
+
for (let attempt = 0; attempt <= LAST; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
answer = parseAnswer(await ask({ task: 'strategy', instruction: instruction + feedback, card, research: hasOffer ? { summary: research.summary, news: research.news } : {}, context, offer, evidence: hasOffer ? evidence : [] }));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (attempt >= 1) throw error;
|
|
61
|
+
feedback = ' 이전 답이 올바른 JSON 객체가 아니었다. 지정한 필드를 갖춘 JSON 객체만 다시 작성하라.';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const approachData = object(answer.approach);
|
|
65
|
+
const actionData = object(answer.nextAction);
|
|
66
|
+
if ((['who', 'problem', 'proposal', 'channel', 'timing'] as const).some(field => !text(approachData[field]))
|
|
67
|
+
|| !text(actionData.what) || !text(actionData.due)) {
|
|
68
|
+
if (attempt >= 1) throw new Error('접근 전략 필수 필드 없음');
|
|
69
|
+
feedback = ' 이전 답에 접근 전략 또는 다음 행동 필수 필드가 빠졌다. 모두 채워 다시 작성하라.';
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const raw = hasOffer && Array.isArray(object(answer.fit).reasons) ? object(answer.fit).reasons as unknown[] : [];
|
|
73
|
+
reasons = raw.map(object).map(item => ({ text: text(item.text), basis: text(item.basis) }))
|
|
74
|
+
.filter(item => item.text && fitBasis(item.basis)).slice(0, 3);
|
|
75
|
+
const invalidNumber = raw.some(item => !text(object(item).text) || !fitBasis(text(object(item).basis)));
|
|
76
|
+
const claims = reasons.map((reason, i) => ({ i, text: reason.text, basis: reason.basis, evidence: citedEvidence(reason.basis) }));
|
|
77
|
+
const basisFor = (rawBasis: unknown) => {
|
|
78
|
+
const entries = Array.isArray(rawBasis) ? rawBasis : [];
|
|
79
|
+
return entries.map(text).filter(validBasis);
|
|
80
|
+
};
|
|
81
|
+
const approachBasis = object(answer.approachBasis), actionBasis = object(answer.nextActionBasis);
|
|
82
|
+
const approachClaims = (['who', 'problem', 'proposal', 'channel', 'timing'] as const)
|
|
83
|
+
.map(field => ({ field, text: text(approachData[field]), basis: basisFor(approachBasis[field]),
|
|
84
|
+
evidence: basisFor(approachBasis[field]).map(citedEvidence).filter((source): source is NonNullable<typeof source> => !!source) }));
|
|
85
|
+
const actionClaims = (['what', 'due'] as const)
|
|
86
|
+
.map(field => ({ field, text: text(actionData[field]), basis: basisFor(actionBasis[field]),
|
|
87
|
+
evidence: basisFor(actionBasis[field]).map(citedEvidence).filter((source): source is NonNullable<typeof source> => !!source) }));
|
|
88
|
+
const invalidClaims = [...approachClaims.map(claim => ({ claim, raw: approachBasis[claim.field] })),
|
|
89
|
+
...actionClaims.map(claim => ({ claim, raw: actionBasis[claim.field] }))]
|
|
90
|
+
.some(({ claim, raw }) => !Array.isArray(raw) || !claim.basis.length || raw.length !== claim.basis.length ||
|
|
91
|
+
(!hasOffer && claim.basis.some(basis => /^S\d+$/.test(basis))));
|
|
92
|
+
const verdict = parseAnswer(await ask({ task: 'strategy-verify',
|
|
93
|
+
instruction: 'fit 이유는 사실 주장뿐 아니라 적합성 판단 자체도 그 이유에 표시된 basis의 실제 내용(명함 필드·만남 맥락·제안·인용된 출처 제목/본문 조각)에 뒷받침되는지 판정하고, 추측이나 근거 없는 적합성 판단은 unsupportedReasons에 넣어라. 접근 전략과 다음 행동의 사람·회사·시장·고객 약속·확정 일정에 관한 사실 주장을 해당 basis와 인용된 evidence의 title/snippet/url, 명함, context, offer 원문에 대조하라. 전체 evidence가 있어도 각 주장은 자신이 인용한 근거로만 판정. 번호가 존재하는 것만으로 뒷받침된다고 판정하지 말 것. 고객의 미확인 약속·확정 일정은 nextAction에서 특히 unsupportedAction으로 표시할 것. 제안·질문·우리 내부의 향후 계획/목표 기한은 사실 주장이 아니다. JSON 객체만 출력: {"unsupportedReasons":[이유의 i],"unsupportedApproach":[접근 전략의 field],"unsupportedAction":[다음 행동의 field]}.',
|
|
94
|
+
card, context, offer, evidence, claims, approach: approachClaims, nextAction: actionClaims }));
|
|
95
|
+
if (!Array.isArray(verdict.unsupportedReasons) || !Array.isArray(verdict.unsupportedApproach) || !Array.isArray(verdict.unsupportedAction)) throw new Error('전략 근거 검증 응답을 읽지 못했다');
|
|
96
|
+
const badReasons = new Set(verdict.unsupportedReasons.filter((i): i is number => typeof i === 'number' && Number.isInteger(i) && i >= 0 && i < reasons.length));
|
|
97
|
+
const badApproach = verdict.unsupportedApproach.filter(field => approachClaims.some(claim => claim.field === field));
|
|
98
|
+
const badAction = verdict.unsupportedAction.filter(field => actionClaims.some(claim => claim.field === field));
|
|
99
|
+
if (verdict.unsupportedReasons.some(i => !badReasons.has(i as number)) || verdict.unsupportedApproach.length !== badApproach.length || verdict.unsupportedAction.length !== badAction.length) throw new Error('전략 근거 검증 응답을 읽지 못했다');
|
|
100
|
+
reasons = reasons.filter((_, i) => !badReasons.has(i));
|
|
101
|
+
if (!invalidNumber && !invalidClaims && (!hasOffer || (reasons.length >= 3 && new Set(reasons.map(reason => reason.basis)).size >= 3)) && badReasons.size === 0 && badApproach.length === 0 && badAction.length === 0) break;
|
|
102
|
+
// Fit-only shortfalls keep the old two tries (the answer is used with fewer reasons); only rejected approach/action
|
|
103
|
+
// claims get the third try.
|
|
104
|
+
if (attempt >= 1 && !(badApproach.length || badAction.length || invalidClaims)) break;
|
|
105
|
+
if (attempt === LAST && (badApproach.length || badAction.length || invalidClaims)) throw new Error(`근거 없는 접근 전략/다음 행동: ${[...badApproach, ...badAction].join(', ') || '근거 누락'}`);
|
|
106
|
+
// Name the rejected fields: a site snippet («we build automations») invites a «problem» the page never states
|
|
107
|
+
// (10-01 dry run, card-followup 0.2.4: strategy failed twice on who/problem once research found the company site).
|
|
108
|
+
const rejected = [...badApproach, ...badAction];
|
|
109
|
+
feedback = ' 이전 답에서 존재하지 않는 S# 또는 근거 내용과 불일치하는 이유·접근 전략·다음 행동을 제외하고, 근거 세 개와 주장별 근거를 다시 작성하라. 고객 약속·확정 일정은 확인된 근거가 없으면 쓰지 말고 내부 제안·목표 기한만 적어라.'
|
|
110
|
+
+ (rejected.length ? ` 특히 ${rejected.join(', ')} 는 근거와 맞지 않았다 — 그 필드는 출처가 «직접 말한» 사실만 S# 로 인용하고, 출처가 말하지 않은 회사의 문제·결정권은 «만남 맥락(context)에서 확인할 질문» 꼴로 바꾸거나 basis 를 assumption 으로 적어 추정임을 밝혀라.` : '');
|
|
111
|
+
}
|
|
112
|
+
const fitData = object(answer.fit);
|
|
113
|
+
const score = typeof fitData.score === 'number' && Number.isFinite(fitData.score) && fitData.score >= 0 && fitData.score <= 100 ? fitData.score : null;
|
|
114
|
+
const label = fitData.label;
|
|
115
|
+
const grounded = reasons.length >= 3 && new Set(reasons.map(reason => reason.basis)).size >= 3;
|
|
116
|
+
const fit: Fit = hasOffer
|
|
117
|
+
? { score: grounded ? score : null, label: grounded && score !== null && (label === 'high' || label === 'medium' || label === 'low') ? label : 'unknown', reasons }
|
|
118
|
+
: { score: null, label: 'unknown', reasons: [{ text: 'offer 입력이 없어 맞음을 판정하지 않았다', basis: 'offer' }] };
|
|
119
|
+
const approachData = object(answer.approach), actionData = object(answer.nextAction);
|
|
120
|
+
const approach: Approach = {
|
|
121
|
+
who: text(approachData.who), problem: text(approachData.problem), proposal: text(approachData.proposal),
|
|
122
|
+
channel: text(approachData.channel), timing: text(approachData.timing),
|
|
123
|
+
};
|
|
124
|
+
const nextAction: NextAction = { what: text(actionData.what), due: text(actionData.due) };
|
|
125
|
+
return { fit, approach, nextAction };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function crmRow(card: Data, fit: Fit, approach: Approach, nextAction: NextAction, context: string, at: Date | string): CrmRow {
|
|
129
|
+
return {
|
|
130
|
+
name: text(card.name), company: text(card.company), title: text(card.title), email: text(card.email),
|
|
131
|
+
interest: approach.problem, met_context: context, fit_score: fit.score === null ? '' : String(fit.score),
|
|
132
|
+
fit_label: fit.label, next_action: nextAction.what, due: nextAction.due,
|
|
133
|
+
updated_at: at instanceof Date ? at.toISOString() : at,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseCsv(content: string): string[][] {
|
|
138
|
+
const rows: string[][] = [];
|
|
139
|
+
let row: string[] = [], field = '', quoted = false;
|
|
140
|
+
for (let i = 0; i < content.length; i++) {
|
|
141
|
+
const char = content[i]!;
|
|
142
|
+
if (quoted) {
|
|
143
|
+
if (char === '"' && content[i + 1] === '"') { field += '"'; i++; }
|
|
144
|
+
else if (char === '"') quoted = false;
|
|
145
|
+
else field += char;
|
|
146
|
+
} else if (char === '"') quoted = true;
|
|
147
|
+
else if (char === ',') { row.push(field); field = ''; }
|
|
148
|
+
else if (char === '\n' || char === '\r') {
|
|
149
|
+
if (char === '\r' && content[i + 1] === '\n') i++;
|
|
150
|
+
row.push(field); rows.push(row); row = []; field = '';
|
|
151
|
+
} else field += char;
|
|
152
|
+
}
|
|
153
|
+
if (quoted) throw new Error('CRM CSV 따옴표가 닫히지 않음');
|
|
154
|
+
if (field || row.length) { row.push(field); rows.push(row); }
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const quote = (value: string) => /[",\r\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
159
|
+
|
|
160
|
+
export function upsertCrm(path: string, row: CrmRow): void {
|
|
161
|
+
const prior = existsSync(path) ? parseCsv(readFileSync(path, 'utf8')) : [];
|
|
162
|
+
if (prior.length && (prior[0]!.length !== columns.length || columns.some((col, i) => prior[0]![i] !== col))) throw new Error('CRM CSV 헤더 불일치');
|
|
163
|
+
const records = prior.slice(1).map(values => Object.fromEntries(columns.map((column, i) => [column, values[i] ?? ''])) as CrmRow);
|
|
164
|
+
const same = (left: CrmRow, right: CrmRow) => right.email
|
|
165
|
+
? !!left.email && left.email.toLowerCase() === right.email.toLowerCase()
|
|
166
|
+
: !left.email && left.name === right.name && left.company === right.company;
|
|
167
|
+
const index = records.findIndex(existing => same(existing, row));
|
|
168
|
+
if (index >= 0) records[index] = row;
|
|
169
|
+
else records.push(row);
|
|
170
|
+
records.sort((a, b) => (b.fit_score === '' ? -1 : Number(b.fit_score)) - (a.fit_score === '' ? -1 : Number(a.fit_score)));
|
|
171
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
172
|
+
writeFileSync(path, [columns.join(','), ...records.map(record => columns.map(column => quote(record[column])).join(','))].join('\n') + '\n', { mode: 0o600 });
|
|
173
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "card-followup",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"description": "Business card photo to researched follow-up and LinkedIn drafts. Drafts are never sent.",
|
|
6
6
|
"extensions": {
|
|
7
7
|
"ai.elanous": {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# doc-draft
|
|
2
|
+
|
|
3
|
+
Drafts one document from a request: an executive one-pager, a promo/event post, or a memo. **Drafts only; never published.** It does not post, send, or pay for anything.
|
|
4
|
+
|
|
5
|
+
Run from the repository without installing the plugin (install it for COO to discover `doc-draft`):
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
elanous graph run plugins/doc-draft/graphs/doc-draft.yaml --input '{"kind":"exec-onepager","topic":"Summarize the event readiness check for leadership"}' --json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Inputs: `kind` (required: `exec-onepager`, `promo-post`, `memo`), `topic` (required one-line request), `context` (optional previous seat results or source summary), `audience` (optional), `language` (optional, default `ko`), and `outDir` (optional, default beside the graph run state file). The `examples/brief-sample.json` keys are read by the COO planner; its values are illustrative only.
|
|
12
|
+
|
|
13
|
+
The `draft` step calls `elanous ask --bare --json` from a fresh empty temporary folder, then `check` checks the draft and asks for a review against the requested items and source context. An older CLI that rejects `--bare` falls back to `ask --json`. `DOC_DRAFT_ELANOUS_BIN` substitutes the CLI executable in tests. Each step emits final-line JSON; failures route to `failed`. Only after `check` succeeds does `report` write `outDir/draft.md` (one Markdown title line plus body), with final-line JSON `{ "outcome": "ok", "file": "…/draft.md", "words": 123 }`. An executive one-pager includes 요약, 핵심 숫자, 결정 요청, and 다음 단계; missing figures are marked as unprovided rather than invented. If `draft.md` already exists in `outDir`, `report` fails without replacing it; choose a fresh output directory for a new run. No publication or delivery happens.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
graph_id: doc-draft
|
|
2
|
+
version: 1
|
|
3
|
+
entry_node: draft
|
|
4
|
+
terminal_nodes: [done, failed]
|
|
5
|
+
nodes:
|
|
6
|
+
- { node_id: draft, kind: agent, recipe: 'cmd:draft', max_visits: 1 }
|
|
7
|
+
- { node_id: check, kind: agent, recipe: 'cmd:check', max_visits: 1 }
|
|
8
|
+
- { node_id: report, kind: agent, recipe: 'cmd:report', max_visits: 1 }
|
|
9
|
+
- { node_id: done, kind: gate, max_visits: 1 }
|
|
10
|
+
- { node_id: failed, kind: gate, max_visits: 1 }
|
|
11
|
+
edges:
|
|
12
|
+
- { from: draft, on: outcome, map: { ok: check, fail: failed } }
|
|
13
|
+
- { from: check, on: outcome, map: { ok: report, fail: failed } }
|
|
14
|
+
- { from: report, on: outcome, map: { ok: done, fail: failed } }
|