elanous 0.2.6 → 0.2.8
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-3084c58c1725a40b.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/7032-f4ef698518fba61c.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-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.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-8f5ff96c522b5f55.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-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.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-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.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-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.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/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_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/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +6 -5
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- 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 +58 -13
- 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 +57 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +164 -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 +109 -21
- 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/persona-presets/lint.ts +79 -0
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +108 -22
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +124 -0
- package/scripts/release-loop/ops-upgrade-node.ts +95 -0
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -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/auth-methods.ts +30 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +96 -20
- 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/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -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/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +41 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/import-cli.ts +60 -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/mcp-login.ts +13 -4
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/persona-cli.ts +116 -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 +50 -8
- 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/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +37 -16
- package/src/dashboard/index.ts +18 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +73 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +94 -12
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +146 -0
- package/src/exec-requests/runner.ts +378 -0
- package/src/exec-requests/store.ts +78 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +203 -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/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +168 -21
- package/src/input-core/text-entry.ts +13 -5
- 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-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +221 -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 +128 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/error-reports.ts +108 -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 +82 -8
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +28 -1
- package/src/nexus/setup-status.ts +11 -7
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/browser-availability.ts +14 -0
- 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-quota-policy.ts +16 -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 +34 -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 +90 -0
- package/src/onboarding.ts +287 -26
- package/src/persona/presets.ts +105 -0
- 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 +37 -13
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/orchestrate.ts +18 -11
- 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/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/skills/trigger-extract.ts +1 -1
- 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-command-job.ts +16 -8
- 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-commands.ts +13 -0
- package/src/telegram.ts +72 -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 +189 -15
- 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 → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -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 } }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
type Data = Record<string, unknown>;
|
|
6
|
+
const object = (value: unknown): Data => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Data : {};
|
|
7
|
+
const text = (value: unknown): string => typeof value === 'string' ? value.trim() : '';
|
|
8
|
+
const errorText = (error: unknown): string => (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, ' ').slice(0, 240);
|
|
9
|
+
const limits = { 'exec-onepager': 450, 'promo-post': 700, memo: 600 } as const;
|
|
10
|
+
type Kind = keyof typeof limits;
|
|
11
|
+
export function normalizeKind(raw: string): string {
|
|
12
|
+
const k = raw.trim().toLowerCase();
|
|
13
|
+
if (/^(exec-onepager|promo-post|memo)$/.test(k)) return k;
|
|
14
|
+
if (/(exec|executive|one[- ]?pager|임원|보고)/.test(k)) return 'exec-onepager';
|
|
15
|
+
if (/(promo|event|post|announce|홍보|행사|소개|공지)/.test(k)) return 'promo-post';
|
|
16
|
+
if (/(memo|note|메모|노트)/.test(k)) return 'memo';
|
|
17
|
+
return k;
|
|
18
|
+
}
|
|
19
|
+
const sections = ['요약', '핵심 숫자', '결정 요청', '다음 단계'];
|
|
20
|
+
const wordCount = (value: string): number => value.trim().split(/\s+/u).filter(Boolean).length;
|
|
21
|
+
|
|
22
|
+
async function ask(prompt: string): Promise<string> {
|
|
23
|
+
const neutralDir = mkdtempSync(join(tmpdir(), 'doc-draft-'));
|
|
24
|
+
const { ELANOUS_TOOL_CWD: _tool, ...rest } = process.env;
|
|
25
|
+
const env = { ...rest, PWD: neutralDir };
|
|
26
|
+
const cli = process.env.DOC_DRAFT_ELANOUS_BIN || 'elanous';
|
|
27
|
+
const invoke = async (bare: boolean): Promise<string> => {
|
|
28
|
+
const proc = Bun.spawn([cli, 'ask', ...(bare ? ['--bare'] : []), '--json', prompt], {
|
|
29
|
+
cwd: neutralDir, env, stdout: 'pipe', stderr: 'pipe',
|
|
30
|
+
});
|
|
31
|
+
const [stdout, stderr, exit] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
32
|
+
if (exit !== 0) throw new Error((stderr || stdout).trim() || `CLI exited ${exit}`);
|
|
33
|
+
const last = stdout.trim().split('\n').at(-1);
|
|
34
|
+
if (!last) throw new Error('CLI returned no JSON');
|
|
35
|
+
const reply = object(JSON.parse(last) as unknown).reply;
|
|
36
|
+
if (typeof reply !== 'string' || !reply.trim()) throw new Error('ask returned no reply');
|
|
37
|
+
return reply.trim();
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
try { return await invoke(true); }
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!/unknown option '--bare'/.test(errorText(error))) throw error;
|
|
43
|
+
return await invoke(false);
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
try { rmdirSync(neutralDir); }
|
|
47
|
+
catch (error) {
|
|
48
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOTEMPTY' && (error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validate(markdown: string, kind: Kind): string {
|
|
54
|
+
const lines = markdown.split(/\r?\n/u);
|
|
55
|
+
if (!/^# [^\n#]+$/u.test(lines[0] ?? '') || !lines.slice(1).join('').trim()) return 'title or body missing';
|
|
56
|
+
const words = wordCount(markdown);
|
|
57
|
+
if (words < 15 || words > limits[kind]) return `length must be 15–${limits[kind]} words`;
|
|
58
|
+
if (/\[[^\]]*\]|\{\{[^}]*\}\}|<[^>]+>|(?:TODO|TBD|작성 예정|내용 입력|빈칸)/iu.test(markdown)) return 'unfilled placeholder';
|
|
59
|
+
if (/(?:게시|발행|발송|전송|결제)(?:\s*완료|했|됐|되었|하였)|\b(?:published|posted|sent|paid)\b/iu.test(markdown)) return 'unverified publication or transaction claim';
|
|
60
|
+
if (kind === 'exec-onepager' && sections.some(section => {
|
|
61
|
+
const match = new RegExp(`^## ${section}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, 'mu').exec(markdown);
|
|
62
|
+
return !match || !match[1]?.trim();
|
|
63
|
+
})) return 'one-pager section missing or empty';
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const contextPath = process.env.ELANOUS_GRAPH_CONTEXT ?? '';
|
|
69
|
+
const context = object(JSON.parse(readFileSync(contextPath, 'utf8')) as unknown);
|
|
70
|
+
const input = object(context.input);
|
|
71
|
+
const outputs = object(context.outputs);
|
|
72
|
+
const stateDirectory = dirname(contextPath);
|
|
73
|
+
if (!stateDirectory.endsWith('.json.contexts')) throw new Error('invalid graph run context');
|
|
74
|
+
const defaultOutDir = stateDirectory.slice(0, -'.json.contexts'.length);
|
|
75
|
+
// 계획기(COO)는 입력 «키»만 알고 허용 값은 모른다 — «executive one-pager»·«임원 보고»·«홍보 글» 같은 풀이도 받는다(10-01 운영 실측).
|
|
76
|
+
const kind = normalizeKind(text(input.kind));
|
|
77
|
+
if (!Object.hasOwn(limits, kind)) throw new Error('kind must be exec-onepager, promo-post, or memo');
|
|
78
|
+
const typedKind = kind as Kind;
|
|
79
|
+
const topic = text(input.topic);
|
|
80
|
+
if (!topic) throw new Error('topic is required');
|
|
81
|
+
const step = process.argv[2] ?? '';
|
|
82
|
+
if (step === 'draft') {
|
|
83
|
+
const prompt = `Write ONE ${kind} document in ${text(input.language) || 'ko'} for ${text(input.audience) || 'the intended reader'}. Request: ${topic}. Context (source material, not a claim to invent): ${text(input.context) || '(none)'}. Return Markdown only: one # title line and a substantive body of 15–${limits[typedKind]} whitespace-separated words. Cover every item in the request. ${kind === 'exec-onepager' ? 'Include exactly these four ## sections with substantive content: 요약, 핵심 숫자, 결정 요청, 다음 단계. For unknown numeric figures write “자료 미제공 — 확인 필요” under 핵심 숫자, rather than inventing numbers.' : ''} No empty placeholders or fabricated facts. This is a DRAFT only: never claim it was published, sent, paid for, or approved; do not perform those actions.`;
|
|
84
|
+
const markdown = await ask(prompt);
|
|
85
|
+
console.log(JSON.stringify({ outcome: 'ok', markdown }));
|
|
86
|
+
} else if (step === 'check') {
|
|
87
|
+
const markdown = text(object(outputs.draft).markdown);
|
|
88
|
+
const error = validate(markdown, typedKind);
|
|
89
|
+
if (error) throw new Error(error);
|
|
90
|
+
const response = await ask(`Review this draft against the original request. Check length, empty fields, missing requested items, and invented claims (including claims it was published, sent, paid for, or approved). Request: ${topic}. Context: ${text(input.context) || '(none)'}. Draft: ${markdown}. Return ONLY JSON {"ok":true|false,"reason":"..."}. Set ok=false for any missing request item or unverifiable factual claim. A clearly labelled missing number is allowed; do not invent facts.`);
|
|
91
|
+
const verdict = object(JSON.parse(response.replace(/^```(?:json)?\s*|\s*```$/gi, '').trim()) as unknown);
|
|
92
|
+
if (verdict.ok !== true) throw new Error(text(verdict.reason) || 'draft review failed');
|
|
93
|
+
console.log(JSON.stringify({ outcome: 'ok', markdown, words: wordCount(markdown) }));
|
|
94
|
+
} else if (step === 'report') {
|
|
95
|
+
const checked = object(outputs.check);
|
|
96
|
+
if (checked.outcome !== 'ok') throw new Error('missing checked draft');
|
|
97
|
+
const markdown = text(checked.markdown);
|
|
98
|
+
const error = validate(markdown, typedKind);
|
|
99
|
+
if (error) throw new Error(error);
|
|
100
|
+
const outDir = text(input.outDir) || defaultOutDir;
|
|
101
|
+
mkdirSync(outDir, { recursive: true });
|
|
102
|
+
const file = join(outDir, 'draft.md');
|
|
103
|
+
try {
|
|
104
|
+
writeFileSync(file, markdown + '\n', { flag: 'wx', mode: 0o600 });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`draft already exists: ${file}`);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
console.log(JSON.stringify({ outcome: 'ok', file, words: wordCount(markdown) }));
|
|
110
|
+
} else throw new Error(`unknown doc-draft step: ${step}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.log(JSON.stringify({ outcome: 'fail', error: errorText(error) }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
|
+
"name": "doc-draft",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"description": "Drafts one document from a request — executive one-pager, promo or event post, memo. Drafts only; never published.",
|
|
6
|
+
"extensions": {
|
|
7
|
+
"ai.elanous": {
|
|
8
|
+
"graphs": [
|
|
9
|
+
"./graphs/doc-draft.yaml"
|
|
10
|
+
],
|
|
11
|
+
"capabilities": [
|
|
12
|
+
"fs:workdir",
|
|
13
|
+
"proc:bun",
|
|
14
|
+
"proc:elanous"
|
|
15
|
+
],
|
|
16
|
+
"connectors": [],
|
|
17
|
+
"pricing": {
|
|
18
|
+
"model": "free"
|
|
19
|
+
},
|
|
20
|
+
"trust": "official",
|
|
21
|
+
"category": "Productivity"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|