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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const CONSULT_MAX_BYTES = 8 * 1024;
|
|
6
|
+
export const CONSULT_QUEUE_MAX_AGE_MS = 7 * 24 * 3600_000;
|
|
7
|
+
|
|
8
|
+
export interface ConsultInput {
|
|
9
|
+
name: string;
|
|
10
|
+
org?: string;
|
|
11
|
+
kind: 'company' | 'personal';
|
|
12
|
+
interest: 'A' | 'B';
|
|
13
|
+
contact: string;
|
|
14
|
+
consent: true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QueuedConsult { id: string; receivedAt: string; consult: ConsultInput }
|
|
18
|
+
|
|
19
|
+
const singleLine = (value: string): string => value.trim().replace(/[\r\n\u2028\u2029]+/g, ' ');
|
|
20
|
+
|
|
21
|
+
/** The Primary accepts optional org and requires nonblank name/contact, kind, interest and explicit consent. */
|
|
22
|
+
export function validateConsult(body: unknown): { ok: true; consult: ConsultInput } | { ok: false; field: string } {
|
|
23
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return { ok: false, field: 'body' };
|
|
24
|
+
const b = body as Record<string, unknown>;
|
|
25
|
+
if (typeof b.name !== 'string' || !b.name.trim() || b.name.length > 256) return { ok: false, field: 'name' };
|
|
26
|
+
if (b.org !== undefined && (typeof b.org !== 'string' || !b.org.trim() || b.org.length > 256)) return { ok: false, field: 'org' };
|
|
27
|
+
if (b.kind !== 'company' && b.kind !== 'personal') return { ok: false, field: 'kind' };
|
|
28
|
+
if (b.interest !== 'A' && b.interest !== 'B') return { ok: false, field: 'interest' };
|
|
29
|
+
if (typeof b.contact !== 'string' || !b.contact.trim() || b.contact.length > 512) return { ok: false, field: 'contact' };
|
|
30
|
+
if (b.consent !== true) return { ok: false, field: 'consent' };
|
|
31
|
+
return { ok: true, consult: { name: singleLine(b.name), ...(typeof b.org === 'string' ? { org: singleLine(b.org) } : {}),
|
|
32
|
+
kind: b.kind, interest: b.interest, contact: b.contact.trim(), consent: true } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Sliding windows; neither rejected hits nor invalid submissions consume quota. */
|
|
36
|
+
export class ConsultLimiter {
|
|
37
|
+
private readonly ips = new Map<string, number[]>();
|
|
38
|
+
private daily: number[] = [];
|
|
39
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
40
|
+
|
|
41
|
+
admit(ip: string): number | null {
|
|
42
|
+
const t = this.now();
|
|
43
|
+
const hits = (this.ips.get(ip) ?? []).filter(x => t - x < 600_000);
|
|
44
|
+
this.daily = this.daily.filter(x => t - x < 86_400_000);
|
|
45
|
+
const wait = hits.length >= 3 ? 600_000 - (t - hits[0]!) :
|
|
46
|
+
this.daily.length >= 200 ? 86_400_000 - (t - this.daily[0]!) : null;
|
|
47
|
+
if (wait !== null) { this.ips.set(ip, hits); return Math.max(1, Math.ceil(wait / 1000)); }
|
|
48
|
+
hits.push(t);
|
|
49
|
+
this.ips.set(ip, hits);
|
|
50
|
+
this.daily.push(t);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Atomic JSONL rewrite on every mutation, with a private file and directory. */
|
|
56
|
+
export class ConsultQueue {
|
|
57
|
+
readonly path: string;
|
|
58
|
+
constructor(root: string, private readonly log: (event: string, data: Record<string, unknown>) => void = () => {}) {
|
|
59
|
+
this.path = join(root, 'hooks', 'consult-queue.jsonl');
|
|
60
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private read(): QueuedConsult[] {
|
|
64
|
+
try { return readFileSync(this.path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as QueuedConsult); }
|
|
65
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private write(items: QueuedConsult[]): void {
|
|
69
|
+
const temp = `${this.path}.${randomUUID()}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
const fd = openSync(temp, 'wx', 0o600);
|
|
72
|
+
try { writeSync(fd, items.map(item => JSON.stringify(item)).join('\n') + (items.length ? '\n' : '')); fsyncSync(fd); }
|
|
73
|
+
finally { closeSync(fd); }
|
|
74
|
+
renameSync(temp, this.path);
|
|
75
|
+
} finally { rmSync(temp, { force: true }); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
enqueue(item: QueuedConsult): void { this.write([...this.read(), item]); }
|
|
79
|
+
entries(now: number = Date.now()): QueuedConsult[] {
|
|
80
|
+
const items = this.read();
|
|
81
|
+
const kept = items.filter(item => Number.isFinite(Date.parse(item.receivedAt)) && now - Date.parse(item.receivedAt) <= CONSULT_QUEUE_MAX_AGE_MS);
|
|
82
|
+
if (kept.length !== items.length) { this.write(kept); this.log('dropped', { expired: items.length - kept.length }); }
|
|
83
|
+
return kept;
|
|
84
|
+
}
|
|
85
|
+
count(): number { return existsSync(this.path) ? this.read().length : 0; }
|
|
86
|
+
delivered(id: string): void { this.write(this.read().filter(item => item.id !== id)); }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Accept only a validated body; responses and logs never contain user-provided values. */
|
|
90
|
+
export async function handleConsultPost(request: Request, deps: {
|
|
91
|
+
limiter: ConsultLimiter; queue: ConsultQueue; now?: () => number; ip?: string;
|
|
92
|
+
log?: (event: string, data: Record<string, unknown>) => void; wake?: () => void;
|
|
93
|
+
}): Promise<Response> {
|
|
94
|
+
const log = deps.log ?? (() => {});
|
|
95
|
+
const declared = Number(request.headers.get('content-length') ?? '0');
|
|
96
|
+
if (declared > CONSULT_MAX_BYTES) { log('rejected', { reason: 'too-large' }); return Response.json({ error: 'too-large' }, { status: 413 }); }
|
|
97
|
+
const chunks: Uint8Array[] = [];
|
|
98
|
+
let size = 0;
|
|
99
|
+
const reader = request.body?.getReader();
|
|
100
|
+
try {
|
|
101
|
+
if (reader) {
|
|
102
|
+
while (true) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
size += value.byteLength;
|
|
106
|
+
if (size > CONSULT_MAX_BYTES) {
|
|
107
|
+
await reader.cancel().catch(() => {});
|
|
108
|
+
log('rejected', { reason: 'too-large' });
|
|
109
|
+
return Response.json({ error: 'too-large' }, { status: 413 });
|
|
110
|
+
}
|
|
111
|
+
chunks.push(value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
log('rejected', { reason: 'body' });
|
|
116
|
+
return Response.json({ error: 'invalid', field: 'body' }, { status: 400 });
|
|
117
|
+
} finally { reader?.releaseLock(); }
|
|
118
|
+
let body: unknown;
|
|
119
|
+
try {
|
|
120
|
+
const raw = new Uint8Array(size);
|
|
121
|
+
let offset = 0;
|
|
122
|
+
for (const chunk of chunks) { raw.set(chunk, offset); offset += chunk.byteLength; }
|
|
123
|
+
body = JSON.parse(new TextDecoder().decode(raw));
|
|
124
|
+
}
|
|
125
|
+
catch { log('rejected', { reason: 'body' }); return Response.json({ error: 'invalid', field: 'body' }, { status: 400 }); }
|
|
126
|
+
const checked = validateConsult(body);
|
|
127
|
+
if (!checked.ok) { log('rejected', { reason: checked.field }); return Response.json({ error: 'invalid', field: checked.field }, { status: 400 }); }
|
|
128
|
+
const ip = deps.ip ?? 'unknown';
|
|
129
|
+
const wait = deps.limiter.admit(ip);
|
|
130
|
+
if (wait !== null) {
|
|
131
|
+
log('rejected', { reason: 'rate-limited' });
|
|
132
|
+
return Response.json({ error: 'rate-limited', retryAfterSec: wait }, { status: 429, headers: { 'Retry-After': String(wait) } });
|
|
133
|
+
}
|
|
134
|
+
const id = randomUUID();
|
|
135
|
+
try { deps.queue.enqueue({ id, receivedAt: new Date((deps.now ?? Date.now)()).toISOString(), consult: checked.consult }); }
|
|
136
|
+
catch { log('rejected', { reason: 'queue-unavailable' }); return Response.json({ error: 'queue-unavailable' }, { status: 503 }); }
|
|
137
|
+
log('received', { queued: true, kind: checked.consult.kind, interest: checked.consult.interest,
|
|
138
|
+
nameLength: checked.consult.name.length, contactLength: checked.consult.contact.length, orgLength: checked.consult.org?.length ?? 0 });
|
|
139
|
+
deps.wake?.();
|
|
140
|
+
return Response.json({ id }, { status: 202 });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Forward in arrival order; a failure leaves that item and all following items queued. */
|
|
144
|
+
export async function drainConsults(queue: ConsultQueue, forward: (item: QueuedConsult) => Promise<boolean>, now: number = Date.now()): Promise<{ delivered: number; failed: number }> {
|
|
145
|
+
let delivered = 0;
|
|
146
|
+
let failed = 0;
|
|
147
|
+
for (const item of queue.entries(now)) {
|
|
148
|
+
let ok = false;
|
|
149
|
+
try { ok = await forward(item); } catch { ok = false; }
|
|
150
|
+
if (ok) { queue.delivered(item.id); delivered++; } else { failed++; break; }
|
|
151
|
+
}
|
|
152
|
+
return { delivered, failed };
|
|
153
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
3
7
|
import { openMsgStore } from '../msg/msg-store.js';
|
|
4
8
|
import { CardStore, redactSecrets } from '../task-cards/card-store.js';
|
|
5
9
|
import { isGithubSpamText, PUBLIC_GITHUB_REPO } from './providers.js';
|
|
@@ -72,8 +76,57 @@ function object(value: unknown): Record<string, unknown> | undefined {
|
|
|
72
76
|
}
|
|
73
77
|
function gitSha(value: unknown): value is string { return typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value); }
|
|
74
78
|
|
|
79
|
+
export interface ShadowReviewResult {
|
|
80
|
+
reviewed: boolean;
|
|
81
|
+
verdict: 'pass' | 'warn' | 'fail' | null;
|
|
82
|
+
summary: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Runs the read-only self-review command from a detached, disposable worktree of trusted HEAD.
|
|
86
|
+
* The PR is never checked out: only its remote diff enters the reviewer prompt. */
|
|
87
|
+
export async function runGithubShadowReview(number: number, repo = resolve(process.cwd())): Promise<ShadowReviewResult> {
|
|
88
|
+
if (!Number.isSafeInteger(number) || number < 1) throw new Error('invalid PR number');
|
|
89
|
+
const dir = mkdtempSync(join(tmpdir(), 'elanous-review-shadow-'));
|
|
90
|
+
const worktree = join(dir, 'repo');
|
|
91
|
+
let attached = false;
|
|
92
|
+
try {
|
|
93
|
+
const added = spawnSync('git', ['-C', repo, 'worktree', 'add', '--detach', worktree, 'HEAD'], { encoding: 'utf8', timeout: 30_000 });
|
|
94
|
+
if (added.status !== 0) throw new Error('isolated worktree unavailable');
|
|
95
|
+
attached = true;
|
|
96
|
+
if (existsSync(join(repo, 'node_modules'))) symlinkSync(join(repo, 'node_modules'), join(worktree, 'node_modules'), 'dir');
|
|
97
|
+
const output = await new Promise<string>((done, fail) => {
|
|
98
|
+
const child = spawn(process.execPath, [join(worktree, 'bin/elanous.mjs'), '--test', 'self', 'review', String(number), '--json'], {
|
|
99
|
+
cwd: worktree, env: { ...process.env, GH_REPO: PUBLIC_GITHUB_REPO }, stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
let stdout = '';
|
|
102
|
+
let stderr = '';
|
|
103
|
+
const timer = setTimeout(() => child.kill(), 360_000);
|
|
104
|
+
child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); if (stdout.length > 100_000) child.kill(); });
|
|
105
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); if (stderr.length > 100_000) child.kill(); });
|
|
106
|
+
child.on('error', error => { clearTimeout(timer); fail(error); });
|
|
107
|
+
child.on('close', code => { clearTimeout(timer); code === 0 ? done(stdout) : fail(new Error(`self review exited ${code}: ${stderr.slice(0, 200)}`)); });
|
|
108
|
+
});
|
|
109
|
+
const parsed: unknown = JSON.parse(output);
|
|
110
|
+
const result = object(parsed);
|
|
111
|
+
if (!result) throw new Error('self review returned no result');
|
|
112
|
+
const verdict = result.reviewed === true && ['pass', 'warn', 'fail'].includes(String(result.verdict))
|
|
113
|
+
? result.verdict as 'pass' | 'warn' | 'fail' : null;
|
|
114
|
+
const findings = [...(Array.isArray(result.mustFix) ? result.mustFix : []), ...(Array.isArray(result.shouldFix) ? result.shouldFix : [])]
|
|
115
|
+
.filter((item): item is string => typeof item === 'string').slice(0, 8);
|
|
116
|
+
return { reviewed: result.reviewed === true && verdict !== null, verdict,
|
|
117
|
+
summary: redactSecrets(verdict ? `${verdict}: ${findings.join('; ') || 'no findings'}` : `review unavailable: ${String(result.error ?? 'not reviewed')}`).slice(0, 2000) };
|
|
118
|
+
} finally {
|
|
119
|
+
if (attached) {
|
|
120
|
+
rmSync(join(worktree, 'node_modules'), { force: true });
|
|
121
|
+
spawnSync('git', ['-C', repo, 'worktree', 'remove', '--force', worktree], { timeout: 30_000 });
|
|
122
|
+
}
|
|
123
|
+
rmSync(dir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
75
127
|
/** Separate shadow ledger/card path: never enter dispatchHook's seat wake or steward launch/report stages. */
|
|
76
|
-
export async function recordGithubShadow(event: QueuedHook, root: string, fetchFn: typeof fetch = fetch
|
|
128
|
+
export async function recordGithubShadow(event: QueuedHook, root: string, fetchFn: typeof fetch = fetch,
|
|
129
|
+
review: (number: number) => Promise<ShadowReviewResult> = runGithubShadowReview): Promise<void> {
|
|
77
130
|
const github = event.task.github;
|
|
78
131
|
if (event.provider !== 'github' || !github || github.repository !== PUBLIC_GITHUB_REPO ||
|
|
79
132
|
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(event.eventId) ||
|
|
@@ -94,6 +147,7 @@ export async function recordGithubShadow(event: QueuedHook, root: string, fetchF
|
|
|
94
147
|
.run(key, github.repository, github.type, github.number, judgment.verdict, judgment.reason, event.task.external.url ?? '');
|
|
95
148
|
const recorded = store.db.query('SELECT verdict, reason FROM github_shadow_judgments WHERE event_key = ?')
|
|
96
149
|
.get(key) as ShadowJudgment;
|
|
150
|
+
debug.log('steward.intake', 'judged', { eventKey: key, number: github.number, type: github.type, verdict: recorded.verdict, reason: recorded.reason });
|
|
97
151
|
const cards = new CardStore(root);
|
|
98
152
|
try {
|
|
99
153
|
const goalId = `github:${github.repository}:${github.type}:${github.number}`;
|
|
@@ -104,5 +158,21 @@ export async function recordGithubShadow(event: QueuedHook, root: string, fetchF
|
|
|
104
158
|
if (!card.sections.some(section => section.key === sectionKey))
|
|
105
159
|
cards.appendSection(card.id, { key: sectionKey, owner: 'steward', content: redactSecrets(content) });
|
|
106
160
|
} finally { cards.close(); }
|
|
161
|
+
if (github.type === 'pull_request' && recorded.verdict === 'accepted') {
|
|
162
|
+
store.db.exec(`CREATE TABLE IF NOT EXISTS github_shadow_reviews (
|
|
163
|
+
event_key TEXT PRIMARY KEY, repository TEXT NOT NULL, item_number INTEGER NOT NULL,
|
|
164
|
+
reviewed INTEGER NOT NULL, verdict TEXT, summary TEXT NOT NULL
|
|
165
|
+
)`);
|
|
166
|
+
if (!store.db.query('SELECT event_key FROM github_shadow_reviews WHERE event_key = ?').get(key)) {
|
|
167
|
+
let result: ShadowReviewResult;
|
|
168
|
+
try { result = await review(github.number); }
|
|
169
|
+
catch (error) { result = { reviewed: false, verdict: null, summary: `review unavailable: ${error instanceof Error ? error.message : String(error)}` }; }
|
|
170
|
+
store.db.query(`INSERT OR IGNORE INTO github_shadow_reviews
|
|
171
|
+
(event_key, repository, item_number, reviewed, verdict, summary) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
172
|
+
.run(key, github.repository, github.number, result.reviewed ? 1 : 0, result.verdict,
|
|
173
|
+
redactSecrets(result.summary).slice(0, 2000));
|
|
174
|
+
debug.log('steward.review-shadow', 'recorded', { eventKey: key, number: github.number, reviewed: result.reviewed, verdict: result.verdict });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
107
177
|
} finally { store.close(); }
|
|
108
178
|
}
|
package/src/hooks/receiver.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { getUserConfig, parseEventsConfig, userConfigPath, type EventsConfig } f
|
|
|
4
4
|
import { dispatchHook, type HookWake } from './dispatch.js';
|
|
5
5
|
import { getSecretAsync } from '../nexus/config/secrets/index.js';
|
|
6
6
|
import { HookQueue, type QueuedHook } from './queue.js';
|
|
7
|
-
import { recordGithubShadow } from './github-shadow.js';
|
|
7
|
+
import { recordGithubShadow, type ShadowReviewResult } from './github-shadow.js';
|
|
8
8
|
import { toExternalTask, verifyWebhook, type HookProvider } from './providers.js';
|
|
9
9
|
import { drainReports, handleReportPost, ReportLimiter, ReportQueue, type ReceivedReport } from './error-report.js';
|
|
10
|
+
import { ConsultLimiter, ConsultQueue, drainConsults, handleConsultPost, type QueuedConsult } from './consult-intake.js';
|
|
10
11
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
11
12
|
|
|
12
13
|
export interface HookSecrets {
|
|
@@ -27,8 +28,15 @@ export interface HookReceiverOptions {
|
|
|
27
28
|
wakeSeat?: HookWake;
|
|
28
29
|
/** ER2 — forward one queued error report to the Primary; default = POST /v1/reports/ingest over the tailnet. */
|
|
29
30
|
forwardReport?: (item: ReceivedReport) => Promise<boolean>;
|
|
31
|
+
/** CS1 — forward a queued consultation to the Primary's consult ledger. */
|
|
32
|
+
forwardConsult?: (item: QueuedConsult) => Promise<boolean>;
|
|
33
|
+
/** CS1 — the public consult route is a write path from the open internet, so it stays closed (404) until
|
|
34
|
+
* `hooks.consult.enabled === true` in user config (대표 decision D-20261003-09). Tests pass it directly. */
|
|
35
|
+
consultEnabled?: boolean;
|
|
30
36
|
/** GET-only public metadata lookup; failure forces human review. */
|
|
31
37
|
githubFetch?: typeof fetch;
|
|
38
|
+
/** Test seam for the isolated read-only self review; never publishes externally. */
|
|
39
|
+
githubShadowReview?: (number: number) => Promise<ShadowReviewResult>;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/** `/v1/tasks` lives on the Primary's nexus API, not on the control plane — so the address is its own setting. */
|
|
@@ -53,11 +61,66 @@ async function forwardReportToPrimary(item: ReceivedReport): Promise<boolean> {
|
|
|
53
61
|
return response.ok;
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
async function forwardConsultToPrimary(item: QueuedConsult): Promise<boolean> {
|
|
65
|
+
const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { hooks?: { primaryTokenRef?: string; primaryUrl?: unknown } };
|
|
66
|
+
const ref = config.hooks?.primaryTokenRef;
|
|
67
|
+
if (!ref) throw new Error('primary token reference missing');
|
|
68
|
+
const token = await getSecretAsync(ref);
|
|
69
|
+
if (!token) throw new Error('primary token missing');
|
|
70
|
+
const response = await fetch(new URL('/v1/consult-requests', hooksPrimaryUrl(config)), {
|
|
71
|
+
method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
72
|
+
body: JSON.stringify(item.consult), signal: AbortSignal.timeout(10_000),
|
|
73
|
+
});
|
|
74
|
+
return response.ok;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Raw read like the report forwarder: only an explicit `hooks.consult.enabled: true` opens the route. */
|
|
78
|
+
function consultRouteEnabled(): boolean {
|
|
79
|
+
try {
|
|
80
|
+
const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { hooks?: { consult?: { enabled?: unknown } } };
|
|
81
|
+
return config.hooks?.consult?.enabled === true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function consultOrigin(origin: string | null): string | null {
|
|
88
|
+
if (!origin) return null;
|
|
89
|
+
try {
|
|
90
|
+
const url = new URL(origin);
|
|
91
|
+
if (url.protocol !== 'https:' || url.origin !== origin) return null;
|
|
92
|
+
return url.hostname === 'elanous.ai' || url.hostname === 'www.elanous.ai' ||
|
|
93
|
+
/^[a-z0-9-]+\.vercel\.app$/i.test(url.hostname) ? origin : null;
|
|
94
|
+
} catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
export function startHookReceiver(options: HookReceiverOptions): { url: string; stop: () => void; queue: HookQueue } {
|
|
57
98
|
const queue = new HookQueue(options.root);
|
|
58
99
|
const reportQueue = new ReportQueue(options.root ?? effectiveInstanceRoot(), (event, data) => debug.log('hooks.reports', event, data));
|
|
59
100
|
const reportLimiter = new ReportLimiter(options.now ?? Date.now);
|
|
60
101
|
const forwardReport = options.forwardReport ?? forwardReportToPrimary;
|
|
102
|
+
const consultEnabled = options.consultEnabled ?? consultRouteEnabled();
|
|
103
|
+
debug.log('hooks.consult', consultEnabled ? 'route-open' : 'route-closed', {});
|
|
104
|
+
const consultQueue = new ConsultQueue(options.root ?? effectiveInstanceRoot(), (_event, data) =>
|
|
105
|
+
debug.log('hooks.consult', 'rejected', { reason: 'expired', ...data }));
|
|
106
|
+
const consultLimiter = new ConsultLimiter(options.now ?? Date.now);
|
|
107
|
+
const forwardConsult = options.forwardConsult ?? forwardConsultToPrimary;
|
|
108
|
+
let consultFailures = 0;
|
|
109
|
+
let consultNextAt = 0;
|
|
110
|
+
let consultsDraining = false;
|
|
111
|
+
async function drainConsultQueue(): Promise<void> {
|
|
112
|
+
if (stopped || consultsDraining || Date.now() < consultNextAt) return;
|
|
113
|
+
consultsDraining = true;
|
|
114
|
+
try {
|
|
115
|
+
const result = await drainConsults(consultQueue, forwardConsult, (options.now ?? Date.now)());
|
|
116
|
+
if (result.delivered) debug.log('hooks.consult', 'forwarded', { queued: consultQueue.count(), delivered: result.delivered });
|
|
117
|
+
if (result.failed) {
|
|
118
|
+
consultFailures++;
|
|
119
|
+
consultNextAt = Date.now() + Math.min(300_000, Math.max(1, options.retryBaseMs ?? 1000) * 2 ** Math.min(consultFailures - 1, 20));
|
|
120
|
+
debug.log('hooks.consult', 'forward-failed', { queued: consultQueue.count(), reason: 'delivery-failed' });
|
|
121
|
+
} else consultFailures = 0;
|
|
122
|
+
} finally { consultsDraining = false; }
|
|
123
|
+
}
|
|
61
124
|
let reportFailures = 0;
|
|
62
125
|
let reportNextAt = 0;
|
|
63
126
|
let reportsDraining = false;
|
|
@@ -79,7 +142,7 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
|
|
|
79
142
|
// L14: public-repository intake events take the shadow ledger path; every other event keeps the seat dispatch path.
|
|
80
143
|
const dispatch = options.forward ?? ((event: QueuedHook) => dispatchHook(event, options.root ?? effectiveInstanceRoot(), events, options.wakeSeat).then(() => 204));
|
|
81
144
|
const forward = (event: QueuedHook) => event.provider === 'github' && event.task.github
|
|
82
|
-
? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch).then(() => 204)
|
|
145
|
+
? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch, options.githubShadowReview).then(() => 204)
|
|
83
146
|
: dispatch(event);
|
|
84
147
|
const attempts = new Map<string, { next: number; failures: number }>();
|
|
85
148
|
const pending = new Set<string>();
|
|
@@ -111,18 +174,34 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
|
|
|
111
174
|
}
|
|
112
175
|
} finally {
|
|
113
176
|
void drainReportQueue();
|
|
177
|
+
void drainConsultQueue();
|
|
114
178
|
draining = false;
|
|
115
179
|
if (!stopped) timer = setTimeout(() => { void drain(); }, Math.min(1000, options.retryBaseMs ?? 1000));
|
|
116
180
|
}
|
|
117
181
|
}
|
|
118
182
|
const server = Bun.serve({ hostname: options.host ?? '127.0.0.1', port: options.port,
|
|
119
|
-
async fetch(request) {
|
|
183
|
+
async fetch(request, server) {
|
|
120
184
|
const url = new URL(request.url);
|
|
121
185
|
if (request.method === 'GET' && url.pathname === '/hooks/health')
|
|
122
186
|
return Response.json({ ok: true, queued: queue.count(), reportsQueued: reportQueue.count() });
|
|
123
187
|
if (request.method === 'POST' && url.pathname === '/v1/reports')
|
|
124
188
|
return handleReportPost(request, { limiter: reportLimiter, queue: reportQueue, ...(options.now ? { now: options.now } : {}),
|
|
125
189
|
log: (event, data) => debug.log('hooks.reports', event, data), wake: () => { reportNextAt = 0; void drainReportQueue(); } });
|
|
190
|
+
if (consultEnabled && url.pathname === '/v1/consult' && (request.method === 'POST' || request.method === 'OPTIONS')) {
|
|
191
|
+
const origin = consultOrigin(request.headers.get('origin'));
|
|
192
|
+
if (!origin) {
|
|
193
|
+
debug.log('hooks.consult', 'rejected', { reason: 'origin' });
|
|
194
|
+
return Response.json({ error: 'forbidden' }, { status: 403 });
|
|
195
|
+
}
|
|
196
|
+
const headers = { 'Access-Control-Allow-Origin': origin, Vary: 'Origin',
|
|
197
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' };
|
|
198
|
+
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers });
|
|
199
|
+
const response = await handleConsultPost(request, { limiter: consultLimiter, queue: consultQueue, ...(options.now ? { now: options.now } : {}),
|
|
200
|
+
ip: server.requestIP(request)?.address ?? 'unknown',
|
|
201
|
+
log: (event, data) => debug.log('hooks.consult', event, data), wake: () => { consultNextAt = 0; void drainConsultQueue(); } });
|
|
202
|
+
for (const [key, value] of Object.entries(headers)) response.headers.set(key, value);
|
|
203
|
+
return response;
|
|
204
|
+
}
|
|
126
205
|
if (request.method !== 'POST' || !/^\/hooks\/(linear|asana|github)$/.test(url.pathname)) return new Response('Not Found', { status: 404 });
|
|
127
206
|
const provider = url.pathname.slice('/hooks/'.length) as HookProvider;
|
|
128
207
|
const reject = (reason: string) => {
|