elanous 0.2.16 → 0.2.18
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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.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 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- 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/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/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/driver.ts +53 -10
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -5,30 +5,36 @@ import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-st
|
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
6
6
|
import { type ChecklistItem } from '../../release-loop/checklist.js';
|
|
7
7
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
-
import { getUserConfig } from '../../user-config.js';
|
|
8
|
+
import { getUserConfig, type OrchestratorLoopConfig } from '../../user-config.js';
|
|
9
9
|
import { debug } from '../../debug/log.js';
|
|
10
10
|
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
|
|
11
|
-
import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
|
|
12
|
-
import { harnessQueueOutcome } from '../../harness/harness-queue.js';
|
|
11
|
+
import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
|
|
12
|
+
import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
|
|
13
|
+
import { placeOnGrid, type GridHost } from './grid-place.js';
|
|
14
|
+
import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
|
|
15
|
+
import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
|
|
16
|
+
import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
|
|
13
17
|
|
|
14
18
|
export type Window = '08' | '12' | '18';
|
|
15
19
|
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
|
|
16
20
|
export type Mode = 'off' | 'shadow' | 'live';
|
|
17
21
|
const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
|
|
18
|
-
export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
|
|
22
|
+
export interface Cell { id: string; title: string; kind?: string; seat?: string; host?: string; gridHost?: boolean; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
|
|
19
23
|
export interface TickState {
|
|
20
24
|
runId: string; window: Window; mode: Mode; day: string;
|
|
21
25
|
cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
|
|
22
26
|
reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
|
|
23
27
|
runSummaries?: string[];
|
|
24
28
|
launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
|
|
29
|
+
/** ORCH-TA-HAND — cells handed to TASK-AGENT this run (card id · mode · launched). */
|
|
30
|
+
handed?: Array<{ key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean }>;
|
|
25
31
|
queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
|
|
26
32
|
steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
|
|
27
33
|
nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
|
|
28
34
|
missing: string[];
|
|
29
35
|
}
|
|
30
36
|
/** FLOW1a contract: with `{ shadow: true }` the splitter only proposes cells and writes nothing (cards, ledgers, LLM side effects excepted). */
|
|
31
|
-
export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
|
|
37
|
+
export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>;
|
|
32
38
|
/** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
|
|
33
39
|
export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
|
|
34
40
|
export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
@@ -36,12 +42,50 @@ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown
|
|
|
36
42
|
export interface TickDeps {
|
|
37
43
|
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
38
44
|
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
39
|
-
|
|
45
|
+
gridHosts?: readonly GridHost[];
|
|
46
|
+
seatTurn?: (seat: string, root: string, host?: string, options?: Pick<SeatDeps, 'enqueue'>) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
|
|
40
47
|
queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
|
|
48
|
+
/** 발사 전 대기열 항목의 풀 힌트 갱신(주입 안 하면 harness-queue `setQueuedPoolHint`). */
|
|
49
|
+
setPoolHint?: (queueId: string, poolHint: string | undefined, root: string) => Promise<boolean>;
|
|
41
50
|
loadAdapter?: (path: string, name: string) => Promise<unknown>;
|
|
42
51
|
checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
|
|
43
52
|
observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
|
|
44
53
|
print?: (line: string) => void;
|
|
54
|
+
/** ORCH-TA-HAND seams — absent = config `loops.orchestrator.handToTaskAgent(Cell)` · real `handTask` · `defaultTaskLauncher`. */
|
|
55
|
+
handToTaskAgent?: 'off' | 'shadow' | 'live';
|
|
56
|
+
handToTaskAgentCell?: string;
|
|
57
|
+
handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
|
|
58
|
+
taskLauncher?: TaskLauncher;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
|
|
62
|
+
function handedLedger(root: string): string { return join(root, 'loop', 'orchestrator', 'handed.jsonl'); }
|
|
63
|
+
type HandedRow = { key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean; runId?: string };
|
|
64
|
+
/** Completed hands (`status: 'handed'`) by ledger key — a re-entered run restores its `handed[]` from these. */
|
|
65
|
+
function handedRows(path: string): Map<string, HandedRow> {
|
|
66
|
+
const rows = new Map<string, HandedRow>();
|
|
67
|
+
if (!existsSync(path)) return rows;
|
|
68
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
69
|
+
if (!line.trim()) continue;
|
|
70
|
+
let row: Partial<HandedRow> & { status?: unknown };
|
|
71
|
+
try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
|
|
72
|
+
if (row.status === 'handed' && typeof row.key === 'string' && typeof row.cardId === 'string' && typeof row.cell === 'string'
|
|
73
|
+
&& (row.mode === 'shadow' || row.mode === 'live')) rows.set(row.key, { key: row.key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched === true, ...(typeof row.runId === 'string' ? { runId: row.runId } : {}) });
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
function handedKeys(path: string): Set<string> {
|
|
78
|
+
if (!existsSync(path)) return new Set();
|
|
79
|
+
const keys = new Set<string>();
|
|
80
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
81
|
+
if (!line.trim()) continue;
|
|
82
|
+
let row: { key?: unknown; status?: unknown };
|
|
83
|
+
try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
|
|
84
|
+
if (typeof row.key !== 'string') continue;
|
|
85
|
+
// A released claim (a shadow hand that failed before its card) frees the key again — the last row decides.
|
|
86
|
+
if (row.status === 'released') keys.delete(row.key); else keys.add(row.key);
|
|
87
|
+
}
|
|
88
|
+
return keys;
|
|
45
89
|
}
|
|
46
90
|
|
|
47
91
|
export function kstWindow(now: Date): Window {
|
|
@@ -122,10 +166,138 @@ function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id'
|
|
|
122
166
|
try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
|
|
123
167
|
finally { db.close(); }
|
|
124
168
|
}
|
|
169
|
+
/** 명시 인벤토리: 없으면 undefined, «있는데 못 읽으면» null — null 은 풀 명부로 대체하지 않는다. */
|
|
170
|
+
function readGridHosts(): readonly GridHost[] | null | undefined {
|
|
171
|
+
const supplied = process.env.ELANOUS_GRID_HOSTS;
|
|
172
|
+
if (supplied === undefined) return undefined;
|
|
173
|
+
try {
|
|
174
|
+
const inventory: unknown = JSON.parse(supplied);
|
|
175
|
+
if (!Array.isArray(inventory) || !inventory.every(row => row && typeof row === 'object' &&
|
|
176
|
+
typeof row.name === 'string' && row.name.trim() !== '' && Array.isArray(row.capabilities) &&
|
|
177
|
+
row.capabilities.every((capability: unknown) => typeof capability === 'string') &&
|
|
178
|
+
typeof row.available === 'boolean')) return null;
|
|
179
|
+
return inventory.map(row => ({ name: row.name as string, capabilities: new Set<string>(row.capabilities), available: row.available as boolean }));
|
|
180
|
+
} catch { return null; }
|
|
181
|
+
}
|
|
182
|
+
/** 주입 > 명시 인벤토리 > 풀 명부. 명시 인벤토리를 못 읽으면 제안하지 않는다(기존 경로 그대로). */
|
|
183
|
+
function resolveGridHosts(deps: TickDeps, observe: (event: string, reason: string, count: number) => void): readonly GridHost[] | undefined {
|
|
184
|
+
if (deps.gridHosts) return deps.gridHosts;
|
|
185
|
+
const supplied = readGridHosts();
|
|
186
|
+
if (supplied === null) { observe('node-missing', 'grid-inventory-unreadable', 0); return undefined; }
|
|
187
|
+
return supplied ?? gridHostsFromPodPool();
|
|
188
|
+
}
|
|
189
|
+
function preferredPoolSpec(host: string): string | undefined {
|
|
190
|
+
const config = getUserConfig();
|
|
191
|
+
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
192
|
+
if (!spec) return undefined;
|
|
193
|
+
try {
|
|
194
|
+
const members = parsePodPool(spec);
|
|
195
|
+
const member = members.find(candidate => (candidate.sshHost ?? candidate.context) === host);
|
|
196
|
+
if (!member) return undefined;
|
|
197
|
+
const parts = spec.split(',').map(part => part.trim());
|
|
198
|
+
const preferred = parts.find(part => part === member.context ||
|
|
199
|
+
['@', ':', '#'].some(separator => part.startsWith(`${member.context}${separator}`)));
|
|
200
|
+
return preferred ? [preferred, ...parts.filter(part => part !== preferred)].join(',') : undefined;
|
|
201
|
+
} catch { return undefined; }
|
|
202
|
+
}
|
|
203
|
+
function gridHostsFromPodPool(): readonly GridHost[] | undefined {
|
|
204
|
+
try {
|
|
205
|
+
const config = getUserConfig();
|
|
206
|
+
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
207
|
+
if (!spec) return undefined;
|
|
208
|
+
const members = parsePodPool(spec);
|
|
209
|
+
const checked = checkPodPool(members, leaseKubectl);
|
|
210
|
+
const ready = new Set(checked.ready.map(member => member.context));
|
|
211
|
+
return members.map(member => ({
|
|
212
|
+
name: member.sshHost ?? member.context,
|
|
213
|
+
capabilities: new Set(['pod']),
|
|
214
|
+
available: ready.has(member.context),
|
|
215
|
+
}));
|
|
216
|
+
} catch { return undefined; }
|
|
217
|
+
}
|
|
125
218
|
function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
|
|
126
|
-
function readRequests(root: string): Array<{ key: string; status: string }> {
|
|
219
|
+
function readRequests(root: string): Array<{ key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }> {
|
|
127
220
|
const path = journal(root);
|
|
128
|
-
return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string }) : [];
|
|
221
|
+
return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }) : [];
|
|
222
|
+
}
|
|
223
|
+
function refreshGridHosts(cells: Cell[], hosts: readonly GridHost[] | undefined, mode: Mode,
|
|
224
|
+
root: string, observe: (event: string, reason: string, count: number, targetLoopId?: string) => void): void {
|
|
225
|
+
if (hosts === undefined) return;
|
|
226
|
+
for (const cell of cells) {
|
|
227
|
+
if (mode !== 'live' || cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) {
|
|
228
|
+
observe('exchange', `would-host:${cell.id}:${placeOnGrid(cell, hosts).host ?? 'null'}`, 1);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const path = journal(root);
|
|
232
|
+
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
233
|
+
// 의뢰가 이미 대기열을 떠났으면(발사·완료) 제안만 관측하고 칸의 호스트는 그 행의 확정 호스트로 되돌린다.
|
|
234
|
+
const keepLaunched = (row: { host?: string; gridHost?: boolean }): void => {
|
|
235
|
+
if (row.host) cell.host = row.host; else delete cell.host;
|
|
236
|
+
if (row.gridHost) cell.gridHost = true; else delete cell.gridHost;
|
|
237
|
+
};
|
|
238
|
+
let changedFrom: string | undefined;
|
|
239
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
240
|
+
// 상태 확인 · 칸 변경 · 행 갱신은 모두 같은 잠금 안의 한 결정이다 — 다른 틱이 그 사이 행을 만들거나 바꿔도 덮지 않는다.
|
|
241
|
+
withFileLockSync(`${path}.lock`, () => {
|
|
242
|
+
const current = readRequests(root).filter(row => row.key === key).at(-1);
|
|
243
|
+
// 호스트 결정도 잠금 안 — 상태 확인과 같은 순간의 명부로 고른다.
|
|
244
|
+
const choice = placeOnGrid(cell, hosts);
|
|
245
|
+
observe('exchange', `would-host:${cell.id}:${choice.host ?? 'null'}`, 1);
|
|
246
|
+
if (current && current.status !== 'queued') { keepLaunched(current); return; }
|
|
247
|
+
if (choice.host) { cell.host = choice.host; cell.gridHost = true; }
|
|
248
|
+
else if (cell.gridHost || current?.gridHost) { delete cell.host; delete cell.gridHost; }
|
|
249
|
+
if (!current || (current.host === cell.host && current.gridHost === cell.gridHost && (current.kind === cell.kind || !cell.kind))) return;
|
|
250
|
+
if (current.host !== cell.host) changedFrom = current.host;
|
|
251
|
+
const updated = { ...current };
|
|
252
|
+
delete updated.host;
|
|
253
|
+
delete updated.gridHost;
|
|
254
|
+
if (cell.host) updated.host = cell.host;
|
|
255
|
+
if (cell.gridHost) updated.gridHost = true;
|
|
256
|
+
if (cell.kind) updated.kind = cell.kind;
|
|
257
|
+
appendFileSync(path, `${JSON.stringify(updated)}\n`);
|
|
258
|
+
});
|
|
259
|
+
if (changedFrom !== undefined) observe('exchange', `rehost:${cell.id}:${changedFrom}→${cell.host ?? 'none'}`, 1, SEAT_LOOP_IDS[cell.seat]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 재배치된 의뢰가 앞 틱에서 이미 대기열에 들어갔다면(아직 발사 전) 그 항목의 풀 힌트도 의뢰 행의 새 호스트로 맞춘다.
|
|
264
|
+
* 의뢰 키 ↔ 대기열 id 는 live 틱 상태들의 `launched` 에서 찾는다(날짜를 넘겨 대기 중인 항목도). 의뢰 행이 아직 queued 일 때만 옮긴다.
|
|
265
|
+
*/
|
|
266
|
+
async function syncQueuedPoolHints(root: string, current: NonNullable<TickState['launched']>, deps: TickDeps,
|
|
267
|
+
observe: (event: string, reason: string, count: number) => void): Promise<void> {
|
|
268
|
+
const dir = join(root, 'loop', 'orchestrator');
|
|
269
|
+
const turns = [...current];
|
|
270
|
+
if (existsSync(dir)) for (const name of readdirSync(dir).filter(file => /^[a-zA-Z0-9._-]+\.json$/.test(file))) {
|
|
271
|
+
try {
|
|
272
|
+
const other = JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState;
|
|
273
|
+
if (other.mode === 'live') turns.push(...(other.launched ?? []));
|
|
274
|
+
} catch { /* 깨진 상태 파일은 건너뛴다 */ }
|
|
275
|
+
}
|
|
276
|
+
const latest = new Map(readRequests(root).map(row => [row.key, row]));
|
|
277
|
+
const seen = new Set<string>();
|
|
278
|
+
for (const turn of turns) {
|
|
279
|
+
if (!turn.queueId || !turn.key || seen.has(turn.queueId)) continue;
|
|
280
|
+
seen.add(turn.queueId);
|
|
281
|
+
const row = latest.get(turn.key);
|
|
282
|
+
if (!row || row.kind !== 'pod' || row.status !== 'queued') continue;
|
|
283
|
+
// GRID 가 고른 행이면 그 호스트 쪽 힌트로, GRID 가 호스트를 거둔 행(둘 다 막힘)이면 GRID 가 넣었던 힌트만 지운다.
|
|
284
|
+
// GRID 와 무관한 기존 pod 의뢰의 힌트(ORCH1·명시 풀)는 건드리지 않는다.
|
|
285
|
+
const gridChosen = !!(row.gridHost && row.host);
|
|
286
|
+
if (!gridChosen && row.host) continue;
|
|
287
|
+
const hint = gridChosen ? preferredPoolSpec(row.host!) : undefined;
|
|
288
|
+
if (gridChosen && !hint) continue;
|
|
289
|
+
const key = turn.key;
|
|
290
|
+
// 갱신 직전 대기열 잠금 안에서: 의뢰가 아직 queued 이고 행의 호스트가 계산 때와 같으며,
|
|
291
|
+
// 지우는 경우엔 지금 대기열 힌트가 GRID 가 만든 꼴일 때만.
|
|
292
|
+
const stillQueued = (item: { poolHint?: string; poolHintSource?: 'grid' }): boolean => {
|
|
293
|
+
const now = readRequests(root).filter(r => r.key === key).at(-1);
|
|
294
|
+
if (now?.status !== 'queued' || now.host !== row.host || now.gridHost !== row.gridHost) return false;
|
|
295
|
+
// 지우는 쪽은 GRID 가 소유한 힌트만(소유 기록 확인) — 같은 문자열이라도 사람이 넣은 힌트는 남긴다.
|
|
296
|
+
return gridChosen || item.poolHintSource === 'grid';
|
|
297
|
+
};
|
|
298
|
+
if (await (deps.setPoolHint ?? ((id: string, value: string | undefined, stateRoot: string) => setQueuedPoolHint(id, value, { root: stateRoot }, stillQueued)))(turn.queueId, hint, root))
|
|
299
|
+
observe('exchange', `rehost-queue:${turn.key}:${hint ?? 'none'}`, 1);
|
|
300
|
+
}
|
|
129
301
|
}
|
|
130
302
|
function stateFile(root: string, runId: string): string {
|
|
131
303
|
if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
|
|
@@ -215,7 +387,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
215
387
|
const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
|
|
216
388
|
for (const cell of cells) {
|
|
217
389
|
if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
|
|
218
|
-
state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
|
|
390
|
+
state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
|
|
219
391
|
}
|
|
220
392
|
observe('exchange', `split:${card.id}`, cells.length);
|
|
221
393
|
if (split && cells.length && mode === 'live') {
|
|
@@ -226,6 +398,9 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
226
398
|
}
|
|
227
399
|
}
|
|
228
400
|
} else if (node === 'place') {
|
|
401
|
+
const gridHosts = resolveGridHosts(deps, observe);
|
|
402
|
+
refreshGridHosts(state.cells, gridHosts, mode, root, observe);
|
|
403
|
+
if (mode === 'live') await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
|
|
229
404
|
const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
|
|
230
405
|
const available = typeof place === 'function';
|
|
231
406
|
if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
|
|
@@ -251,13 +426,94 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
251
426
|
}
|
|
252
427
|
} else if (node === 'delegate') {
|
|
253
428
|
const path = journal(root);
|
|
429
|
+
// 모드와 칸 필터는 따로 푼다 — 주입으로 모드만 줘도 설정의 칸 필터는 그대로 먹는다.
|
|
430
|
+
const handConfig: Partial<OrchestratorLoopConfig> = deps.handToTaskAgent !== undefined && deps.handToTaskAgentCell !== undefined ? {} : getUserConfig().loops?.orchestrator ?? {};
|
|
431
|
+
// live 로 넘긴 칸은 지금 설정(off·칸 필터)과 무관하게 자리 경로로 다시 안 간다 — 발사 주체가 둘이 되지 않게.
|
|
432
|
+
// ⭐ 넘김 claim 과 자리 저널 기록은 «같은 잠금»(자리 저널 잠금) 아래에서 서로를 다시 읽는다 — 동시 틱이 낡은 값을 보지 않게.
|
|
433
|
+
const ledger = handedLedger(root);
|
|
434
|
+
const liveHanded = handedKeys(ledger);
|
|
254
435
|
const keys = new Set(readRequests(root).map(row => row.key));
|
|
436
|
+
// 넘김 뒤 상태 저장 전에 죽은 같은 run 이 다시 돌면 원장에서 handed[] 를 되살린다.
|
|
437
|
+
const ledgerHanded = handedRows(ledger);
|
|
438
|
+
const restoreHanded = (handKey: string, key: string, rows: Map<string, HandedRow> = handedRows(ledger)): void => {
|
|
439
|
+
const row = rows.get(handKey);
|
|
440
|
+
// 이 run 이 넘긴 것만 되살린다 — 다른 run 의 넘김을 «이 run 에서 넘겼다»로 적지 않게.
|
|
441
|
+
if (!row || row.runId !== runId || (state.handed ?? []).some(entry => entry.key === key && entry.mode === row.mode)) return;
|
|
442
|
+
(state.handed ??= []).push({ key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched });
|
|
443
|
+
};
|
|
444
|
+
const journalHas = (key: string): boolean => {
|
|
445
|
+
if (!existsSync(path)) return false;
|
|
446
|
+
return readFileSync(path, 'utf8').split('\n').some(line => {
|
|
447
|
+
if (!line) return false;
|
|
448
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
|
|
449
|
+
});
|
|
450
|
+
};
|
|
255
451
|
for (const cell of state.cells) {
|
|
256
452
|
if (cell.origin !== 'flow1a' || !cell.seat) continue;
|
|
257
453
|
const targetLoopId = SEAT_LOOP_IDS[cell.seat];
|
|
258
454
|
if (!targetLoopId) continue;
|
|
259
455
|
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
456
|
+
// 저널 키로 건너뛰기 «전에» 원장에서 복구한다 — shadow 넘김 ⊕ 자리 기록 뒤 상태 저장 전에 죽은 run 도 handed[] 를 되찾게.
|
|
457
|
+
restoreHanded(key, key, ledgerHanded);
|
|
458
|
+
restoreHanded(`shadow:${key}`, key, ledgerHanded);
|
|
260
459
|
if (keys.has(key)) continue;
|
|
460
|
+
if (liveHanded.has(key)) { keys.add(key); observe('exchange', `handed-to-task-agent:${key}`, 1, 'task-agent'); continue; }
|
|
461
|
+
// ORCH-TA-HAND: hand the picked cell to TASK-AGENT. shadow = card only (the seat path below is unchanged);
|
|
462
|
+
// live (tick live ⊕ hand live) = launch through TASK-AGENT and skip the seat journal so the cell launches once.
|
|
463
|
+
const handMode = deps.handToTaskAgent ?? handConfig.handToTaskAgent ?? 'off';
|
|
464
|
+
const handCell = deps.handToTaskAgentCell ?? handConfig.handToTaskAgentCell;
|
|
465
|
+
if (handMode !== 'off' && (!handCell || handCell === cell.id) && (TASK_SEATS as readonly string[]).includes(cell.seat)) {
|
|
466
|
+
// live 는 tick 도 live 이고 배치(유효한 판)가 된 칸만 — 나머지는 카드만(shadow · 배치 전 칸도 표본이 된다).
|
|
467
|
+
const handLive = handMode === 'live' && mode === 'live' && validVersion(cell.version);
|
|
468
|
+
// shadow 표본은 따로 센다 — 자리 경로로 아직 안 간 칸(shadow 틱·배치 전)이 나중에 live 로 바뀌면 그때 뜬다.
|
|
469
|
+
// ⚠️ 이미 자리 저널에 들어간 칸은 live 로 바꿔도 다시 안 넘긴다(keys.has 에서 끝) — 한 칸의 발사 주체는 하나다(의도).
|
|
470
|
+
const handKey = handLive ? key : `shadow:${key}`;
|
|
471
|
+
mkdirSync(dirname(ledger), { recursive: true });
|
|
472
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
473
|
+
// Claim under the seat-journal lock — the same lock the seat append takes, so a live claim and a seat row never both land.
|
|
474
|
+
const claimed = withFileLockSync(`${path}.lock`, () => {
|
|
475
|
+
if (handedKeys(ledger).has(handKey)) return false;
|
|
476
|
+
if (handLive && journalHas(key)) return false;
|
|
477
|
+
appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, seat: cell.seat, mode: handLive ? 'live' : 'shadow', status: 'claimed', runId, at: now.toISOString() })}\n`);
|
|
478
|
+
return true;
|
|
479
|
+
});
|
|
480
|
+
if (claimed) {
|
|
481
|
+
try {
|
|
482
|
+
const result = await (deps.handTask ?? (async (opts: HandTaskOptions) => (await import('../../task-agent/task-hand.js')).handTask(opts)))({
|
|
483
|
+
text: cell.title, seat: cell.seat as TaskSeat, checklistId: cell.id, live: handLive, now: () => now,
|
|
484
|
+
statePath: join(root, 'task-agent-actions.json'),
|
|
485
|
+
...(handLive ? { launcher: deps.taskLauncher ?? (async (args: string[]) => (await import('../../cli/tasks-cli.js')).defaultTaskLauncher(args)) } : {}),
|
|
486
|
+
});
|
|
487
|
+
withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched, status: 'handed', runId, at: now.toISOString() })}\n`));
|
|
488
|
+
(state.handed ??= []).push({ key, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched });
|
|
489
|
+
observe('hand-to-task-agent', `${key}:${result.card.id}:${result.mode}${result.launched ? ':launched' : ''}`, 1, 'task-agent');
|
|
490
|
+
if (handLive) { keys.add(key); continue; }
|
|
491
|
+
} catch (error) {
|
|
492
|
+
observe('hand-to-task-agent-failed', `${key}:${String(error).slice(0, 160)}`, 1, 'task-agent');
|
|
493
|
+
// A failed live hand stays claimed (no relaunch loop) and does not fall back to the seat path — one launcher per cell.
|
|
494
|
+
if (handLive) { keys.add(key); continue; }
|
|
495
|
+
// A failed shadow hand launched nothing — release the claim so a later tick can write the card,
|
|
496
|
+
// but only when no card for this cell was written (a failure after the card must not make a second one).
|
|
497
|
+
let cardWritten = true;
|
|
498
|
+
try {
|
|
499
|
+
const storePath = join(root, 'task-agent-actions.json');
|
|
500
|
+
if (!existsSync(storePath)) cardWritten = false;
|
|
501
|
+
else {
|
|
502
|
+
// 상태를 믿을 수 있을 때만 «카드 0장»으로 읽는다 — tasks 가 없거나 꼴이 틀리면 claim 을 남긴다.
|
|
503
|
+
const tasks = readTaskAgentState<{ tasks?: unknown }>(storePath).tasks;
|
|
504
|
+
if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)
|
|
505
|
+
&& Object.values(tasks).every(card => card && typeof card === 'object' && !Array.isArray(card))) {
|
|
506
|
+
cardWritten = Object.values(tasks as Record<string, TaskAgentCard>).some(card => card.checklistId === cell.id && card.text === cell.title);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch { cardWritten = true; /* unreadable store → keep the claim (never risk a duplicate) */ }
|
|
510
|
+
if (!cardWritten) withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, status: 'released', runId, reason: String(error).slice(0, 160), at: now.toISOString() })}\n`));
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
restoreHanded(handKey, key);
|
|
514
|
+
if (handLive) { keys.add(key); continue; }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
261
517
|
if (!validVersion(cell.version)) {
|
|
262
518
|
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
|
|
263
519
|
continue;
|
|
@@ -267,22 +523,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
267
523
|
mkdirSync(dirname(path), { recursive: true });
|
|
268
524
|
// TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
|
|
269
525
|
// same lock as the traffic node — an invalid row stops request intake for every seat.
|
|
270
|
-
const appended = withFileLockSync(`${path}.lock`, () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
})) return false;
|
|
276
|
-
appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.host !== undefined ? { host: cell.host } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
|
|
526
|
+
const appended = withFileLockSync(`${path}.lock`, (): boolean | 'handed' => {
|
|
527
|
+
if (journalHas(key)) return false;
|
|
528
|
+
// 잠금 안에서 다시 본다 — 동시 틱이 이 칸을 방금 live 로 넘겼으면 자리 경로로 보내지 않는다.
|
|
529
|
+
if (handedKeys(ledger).has(key)) return 'handed';
|
|
530
|
+
appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}), ...(cell.gridHost ? { gridHost: true } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
|
|
277
531
|
return true;
|
|
278
532
|
});
|
|
533
|
+
if (appended === 'handed') { observe('exchange', `seat-suppressed-handed:${key}`, 1, 'task-agent'); continue; }
|
|
279
534
|
if (!appended) continue;
|
|
280
535
|
state.delegated++;
|
|
281
536
|
observe('exchange', 'queued', 1, targetLoopId);
|
|
282
537
|
}
|
|
283
538
|
} else if (node === 'launch') {
|
|
539
|
+
// shadow 는 저널을 읽지 않는다 — 깨진 live 저널이 관측 전용 틱을 넘어뜨리지 않게.
|
|
540
|
+
const latestRequests = new Map(mode === 'live' ? readRequests(root).map(row => [row.key, row]) : []);
|
|
284
541
|
const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
|
|
285
|
-
&& validVersion(cell.version) && (mode === 'shadow' ||
|
|
542
|
+
&& validVersion(cell.version) && (mode === 'shadow' || latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued'));
|
|
286
543
|
const seats = [...new Set(eligible.map(cell => cell.seat!))];
|
|
287
544
|
state.launched = [];
|
|
288
545
|
for (const seat of seats) {
|
|
@@ -293,9 +550,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
293
550
|
state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
|
|
294
551
|
observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
|
|
295
552
|
} else {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
553
|
+
const selected = eligible.find(cell => cell.seat === seat && latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued');
|
|
554
|
+
const host = selected && latestRequests.get(`orch:${selected.cardId}:${selected.id}`)?.host;
|
|
555
|
+
const pools = new Map<string, string>([...latestRequests].flatMap(([key, row]) => {
|
|
556
|
+
if (row.status !== 'queued' || row.seat !== seat || row.kind !== 'pod' || !row.gridHost || !row.host || !key.startsWith('orch:')) return [];
|
|
557
|
+
const pool = preferredPoolSpec(row.host);
|
|
558
|
+
// 풀 힌트는 «설정된 풀»의 순서 바꾸기다 — 고른 호스트가 풀에 없으면 힌트를 지어내지 않고 그 사실을 남긴다.
|
|
559
|
+
if (!pool) observe('exchange', `grid-host-unmapped:${key}:${row.host}`, 1, SEAT_LOOP_IDS[seat]);
|
|
560
|
+
return pool ? [[key, pool] as const] : [];
|
|
561
|
+
}));
|
|
562
|
+
const enqueue: SeatDeps['enqueue'] | undefined = pools.size
|
|
563
|
+
? (queuedSeat, text, queuedRoot, idempotencyKey, item) => addHarnessQueue({ seat: queuedSeat, say: text, idempotencyKey,
|
|
564
|
+
...(item.source === 'request' && pools.has(item.id) ? { poolHint: pools.get(item.id), poolHintSource: 'grid' as const } : {}) }, { root: queuedRoot })
|
|
565
|
+
: undefined;
|
|
566
|
+
const options = enqueue ? { enqueue } : undefined;
|
|
567
|
+
const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string, _preferred?: string, overrides?: Pick<SeatDeps, 'enqueue'>) =>
|
|
568
|
+
runSeatLoopOnce(assigned, { root: stateRoot,
|
|
569
|
+
config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] }, ...overrides })))(seat, root, host, options);
|
|
299
570
|
// 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
|
|
300
571
|
const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
|
|
301
572
|
const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
|
|
@@ -318,6 +589,10 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
318
589
|
state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
|
|
319
590
|
state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
|
|
320
591
|
}
|
|
592
|
+
if (mode === 'live') {
|
|
593
|
+
refreshGridHosts(state.cells, resolveGridHosts(deps, observe), mode, root, observe);
|
|
594
|
+
await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
|
|
595
|
+
}
|
|
321
596
|
}
|
|
322
597
|
state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
|
|
323
598
|
const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
|
|
@@ -46,6 +46,7 @@ export const FEATURE_MATURITY = {
|
|
|
46
46
|
'/ops/seats': { pwa: 'ops' },
|
|
47
47
|
'/ops/checklist': { pwa: 'ops' },
|
|
48
48
|
'/ceo': { pwa: 'ops' },
|
|
49
|
+
'/decisions': { pwa: 'ops' },
|
|
49
50
|
'/dashboard': { pwa: 'ops' },
|
|
50
51
|
'/bots': { pwa: 'ops' },
|
|
51
52
|
'/botlab': { pwa: 'ops' },
|
|
@@ -75,7 +76,7 @@ export const FEATURE_MATURITY = {
|
|
|
75
76
|
schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
|
|
76
77
|
decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
|
|
77
78
|
env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
|
|
78
|
-
config: 'stable', tasks: 'stable', brief: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
79
|
+
config: 'stable', tasks: 'stable', brief: 'beta', notify: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
79
80
|
onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
|
|
80
81
|
agent: 'stable', registry: 'beta', tier: 'stable', local: 'beta', scheduler: 'beta',
|
|
81
82
|
attach: 'beta', wf: 'beta', nexus: 'stable', phone: 'beta', token: 'beta',
|
|
@@ -166,7 +167,7 @@ export const FEATURE_MATURITY = {
|
|
|
166
167
|
},
|
|
167
168
|
discordCommand: {
|
|
168
169
|
status: 'stable', sessions: 'stable', new: 'stable', fork: 'stable',
|
|
169
|
-
persona: 'beta', poll: 'beta', attach: 'beta', now: 'beta',
|
|
170
|
+
persona: 'beta', poll: 'beta', attach: 'beta', now: 'beta', loops: 'beta',
|
|
170
171
|
brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool',
|
|
171
172
|
relay: 'ops', showroom: 'ops', bots: 'ops', bot: 'ops', screen: 'ops', chart: 'ops',
|
|
172
173
|
routines: 'ops', botsay: 'ops', 'voice-join': 'ops', 'voice-leave': 'ops', 'voice-status': 'ops',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { debug } from '../../debug/log.js';
|
|
2
2
|
import { DecisionLedger, type DecisionEntry } from '../../decisions/decision-ledger.js';
|
|
3
|
+
import { isIrreversible } from '../../decisions/decision-cards.js';
|
|
3
4
|
import { jsonResponse } from './json-response.js';
|
|
4
5
|
|
|
5
6
|
export const DECISIONS_PATH = '/v1/decisions';
|
|
@@ -27,10 +28,19 @@ export async function handleDecisions(req: Request, deps: DecisionsRouteDeps = {
|
|
|
27
28
|
id: entry.id,
|
|
28
29
|
title: entry.title,
|
|
29
30
|
situation: entry.scqa.s.slice(0, 200),
|
|
30
|
-
options: entry.options.map(option => ({ id: option.key, label: option.label })),
|
|
31
|
+
options: entry.options.map(option => ({ id: option.key, label: option.label, consequence: option.consequence })),
|
|
31
32
|
recommendation: entry.recommendation,
|
|
32
33
|
raisedAt: entry.raisedAt,
|
|
33
34
|
dueAt: entry.dueAt,
|
|
35
|
+
scqa: entry.scqa,
|
|
36
|
+
category: entry.category,
|
|
37
|
+
irreversible: isIrreversible(entry),
|
|
38
|
+
raisedBy: { agent: entry.raisedBy.agent, ...(entry.raisedBy.track ? { track: entry.raisedBy.track } : {}) },
|
|
39
|
+
alternative: entry.alternative,
|
|
40
|
+
dissent: entry.dissent,
|
|
41
|
+
crossCheck: entry.crossCheck,
|
|
42
|
+
crossCheckSkipped: entry.crossCheckSkipped,
|
|
43
|
+
pendingQuestion: entry.pendingQuestion,
|
|
34
44
|
}));
|
|
35
45
|
debug.log('decisions.api', 'listed', { count: decisions.length });
|
|
36
46
|
return jsonResponse({ decisions });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getUserConfig } from '../../user-config.js';
|
|
2
|
+
import { readHqLease, type HqDeps } from '../../hq/hq.js';
|
|
3
|
+
import { measurePoolLease, type PoolLeaseMeasure } from '../../task-orchestrator/surfaces/pod-lease.js';
|
|
4
|
+
import { parsePodPool, resolvePodPoolSpec, type PodPoolMember, type PoolKubectl } from '../../task-orchestrator/surfaces/pod-pool.js';
|
|
5
|
+
|
|
6
|
+
export interface GridData {
|
|
7
|
+
/** null means the arbiter could not be read, not that no lease exists. */
|
|
8
|
+
hq: { record: ReturnType<typeof readHqLease>['record']; ageSeconds: number | null; expired: boolean | null; reason: string | null };
|
|
9
|
+
/** Configured member order; capacity comes from measurePoolLease when available. occupied is running + pending, or null on measurement failure. */
|
|
10
|
+
members: Array<{ context: string; capacity: number; running: number | null; pending: number | null; occupied: number | null; reason: string | null }>;
|
|
11
|
+
poolReason: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GridDeps {
|
|
15
|
+
hq?: HqDeps;
|
|
16
|
+
readHq?: () => ReturnType<typeof readHqLease>;
|
|
17
|
+
poolSpec?: () => string | null;
|
|
18
|
+
/** Optional read-only kubectl seam; never use a probe that creates Pods. */
|
|
19
|
+
kubectl?: PoolKubectl;
|
|
20
|
+
measure?: (members: readonly PodPoolMember[]) => PoolLeaseMeasure;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Independent, fail-soft read snapshots. No lease status/heartbeat, DNS probe Pod, host reservation or writes. */
|
|
24
|
+
export function buildGridData(deps: GridDeps = {}): GridData {
|
|
25
|
+
let hq: GridData['hq'];
|
|
26
|
+
try {
|
|
27
|
+
hq = { ...(deps.readHq ?? (() => readHqLease(deps.hq)))(), reason: null };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
hq = { record: null, ageSeconds: null, expired: null, reason: String(error) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let members: GridData['members'] = [];
|
|
33
|
+
let poolReason: string | null = null;
|
|
34
|
+
try {
|
|
35
|
+
const spec = (deps.poolSpec ?? (() => resolvePodPoolSpec(undefined, process.env, () => {
|
|
36
|
+
const config = getUserConfig();
|
|
37
|
+
return config.harness?.podPool ?? config.pod?.pool;
|
|
38
|
+
})))();
|
|
39
|
+
if (spec) {
|
|
40
|
+
const configured = parsePodPool(spec);
|
|
41
|
+
try {
|
|
42
|
+
const measured = (deps.measure ?? ((pool) => measurePoolLease(pool, { skipDnsProbe: true, ...(deps.kubectl ? { kubectl: deps.kubectl } : {}) })))(configured);
|
|
43
|
+
const byContext = new Map(measured.members.map((m) => [m.context, m]));
|
|
44
|
+
members = configured.map((member) => {
|
|
45
|
+
const row = byContext.get(member.context);
|
|
46
|
+
const running = row?.running ?? null;
|
|
47
|
+
const pending = row?.pending ?? null;
|
|
48
|
+
return { context: member.context, capacity: row?.capacity ?? member.capacity, running, pending,
|
|
49
|
+
occupied: running === null || pending === null ? null : running + pending,
|
|
50
|
+
reason: row?.reason ?? (row ? null : '측정 불가') };
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
poolReason = String(error);
|
|
54
|
+
members = configured.map(({ context, capacity }) => ({ context, capacity, running: null, pending: null, occupied: null, reason: poolReason }));
|
|
55
|
+
}
|
|
56
|
+
} else poolReason = 'pool not configured';
|
|
57
|
+
} catch (error) {
|
|
58
|
+
poolReason = String(error);
|
|
59
|
+
}
|
|
60
|
+
return { hq, members, poolReason };
|
|
61
|
+
}
|
|
@@ -22,6 +22,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import { isAbsolute } from 'node:path';
|
|
23
23
|
import { createProject, listProjects } from '../../project/project-store.js';
|
|
24
24
|
import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
|
|
25
|
+
import { buildGridData, type GridDeps } from './grid.js';
|
|
25
26
|
import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
|
|
26
27
|
import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
|
|
27
28
|
import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
|
|
@@ -341,7 +342,7 @@ import {
|
|
|
341
342
|
handleTasksList,
|
|
342
343
|
handleTaskDetail,
|
|
343
344
|
} from './tasks-scheduler.js';
|
|
344
|
-
import { handleTaskCardsGet, handleTaskCardsWishPost } from './task-cards-api.js';
|
|
345
|
+
import { handleTaskCardsGet, handleTaskCardsWishPost, handleTaskCardsClosePost } from './task-cards-api.js';
|
|
345
346
|
import { handleTaskCreatePost, handleTaskApprovePost } from './tasks-create.js';
|
|
346
347
|
import { matchIngestAuthorization } from './ingest-token.js';
|
|
347
348
|
import {
|
|
@@ -478,6 +479,8 @@ export interface NexusHttpServerOpts {
|
|
|
478
479
|
/** Optional installer state root for isolated API consumers and tests. */
|
|
479
480
|
pluginStateRoot?: string;
|
|
480
481
|
outputs?: OutputsDeps;
|
|
482
|
+
/** Read-only grid measurement seam; omitted in production for live HQ and Pod pool reads. */
|
|
483
|
+
grid?: GridDeps;
|
|
481
484
|
decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
|
|
482
485
|
cardFollowup?: CardFollowupJobs;
|
|
483
486
|
skillExec?: SkillExecJobs;
|
|
@@ -1385,6 +1388,12 @@ export async function routeRequest(
|
|
|
1385
1388
|
});
|
|
1386
1389
|
}
|
|
1387
1390
|
|
|
1391
|
+
// routeRequest is the live GET-only caller of buildGridData; keep it behind the owner auth gate.
|
|
1392
|
+
if (pathname === '/v1/grid') {
|
|
1393
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1394
|
+
if (method !== 'GET') return jsonResponse({ error: 'method-not-allowed', method }, 405);
|
|
1395
|
+
return jsonResponse(buildGridData(opts.grid));
|
|
1396
|
+
}
|
|
1388
1397
|
if (method === 'GET' && pathname === '/v1/outputs') {
|
|
1389
1398
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1390
1399
|
return handleOutputsGet(req, opts.outputs);
|
|
@@ -1457,6 +1466,15 @@ export async function routeRequest(
|
|
|
1457
1466
|
const mutated = await handleGraphsMutation(pathname, req);
|
|
1458
1467
|
if (mutated) return mutated;
|
|
1459
1468
|
}
|
|
1469
|
+
const cardClose = /^\/v1\/task-cards\/([^/]+)\/close$/.exec(pathname);
|
|
1470
|
+
if (method === 'POST' && cardClose) {
|
|
1471
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1472
|
+
if (bearerCredential(req, opts.metaApi) !== 'bearer-match') return jsonResponse({ error: 'forbidden' }, 403);
|
|
1473
|
+
let id: string;
|
|
1474
|
+
try { id = decodeURIComponent(cardClose[1]!); }
|
|
1475
|
+
catch { return jsonResponse({ error: 'not_found' }, 404); }
|
|
1476
|
+
return handleTaskCardsClosePost(req, id);
|
|
1477
|
+
}
|
|
1460
1478
|
if (method === 'POST' && pathname === '/v1/task-cards/wish') {
|
|
1461
1479
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1462
1480
|
return handleTaskCardsWishPost(req);
|