elanous 0.2.11 → 0.2.13
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -17,6 +17,8 @@ import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
|
17
17
|
import { resolveChannelBotToken } from '../channel-bot-token.js';
|
|
18
18
|
import { isOperationalKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './telegram-kind-route.js';
|
|
19
19
|
import { debug } from '../debug/log.js';
|
|
20
|
+
import { registerLogStoreSink, setLogInstanceName } from '../mss/logging/log-store.js';
|
|
21
|
+
import type { LogSink } from '../mss/logging/sink.js';
|
|
20
22
|
import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
|
|
21
23
|
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
22
24
|
// ★ origin 되돌림(대표 2026-07-12) — 미션 알림을 발신 채널(메인 Q&A 봇)로 되돌린다. type-only
|
|
@@ -36,6 +38,29 @@ function nexusUrl(): string | null {
|
|
|
36
38
|
const ACP_TOKEN_PATH = join(getElanousConfigDir(), 'acp-token');
|
|
37
39
|
const DEFERRED_PATH = conatusPath('outbound_deferred.jsonl');
|
|
38
40
|
|
|
41
|
+
// CLI/cron do not inherit the daemon's StoreSink. Register once before the first
|
|
42
|
+
// send, but never attach a second sink to a daemon which already owns one.
|
|
43
|
+
let outboundLogSink: (() => void) | null = null;
|
|
44
|
+
function ensureOutboundLogSink(): void {
|
|
45
|
+
if (outboundLogSink || inProcessOutbound || process.env.NODE_ENV === 'test') return;
|
|
46
|
+
try {
|
|
47
|
+
const logs = getUserConfig().logs;
|
|
48
|
+
setLogInstanceName(logs.instanceName);
|
|
49
|
+
outboundLogSink = registerLogStoreSink((s: LogSink) => debug.registerSink(s), 'outbound', logs.retention);
|
|
50
|
+
if (outboundLogSink) process.once('exit', outboundLogSink);
|
|
51
|
+
} catch { /* observations must not prevent delivery */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function botLabel(token: string): string {
|
|
55
|
+
// Telegram bot IDs are public numeric IDs; never log the token's secret suffix.
|
|
56
|
+
const id = token.split(':', 1)[0];
|
|
57
|
+
return /^\d+$/.test(id ?? '') ? `telegram:${id}` : 'telegram:configured';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeObservationLabel(value: string): string {
|
|
61
|
+
return /^[a-zA-Z0-9_./-]{1,160}$/.test(value) ? value : 'unknown';
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
// ── 발송 관측 (대표 지시 2026-07-15): 발송 시각·mode·밀림(burst/lag) 를 logs.db 에 남겨
|
|
40
65
|
// "실시간인지 밀린 것인지" 를 `elanous logs --category outbound.send` 로 판단 가능하게. ──
|
|
41
66
|
/** 밀림(버스트) 판정 — 최근 창 내 이 수 이상 발송이 몰리면 밀려 나가는 중으로 본다. */
|
|
@@ -61,7 +86,7 @@ function logSend(
|
|
|
61
86
|
extra: Record<string, unknown> = {},
|
|
62
87
|
opts?: { level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'critical' },
|
|
63
88
|
): void {
|
|
64
|
-
try { debug.log('outbound.send', mode, { kind, ...extra }, opts); } catch { /* fail-open */ }
|
|
89
|
+
try { debug.log('outbound.send', mode, { kind: safeObservationLabel(kind), ...extra }, opts); } catch { /* fail-open */ }
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
/** 밀림 경고 임계 env 이름 — 비교 값은 여기서만 읽는다(호출부에 리터럴을 박지 않는다). */
|
|
@@ -143,12 +168,13 @@ function resolveBotToken(botId?: string): string | null {
|
|
|
143
168
|
}
|
|
144
169
|
|
|
145
170
|
/** origin(발신 채널)으로 직접 발송. 실패하면 호출자가 report 폴백/재시도한다. */
|
|
146
|
-
function deliverToOrigin(origin: MissionOrigin, text: string): boolean {
|
|
171
|
+
function deliverToOrigin(origin: MissionOrigin, text: string, onBot?: (bot: string) => void): boolean {
|
|
147
172
|
if (origin.channel === 'discord') {
|
|
148
173
|
if (!origin.channelId) return false;
|
|
149
174
|
try {
|
|
150
175
|
const token = resolveChannelBotToken('discord', getUserConfig())?.token;
|
|
151
176
|
if (!token) return false;
|
|
177
|
+
onBot?.('discord:configured');
|
|
152
178
|
const channelId = origin.discordThreadId || origin.channelId;
|
|
153
179
|
const out = spillLongContent(text).text;
|
|
154
180
|
for (let start = 0; start < out.length;) {
|
|
@@ -168,19 +194,21 @@ function deliverToOrigin(origin: MissionOrigin, text: string): boolean {
|
|
|
168
194
|
if (origin.channel !== 'telegram' || origin.chatId == null) return false;
|
|
169
195
|
const token = resolveBotToken(origin.botId);
|
|
170
196
|
if (!token) return false;
|
|
197
|
+
onBot?.(botLabel(token));
|
|
171
198
|
try { return sendTelegramRaw(token, origin.chatId, text, origin.threadId); } catch { return false; }
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
/** 보류분 일괄 발송 — 묶음 1건(아침 폭주 방지). 발송 건수 반환.
|
|
175
202
|
* 무음 창 밖 첫 sendOutbound 가 자동 호출 + 06:31 플러시 크론이 보장. */
|
|
176
203
|
export function flushDeferred(path = DEFERRED_PATH): number {
|
|
204
|
+
ensureOutboundLogSink();
|
|
177
205
|
const observeFlush = (count: number, lagMin: number, kinds: string[]): void => {
|
|
178
206
|
const lagWarnMin = flushLagWarnMin();
|
|
179
207
|
const over = count > 0 && lagMin > lagWarnMin;
|
|
180
208
|
logSend(
|
|
181
209
|
'flush',
|
|
182
210
|
'deferred-batch',
|
|
183
|
-
{ count, lagMin, kinds, path, lagWarnMin },
|
|
211
|
+
{ count, lagMin, kinds: kinds.map(safeObservationLabel), path, lagWarnMin },
|
|
184
212
|
over ? { level: 'warn' } : undefined,
|
|
185
213
|
);
|
|
186
214
|
};
|
|
@@ -209,14 +237,28 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
209
237
|
...list.map(i => `\n── [${kst(i.ts)} · ${i.kind}] ──\n${i.text}`),
|
|
210
238
|
].join('\n');
|
|
211
239
|
observeFlush(items.length, lagMin, [...new Set(items.map(i => i.kind))]);
|
|
240
|
+
const sendBatch = (text: string, kind: string, origin?: MissionOrigin): boolean => {
|
|
241
|
+
let bot = 'unknown';
|
|
242
|
+
const path = origin
|
|
243
|
+
? (deliverToOrigin(origin, text, label => { bot = label; }) ? 'origin' : false)
|
|
244
|
+
: deliver(text, kind, label => { bot = label; }, 'deferred-flush');
|
|
245
|
+
if (path === 'daemon') return true;
|
|
246
|
+
try {
|
|
247
|
+
debug.log('outbound.send', path ? 'sent' : 'failed', {
|
|
248
|
+
kind: safeObservationLabel(kind), source: 'deferred-flush', bot,
|
|
249
|
+
chars: text.length, path: path || 'undeliverable',
|
|
250
|
+
});
|
|
251
|
+
} catch { /* observation must not change delivery */ }
|
|
252
|
+
return path !== false;
|
|
253
|
+
};
|
|
212
254
|
let delivered = 0;
|
|
213
255
|
// origin 없는 매매 알림만 report 묶음. 그 외는 kind 별로 운영 봇 경계를 유지한다.
|
|
214
256
|
const noOrigin = items.filter(i => !i.origin);
|
|
215
257
|
const tradingBatch = noOrigin.filter(i => !isOperationalKind(i.kind));
|
|
216
|
-
if (tradingBatch.length &&
|
|
258
|
+
if (tradingBatch.length && sendBatch(fmt(tradingBatch), 'report')) delivered += tradingBatch.length;
|
|
217
259
|
for (const kind of new Set(noOrigin.filter(i => isOperationalKind(i.kind)).map(i => i.kind))) {
|
|
218
260
|
const batch = noOrigin.filter(i => i.kind === kind);
|
|
219
|
-
if (
|
|
261
|
+
if (sendBatch(fmt(batch), kind)) delivered += batch.length;
|
|
220
262
|
}
|
|
221
263
|
// origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
|
|
222
264
|
const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
|
|
@@ -228,7 +270,10 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
228
270
|
if (!groups.has(key)) groups.set(key, { origin: it.origin, list: [] });
|
|
229
271
|
groups.get(key)!.list.push(it);
|
|
230
272
|
}
|
|
231
|
-
for (const g of groups.values()) {
|
|
273
|
+
for (const g of groups.values()) {
|
|
274
|
+
const kinds = new Set(g.list.map(i => i.kind));
|
|
275
|
+
if (sendBatch(fmt(g.list), kinds.size === 1 ? g.list[0]!.kind : 'mixed', g.origin)) delivered += g.list.length;
|
|
276
|
+
}
|
|
232
277
|
// 전량 배달 성공 시에만 파일 제거(부분 실패는 다음 flush 재시도 — 성공분 중복은 드문 엣지 수용).
|
|
233
278
|
if (delivered >= items.length) { try { unlinkSync(path); } catch { /* */ } }
|
|
234
279
|
return delivered;
|
|
@@ -268,17 +313,19 @@ export function sendOutbound(text: string, kind = 'alert', origin?: (MissionOrig
|
|
|
268
313
|
const source = origin?.surface || (callerPath
|
|
269
314
|
? relative(repoRoot, callerPath.startsWith('file://') ? fileURLToPath(callerPath) : callerPath).replace(/\\/g, '/')
|
|
270
315
|
: 'unknown');
|
|
271
|
-
|
|
272
|
-
|
|
316
|
+
ensureOutboundLogSink();
|
|
317
|
+
let bot = origin?.channel === 'discord' ? 'discord:configured' : 'unknown';
|
|
318
|
+
const logOutcome = (event: 'sent' | 'deferred' | 'failed', path?: string): void => {
|
|
319
|
+
try { debug.log('outbound.send', event, { kind: safeObservationLabel(kind), source: safeObservationLabel(source), chars: text.length, bot, ...(path ? { path } : {}) }); } catch { /* fail-open */ }
|
|
273
320
|
};
|
|
274
321
|
// ★ 무음 우회(대표 2026-07-14) — 사용자가 최근(기본 30분) genuine 인텐트(타이핑/버튼탭)를 냈으면
|
|
275
322
|
// 깨어있으므로 야간 무음이어도 즉시 발송(사용자 발원 흐름의 결과물이 아침까지 묶이지 않게).
|
|
276
323
|
// 우회 시 flushDeferred 로 그간 보류분도 함께 전달(사용자가 지금 볼 수 있음).
|
|
277
324
|
if (inQuietHours() && !userRecentlyActive()) {
|
|
278
325
|
const queued = deferOutbound(text, kind, origin);
|
|
279
|
-
recordOutbound(text, kind); //
|
|
280
|
-
logOutcome(queued ? 'deferred' : 'failed');
|
|
281
|
-
return
|
|
326
|
+
if (queued) recordOutbound(text, kind); // 성공한 보류만 회상 대상
|
|
327
|
+
logOutcome(queued ? 'deferred' : 'failed', queued ? 'deferred' : 'queue-failed');
|
|
328
|
+
return queued; // 적재가 성공했을 때만 보류 수락
|
|
282
329
|
}
|
|
283
330
|
const bypass = inQuietHours();
|
|
284
331
|
if (bypass) console.error('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
|
|
@@ -288,16 +335,15 @@ export function sendOutbound(text: string, kind = 'alert', origin?: (MissionOrig
|
|
|
288
335
|
const { recentCount, burst } = recentSendBurst();
|
|
289
336
|
logSend(bypass ? 'quiet-bypass' : 'realtime', kind, { burst, recentCount, ...(burst ? { backlog: true } : {}) });
|
|
290
337
|
// ★ origin 되돌림(무음 밖) — 발신 채널로 직접 발송. 성공 시 종료, 실패면 report 폴백.
|
|
291
|
-
if (origin && deliverToOrigin(origin, text)) {
|
|
338
|
+
if (origin && deliverToOrigin(origin, text, (label) => { bot = label; })) {
|
|
292
339
|
recordOutbound(text, kind);
|
|
293
|
-
logOutcome('sent');
|
|
340
|
+
logOutcome('sent', 'origin');
|
|
294
341
|
return true;
|
|
295
342
|
}
|
|
296
|
-
const path = deliver(text, kind);
|
|
297
|
-
//
|
|
298
|
-
// 가 기록하므로 클라는 중복 금지 — 직접 폴백(direct·데몬 다운)만 클라가 기록.
|
|
343
|
+
const path = deliver(text, kind, (label) => { bot = label; }, source);
|
|
344
|
+
// 데몬 경유는 실제 라우터의 최종 결과만 기록한다. 직접 폴백과 미전달만 여기서 기록.
|
|
299
345
|
if (path === 'direct') recordOutbound(text, kind);
|
|
300
|
-
logOutcome(path === false ? 'failed' : 'sent');
|
|
346
|
+
if (path !== 'daemon') logOutcome(path === false ? 'failed' : 'sent', path === false ? 'undeliverable' : path);
|
|
301
347
|
return path !== false;
|
|
302
348
|
}
|
|
303
349
|
|
|
@@ -318,7 +364,14 @@ export function classifyDaemonResponse(j: unknown): DaemonPathClass {
|
|
|
318
364
|
/** 데몬 경로를 못 쓴 이유를 남긴다. 관측 실패가 발송을 막지 않음(recordOutbound 과 같은 fail-soft).
|
|
319
365
|
* 비-ok 분류는 크론 운영자가 읽는 표준 출력에도 한 줄 — 싱크 미등록 스크립트에서도 즉시 보이게. */
|
|
320
366
|
function logDaemonPath(classification: DaemonPathClass, kind: string, extra: Record<string, unknown> = {}): void {
|
|
321
|
-
|
|
367
|
+
ensureOutboundLogSink();
|
|
368
|
+
try {
|
|
369
|
+
debug.log('outbound.send', 'daemon-path', {
|
|
370
|
+
classification, kind: safeObservationLabel(kind),
|
|
371
|
+
...('hasToken' in extra ? { hasToken: extra.hasToken === true } : {}),
|
|
372
|
+
...('inProcess' in extra ? { inProcess: extra.inProcess === true } : {}),
|
|
373
|
+
});
|
|
374
|
+
} catch { /* fail-soft */ }
|
|
322
375
|
if (classification === 'ok') return;
|
|
323
376
|
try { console.error(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
|
|
324
377
|
}
|
|
@@ -326,23 +379,33 @@ function logDaemonPath(classification: DaemonPathClass, kind: string, extra: Rec
|
|
|
326
379
|
/** OB8 — the daemon registers its own in-process sender at startup. Inside the daemon, deliver() must never curl
|
|
327
380
|
* its own `/v1/outbound`: the curl is synchronous, the event loop that would answer it is the one it blocks, and the
|
|
328
381
|
* whole daemon freezes until the curl times out (~25 s · `unreachable`) — the message is lost (CS1 · 10-01). */
|
|
329
|
-
export type InProcessOutbound = (text: string, kind: string) => Promise<boolean>;
|
|
382
|
+
export type InProcessOutbound = (text: string, kind: string, source?: string, deferFailureObservation?: boolean) => Promise<boolean>;
|
|
330
383
|
let inProcessOutbound: InProcessOutbound | null = null;
|
|
331
384
|
export function setInProcessOutbound(send: InProcessOutbound | null): void { inProcessOutbound = send; }
|
|
332
385
|
|
|
333
386
|
/** elanous `/v1/outbound` 우선 → 실패 시 텔레그램 직접. 성공 경로 반환(원장 중복방지용). */
|
|
334
|
-
export function deliver(text: string, kind = 'alert'): 'daemon' | 'direct' | false {
|
|
387
|
+
export function deliver(text: string, kind = 'alert', onBot?: (bot: string) => void, source?: string): 'daemon' | 'direct' | false {
|
|
335
388
|
// 0) inside the daemon — route in-process, asynchronously; the caller's synchronous answer is «accepted».
|
|
336
389
|
if (inProcessOutbound && process.env.SEND_VIA_ELANOUS !== '0') {
|
|
390
|
+
onBot?.('daemon:configured');
|
|
337
391
|
const send = inProcessOutbound;
|
|
338
|
-
|
|
392
|
+
const fallback = (): void => {
|
|
393
|
+
let bot = 'unknown';
|
|
394
|
+
const ok = sendTelegramDirect(text, kind, {}, label => { bot = label; });
|
|
395
|
+
if (ok) recordOutbound(text, kind);
|
|
396
|
+
try { debug.log('outbound.send', ok ? 'sent' : 'failed', {
|
|
397
|
+
kind: safeObservationLabel(kind), source: safeObservationLabel(source ?? 'daemon-fallback'),
|
|
398
|
+
bot, path: ok ? 'direct' : 'undeliverable', chars: text.length,
|
|
399
|
+
}); } catch { /* fail-open */ }
|
|
400
|
+
};
|
|
401
|
+
void send(text, kind, source, true)
|
|
339
402
|
.then((ok) => {
|
|
340
403
|
logDaemonPath(ok ? 'ok' : 'rejected', kind, { inProcess: true });
|
|
341
|
-
if (!ok
|
|
404
|
+
if (!ok) fallback();
|
|
342
405
|
})
|
|
343
|
-
.catch((
|
|
344
|
-
logDaemonPath('rejected', kind, { inProcess: true
|
|
345
|
-
|
|
406
|
+
.catch(() => {
|
|
407
|
+
logDaemonPath('rejected', kind, { inProcess: true });
|
|
408
|
+
fallback();
|
|
346
409
|
});
|
|
347
410
|
return 'daemon';
|
|
348
411
|
}
|
|
@@ -351,20 +414,22 @@ export function deliver(text: string, kind = 'alert'): 'daemon' | 'direct' | fal
|
|
|
351
414
|
if (process.env.SEND_VIA_ELANOUS !== '0') {
|
|
352
415
|
let token = '';
|
|
353
416
|
try { if (existsSync(ACP_TOKEN_PATH)) token = readFileSync(ACP_TOKEN_PATH, 'utf-8').trim(); } catch { /* no token */ }
|
|
354
|
-
const headers = ['Content-Type: application/json',
|
|
417
|
+
const headers = ['Content-Type: application/json', 'X-Elanous-Client-Fallback: direct',
|
|
418
|
+
...(source ? [`X-Elanous-Outbound-Source: ${safeObservationLabel(source)}`] : []),
|
|
419
|
+
...(token ? [`Authorization: Bearer ${token}`] : [])];
|
|
355
420
|
const nexus = nexusUrl();
|
|
356
421
|
const j = nexus
|
|
357
422
|
? curlPost(`${nexus}/v1/outbound`, JSON.stringify({ text, markdown: false, kind }), headers)
|
|
358
423
|
: null;
|
|
359
424
|
const classification = classifyDaemonResponse(j);
|
|
360
|
-
if (classification === 'ok') return 'daemon';
|
|
425
|
+
if (classification === 'ok') { onBot?.('daemon:configured'); return 'daemon'; }
|
|
361
426
|
daemonPath = nexus ? classification : 'not-found';
|
|
362
427
|
const extra: Record<string, unknown> = { hasToken: token.length > 0 };
|
|
363
428
|
if (j && typeof j === 'object' && 'error' in (j as object)) extra.error = (j as { error?: unknown }).error;
|
|
364
429
|
logDaemonPath(classification, kind, extra);
|
|
365
430
|
}
|
|
366
431
|
// 2) fallback: 텔레그램 sendMessage 직접(3900자 분할) — 데몬 미경유라 클라가 원장 기록.
|
|
367
|
-
if (sendTelegramDirect(text, kind)) return 'direct';
|
|
432
|
+
if (sendTelegramDirect(text, kind, {}, onBot)) return 'direct';
|
|
368
433
|
reportUndeliverable(kind, daemonPath);
|
|
369
434
|
return false;
|
|
370
435
|
}
|
|
@@ -379,7 +444,8 @@ function reportUndeliverable(kind: string, daemonPath: DaemonPathClass | 'not-fo
|
|
|
379
444
|
root = effectiveInstanceRoot();
|
|
380
445
|
universe = root === prodInstanceRoot() ? 'prod' : 'test';
|
|
381
446
|
} catch { /* the report still goes out */ }
|
|
382
|
-
|
|
447
|
+
ensureOutboundLogSink();
|
|
448
|
+
try { debug.log('outbound.send', 'undeliverable', { kind: safeObservationLabel(kind), daemonPath, universe, root }); } catch { /* fail-soft */ }
|
|
383
449
|
const hint = universe === 'prod'
|
|
384
450
|
? '운영 데몬이 떠 있는지 확인: elanous nexus show'
|
|
385
451
|
: '운영으로 보내려면 설치본 elanous 로 실행하거나 ELANOUS_STATE_DIR=~/.elanous ELANOUS_CONFIG_DIR=~/.elanous 를 준다';
|
|
@@ -411,6 +477,7 @@ function sendTelegramRaw(token: string, chatId: string | number, text: string, t
|
|
|
411
477
|
export function sendTelegramDirect(
|
|
412
478
|
text: string, kind?: string,
|
|
413
479
|
deps: { config?: UserConfig; sendRaw?: typeof sendTelegramRaw; legacyEnv?: typeof conatusEnv } = {},
|
|
480
|
+
onBot?: (bot: string) => void,
|
|
414
481
|
): boolean {
|
|
415
482
|
const sendRaw = deps.sendRaw ?? sendTelegramRaw;
|
|
416
483
|
let cfg: UserConfig | undefined;
|
|
@@ -418,7 +485,7 @@ export function sendTelegramDirect(
|
|
|
418
485
|
if (cfg) {
|
|
419
486
|
try {
|
|
420
487
|
const target = kindRouteTarget(cfg, kind);
|
|
421
|
-
if (target) return sendRaw(target.botToken, String(target.chatId), text);
|
|
488
|
+
if (target) { onBot?.(botLabel(target.botToken)); return sendRaw(target.botToken, String(target.chatId), text); }
|
|
422
489
|
} catch { /* unavailable channel routing: fall through */ }
|
|
423
490
|
// A declared table is authoritative: missing roles cannot escape to an env bot.
|
|
424
491
|
if (cfg.telegram.channels?.length) {
|
|
@@ -437,7 +504,7 @@ export function sendTelegramDirect(
|
|
|
437
504
|
} catch { /* unavailable main bot */ }
|
|
438
505
|
const rcToken = cfg?.telegram.reportChannel?.botToken;
|
|
439
506
|
logKindRouteFallback(kind, home ? 'main-home' : 'none', !!mainToken && !!cfg?.telegram.reportChannel && (!rcToken || rcToken === mainToken));
|
|
440
|
-
if (home) return sendRaw(home.botToken, String(home.chatId), text);
|
|
507
|
+
if (home) { onBot?.(botLabel(home.botToken)); return sendRaw(home.botToken, String(home.chatId), text); }
|
|
441
508
|
console.error('[outbound] 운영 kind 메인 봇/홈 미설정 — 콘솔 출력\n' + text);
|
|
442
509
|
return false;
|
|
443
510
|
}
|
|
@@ -450,5 +517,6 @@ export function sendTelegramDirect(
|
|
|
450
517
|
}
|
|
451
518
|
logKindRouteFallback(kind, tok && chat ? 'conatus-env' : 'none', false);
|
|
452
519
|
if (!tok || !chat) { console.error('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
|
|
520
|
+
onBot?.(botLabel(tok));
|
|
453
521
|
return sendRaw(tok, chat, text);
|
|
454
522
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sendOutbound, setInProcessOutbound } from './outbound-alert.js';
|
|
2
|
+
import { setUserConfigOverlay } from '../user-config.js';
|
|
3
|
+
|
|
4
|
+
// Focused subprocess fixture: isolated state/config roots and a fake curl on PATH.
|
|
5
|
+
setUserConfigOverlay(cfg => ({ ...cfg, telegram: { ...cfg.telegram, botToken: '123456:private-secret', homeChannel: 98765 } }));
|
|
6
|
+
process.env.SEND_VIA_ELANOUS = '0';
|
|
7
|
+
setInProcessOutbound(null);
|
|
8
|
+
if (!sendOutbound('SECRET-BODY-DO-NOT-LOG', 'ops-alert', { channel: 'cli', surface: process.env.OUTBOUND_PROBE_SOURCE ?? 'outbound-log-probe' })) process.exitCode = 1;
|
|
@@ -26,17 +26,20 @@ export function activeRunsForGoal(goalId: string, deps: ActiveRunsForGoalDeps =
|
|
|
26
26
|
const loadLedger = deps.loadLedger ?? loadRunLedger;
|
|
27
27
|
const readGoalDocument = deps.readGoalDocument ?? ((path: string) => readFileSync(path, 'utf8'));
|
|
28
28
|
let unknown = observation.completeness !== 'complete' || observation.pty.unreadable.length > 0;
|
|
29
|
+
const unknownReasons = new Set<string>();
|
|
30
|
+
if (observation.completeness !== 'complete') unknownReasons.add(`ledger observation ${observation.completeness}`);
|
|
31
|
+
for (const root of observation.pty.unreadable) unknownReasons.add(`unreadable PTY: ${root}`);
|
|
29
32
|
const active = new Set<string>();
|
|
30
33
|
for (const run of observation.entries) {
|
|
31
34
|
if (run.status === 'ended-unclosed') continue;
|
|
32
|
-
if (run.ledgerDirectories.length === 0) { unknown = true; continue; }
|
|
35
|
+
if (run.ledgerDirectories.length === 0) { unknown = true; unknownReasons.add(`${run.runId}: ledger directory absent`); continue; }
|
|
33
36
|
let identified = false;
|
|
34
37
|
for (const directory of run.ledgerDirectories) {
|
|
35
38
|
try {
|
|
36
39
|
const ledger = loadLedger(run.runId, directory);
|
|
37
|
-
if (ledger === null) { unknown = true; continue; }
|
|
40
|
+
if (ledger === null) { unknown = true; unknownReasons.add(`${run.runId}: ledger unavailable`); continue; }
|
|
38
41
|
const ids = new Set(ledger.flatMap((entry) => entry.goalId ? [entry.goalId] : []));
|
|
39
|
-
if (ids.size !== 1) { unknown = true; continue; }
|
|
42
|
+
if (ids.size !== 1) { unknown = true; unknownReasons.add(`${run.runId}: expected one goalId, found ${ids.size}`); continue; }
|
|
40
43
|
identified = true;
|
|
41
44
|
let matches = ids.has(goalId);
|
|
42
45
|
if (!matches && goalId.startsWith('request-') && run.status === 'running' && run.ptyRefs.length > 0) {
|
|
@@ -45,20 +48,26 @@ export function activeRunsForGoal(goalId: string, deps: ActiveRunsForGoalDeps =
|
|
|
45
48
|
try {
|
|
46
49
|
const ask = extractVerbatimOriginalAsk(readGoalDocument(document));
|
|
47
50
|
matches = ask?.range !== undefined && launchRequestId(ask.ask) === goalId;
|
|
48
|
-
} catch { unknown = true; }
|
|
51
|
+
} catch { unknown = true; unknownReasons.add(`${run.runId}: goal document unreadable`); }
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
if (matches) {
|
|
52
55
|
if (run.status === 'running' && run.ptyRefs.length > 0) active.add(run.runId);
|
|
53
|
-
else unknown = true;
|
|
56
|
+
else { unknown = true; unknownReasons.add(`${run.runId}: matching ledger without live PTY`); }
|
|
54
57
|
}
|
|
55
|
-
} catch { unknown = true; }
|
|
58
|
+
} catch { unknown = true; unknownReasons.add(`${run.runId}: ledger read failed`); }
|
|
56
59
|
}
|
|
57
60
|
if (!identified) unknown = true;
|
|
58
61
|
}
|
|
59
62
|
// Positive evidence wins even if a different process or store is unreadable.
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if (active.size > 0) return [...active].sort();
|
|
64
|
+
if (unknown) {
|
|
65
|
+
try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [...unknownReasons] }); } catch { /* observation is fail-soft */ }
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
} catch (error) {
|
|
70
|
+
try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [error instanceof Error ? error.message : String(error)] }); } catch { /* observation is fail-soft */ }
|
|
62
71
|
return 'unknown';
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { generateFieldNote, type FieldPhoto, type TranscriptEntry } from './index.js';
|
|
6
|
+
import { transcribeFieldAudio, type FieldAudio, type TranscriptionEngine } from './transcribe.js';
|
|
7
|
+
import type { TgIncoming, TgIncomingAttachment } from '../telegram.js';
|
|
8
|
+
|
|
9
|
+
interface SavedMedia { kind: 'photo' | 'voice' | 'audio'; file: string; receivedAt: string; messageId: number }
|
|
10
|
+
interface NoteWindow { title: string; start: string; end: string; status: 'open' | 'done'; openedBy: number; media: SavedMedia[]; notePath?: string }
|
|
11
|
+
|
|
12
|
+
export interface TelegramFieldNoteDeps {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
download: (fileId: string) => Promise<{ localPath: string }>;
|
|
15
|
+
reply: (ctx: TgIncoming, text: string) => Promise<void>;
|
|
16
|
+
engine?: TranscriptionEngine;
|
|
17
|
+
decodeAudio?: (file: string, recordedAt: string) => FieldAudio;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Decode Telegram's Ogg/Opus (or audio file) into NOTE3a's 16kHz mono float PCM. */
|
|
22
|
+
export function decodeFieldAudio(file: string, recordedAt: string): FieldAudio {
|
|
23
|
+
const result = spawnSync('ffmpeg', ['-v', 'error', '-i', file, '-f', 'f32le', '-ac', '1', '-ar', '16000', 'pipe:1'], { maxBuffer: 1024 * 1024 * 1024 });
|
|
24
|
+
if (result.status !== 0) throw new Error(`음성 디코딩 실패: ${result.stderr.toString().trim() || result.error?.message || 'ffmpeg'}`);
|
|
25
|
+
if (result.stdout.length % 4) throw new Error('음성 디코딩 실패: 잘못된 PCM 길이');
|
|
26
|
+
const bytes = result.stdout;
|
|
27
|
+
const samples = new Float32Array(bytes.length / 4);
|
|
28
|
+
for (let i = 0; i < samples.length; i++) samples[i] = bytes.readFloatLE(i * 4);
|
|
29
|
+
return { samples, sampleRate: 16000, recordedAt };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function windowPath(ctx: TgIncoming, root: string): string {
|
|
33
|
+
const bot = (ctx.botId ?? 'bot').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
34
|
+
return join(root, 'field-notes', 'telegram', bot, String(ctx.chatId), String(ctx.threadId ?? 0), String(ctx.userId), 'window.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readWindow(path: string): NoteWindow | null {
|
|
38
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as NoteWindow : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveWindow(path: string, window: NoteWindow): void {
|
|
42
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
43
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(temp, JSON.stringify(window, null, 2) + '\n');
|
|
46
|
+
renameSync(temp, path);
|
|
47
|
+
} finally {
|
|
48
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseWindow(text: string, now: Date): { title: string; start: string; end: string } | null {
|
|
53
|
+
const match = /^노트:\s*(.+?)\s+(\d{2}):(\d{2})\s*[~~]\s*(\d{2}):(\d{2})$/.exec(text.trim());
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const [, title, h1, m1, h2, m2] = match;
|
|
56
|
+
if (!Number.isFinite(now.getTime()) || +h1! > 23 || +h2! > 23 || +m1! > 59 || +m2! > 59) return null;
|
|
57
|
+
const local = new Date(now.getTime() + 9 * 60 * 60_000).toISOString().slice(0, 10);
|
|
58
|
+
const start = new Date(`${local}T${h1}:${m1}:00+09:00`);
|
|
59
|
+
const end = new Date(`${local}T${h2}:${m2}:00+09:00`);
|
|
60
|
+
if (end <= start) return null;
|
|
61
|
+
return { title: title!.trim(), start: start.toISOString(), end: end.toISOString() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function handleTelegramFieldNote(ctx: TgIncoming, deps: TelegramFieldNoteDeps): Promise<boolean> {
|
|
65
|
+
const text = ctx.text.trim();
|
|
66
|
+
const path = windowPath(ctx, deps.rootDir);
|
|
67
|
+
if (text.startsWith('노트:')) {
|
|
68
|
+
const parsed = parseWindow(text, ctx.receivedAt ? new Date(ctx.receivedAt) : (deps.now ?? (() => new Date()))());
|
|
69
|
+
if (!parsed) await deps.reply(ctx, '노트 시간 형식: 노트: 세미나 18:00~20:00');
|
|
70
|
+
else {
|
|
71
|
+
const previous = readWindow(path);
|
|
72
|
+
if (previous?.status === 'open' && previous.media.length) {
|
|
73
|
+
await deps.reply(ctx, '열린 노트 창이 있습니다 — 먼저 노트 만들기를 완료하세요.');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
saveWindow(path, { ...parsed, status: 'open', openedBy: ctx.messageId, media: [] });
|
|
77
|
+
await deps.reply(ctx, `노트 창 열림 · ${parsed.title} · ${text.slice(text.lastIndexOf(' ') + 1)}`);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (text !== '노트 만들기' && !ctx.attachments.some((a) => a.kind === 'photo' || a.kind === 'voice' || a.kind === 'audio')) return false;
|
|
82
|
+
const window = readWindow(path);
|
|
83
|
+
if (text === '노트 만들기') {
|
|
84
|
+
if (!window || window.status !== 'open') {
|
|
85
|
+
await deps.reply(ctx, '열린 노트 창 없음');
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (window.media.some((item) => item.kind !== 'photo') && !deps.engine) {
|
|
89
|
+
await deps.reply(ctx, '전사 엔진 없음 — NOTE3a 전사 엔진을 설정해 주세요. 모은 파일은 유지됩니다.');
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const photos: FieldPhoto[] = window.media.filter((item) => item.kind === 'photo').map((item) => ({ file: item.file, exifTakenAt: item.receivedAt }));
|
|
94
|
+
const transcript: TranscriptEntry[] = [];
|
|
95
|
+
for (const item of window.media.filter((m) => m.kind !== 'photo')) {
|
|
96
|
+
const audio = (deps.decodeAudio ?? decodeFieldAudio)(item.file, item.receivedAt);
|
|
97
|
+
transcript.push(...await transcribeFieldAudio(audio, deps.engine!));
|
|
98
|
+
}
|
|
99
|
+
const markdown = await generateFieldNote(photos, transcript, {
|
|
100
|
+
summarize: async (entries) => entries.map((entry) => `${entry.timestamp} · ${entry.speaker}: ${entry.text}`).join('\n'),
|
|
101
|
+
});
|
|
102
|
+
const notePath = join(path, '..', `note-${window.openedBy}.md`);
|
|
103
|
+
const noteTemp = `${notePath}.${randomUUID()}.tmp`;
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(noteTemp, `# ${window.title}\n\n시간 창: ${window.start} ~ ${window.end}\n\n${markdown}`);
|
|
106
|
+
renameSync(noteTemp, notePath);
|
|
107
|
+
} finally {
|
|
108
|
+
if (existsSync(noteTemp)) unlinkSync(noteTemp);
|
|
109
|
+
}
|
|
110
|
+
saveWindow(path, { ...window, status: 'done', notePath });
|
|
111
|
+
await deps.reply(ctx, `노트 생성 · ${notePath}`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await deps.reply(ctx, `노트 생성 실패 · ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (!window || window.status !== 'open') return false;
|
|
118
|
+
const media = ctx.attachments.filter((a): a is TgIncomingAttachment & { kind: 'photo' | 'voice' | 'audio' } =>
|
|
119
|
+
a.kind === 'photo' || a.kind === 'voice' || a.kind === 'audio');
|
|
120
|
+
if (!media.length) return false;
|
|
121
|
+
const receivedAt = ctx.receivedAt ?? (deps.now ?? (() => new Date()))().toISOString();
|
|
122
|
+
if (ctx.messageId <= window.openedBy || receivedAt < window.start || receivedAt > window.end) return false;
|
|
123
|
+
const dir = join(path, '..', 'media');
|
|
124
|
+
mkdirSync(dir, { recursive: true });
|
|
125
|
+
try {
|
|
126
|
+
for (const [index, item] of media.entries()) {
|
|
127
|
+
if (window.media.some((saved) => saved.messageId === ctx.messageId && saved.kind === item.kind)) continue;
|
|
128
|
+
const downloaded = await deps.download(item.fileId);
|
|
129
|
+
const extension = item.kind === 'photo' ? 'jpg' : item.kind === 'voice' ? 'ogg'
|
|
130
|
+
: item.mimeType === 'audio/mpeg' ? 'mp3' : item.mimeType === 'audio/ogg' ? 'ogg' : 'audio';
|
|
131
|
+
const file = join(dir, `${ctx.messageId}-${index}.${extension}`);
|
|
132
|
+
try { copyFileSync(downloaded.localPath, file); }
|
|
133
|
+
finally { try { unlinkSync(downloaded.localPath); } catch { /* download may be reused by the caller */ } }
|
|
134
|
+
window.media.push({ kind: item.kind, file, receivedAt, messageId: ctx.messageId });
|
|
135
|
+
saveWindow(path, window);
|
|
136
|
+
}
|
|
137
|
+
await deps.reply(ctx, `노트 파일 모음 · 사진 ${window.media.filter((m) => m.kind === 'photo').length}장 · 음성 ${window.media.filter((m) => m.kind !== 'photo').length}개`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await deps.reply(ctx, `노트 파일 저장 실패 · ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
@@ -23,11 +23,11 @@ export interface NodeKindEntry {
|
|
|
23
23
|
export const HARNESS_CORE_KINDS = ['agent', 'gate', 'git', 'judge', 'observe', 'hitl', 'subgraph'] as const;
|
|
24
24
|
|
|
25
25
|
// Workflow validation, the palette, and executor variant labels read the same keys.
|
|
26
|
-
export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow'] as const;
|
|
26
|
+
export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow', 'knowledge'] as const;
|
|
27
27
|
|
|
28
28
|
const descriptions: Record<GraphKind, readonly string[]> = {
|
|
29
29
|
harness: ['Run an agent', 'Check an execution gate', 'Perform git operations', 'Judge an outcome', 'Observe a result', 'Request human input', 'Run a child graph'],
|
|
30
|
-
workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow'],
|
|
30
|
+
workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow', 'Search the vault for context'],
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const kinds = new Map<GraphKind, Map<string, NodeKindEntry>>();
|
|
@@ -26,7 +26,7 @@ export function raiseApprovalCard(state: GraphRunState, pending: Pending, deps:
|
|
|
26
26
|
const nodeId = pending.nodeId;
|
|
27
27
|
const identity = { graphId: state.graphId, runId: state.runId, nodeId };
|
|
28
28
|
const visit = state.path.filter(node => node === nodeId).length;
|
|
29
|
-
if (state.dryRun || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
|
|
29
|
+
if (state.dryRun || state.growthPark || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
|
|
30
30
|
state.pending.decision || state.path.at(-1) !== nodeId || visit < 1 ||
|
|
31
31
|
existsSync(`${join(root, 'graph-runs', state.graphId, `${state.runId}.json`)}.${state.path.length}.decision.json`)) {
|
|
32
32
|
observation('skipped', identity);
|
|
@@ -88,7 +88,7 @@ function applyOne(entry: DecisionEntry, identity: { graphId: string; runId: stri
|
|
|
88
88
|
observation('skipped', identity, entry.id, entry.choice);
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
|
-
if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.status !== 'awaiting-approval' ||
|
|
91
|
+
if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.growthPark || state.status !== 'awaiting-approval' ||
|
|
92
92
|
!state.pending || state.pending.nodeId !== nodeId || state.pending.decision || state.path.at(-1) !== nodeId ||
|
|
93
93
|
state.path.filter(node => node === nodeId).length !== visit ||
|
|
94
94
|
existsSync(`${statePath}.${state.path.length}.decision.json`)) {
|
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
import { streamLLM } from '../llm.js';
|
|
1
|
+
import { PROVIDERS, streamLLM } from '../llm.js';
|
|
2
|
+
import { getUserConfig, resolveRoleLlm, type UserConfig } from '../user-config.js';
|
|
2
3
|
import type { GrowthProposal, GrowthProposer } from './graph-grow.js';
|
|
3
4
|
|
|
4
5
|
type GrowthLLM = (prompt: string) => Promise<string>;
|
|
5
|
-
const OUTPUT_TAIL_LENGTH =
|
|
6
|
+
const OUTPUT_TAIL_LENGTH = 2000;
|
|
7
|
+
|
|
8
|
+
export function graphGrowthLlmOptions(config: UserConfig): { model?: string; provider?: string } {
|
|
9
|
+
const role = config.roleLlm?.['graph-grow'];
|
|
10
|
+
if (!role || (!role.provider && !role.model && !role.tier)) return {};
|
|
11
|
+
const selected = resolveRoleLlm('graph-grow', { config });
|
|
12
|
+
return { model: selected.model, provider: selected.provider };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function defaultGrowthLLM(prompt: string): Promise<string> {
|
|
16
|
+
const selected = graphGrowthLlmOptions(getUserConfig());
|
|
17
|
+
return streamLLM([{ role: 'user', content: prompt }], () => {}, {
|
|
18
|
+
...(selected.model ? { model: selected.model, provider: PROVIDERS[selected.provider!] } : {}),
|
|
19
|
+
reasoningEffort: 'low',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
/** Model output is untrusted: structural and side-effect validation belongs to proposeGrowth. */
|
|
8
|
-
export function createLLMGrowthProposer(callLLM: GrowthLLM =
|
|
9
|
-
streamLLM([{ role: 'user', content: prompt }], () => {}, { reasoningEffort: 'low' })): GrowthProposer {
|
|
24
|
+
export function createLLMGrowthProposer(callLLM: GrowthLLM = defaultGrowthLLM): GrowthProposer {
|
|
10
25
|
return async (input: Parameters<GrowthProposer>[0]) => {
|
|
11
26
|
const serializedOutput = typeof input.output === 'string' ? input.output : input.output === undefined ? '' : JSON.stringify(input.output);
|
|
12
|
-
const graphSummary = {
|
|
13
|
-
graphId: input.graph.graphId,
|
|
14
|
-
nodes: input.graph.nodes.map(({ nodeId, kind, recipe }) => ({ nodeId, kind, recipe })),
|
|
15
|
-
edges: input.graph.edges.map(({ from, to, on, map, fallback }) => ({ from, ...(to === undefined ? {} : { to }),
|
|
16
|
-
...(on === undefined ? {} : { on }), ...(map === undefined ? {} : { map }), ...(fallback === undefined ? {} : { fallback }) })),
|
|
17
|
-
};
|
|
18
27
|
const prompt = [
|
|
19
|
-
'Propose exactly one new contracted graph node to handle the unseen outcome
|
|
20
|
-
'Reply with only JSON: {"node":{"nodeId":"...","kind":"
|
|
28
|
+
'Propose exactly one new contracted graph node to handle the unseen outcome and one return edge to an existing node.',
|
|
29
|
+
'Reply with only JSON: {"node":{"nodeId":"...","kind":"agent","recipe":"none","maxVisits":1,"contract":{"inputs":[],"tools":"read-only","outputs":[]}},"returnTo":"existing-node-id","reason":"..."}.',
|
|
30
|
+
'Use only an existing recipe type (none, an existing cmd:/approval: recipe ID, or a catalog role); never invent commands. The missing-outcome edge and the return edge are built from node and returnTo after validation.',
|
|
21
31
|
'Do not alter existing nodes or edges. The proposal will be validated independently; do not assume a recipe is safe.',
|
|
22
|
-
`Graph
|
|
32
|
+
`Graph snapshot: ${JSON.stringify(input.graph)}`,
|
|
23
33
|
`Blocked node ID: ${JSON.stringify(input.nodeId)}`,
|
|
24
34
|
`Unseen outcome: ${JSON.stringify(input.outcome)}`,
|
|
25
35
|
`Blocked node output tail: ${JSON.stringify(serializedOutput.slice(-OUTPUT_TAIL_LENGTH))}`,
|