elanous 0.2.21 → 0.2.22
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/1307-08b716476f1d50af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
- package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.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/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/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/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/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/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/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/graphs/catalog/catalog.yaml +1 -1
- package/graphs/drafts/recipes.yaml +2 -2
- package/graphs/test-diet/recipes.yaml +3 -0
- package/graphs/test-diet/test-diet.yaml +3 -1
- package/package.json +1 -1
- package/scripts/f-session-monitor.ts +69 -0
- package/scripts/hq/standby-snapshot.ts +7 -3
- package/scripts/knowledge-compress.ts +11 -0
- package/scripts/nightly-docops.ts +90 -3
- package/scripts/research-coo-admin-dates.ts +39 -0
- package/scripts/skills/public-check.ts +7 -2
- package/scripts/smart-merge/resync-shadow.ts +5 -3
- package/scripts/test-diet/lib.ts +81 -1
- package/scripts/test-diet/node.ts +48 -2
- package/scripts/verify-coo-admin-evidence.ts +83 -0
- package/src/acp/client.ts +4 -2
- package/src/agent/run-context.ts +4 -0
- package/src/agent-env/profile.ts +25 -5
- package/src/agent-mission/driver.ts +26 -12
- package/src/agent-mission/supervisor-metrics.ts +17 -2
- package/src/agent-substrate/acp-reviewer.ts +12 -5
- package/src/agent-substrate/pr-reviewer.ts +11 -4
- package/src/agent-substrate/review-intent.ts +102 -3
- package/src/autopilot/build/llm-conflict-merge.ts +22 -8
- package/src/autopilot/pr-manager.ts +18 -8
- package/src/away/release-watch.ts +47 -8
- package/src/briefing/brief-items.ts +104 -11
- package/src/chat/index.ts +1 -0
- package/src/cli/brief-cli.ts +20 -2
- package/src/cli/config-classify.ts +83 -0
- package/src/cli/control-cli.ts +14 -0
- package/src/cli/decisions-cli.ts +26 -16
- package/src/cli/logs-model-input.ts +1 -1
- package/src/cli/pr-cli.ts +65 -8
- package/src/cli/release-cli.ts +21 -4
- package/src/cli/tasks-cli.ts +108 -1
- package/src/connectors/types.ts +6 -6
- package/src/context-bus/context-now-surfaces.ts +18 -0
- package/src/context-bus/coord-events.ts +32 -4
- package/src/context-bus/long-term-memory.ts +23 -6
- package/src/context-bus/nightly-condense.ts +169 -21
- package/src/control/control-status.ts +87 -0
- package/src/control-plane/light-member.ts +5 -3
- package/src/control-plane/member.ts +172 -7
- package/src/core-turn/run-goal-loop.ts +22 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
- package/src/decisions/decision-cards.ts +20 -1
- package/src/decisions/decision-ledger.ts +11 -8
- package/src/decisions/web-push-decision-cards.ts +19 -3
- package/src/discord-self-message.ts +2 -2
- package/src/domains/coo-admin-dates.evidence.json +876 -0
- package/src/domains/coo-admin-tool.ts +48 -14
- package/src/domains/ops-status.ts +53 -0
- package/src/domains/outbound-alert.ts +24 -1
- package/src/exec-requests/planner.ts +6 -0
- package/src/graph-kinds/registry.ts +6 -6
- package/src/graph-runner/graph-cli.ts +34 -2
- package/src/graph-runner/graph-run-api.ts +7 -0
- package/src/graph-runner/graph-tick.ts +73 -12
- package/src/graph-runner/runner.ts +43 -11
- package/src/graph-unify/wf-to-graph.ts +79 -0
- package/src/graph-wizard/generate.ts +34 -2
- package/src/graph-wizard/steps.ts +13 -0
- package/src/harness/harness-cli-command.ts +121 -23
- package/src/harness/harness-queue.ts +58 -14
- package/src/harness/launch-advice.ts +23 -0
- package/src/index.ts +36 -7
- package/src/knowledge/conclusion-compress.ts +93 -0
- package/src/knowledge/know-find.ts +9 -2
- package/src/knowledge/query.ts +20 -0
- package/src/llm.ts +26 -16
- package/src/loops/neighbor-runtime.ts +88 -0
- package/src/loops/orchestrator/author-depth.ts +29 -11
- package/src/loops/orchestrator/overlap.ts +7 -1
- package/src/loops/orchestrator/resync-shadow.ts +1 -1
- package/src/loops/orchestrator/tick.ts +35 -2
- package/src/loops/orchestrator/traffic.ts +2 -0
- package/src/loops/registry.ts +5 -0
- package/src/market/publish.ts +17 -1
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/mss/logging/redaction.ts +17 -0
- package/src/nexus/api/draft-metrics.ts +22 -8
- package/src/nexus/api/graph-peer-edit.ts +77 -0
- package/src/nexus/api/graph-wizard-api.ts +9 -1
- package/src/nexus/api/graphs-api.ts +145 -7
- package/src/nexus/api/http-server.ts +93 -8
- package/src/nexus/api/plugins-market.ts +24 -0
- package/src/nexus/index.ts +3 -2
- package/src/oauth/claude-pty-maturity.ts +24 -0
- package/src/oauth/fallback-chain.ts +27 -14
- package/src/plugins/maker/market-bundle.ts +9 -0
- package/src/plugins/maker/plugin-maker.ts +9 -3
- package/src/plugins/maker/wizard-generate.ts +73 -9
- package/src/policy/llm-policy.ts +11 -6
- package/src/release-loop/checklist-board.ts +27 -0
- package/src/release-loop/checklist.ts +38 -0
- package/src/seat-address/seat-address.ts +1 -0
- package/src/seat-loop/seat-loop.ts +174 -20
- package/src/seat-loop/seat-report.ts +29 -1
- package/src/self-dev/dev-cli.ts +90 -8
- package/src/self-dev/dev-pipeline.ts +20 -1
- package/src/self-dev/draft-sweep.ts +45 -8
- package/src/self-dev/orchestrate.ts +3 -0
- package/src/self-dev/review-provider-fallback.ts +83 -0
- package/src/self-dev/run-supervisor.ts +5 -1
- package/src/self-implement/harness-policy-gates.ts +7 -5
- package/src/self-implement/host-regate.ts +28 -14
- package/src/self-implement/l8-merge-queue.ts +80 -8
- package/src/self-implement/orchestrator.ts +68 -27
- package/src/self-implement/seams.ts +5 -3
- package/src/self-implement/self-implement-cli.ts +4 -0
- package/src/shell-env-bootstrap.ts +19 -4
- package/src/steward/triage.ts +25 -8
- package/src/storage/file-lock.ts +33 -4
- package/src/task-agent/completion-evidence.ts +31 -3
- package/src/task-agent/cover.ts +61 -0
- package/src/task-agent/live-moves.ts +126 -3
- package/src/task-agent/shadow.ts +19 -4
- package/src/task-agent/task-hand.ts +2 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
- package/src/telegram-commands.ts +8 -0
- package/src/telegram-project-command.ts +6 -0
- package/src/telegram.ts +2 -2
- package/src/typecheck-ratchet.ts +2 -1
- package/src/user-config.ts +35 -4
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +24 -2
- package/src/workflow-runtime/triggers/registry.ts +27 -0
- package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
- package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
- package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { openMsgStore } from '../msg/msg-store.js';
|
|
3
|
+
import { judgeNeighbor, type Neighbor } from './neighbors.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type LoopHealth = 'healthy' | 'degraded';
|
|
7
|
+
export type NeighborIncident = { observer: string; neighbor: string; state: 'absent'; reason: string; action: Neighbor['onAbsent']['action']; to: string; at: string; lastSeenAt: string | null };
|
|
8
|
+
|
|
9
|
+
export const SEAT_NEIGHBORS: Record<string, Neighbor[]> = Object.fromEntries(['OP', 'TC', 'MK', 'UX'].map(seat => [seat,
|
|
10
|
+
[
|
|
11
|
+
...['OP', 'TC', 'MK', 'UX'].filter(peer => peer !== seat).map(peer => ({
|
|
12
|
+
id: peer === 'MK' ? 'cmo-seat' : `${peer.toLowerCase()}-seat`, exchange: ['status'] as Neighbor['exchange'],
|
|
13
|
+
heartbeat: { everyMinutes: 15, missedTicks: 2 },
|
|
14
|
+
onAbsent: peer === 'OP' ? { action: 'delegate' as const, delegateTo: 'steward' }
|
|
15
|
+
: peer === 'TC' ? { action: 'escalate' as const, delegateTo: 'orchestrator' }
|
|
16
|
+
: { action: 'defer' as const, delegateTo: 'orchestrator' },
|
|
17
|
+
})),
|
|
18
|
+
{ id: 'orchestrator', exchange: ['status'], heartbeat: { everyMinutes: 120, missedTicks: 2 },
|
|
19
|
+
onAbsent: { action: 'escalate', delegateTo: 'human' } },
|
|
20
|
+
],
|
|
21
|
+
]));
|
|
22
|
+
|
|
23
|
+
type Signal = { at: string; health: LoopHealth; ends_at: string | null };
|
|
24
|
+
|
|
25
|
+
function withStore<T>(root: string, fn: (store: ReturnType<typeof openMsgStore>) => T): T {
|
|
26
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
27
|
+
try {
|
|
28
|
+
store.db.exec(`CREATE TABLE IF NOT EXISTS loop_signals (loop_id TEXT PRIMARY KEY, at TEXT NOT NULL, health TEXT NOT NULL, ends_at TEXT);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS loop_watches (observer TEXT NOT NULL, neighbor TEXT NOT NULL, first_at TEXT NOT NULL, PRIMARY KEY(observer, neighbor));
|
|
30
|
+
CREATE TABLE IF NOT EXISTS loop_incidents (observer TEXT NOT NULL, neighbor TEXT NOT NULL, signal_at TEXT NOT NULL,
|
|
31
|
+
reason TEXT NOT NULL, action TEXT NOT NULL, recipient TEXT NOT NULL, at TEXT NOT NULL,
|
|
32
|
+
PRIMARY KEY(observer, neighbor, signal_at, reason));`);
|
|
33
|
+
return fn(store);
|
|
34
|
+
} finally { store.close(); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Durable signal, independent of chat sessions or the seat work-item ledger. */
|
|
38
|
+
export function emitLoopHeartbeat(root: string, loopId: string, health: LoopHealth, now: Date, endsAt?: string): void {
|
|
39
|
+
if (!/^[a-z][a-z0-9-]*$/.test(loopId) || !Number.isFinite(now.getTime())) throw new Error('invalid loop heartbeat');
|
|
40
|
+
if (endsAt && !Number.isFinite(Date.parse(endsAt))) throw new Error('invalid heartbeat end');
|
|
41
|
+
withStore(root, store => {
|
|
42
|
+
store.db.query(`INSERT INTO loop_signals(loop_id, at, health, ends_at) VALUES(?, ?, ?, ?)
|
|
43
|
+
ON CONFLICT(loop_id) DO UPDATE SET at=excluded.at, health=excluded.health, ends_at=excluded.ends_at
|
|
44
|
+
WHERE loop_signals.at <= excluded.at`).run(loopId, now.toISOString(), health, endsAt ?? null);
|
|
45
|
+
});
|
|
46
|
+
debug.log(`loop.${loopId}`, 'heartbeat', { loopId, at: now.toISOString(), health, endsAt: endsAt ?? null });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** First watch starts the missing-signal clock. Action receipt and message commit atomically. */
|
|
50
|
+
export function checkLoopNeighbors(root: string, observer: string, neighbors: readonly Neighbor[], now: Date): NeighborIncident[] {
|
|
51
|
+
return withStore(root, store => {
|
|
52
|
+
const incidents: NeighborIncident[] = [];
|
|
53
|
+
for (const neighbor of neighbors) {
|
|
54
|
+
if (neighbor.id === observer) continue;
|
|
55
|
+
try {
|
|
56
|
+
store.db.exec('BEGIN IMMEDIATE');
|
|
57
|
+
store.db.query('INSERT OR IGNORE INTO loop_watches(observer, neighbor, first_at) VALUES(?, ?, ?)').run(observer, neighbor.id, now.toISOString());
|
|
58
|
+
const signal = store.db.query('SELECT at, health, ends_at FROM loop_signals WHERE loop_id=?').get(neighbor.id) as Signal | null;
|
|
59
|
+
const watch = store.db.query('SELECT first_at FROM loop_watches WHERE observer=? AND neighbor=?').get(observer, neighbor.id) as { first_at: string };
|
|
60
|
+
const judgment = judgeNeighbor({ neighbor, lastSeenAt: signal?.at ?? watch.first_at, now });
|
|
61
|
+
const expired = !!signal?.ends_at && Date.parse(signal.ends_at) <= now.getTime();
|
|
62
|
+
// A declared lease (ends_at in the future) outranks the heartbeat interval: a scheduled loop idle between
|
|
63
|
+
// windows is not absent until its own declared end passes.
|
|
64
|
+
const leased = !!signal?.ends_at && !expired;
|
|
65
|
+
const absent = expired || signal?.health === 'degraded' || (!leased && judgment.state === 'absent');
|
|
66
|
+
if (!absent) { store.db.exec('COMMIT'); continue; }
|
|
67
|
+
const reason = expired ? 'end-expired' : signal?.health === 'degraded' ? 'degraded' : judgment.reason;
|
|
68
|
+
const signalAt = signal?.at ?? watch.first_at;
|
|
69
|
+
const { action, delegateTo: to } = neighbor.onAbsent;
|
|
70
|
+
const inserted = store.db.query(`INSERT OR IGNORE INTO loop_incidents(observer, neighbor, signal_at, reason, action, recipient, at)
|
|
71
|
+
VALUES(?, ?, ?, ?, ?, ?, ?)`).run(observer, neighbor.id, signalAt, reason, action, to, now.toISOString());
|
|
72
|
+
if (inserted.changes) {
|
|
73
|
+
const incident: NeighborIncident = { observer, neighbor: neighbor.id, state: 'absent', reason, action, to,
|
|
74
|
+
at: now.toISOString(), lastSeenAt: signal?.at ?? null };
|
|
75
|
+
store.append({ from: observer, to, kind: `neighbor-${action}`,
|
|
76
|
+
body: `[${observer} → ${to}] ${neighbor.id} ${reason}: ${action}` });
|
|
77
|
+
incidents.push(incident);
|
|
78
|
+
}
|
|
79
|
+
store.db.exec('COMMIT');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
try { store.db.exec('ROLLBACK'); } catch { /* no open transaction */ }
|
|
82
|
+
debug.log('loop.neighbors', 'check-failed', { observer, neighbor: neighbor.id, error: String(error) }, { level: 'warn' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const incident of incidents) debug.log('loop.neighbors', 'absent', incident, { level: 'warn' });
|
|
86
|
+
return incidents;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -12,7 +12,7 @@ import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../
|
|
|
12
12
|
import { checkAuthorInput } from './author-input-check.js';
|
|
13
13
|
import { AuthorLedger } from './author-ledger.js';
|
|
14
14
|
import { collectWork, findOverlaps, type WorkItem } from './overlap.js';
|
|
15
|
-
import { seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
|
|
15
|
+
import { isTrafficLaunch, seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
|
|
16
16
|
|
|
17
17
|
export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'> & { version: string };
|
|
18
18
|
export type AuthorQueueRow = Pick<QueueItem, 'seat' | 'status'> & Partial<Pick<QueueItem, 'input' | 'kind'>>;
|
|
@@ -101,8 +101,12 @@ export interface CollectAuthorDepthDeps {
|
|
|
101
101
|
versions?: () => readonly [string, string];
|
|
102
102
|
ledger?: Pick<AuthorLedger, 'request'>;
|
|
103
103
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
104
|
+
listProcesses?: typeof defaultListHarnessProcesses;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/** Budget for the shadow's process read (it runs inside the queue lock, whose waiters give up after 30 s). */
|
|
108
|
+
export const AUTHOR_DEPTH_PROCESS_DEADLINE_MS = 10_000;
|
|
109
|
+
|
|
106
110
|
function defaultVersions(now: Date, root = releaseLedgerRoot()): readonly [string, string] {
|
|
107
111
|
if (!existsSync(join(root, 'release', 'features.sqlite'))) throw new Error('release schedule ledger unavailable');
|
|
108
112
|
const upcoming = listSchedules(root).filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
|
|
@@ -122,15 +126,6 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
|
|
|
122
126
|
};
|
|
123
127
|
const cfg = deps.caps && deps.running ? null : observe('config', () => getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS);
|
|
124
128
|
const caps = deps.caps ? observe('caps', deps.caps) : cfg === null ? null : observe('caps', () => cfg.seatCaps);
|
|
125
|
-
const running = deps.running ? observe('running', deps.running) : cfg === null ? null : observe('running', () => {
|
|
126
|
-
const observation = defaultListHarnessProcesses();
|
|
127
|
-
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
128
|
-
const processes = observation.records.map(process => ({ ...process,
|
|
129
|
-
seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
|
|
130
|
-
const measured = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: [] });
|
|
131
|
-
if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
|
|
132
|
-
return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
|
|
133
|
-
});
|
|
134
129
|
const queued = observe('queue', deps.queued ?? (() => listHarnessQueue({ root: deps.root })));
|
|
135
130
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
136
131
|
const versions = observe('versions', deps.versions ?? (() => defaultVersions(now, releaseRoot)));
|
|
@@ -139,7 +134,30 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
|
|
|
139
134
|
throw new Error('checklist ledger unavailable');
|
|
140
135
|
return deps.cells?.(version) ?? listChecklist(version, deps.root).items.map(item => ({ ...item, version }));
|
|
141
136
|
}));
|
|
142
|
-
|
|
137
|
+
// Without release cells there is no candidate to compare, and the host reads below are the expensive ones (every launch's
|
|
138
|
+
// cwd · open PRs over the network) — a fresh --test universe has no release ledger, so it skips them (10-08 tick hang).
|
|
139
|
+
const noCells = cells === null && !deps.cells;
|
|
140
|
+
if (noCells && !deps.running && cfg !== null) unreadable.push({ source: 'running', reason: 'skipped: no release cells to compare' });
|
|
141
|
+
const running = deps.running ? observe('running', deps.running) : cfg === null || noCells ? null : observe('running', () => {
|
|
142
|
+
// Only launch processes are counted, so only they need a cwd; the whole read is bounded because the queue tick runs
|
|
143
|
+
// this shadow inside its lock (10-08: ~490 host processes × lsof kept a --test tick for minutes).
|
|
144
|
+
const started = Date.now();
|
|
145
|
+
const observation = (deps.listProcesses ?? defaultListHarnessProcesses)({ include: record => isTrafficLaunch(record.command),
|
|
146
|
+
cwdOnly: true, deadlineMs: AUTHOR_DEPTH_PROCESS_DEADLINE_MS });
|
|
147
|
+
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
148
|
+
const late = observation.records.filter(process => process.cwdFailureReason?.startsWith('observation deadline')).length;
|
|
149
|
+
(deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data)))(
|
|
150
|
+
'loop.orchestrator', 'author-depth-processes', { launches: observation.records.length, late, ms: Date.now() - started,
|
|
151
|
+
deadlineMs: AUTHOR_DEPTH_PROCESS_DEADLINE_MS });
|
|
152
|
+
if (late) throw new Error(`process observation deadline ${AUTHOR_DEPTH_PROCESS_DEADLINE_MS}ms: ${late} launch cwd(s) unread`);
|
|
153
|
+
const processes = observation.records.map(process => ({ ...process,
|
|
154
|
+
seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
|
|
155
|
+
const measured = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: [] });
|
|
156
|
+
if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
|
|
157
|
+
return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
|
|
158
|
+
});
|
|
159
|
+
if (noCells && !deps.overlaps) unreadable.push({ source: 'overlap', reason: 'skipped: no release cells to compare' });
|
|
160
|
+
const overlaps = noCells && !deps.overlaps ? null : observe('overlap', deps.overlaps ?? (() => {
|
|
143
161
|
const observed = collectWork();
|
|
144
162
|
if (observed.unreadableTrees || observed.unreadableGoals || observed.work.some(item => item.unreadable))
|
|
145
163
|
throw new Error(`incomplete work observation: trees=${observed.unreadableTrees} goals=${observed.unreadableGoals}`);
|
|
@@ -19,6 +19,9 @@ export interface CollectWorkDeps {
|
|
|
19
19
|
config?: Pick<OrchestratorLoopConfig, 'seatTrees'>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Budget for collectWork's per-process cwd reads. */
|
|
23
|
+
export const COLLECT_WORK_PROCESS_DEADLINE_MS = 10_000;
|
|
24
|
+
|
|
22
25
|
const repoRoot = resolve(import.meta.dir, '../../..');
|
|
23
26
|
const defaultRunGh = (args: string[]): string => execFileSync('bun', ['bin/elanous.mjs', '--test', 'gh', ...args], {
|
|
24
27
|
cwd: repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 20 * 1024 * 1024,
|
|
@@ -63,7 +66,10 @@ function goalFiles(text: string): string[] {
|
|
|
63
66
|
export function collectWork(deps: CollectWorkDeps = {}): CollectWorkResult {
|
|
64
67
|
const prs = JSON.parse((deps.runGh ?? defaultRunGh)(['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,body,headRefName,files,isDraft,labels'])) as OpenPr[];
|
|
65
68
|
if (!Array.isArray(prs)) throw new Error('PR observation is not a list');
|
|
66
|
-
|
|
69
|
+
// Only goal-carrying launches become work, so only they need a cwd; bounded so a host with hundreds of harness processes
|
|
70
|
+
// cannot hold the queue lock for minutes (10-08). Records past the budget stay cwd-unknown → their goal reads unreadable.
|
|
71
|
+
const observation = deps.listProcesses ? deps.listProcesses()
|
|
72
|
+
: defaultListHarnessProcesses({ include: record => goalPath(record.command) !== null, cwdOnly: true, deadlineMs: COLLECT_WORK_PROCESS_DEADLINE_MS });
|
|
67
73
|
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
68
74
|
const cfg = deps.config ?? getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
|
|
69
75
|
const excluded = { stalled: 0, superseded: 0, draft: 0 };
|
|
@@ -171,7 +171,7 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}): Pro
|
|
|
171
171
|
entry.resolvedFiles = outcome.resolvedFiles ?? [];
|
|
172
172
|
if (outcome.sizeCollapse?.length || outcome.providerFailure?.length || outcome.testDeclarationLoss?.length) {
|
|
173
173
|
entry.verdict = 'guard-tripped';
|
|
174
|
-
} else if (outcome.status === 'merged' || outcome.status === 'llm-resolved') {
|
|
174
|
+
} else if (outcome.status === 'merged' || outcome.status === 'llm-resolved' || outcome.status === 'deterministic-resolved') {
|
|
175
175
|
if (entry.testFiles.length === 0) entry.verdict = 'merge-ok-no-tests';
|
|
176
176
|
else {
|
|
177
177
|
const test = (deps.runTests ?? defaultRunTests)(tmp, entry.testFiles);
|
|
@@ -8,6 +8,7 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
|
8
8
|
import { getUserConfig, type OrchestratorLoopConfig } from '../../user-config.js';
|
|
9
9
|
import { rubricPriority } from '../../release-loop/rubric.js';
|
|
10
10
|
import { debug } from '../../debug/log.js';
|
|
11
|
+
import { emitLoopHeartbeat } from '../neighbor-runtime.js';
|
|
11
12
|
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
|
|
12
13
|
import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
|
|
13
14
|
import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
|
|
@@ -48,7 +49,8 @@ export type PlacementInput = { id: string; title: string; owner: string; priorit
|
|
|
48
49
|
export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
49
50
|
export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
|
|
50
51
|
export interface TickDeps {
|
|
51
|
-
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
52
|
+
root?: string; now?: Date | (() => Date); mode?: Mode; runId?: string; window?: Window;
|
|
53
|
+
heartbeatIntervalMs?: number;
|
|
52
54
|
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
53
55
|
degradation?: () => { llm: boolean; board: boolean };
|
|
54
56
|
gridHosts?: readonly GridHost[];
|
|
@@ -101,6 +103,19 @@ export function kstWindow(now: Date): Window {
|
|
|
101
103
|
const hour = Number(new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(now));
|
|
102
104
|
return hour < 12 ? '08' : hour < 18 ? '12' : '18';
|
|
103
105
|
}
|
|
106
|
+
/** KST schedule of graphs/orchestrator/orchestrator.yaml (`0 8,12,18 * * *`). */
|
|
107
|
+
const ORCHESTRATOR_HOURS_KST = [8, 12, 18] as const;
|
|
108
|
+
/** Grace past the next scheduled window before neighbors may call the orchestrator absent. */
|
|
109
|
+
export const ORCHESTRATOR_LEASE_GRACE_MS = 60 * 60_000;
|
|
110
|
+
/** The next scheduled window strictly after `now` — the orchestrator is idle, not absent, until then. */
|
|
111
|
+
export function nextOrchestratorWindowAt(now: Date): Date {
|
|
112
|
+
const kst = new Date(now.getTime() + 9 * 3_600_000);
|
|
113
|
+
for (let day = 0; day < 2; day++) for (const hour of ORCHESTRATOR_HOURS_KST) {
|
|
114
|
+
const at = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() + day, hour) - 9 * 3_600_000;
|
|
115
|
+
if (at > now.getTime()) return new Date(at);
|
|
116
|
+
}
|
|
117
|
+
throw new Error('unreachable: no orchestrator window within two days');
|
|
118
|
+
}
|
|
104
119
|
function kstDay(now: Date): string {
|
|
105
120
|
return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
106
121
|
}
|
|
@@ -371,7 +386,8 @@ async function optionalAdapter<T>(path: string, name: string): Promise<T | undef
|
|
|
371
386
|
|
|
372
387
|
/** A graph command invokes one node; only the per-run file carries data across processes. */
|
|
373
388
|
export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Promise<TickState> {
|
|
374
|
-
const
|
|
389
|
+
const currentTime = () => typeof deps.now === 'function' ? deps.now() : deps.now ?? new Date();
|
|
390
|
+
const now = currentTime();
|
|
375
391
|
const window = deps.window ?? kstWindow(now);
|
|
376
392
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
377
393
|
const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
|
|
@@ -379,6 +395,16 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
379
395
|
const orchestratorConfig = getUserConfig().loops?.orchestrator;
|
|
380
396
|
const configured = deps.mode ?? orchestratorConfig?.mode;
|
|
381
397
|
const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
|
|
398
|
+
const pulse = (health: 'healthy' | 'degraded', at: Date): void => {
|
|
399
|
+
try { emitLoopHeartbeat(root, 'orchestrator', health, at,
|
|
400
|
+
new Date(nextOrchestratorWindowAt(at).getTime() + ORCHESTRATOR_LEASE_GRACE_MS).toISOString()); }
|
|
401
|
+
catch (error) { debug.log('loop.orchestrator', 'heartbeat-failed', { error: String(error).slice(0, 200) }, { level: 'warn' }); }
|
|
402
|
+
};
|
|
403
|
+
if (mode !== 'off') pulse('healthy', now);
|
|
404
|
+
const heartbeatTimer = mode === 'off' ? undefined : setInterval(() => pulse('healthy', currentTime()), deps.heartbeatIntervalMs ?? 60 * 60_000);
|
|
405
|
+
heartbeatTimer?.unref();
|
|
406
|
+
let nodeError: unknown;
|
|
407
|
+
try {
|
|
382
408
|
const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
|
|
383
409
|
state.steps ??= [];
|
|
384
410
|
if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
|
|
@@ -739,6 +765,13 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
739
765
|
mkdirSync(dirname(path), { recursive: true });
|
|
740
766
|
writeFileSync(path, JSON.stringify(state));
|
|
741
767
|
return state;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
nodeError = error;
|
|
770
|
+
throw error;
|
|
771
|
+
} finally {
|
|
772
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
773
|
+
if (mode !== 'off') pulse(nodeError === undefined ? 'healthy' : 'degraded', currentTime());
|
|
774
|
+
}
|
|
742
775
|
}
|
|
743
776
|
|
|
744
777
|
/** COORD-HA — the production supply of `TickDeps.degradation` (config `loops.orchestrator.degradationSignal`). Shadow (the
|
|
@@ -50,6 +50,8 @@ function realOrResolved(path: string): string {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
|
|
53
|
+
/** A `harness ask|say` launch command line — the only processes `trafficTick` counts. */
|
|
54
|
+
export const isTrafficLaunch = (command: string): boolean => IS_LAUNCH.test(command);
|
|
53
55
|
|
|
54
56
|
/** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
|
|
55
57
|
export function trafficTick({ processes, now, caps, lastLaunchAt, idleSince, openCells, nextRound, totalSlots, idleMinutes = 30 }: {
|
package/src/loops/registry.ts
CHANGED
|
@@ -685,6 +685,11 @@ export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistr
|
|
|
685
685
|
return warnings;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
/** Newest-first run history of one loop without re-listing the registry (health reads a longer window than `loop status`). */
|
|
689
|
+
export function loopRecentRuns(id: string, opts: LoopRegistryOptions = {}, count = 5): LoopRun[] {
|
|
690
|
+
return recentRuns(id, opts.stateRoot ?? effectiveInstanceRoot(), count);
|
|
691
|
+
}
|
|
692
|
+
|
|
688
693
|
export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
|
|
689
694
|
const loop = listLoops(opts).find(entry => entry.id === id);
|
|
690
695
|
if (!loop) throw new Error(`loop not found: ${id}`);
|
package/src/market/publish.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface PublishResult {
|
|
|
91
91
|
ok: boolean;
|
|
92
92
|
sequence: number;
|
|
93
93
|
published: Array<{ name: string; version: string; sha256: string; bundled: string[] }>;
|
|
94
|
+
/** With keepExisting: previous index entries whose name has no folder under pluginsDir, carried over unchanged. */
|
|
95
|
+
kept: Array<{ name: string; version: string; sha256: string }>;
|
|
94
96
|
skipped: Array<{ dir: string; reason: string; hits?: InternalRefHit[] }>;
|
|
95
97
|
/** Published anyway, but a reviewer should look (e.g. a third-party graph relying on core recipes). */
|
|
96
98
|
warnings: Array<{ dir: string; graph: string; reason: 'third-party-core-recipe'; recipes: string[] }>;
|
|
@@ -103,6 +105,8 @@ export function publishMarket(input: {
|
|
|
103
105
|
market: { name: string; displayName: string };
|
|
104
106
|
key: { keyId: string; privateKeyPem: string };
|
|
105
107
|
source?: { repoUrl: string; sha: string; basePath: string };
|
|
108
|
+
/** Carry over previous entries whose name is not a plugin folder under pluginsDir (entry, archive and unpacked files untouched). */
|
|
109
|
+
keepExisting?: boolean;
|
|
106
110
|
now?: Date;
|
|
107
111
|
}): PublishResult {
|
|
108
112
|
let sequence = 1;
|
|
@@ -307,6 +311,18 @@ export function publishMarket(input: {
|
|
|
307
311
|
published.push({ name: manifest.name, version: manifest.version, sha256, bundled });
|
|
308
312
|
debug.log('market.publish', 'bundled', { name: manifest.name, version: manifest.version, skills: bundled });
|
|
309
313
|
}
|
|
314
|
+
const kept: PublishResult['kept'] = [];
|
|
315
|
+
if (input.keepExisting && previous) {
|
|
316
|
+
// Folders under pluginsDir own their names (even when skipped); every other previous entry stays as it was.
|
|
317
|
+
const present = new Set(readdirSync(input.pluginsDir).filter(dir => lstatSync(join(input.pluginsDir, dir)).isDirectory()));
|
|
318
|
+
for (const item of previous.plugins) {
|
|
319
|
+
if (present.has(item.name)) continue;
|
|
320
|
+
plugins.push(item);
|
|
321
|
+
kept.push({ name: item.name, version: item.version, sha256: item.artifact.sha256 });
|
|
322
|
+
debug.log('market.publish', 'kept', { name: item.name, version: item.version });
|
|
323
|
+
}
|
|
324
|
+
plugins.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
325
|
+
}
|
|
310
326
|
const index: MarketplaceIndex = { name: input.market.name, interface: { displayName: input.market.displayName },
|
|
311
327
|
sequence, generatedAt: (input.now ?? new Date()).toISOString(), plugins };
|
|
312
328
|
const bytes = Buffer.from(JSON.stringify(index, null, 2) + '\n');
|
|
@@ -345,7 +361,7 @@ export function publishMarket(input: {
|
|
|
345
361
|
signatureText: readFileSync(join(input.outDir, 'index.sig'), 'utf8'), trustedKeys });
|
|
346
362
|
if (!verified.ok) throw new Error(`index verification failed: ${verified.reason}: ${verified.detail}`);
|
|
347
363
|
for (const item of published) debug.log('market.publish', 'published', { name: item.name, version: item.version, sequence });
|
|
348
|
-
return { ok: !skipped.some(item => item.reason === 'internal-reference'), sequence, published, skipped, warnings };
|
|
364
|
+
return { ok: !skipped.some(item => item.reason === 'internal-reference'), sequence, published, kept, skipped, warnings };
|
|
349
365
|
} catch (error) {
|
|
350
366
|
const rawReason = error instanceof Error ? error.message : 'unknown-error';
|
|
351
367
|
const reason = input.key.privateKeyPem && rawReason.includes(input.key.privateKeyPem) ? 'invalid signing key' : rawReason;
|
|
@@ -117,6 +117,7 @@ export const FEATURE_MATURITY = {
|
|
|
117
117
|
widget: 'tool',
|
|
118
118
|
log: 'tool',
|
|
119
119
|
loops: 'beta',
|
|
120
|
+
board: 'beta',
|
|
120
121
|
persona: 'beta', // G3 b — 대화별 페르소나(GOAL-BENCH B7 #23836)
|
|
121
122
|
memory: 'beta',
|
|
122
123
|
export: 'stable',
|
|
@@ -161,7 +162,7 @@ export const FEATURE_MATURITY = {
|
|
|
161
162
|
ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
|
|
162
163
|
decisions: 'stable', work: 'stable', cancel: 'stable',
|
|
163
164
|
skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
|
|
164
|
-
taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
|
|
165
|
+
taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', board: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
|
|
165
166
|
brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
|
|
166
167
|
harness: 'ops',
|
|
167
168
|
// Existing commands outside the CXO four-grade menu list are owner-only.
|
|
@@ -93,3 +93,20 @@ export function redactLogRecord(rec: LogRecord, opts: RedactOpts = {}): LogRecor
|
|
|
93
93
|
const mask = opts.mask ?? defaultMask;
|
|
94
94
|
return { ...rec, data: redactValue(rec.data, keyBlock, mask, new WeakSet()) };
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/** Pre-bundle knowledge-pack DLP. Reasons are fixed labels, never source text or secret values.
|
|
98
|
+
* This is separate from the opt-in log masking path above. */
|
|
99
|
+
const KNOWLEDGE_PACK_DLP_RULES: ReadonlyArray<{ reason: string; pattern: RegExp }> = [
|
|
100
|
+
{ reason: 'secret-value', pattern: /-----BEGIN (?:[A-Z0-9_-]+ )?PRIVATE KEY(?: BLOCK)?-----|\bBearer\s+[A-Za-z0-9._~+/-]{12,}={0,2}\b|\b(?:sk-[A-Za-z0-9_-]{16,}|xai-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|(?:AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}|AIza[A-Za-z0-9_-]{35})\b|\b(?:api[_-]?key|secret[_-]?key|access[_-]?token|private[_-]?key|token)['"]?\s*[=:]\s*['"]?[A-Za-z0-9_./+~-]{16,}/iu },
|
|
101
|
+
// A key that names a password/secret makes ANY assigned value secret: short or punctuated values count too.
|
|
102
|
+
// Schema flags (`"secret": true`) are not values.
|
|
103
|
+
{ reason: 'secret-value', pattern: /\b(?:password|passwd|pwd|passphrase|client[_-]?secret|secret)['"]?\s*[=:]\s*(?!(?:true|false|null)\b)(?:"[^"\n]+"|'[^'\n]+'|[^\s'",;{}\[\]]{4,})/iu },
|
|
104
|
+
{ reason: 'personal-information', pattern: /\b01[016789]-?\d{3,4}-?\d{4}\b|\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b|\b\d{6}-[1-8]\d{6}\b/iu },
|
|
105
|
+
{ reason: 'internal-identifier', pattern: /(?:사번|사내\s*식별자|내부\s*식별자|employee[_ -]?id|internal[_ -]?id)\s*(?:[:=]|\s)\s*[A-Za-z0-9][A-Za-z0-9_-]{3,}/iu },
|
|
106
|
+
{ reason: 'sales-confidential', pattern: /영업\s*기밀|(?:sales\s+confidential|confidential\s+sales|영업\s*비밀)/iu },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
export function scanKnowledgePackDlp(text: string): string[] {
|
|
110
|
+
const reasons = KNOWLEDGE_PACK_DLP_RULES.filter(({ pattern }) => pattern.test(text)).map(({ reason }) => reason);
|
|
111
|
+
return [...new Set(reasons)];
|
|
112
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { debug } from '../../debug/log.js';
|
|
3
|
-
import type { DraftMetrics } from '../../self-dev/draft-sweep.js';
|
|
3
|
+
import type { DraftMetrics, OverlapMetrics } from '../../self-dev/draft-sweep.js';
|
|
4
|
+
|
|
5
|
+
export type CeoDraftMetrics = DraftMetrics & { readonly overlap: OverlapMetrics | null };
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* DRAFT-METRIC — CEO view source. Opening the page must never trigger a GitHub read:
|
|
@@ -14,7 +16,7 @@ export type DraftMetricsState = 'measuring' | 'ready' | 'unavailable';
|
|
|
14
16
|
export interface DraftMetricsSnapshot {
|
|
15
17
|
/** measuring = no value yet and a collection is running · unavailable = no value and the last collection failed. */
|
|
16
18
|
readonly state: DraftMetricsState;
|
|
17
|
-
readonly metrics:
|
|
19
|
+
readonly metrics: CeoDraftMetrics | null;
|
|
18
20
|
/** When the served value was collected (ISO). null without a value. */
|
|
19
21
|
readonly measuredAt: string | null;
|
|
20
22
|
/** A background collection is in flight. */
|
|
@@ -24,7 +26,7 @@ export interface DraftMetricsSnapshot {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface DraftMetricsSourceOptions {
|
|
27
|
-
readonly collect?: () => Promise<
|
|
29
|
+
readonly collect?: () => Promise<CeoDraftMetrics>;
|
|
28
30
|
readonly ttlMs?: number;
|
|
29
31
|
readonly failureRetryMs?: number;
|
|
30
32
|
readonly now?: () => number;
|
|
@@ -43,17 +45,29 @@ function isMetrics(value: unknown): value is DraftMetrics {
|
|
|
43
45
|
&& nullableNumber('oldestAgeHours') && nullableNumber('conversion48h');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
function isOverlapMetrics(value: unknown): value is OverlapMetrics {
|
|
49
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
50
|
+
const row = value as Record<string, unknown>;
|
|
51
|
+
const count = (key: string) => row[key] === null || (Number.isInteger(row[key]) && (row[key] as number) >= 0);
|
|
52
|
+
const nullableNumber = (key: string) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key]) && (row[key] as number) >= 0);
|
|
53
|
+
return ['launches24h', 'launched', 'unmeasured', 'linked', 'autoLanded', 'salvaged', 'salvageUnmeasured'].every(count)
|
|
54
|
+
&& typeof row.sourceIncomplete === 'boolean' && nullableNumber('secondSiblingMedianHours')
|
|
55
|
+
&& (row.autoRate === null || (typeof row.autoRate === 'number' && Number.isFinite(row.autoRate) && row.autoRate >= 0 && row.autoRate <= 1));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parses the CLI's single JSON line; malformed draft values fail, malformed overlap is unmeasured. */
|
|
59
|
+
export function parseDraftMetricsOutput(stdout: string): CeoDraftMetrics {
|
|
48
60
|
const line = stdout.trim().split('\n').reverse().find((text) => text.trim().startsWith('{'));
|
|
49
61
|
if (!line) throw new Error('draft metrics: no JSON output');
|
|
50
62
|
const parsed: unknown = JSON.parse(line);
|
|
51
63
|
if (!isMetrics(parsed)) throw new Error('draft metrics: malformed output');
|
|
52
64
|
const { inventory, oldestAgeHours, needsOwner, converted48h, cohort48h, conversion48h } = parsed;
|
|
53
|
-
|
|
65
|
+
const candidate = (parsed as DraftMetrics & { overlap?: unknown }).overlap;
|
|
66
|
+
return { inventory, oldestAgeHours, needsOwner, converted48h, cohort48h, conversion48h,
|
|
67
|
+
overlap: isOverlapMetrics(candidate) ? candidate : null };
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
async function collectViaCli(): Promise<
|
|
70
|
+
async function collectViaCli(): Promise<CeoDraftMetrics> {
|
|
57
71
|
const root = join(import.meta.dir, '../../..');
|
|
58
72
|
const proc = Bun.spawn(['bun', join(root, 'bin/elanous.mjs'), 'harness', 'drafts', 'metrics', '--json'], {
|
|
59
73
|
cwd: root, stdout: 'pipe', stderr: 'pipe',
|
|
@@ -75,7 +89,7 @@ export function createDraftMetricsSource(options: DraftMetricsSourceOptions = {}
|
|
|
75
89
|
const ttl = options.ttlMs ?? DRAFT_METRICS_TTL_MS;
|
|
76
90
|
const retry = options.failureRetryMs ?? DRAFT_METRICS_FAILURE_RETRY_MS;
|
|
77
91
|
const now = options.now ?? Date.now;
|
|
78
|
-
let value: { metrics:
|
|
92
|
+
let value: { metrics: CeoDraftMetrics; at: number } | null = null;
|
|
79
93
|
let failure: { reason: string; at: number } | null = null;
|
|
80
94
|
let inFlight: Promise<void> | null = null;
|
|
81
95
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// W9c run guard (TC security review of #25123) — a peer «edit» grant lets another token rewrite a «mine» graph.
|
|
2
|
+
// Running that graph would run the peer's change with this machine's privileges, so a peer save leaves a
|
|
3
|
+
// marker next to the graph's access grants (`<mine>/.access/<id>.peer-edit.json`) and every run entry for a
|
|
4
|
+
// «mine» graph refuses it until the owner approves that exact version (or re-saves it themselves).
|
|
5
|
+
// Fail closed: a marker that exists but cannot be read or parsed refuses the run.
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export const PEER_EDIT_REFUSAL = '상대가 바꾼 그래프 — 변경을 확인하고 승인해야 실행할 수 있다';
|
|
11
|
+
|
|
12
|
+
export interface PeerEditRecord {
|
|
13
|
+
/** `peer:<first 8 hex of the peer token's sha256>` — never the token itself. */
|
|
14
|
+
editedBy: string;
|
|
15
|
+
/** The version id the peer save produced (`pending` while the save is in flight). */
|
|
16
|
+
version: string;
|
|
17
|
+
at: string;
|
|
18
|
+
approved?: { version: string; at: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PeerEditGate =
|
|
22
|
+
| { ok: true }
|
|
23
|
+
| { ok: false; error: 'peer-edit-unapproved'; record: PeerEditRecord }
|
|
24
|
+
| { ok: false; error: 'peer-edit-unreadable'; reason: string };
|
|
25
|
+
|
|
26
|
+
export function peerEditFile(mineDir: string, id: string): string {
|
|
27
|
+
return join(mineDir, '.access', `${id}.peer-edit.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** null = no peer ever saved this graph. Throws when the marker exists but is unreadable or malformed. */
|
|
31
|
+
export function readPeerEdit(mineDir: string, id: string): PeerEditRecord | null {
|
|
32
|
+
let text: string;
|
|
33
|
+
try { text = readFileSync(peerEditFile(mineDir, id), 'utf8'); }
|
|
34
|
+
catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
const parsed: unknown = JSON.parse(text);
|
|
39
|
+
const record = parsed as PeerEditRecord;
|
|
40
|
+
if (!parsed || typeof parsed !== 'object' || typeof record.editedBy !== 'string' || typeof record.version !== 'string'
|
|
41
|
+
|| typeof record.at !== 'string'
|
|
42
|
+
|| (record.approved !== undefined && (typeof record.approved !== 'object' || record.approved === null
|
|
43
|
+
|| typeof record.approved.version !== 'string' || typeof record.approved.at !== 'string'))) {
|
|
44
|
+
throw new Error('invalid peer-edit marker');
|
|
45
|
+
}
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writePeerEdit(mineDir: string, id: string, record: PeerEditRecord): void {
|
|
50
|
+
mkdirSync(join(mineDir, '.access'), { recursive: true, mode: 0o700 });
|
|
51
|
+
const target = peerEditFile(mineDir, id);
|
|
52
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(temp, JSON.stringify(record), { mode: 0o600 });
|
|
55
|
+
renameSync(temp, target);
|
|
56
|
+
} finally { rmSync(temp, { force: true }); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An owner-authorized save replaces the peer's bytes with bytes the owner chose. */
|
|
60
|
+
export function clearPeerEdit(mineDir: string, id: string): void {
|
|
61
|
+
rmSync(peerEditFile(mineDir, id), { force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function peerEditRunGate(mineDir: string, id: string): PeerEditGate {
|
|
65
|
+
let record: PeerEditRecord | null;
|
|
66
|
+
try { record = readPeerEdit(mineDir, id); }
|
|
67
|
+
catch (error) { return { ok: false, error: 'peer-edit-unreadable', reason: String(error).slice(0, 200) }; }
|
|
68
|
+
if (!record || record.approved?.version === record.version) return { ok: true };
|
|
69
|
+
return { ok: false, error: 'peer-edit-unapproved', record };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The 409 body every refusing run entry returns — `reason` is what the editor shows. */
|
|
73
|
+
export function peerEditRefusalBody(id: string, gate: Exclude<PeerEditGate, { ok: true }>): Record<string, unknown> {
|
|
74
|
+
return gate.error === 'peer-edit-unapproved'
|
|
75
|
+
? { error: gate.error, id, reason: PEER_EDIT_REFUSAL, editedBy: gate.record.editedBy, version: gate.record.version, editedAt: gate.record.at }
|
|
76
|
+
: { error: gate.error, id, reason: PEER_EDIT_REFUSAL, detail: gate.reason };
|
|
77
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { debug } from '../../debug/log.js';
|
|
2
|
-
import { GraphWizardInputError, generateGraphFromPrompt, type GraphWizardDeps, type GraphWizardTurn } from '../../graph-wizard/generate.js';
|
|
2
|
+
import { GraphWizardInputError, generateGraphFromPrompt, wizardInstalledPacks, type GraphWizardDeps, type GraphWizardTurn } from '../../graph-wizard/generate.js';
|
|
3
3
|
import { jsonResponse } from './json-response.js';
|
|
4
4
|
|
|
5
|
+
export function handleGraphWizardPacksGet(deps: GraphWizardDeps = {}): Response {
|
|
6
|
+
return jsonResponse({ packs: wizardInstalledPacks(deps) });
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
const MAX_PROMPT = 4000;
|
|
6
10
|
const MAX_YAML = 200_000;
|
|
7
11
|
|
|
@@ -28,6 +32,9 @@ export async function handleGraphWizardPost(req: Request, deps: GraphWizardDeps
|
|
|
28
32
|
if (body.currentYaml !== undefined && (typeof body.currentYaml !== 'string' || body.currentYaml.length > MAX_YAML)) {
|
|
29
33
|
return jsonResponse({ error: 'bad_request', reason: 'currentYaml must be a string' }, 400);
|
|
30
34
|
}
|
|
35
|
+
if (body.packId !== undefined && typeof body.packId !== 'string') {
|
|
36
|
+
return jsonResponse({ error: 'bad_request', reason: 'packId must be a string' }, 400);
|
|
37
|
+
}
|
|
31
38
|
let history: GraphWizardTurn[] | undefined;
|
|
32
39
|
if (body.history !== undefined) {
|
|
33
40
|
if (!Array.isArray(body.history) || !body.history.every((t) => t && typeof t === 'object' &&
|
|
@@ -42,6 +49,7 @@ export async function handleGraphWizardPost(req: Request, deps: GraphWizardDeps
|
|
|
42
49
|
...(body.kind ? { kind: body.kind as 'harness' | 'workflow' } : {}),
|
|
43
50
|
...(typeof body.currentYaml === 'string' && body.currentYaml.trim() ? { currentYaml: body.currentYaml } : {}),
|
|
44
51
|
...(history ? { history } : {}),
|
|
52
|
+
...(typeof body.packId === 'string' ? { packId: body.packId } : {}),
|
|
45
53
|
}, deps);
|
|
46
54
|
return jsonResponse({ ...result, base: result.base ?? null }, result.ok ? 200 : 422);
|
|
47
55
|
} catch (error) {
|