elanous 0.2.25 → 0.2.28
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/{1454-37fd16f479f5554a.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-24453f6a302816b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-21050928a697ed7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +119 -7
- package/scripts/coord-msg-bridge.ts +209 -0
- package/scripts/coord-post.sh +40 -7
- package/scripts/coord-requests-bridge.ts +10 -5
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/expose-rubric-suggest.ts +1 -1
- package/scripts/measure-tsc-affected-roots.ts +10 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/export-check-node.ts +8 -5
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +10 -3
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +32 -2
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/review-observation.ts +9 -2
- package/src/agent-substrate/self-review-cli.ts +62 -16
- package/src/boot/daemon-tools/self-implement.ts +72 -4
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +30 -3
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/control-cli.ts +29 -0
- package/src/cli/doctor-cli.ts +90 -8
- package/src/cli/msg-cli.ts +177 -11
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +133 -18
- package/src/cli/pr-land-overlap-evidence.ts +18 -10
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +34 -5
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/seat-cli.ts +121 -0
- package/src/cli/self-utterances-cli.ts +140 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +528 -31
- package/src/cli/usage-cli.ts +9 -4
- package/src/context-card/cell.ts +40 -0
- package/src/dashboard/boot-timing.ts +35 -0
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +94 -27
- package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
- package/src/debug/log.ts +1 -1
- package/src/domains/morning-report.ts +3 -1
- package/src/domains/release-tool.ts +29 -12
- package/src/domains/surface-events.ts +16 -0
- package/src/elanous-config-dir.ts +2 -2
- package/src/fleet/host-doctor.ts +73 -2
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +104 -15
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +68 -10
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/harness/harness-stop.ts +11 -7
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +69 -9
- package/src/instance/current.ts +2 -6
- package/src/instance/resolve.ts +5 -6
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/llm/openai-sampling-fields.ts +89 -0
- package/src/llm/runtime-model-family.ts +61 -0
- package/src/llm/zai-coding-plan.ts +27 -1
- package/src/llm.ts +13 -89
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/machines/machine-ledger.ts +31 -3
- package/src/maturity/cli-maturity.ts +13 -3
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +149 -17
- package/src/ops/overview-collect.ts +3 -2
- package/src/ops/overview-owner.ts +29 -0
- package/src/patents/ledger.ts +128 -0
- package/src/plugin-hooks/shell-hook.ts +17 -2
- package/src/plugin-missions/evaluator-runner.ts +18 -2
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/plugins/install/plugin-install.ts +11 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/prompt-enhance/enhance.ts +11 -0
- package/src/release-loop/checklist.ts +52 -4
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-loop.ts +5 -1
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/draft-sweep.ts +23 -14
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-dev/orchestrate.ts +6 -2
- package/src/self-dev/run-supervisor.ts +35 -1
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +5 -28
- package/src/self-implement/goal-command-execution.ts +26 -0
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +5 -11
- package/src/self-implement/pwa-import-graph.ts +194 -7
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/self-implement-runtime.ts +126 -21
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/session/index.ts +14 -4
- package/src/session-runtime/index.ts +55 -11
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +105 -19
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +56 -17
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +321 -25
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +66 -2
- package/src/task-agent/shadow.ts +150 -29
- package/src/task-agent/task-hand.ts +133 -8
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
- package/src/telegram.ts +30 -1
- package/src/tool-runtime/tool-search-runtime.ts +25 -0
- package/src/tui.ts +20 -1
- package/src/user-config.ts +11 -60
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
|
@@ -5,6 +5,9 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { debug } from '../../debug/log.js';
|
|
7
7
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import { PR_LABEL_NAMES } from '../../github/pr-labels.js';
|
|
9
|
+
import { listRunLedgers } from '../../self-implement/run-ledger.js';
|
|
10
|
+
import { queryRunningRuns } from '../../self-implement/running-runs.js';
|
|
8
11
|
import {
|
|
9
12
|
defaultGitMergeSeam, defaultLlmResolve, mergeMainWithLlmResolve,
|
|
10
13
|
type LlmMergeOutcome, type MergeGitSeam,
|
|
@@ -16,6 +19,8 @@ interface ResyncShadowEntry {
|
|
|
16
19
|
at: string;
|
|
17
20
|
pr: number;
|
|
18
21
|
head: string;
|
|
22
|
+
beforeHead?: string;
|
|
23
|
+
afterHead?: string;
|
|
19
24
|
mode: ResyncShadowMode;
|
|
20
25
|
pushed: boolean;
|
|
21
26
|
labelRemoved: boolean;
|
|
@@ -27,7 +32,7 @@ interface ResyncShadowEntry {
|
|
|
27
32
|
}
|
|
28
33
|
export interface ResyncShadowSummary { candidates: number; tried: number; saved: number; savedRatio: number | null }
|
|
29
34
|
|
|
30
|
-
type OpenPr = { number: number; headRefName: string; mergeable: string; isDraft: boolean; labels: { name: string }[] };
|
|
35
|
+
type OpenPr = { number: number; headRefName: string; headRefOid?: string; mergeable: string; isDraft: boolean; labels: { name: string }[] };
|
|
31
36
|
export interface ResyncShadowDeps {
|
|
32
37
|
repoRoot?: string;
|
|
33
38
|
instanceRoot?: string;
|
|
@@ -37,6 +42,63 @@ export interface ResyncShadowDeps {
|
|
|
37
42
|
gitSeam?: MergeGitSeam;
|
|
38
43
|
runTests?: (cwd: string, files: string[]) => { ok: boolean; failedTests?: string[]; noTests?: boolean };
|
|
39
44
|
now?: () => Date;
|
|
45
|
+
/** Run ownership is checked before touching a branch; null means the observation failed. */
|
|
46
|
+
activePrNumbers?: (candidatePrs: readonly number[]) => ReadonlySet<number> | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function activePrNumbers(candidatePrs: readonly OpenPr[], repoRoot: string, instanceRoot: string): ReadonlySet<number> | null {
|
|
50
|
+
const owners = new Map<number, { runId: string; branch: string; headCommit: string; terminal: boolean }[]>();
|
|
51
|
+
const branchRuns = new Map<string, Set<string>>();
|
|
52
|
+
const incompleteOwnerPrs = new Set<number>();
|
|
53
|
+
const candidateSet = new Set(candidatePrs.map(pr => pr.number));
|
|
54
|
+
try {
|
|
55
|
+
for (const match of listRunLedgers().matches) {
|
|
56
|
+
const latestStatus = match.entries.filter(entry => entry.event === 'run-status').at(-1)?.data.runStatus;
|
|
57
|
+
const terminal = latestStatus === 'completed' || latestStatus === 'failed' || latestStatus === 'cancelled';
|
|
58
|
+
for (const entry of match.entries) {
|
|
59
|
+
// A competing run can own the branch even when its PR number or worktree event is missing.
|
|
60
|
+
if ((entry.event === 'worktree' || entry.event === 'pr-opened') && typeof entry.data.branch === 'string') {
|
|
61
|
+
const ids = branchRuns.get(entry.data.branch) ?? new Set<string>();
|
|
62
|
+
ids.add(match.runId);
|
|
63
|
+
branchRuns.set(entry.data.branch, ids);
|
|
64
|
+
}
|
|
65
|
+
const number = entry.event === 'pr-opened' ? entry.data.number : undefined;
|
|
66
|
+
if (typeof number !== 'number' || !candidateSet.has(number)) continue;
|
|
67
|
+
const matches = owners.get(number) ?? [];
|
|
68
|
+
if (typeof entry.data.branch !== 'string' || typeof entry.data.headCommit !== 'string')
|
|
69
|
+
incompleteOwnerPrs.add(number);
|
|
70
|
+
else matches.push({ runId: match.runId, branch: entry.data.branch, headCommit: entry.data.headCommit, terminal });
|
|
71
|
+
owners.set(number, matches);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const runIds = [...new Set([...owners.values()].flatMap(rows => rows.map(row => row.runId)))];
|
|
75
|
+
if (runIds.length === 0) return new Set(candidatePrs.map(pr => pr.number));
|
|
76
|
+
const observed = queryRunningRuns({ runIds, caller: 'smart-merge.resync' });
|
|
77
|
+
if (observed.completeness !== 'complete') return null;
|
|
78
|
+
const active = new Set(observed.entries.filter(row => row.status === 'running' || row.status === 'probable-running'
|
|
79
|
+
|| row.status === 'unknown' || row.status === 'ended-unclosed').map(row => row.runId));
|
|
80
|
+
const priorPath = join(instanceRoot, 'orchestrator', 'resync-shadow.jsonl');
|
|
81
|
+
const prior = existsSync(priorPath) ? readFileSync(priorPath, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as ResyncShadowEntry) : [];
|
|
82
|
+
const excluded = new Set<number>();
|
|
83
|
+
for (const pr of candidatePrs) {
|
|
84
|
+
const matches = owners.get(pr.number) ?? [];
|
|
85
|
+
if (incompleteOwnerPrs.has(pr.number) || matches.length !== 1 || !/^[a-f0-9]{40}$/i.test(pr.headRefOid ?? '')) { excluded.add(pr.number); continue; }
|
|
86
|
+
const owner = matches[0]!;
|
|
87
|
+
if (owner.branch !== pr.headRefName || !owner.terminal || !/^[a-f0-9]{40}$/i.test(owner.headCommit)
|
|
88
|
+
|| active.has(owner.runId) || [...(branchRuns.get(pr.headRefName) ?? [])].some(id => id !== owner.runId)) {
|
|
89
|
+
excluded.add(pr.number); continue;
|
|
90
|
+
}
|
|
91
|
+
const remote = command('git', ['ls-remote', 'origin', `refs/heads/${pr.headRefName}`], repoRoot).trim().split(/\s/)[0];
|
|
92
|
+
if (remote !== pr.headRefOid) { excluded.add(pr.number); continue; }
|
|
93
|
+
let proven = owner.headCommit;
|
|
94
|
+
for (const row of prior.filter(row => row.pr === pr.number && row.head === pr.headRefName && row.pushed)) {
|
|
95
|
+
if (row.beforeHead !== proven || !/^[a-f0-9]{40}$/i.test(row.afterHead ?? '')) { proven = ''; break; }
|
|
96
|
+
proven = row.afterHead!;
|
|
97
|
+
}
|
|
98
|
+
if (proven !== remote) excluded.add(pr.number);
|
|
99
|
+
}
|
|
100
|
+
return excluded;
|
|
101
|
+
} catch { return null; }
|
|
40
102
|
}
|
|
41
103
|
|
|
42
104
|
function command(file: string, args: string[], cwd: string, timeout = 60_000): string {
|
|
@@ -51,6 +113,15 @@ function changedTests(gh: (args: string[]) => string, number: number): string[]
|
|
|
51
113
|
path.endsWith('.test.ts') && !isAbsolute(path) && path !== '' && !path.split('/').some(part => part === '..' || part === '.' || part === '')))];
|
|
52
114
|
}
|
|
53
115
|
|
|
116
|
+
function listOpenPrs(gh: (args: string[]) => string): OpenPr[] {
|
|
117
|
+
const observed: unknown = JSON.parse(gh(['pr', 'list', '--state', 'open', '--json', 'number,headRefName,headRefOid,mergeable,isDraft,labels', '--limit', '1000']));
|
|
118
|
+
if (!Array.isArray(observed) || observed.length >= 1000 || observed.some(pr => !pr || !Number.isSafeInteger(pr.number)
|
|
119
|
+
|| typeof pr.headRefName !== 'string' || typeof pr.mergeable !== 'string' || typeof pr.isDraft !== 'boolean'
|
|
120
|
+
|| !Array.isArray(pr.labels) || pr.labels.some((label: unknown) => !label || typeof label !== 'object' || typeof (label as { name?: unknown }).name !== 'string')))
|
|
121
|
+
throw new Error('incomplete or invalid PR observation');
|
|
122
|
+
return observed as OpenPr[];
|
|
123
|
+
}
|
|
124
|
+
|
|
54
125
|
// A PR test must not inherit credentials, reach the network or write outside its own worktree.
|
|
55
126
|
// If the kernel does not provide the required isolation, fail closed before starting bun.
|
|
56
127
|
const TEST_SANDBOX = `import ctypes, os, sys
|
|
@@ -145,16 +216,16 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
|
|
|
145
216
|
if (mode !== 'shadow' && mode !== 'live') throw new Error('mode must be shadow or live');
|
|
146
217
|
const repoRoot = deps.repoRoot ?? resolve(import.meta.dir, '../../..');
|
|
147
218
|
const gh = deps.runGh ?? ((args: string[]) => command('gh', args, repoRoot));
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
219
|
+
const eligible = listOpenPrs(gh).filter(pr => pr.headRefName.startsWith('self-impl/')
|
|
220
|
+
&& (mode === 'shadow' ? pr.mergeable === 'CONFLICTING' : pr.mergeable === 'MERGEABLE')
|
|
221
|
+
&& (mode === 'shadow' || pr.labels.some(label => label.name === PR_LABEL_NAMES.needsRebase))
|
|
222
|
+
&& !pr.labels.some(label => label.name === PR_LABEL_NAMES.running)
|
|
223
|
+
&& !pr.labels.some(label => label.name === PR_LABEL_NAMES.stalled || label.name === PR_LABEL_NAMES.superseded));
|
|
224
|
+
const observeActive = deps.activePrNumbers ?? ((numbers: readonly number[]) => activePrNumbers(
|
|
225
|
+
eligible.filter(pr => numbers.includes(pr.number)), repoRoot, deps.instanceRoot ?? effectiveInstanceRoot()));
|
|
226
|
+
const active = mode === 'live' && eligible.length ? observeActive(eligible.map(pr => pr.number)) : new Set<number>();
|
|
227
|
+
if (active === null) throw new Error('active run observation unavailable — resync stays read-only');
|
|
228
|
+
const candidates = eligible.filter(pr => !active.has(pr.number)).sort((a, b) => a.number - b.number).slice(0, max);
|
|
158
229
|
const ledger = join(deps.instanceRoot ?? effectiveInstanceRoot(), 'orchestrator', 'resync-shadow.jsonl');
|
|
159
230
|
let saved = 0;
|
|
160
231
|
let tried = 0;
|
|
@@ -169,6 +240,9 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
|
|
|
169
240
|
// Explicit destination refspecs make origin/<head> and origin/main fresh even without remote.origin.fetch.
|
|
170
241
|
command('git', ['fetch', 'origin', `+refs/heads/${pr.headRefName}:refs/remotes/origin/${pr.headRefName}`,
|
|
171
242
|
'+refs/heads/main:refs/remotes/origin/main'], repoRoot);
|
|
243
|
+
entry.beforeHead = command('git', ['rev-parse', `origin/${pr.headRefName}`], repoRoot).trim();
|
|
244
|
+
if (mode === 'live' && !deps.activePrNumbers && entry.beforeHead !== pr.headRefOid)
|
|
245
|
+
throw new Error('PR head changed after owner proof — resync stays read-only');
|
|
172
246
|
entry.testFiles = changedTests(gh, pr.number);
|
|
173
247
|
tmp = realpathSync(mkdtempSync(join(tmpdir(), 'resync-shadow-')));
|
|
174
248
|
command('git', ['worktree', 'add', '--detach', tmp, `origin/${pr.headRefName}`], repoRoot);
|
|
@@ -189,10 +263,24 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
|
|
|
189
263
|
} else entry.verdict = outcome.status === 'conflict-unresolved' || outcome.status === 'up-to-date' ? 'unresolved' : 'error';
|
|
190
264
|
if (mode === 'live' && entry.verdict === 'saved') {
|
|
191
265
|
try {
|
|
266
|
+
const lateActive = observeActive([pr.number]);
|
|
267
|
+
if (lateActive === null || lateActive.has(pr.number))
|
|
268
|
+
throw new Error('PR ownership changed before push — resync stays read-only');
|
|
269
|
+
const current = listOpenPrs(gh).filter(row => row.number === pr.number);
|
|
270
|
+
if (current.length !== 1 || current[0]!.headRefName !== pr.headRefName
|
|
271
|
+
|| current[0]!.headRefOid !== pr.headRefOid || current[0]!.mergeable !== 'MERGEABLE'
|
|
272
|
+
|| !current[0]!.labels.some(label => label.name === PR_LABEL_NAMES.needsRebase)
|
|
273
|
+
|| current[0]!.labels.some(label => label.name === PR_LABEL_NAMES.running
|
|
274
|
+
|| label.name === PR_LABEL_NAMES.stalled || label.name === PR_LABEL_NAMES.superseded))
|
|
275
|
+
throw new Error('PR state changed before push — resync stays read-only');
|
|
276
|
+
const immediateHead = command('git', ['ls-remote', 'origin', `refs/heads/${pr.headRefName}`], repoRoot).trim().split(/\s/)[0];
|
|
277
|
+
if (immediateHead !== entry.beforeHead)
|
|
278
|
+
throw new Error('remote head changed before push — resync stays read-only');
|
|
192
279
|
(deps.runGit ?? (args => { command('git', args, tmp!); }))(
|
|
193
280
|
['push', 'origin', `HEAD:refs/heads/${pr.headRefName}`]);
|
|
194
281
|
entry.pushed = true;
|
|
195
|
-
|
|
282
|
+
entry.afterHead = command('git', ['rev-parse', 'HEAD'], tmp).trim();
|
|
283
|
+
gh(['pr', 'edit', String(pr.number), '--remove-label', PR_LABEL_NAMES.needsRebase]);
|
|
196
284
|
entry.labelRemoved = true;
|
|
197
285
|
} catch (error) {
|
|
198
286
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
@@ -16,6 +16,7 @@ import { placeOnGrid, type GridHost } from './grid-place.js';
|
|
|
16
16
|
import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
|
|
17
17
|
import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
|
|
18
18
|
import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
|
|
19
|
+
import { runCoordinatorInbox } from './inbox.js';
|
|
19
20
|
|
|
20
21
|
export type Window = '08' | '12' | '18';
|
|
21
22
|
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
|
|
@@ -31,6 +32,10 @@ export interface TickState {
|
|
|
31
32
|
/** The highest degradation actually applied in this run — the report keeps it even if the signal recovers before report. */
|
|
32
33
|
degradationPeak?: { level: 1 | 2; reason: string };
|
|
33
34
|
deferredCells?: Cell[];
|
|
35
|
+
/** A placement adapter rejected at least one cell; this run cannot claim full completion. */
|
|
36
|
+
placementFailed?: boolean;
|
|
37
|
+
/** Last confirmed live placement write in this run, carried across node processes. */
|
|
38
|
+
boardWrittenAt?: string;
|
|
34
39
|
/** place ran at L2 (skipped) in this run — its unplaced cells are carried even if the board is back by delegate. */
|
|
35
40
|
placeDegraded?: boolean;
|
|
36
41
|
delegatedKeys?: string[];
|
|
@@ -67,6 +72,8 @@ export interface TickDeps {
|
|
|
67
72
|
handToTaskAgentCell?: string;
|
|
68
73
|
handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
|
|
69
74
|
taskLauncher?: TaskLauncher;
|
|
75
|
+
/** Coordinator inbox seam; the real intake reads and replies on the durable message bus. */
|
|
76
|
+
coordinatorInbox?: () => { replied: number; acknowledged: number };
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
|
|
@@ -329,6 +336,41 @@ function stateFile(root: string, runId: string): string {
|
|
|
329
336
|
if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
|
|
330
337
|
return join(root, 'loop', 'orchestrator', `${runId}.json`);
|
|
331
338
|
}
|
|
339
|
+
/** COORD-HA ③ 진행 하트비트 — the last tick that finished its work and the last confirmed placement write. The guardian watches this
|
|
340
|
+
* (RFC-loop-agent-map §A8b: «마지막 틱 완료 ⊕ 보드 마지막 쓰기 시각»), not process liveness — a held node keeps pulsing
|
|
341
|
+
* healthy while this record goes stale, which is exactly the stall it exists to expose. */
|
|
342
|
+
export interface CompletionHeartbeat {
|
|
343
|
+
runId: string;
|
|
344
|
+
node: Node;
|
|
345
|
+
window: Window;
|
|
346
|
+
mode: Mode;
|
|
347
|
+
/** last confirmed live placement write; null before the first such write */
|
|
348
|
+
boardWrittenAt: string | null;
|
|
349
|
+
/** last fully completed run, or null while only board writes have succeeded */
|
|
350
|
+
tickCompletedAt: string | null;
|
|
351
|
+
}
|
|
352
|
+
/** Its own subdirectory, so the run-state scanners (`restorePrevious` · `previousDelegations` · `syncQueuedPoolHints`) never parse it as a TickState. */
|
|
353
|
+
function completionHeartbeatPath(root: string): string { return join(root, 'loop', 'orchestrator', 'heartbeat', 'completion.json'); }
|
|
354
|
+
/** COORD-HA ③ — last completed run and board write, independently. Undefined means neither has been recorded. */
|
|
355
|
+
export function readCompletionHeartbeat(root: string): CompletionHeartbeat | undefined {
|
|
356
|
+
const path = completionHeartbeatPath(root);
|
|
357
|
+
if (!existsSync(path)) return undefined;
|
|
358
|
+
try {
|
|
359
|
+
const { runId, node, window, mode, boardWrittenAt, tickCompletedAt } = JSON.parse(readFileSync(path, 'utf8')) as Partial<CompletionHeartbeat>;
|
|
360
|
+
if (typeof runId !== 'string' || typeof node !== 'string' || typeof window !== 'string' || typeof mode !== 'string'
|
|
361
|
+
|| (boardWrittenAt !== null && (typeof boardWrittenAt !== 'string' || !Number.isFinite(Date.parse(boardWrittenAt))))
|
|
362
|
+
|| (tickCompletedAt !== null && (typeof tickCompletedAt !== 'string' || !Number.isFinite(Date.parse(tickCompletedAt))))) return undefined;
|
|
363
|
+
return { runId, node: node as Node, window: window as Window, mode: mode as Mode, boardWrittenAt, tickCompletedAt };
|
|
364
|
+
} catch { return undefined; }
|
|
365
|
+
}
|
|
366
|
+
/** Temp + rename, so a guardian in another process never reads a torn record. */
|
|
367
|
+
function recordCompletionHeartbeat(root: string, completion: CompletionHeartbeat): void {
|
|
368
|
+
const path = completionHeartbeatPath(root);
|
|
369
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
370
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
371
|
+
writeFileSync(temp, JSON.stringify(completion));
|
|
372
|
+
renameSync(temp, path);
|
|
373
|
+
}
|
|
332
374
|
function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells' | 'launched'> | null {
|
|
333
375
|
const dir = join(root, 'loop', 'orchestrator');
|
|
334
376
|
if (!existsSync(dir)) return null;
|
|
@@ -430,6 +472,15 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
430
472
|
try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
|
|
431
473
|
catch { /* observation must not change the run */ }
|
|
432
474
|
};
|
|
475
|
+
// A graph node may be replayed with the same runId, and 12/18 skip intake.
|
|
476
|
+
// Drain the bus on every tick command; the message transaction prevents duplicate answers.
|
|
477
|
+
if (mode !== 'off') {
|
|
478
|
+
const inbox = (deps.coordinatorInbox ?? (() => runCoordinatorInbox(undefined, 100, join(root, 'msg', 'messages.db'))))();
|
|
479
|
+
if (inbox.replied || inbox.acknowledged) {
|
|
480
|
+
observe('exchange', 'coordinator-inbox-replied', inbox.replied);
|
|
481
|
+
observe('exchange', 'coordinator-inbox-acknowledged', inbox.acknowledged);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
433
484
|
if (mode !== 'off' && activeNode(node, window) && state.nodes[node] !== 'ok') {
|
|
434
485
|
if (node === 'intake') {
|
|
435
486
|
if (mode === 'live' && deps.cards) { state.cards = wishCards(deps.cards()); state.skippedCards = []; }
|
|
@@ -495,11 +546,19 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
495
546
|
if (validVersion(placed?.version)) {
|
|
496
547
|
cell.version = placed.version;
|
|
497
548
|
state.placed++;
|
|
549
|
+
// The live placer has returned from its board write. A shadow dry run or null result is not a write.
|
|
550
|
+
if (mode === 'live') state.boardWrittenAt = currentTime().toISOString();
|
|
498
551
|
observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
|
|
499
|
-
} else {
|
|
552
|
+
} else {
|
|
553
|
+
state.unplaced = (state.unplaced ?? 0) + 1;
|
|
554
|
+
if (mode === 'live') state.placementFailed = true;
|
|
555
|
+
observe('exchange', 'unplaced-no-release', 1);
|
|
556
|
+
}
|
|
500
557
|
} catch (error) {
|
|
501
558
|
state.unplaced = (state.unplaced ?? 0) + 1;
|
|
559
|
+
state.placementFailed = true;
|
|
502
560
|
observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
|
|
561
|
+
// Preserve the cell-by-cell recovery contract; the run completion record is withheld at report.
|
|
503
562
|
}
|
|
504
563
|
}
|
|
505
564
|
}
|
|
@@ -762,8 +821,20 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
762
821
|
state.nodes[node] = 'skipped';
|
|
763
822
|
}
|
|
764
823
|
observe('tick', state.nodes[node], state.cells.length);
|
|
824
|
+
if (mode === 'live' && node === 'place' && state.boardWrittenAt) {
|
|
825
|
+
// A board write can succeed even when a later cell fails. Advance its clock without claiming run completion.
|
|
826
|
+
const previous = readCompletionHeartbeat(root);
|
|
827
|
+
recordCompletionHeartbeat(root, previous
|
|
828
|
+
? { ...previous, boardWrittenAt: state.boardWrittenAt }
|
|
829
|
+
: { runId, node, window, mode, boardWrittenAt: state.boardWrittenAt, tickCompletedAt: null });
|
|
830
|
+
}
|
|
765
831
|
mkdirSync(dirname(path), { recursive: true });
|
|
766
832
|
writeFileSync(path, JSON.stringify(state));
|
|
833
|
+
if (mode !== 'off' && node === 'report' && !state.placementFailed && !state.placeDegraded) {
|
|
834
|
+
// Only the last node can attest that the whole run completed. Carry the most recent confirmed board write.
|
|
835
|
+
const boardWrittenAt = state.boardWrittenAt ?? readCompletionHeartbeat(root)?.boardWrittenAt ?? null;
|
|
836
|
+
recordCompletionHeartbeat(root, { runId, node, window, mode, boardWrittenAt, tickCompletedAt: currentTime().toISOString() });
|
|
837
|
+
}
|
|
767
838
|
return state;
|
|
768
839
|
} catch (error) {
|
|
769
840
|
nodeError = error;
|
|
@@ -10,6 +10,16 @@ export interface MachineRow {
|
|
|
10
10
|
locationStatus: string;
|
|
11
11
|
role: string;
|
|
12
12
|
duties: string;
|
|
13
|
+
/** Fleet intent only. Live membership comes from Nexus machine resources. */
|
|
14
|
+
fleet?: MachineFleetIntent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MachineFleetIntent {
|
|
18
|
+
sshName: string;
|
|
19
|
+
role: 'worker' | 'hq';
|
|
20
|
+
platform: 'darwin' | 'linux' | 'wsl';
|
|
21
|
+
podPool: string;
|
|
22
|
+
cap: number;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export interface DeviceRow {
|
|
@@ -59,18 +69,36 @@ export function validateMachineLedger(value: unknown): MachineLedger {
|
|
|
59
69
|
keys(data, ['title', 'policy', 'machines', 'devices', 'rules', 'changes'], 'ledger');
|
|
60
70
|
if (!Array.isArray(data.machines) || !Array.isArray(data.devices)) throw new Error('invalid machines or devices');
|
|
61
71
|
const ids = new Set<string>();
|
|
62
|
-
const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string): T[] => entries.map((entry, index) => {
|
|
72
|
+
const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string, extraFields: string[] = []): T[] => entries.map((entry, index) => {
|
|
63
73
|
const row = record(entry, `${label}[${index}]`);
|
|
64
|
-
keys(row, fields, label);
|
|
74
|
+
keys(row, [...fields, ...extraFields], label);
|
|
65
75
|
const result = Object.fromEntries(fields.map((field) => [field, text(row[field], `${label}.${field}`)])) as T;
|
|
66
76
|
if (!idPattern.test(result.id) || ids.has(result.id)) throw new Error(`empty, duplicate or invalid id: ${result.id}`);
|
|
67
77
|
ids.add(result.id);
|
|
68
78
|
return result;
|
|
69
79
|
});
|
|
80
|
+
const machines = rows<MachineRow>(data.machines, ['id', 'name', 'specs', 'locationStatus', 'role', 'duties'], 'machines', ['fleet']);
|
|
81
|
+
const sshNames = new Set<string>();
|
|
82
|
+
for (let index = 0; index < machines.length; index++) {
|
|
83
|
+
const raw = record(data.machines[index], `machines[${index}]`);
|
|
84
|
+
if (raw.fleet === undefined) continue;
|
|
85
|
+
const fleet = record(raw.fleet, `machines[${index}].fleet`);
|
|
86
|
+
keys(fleet, ['sshName', 'role', 'platform', 'podPool', 'cap'], 'fleet');
|
|
87
|
+
const sshName = text(fleet.sshName, 'fleet.sshName');
|
|
88
|
+
const role = text(fleet.role, 'fleet.role');
|
|
89
|
+
const platform = text(fleet.platform, 'fleet.platform');
|
|
90
|
+
const podPool = text(fleet.podPool, 'fleet.podPool');
|
|
91
|
+
if (!idPattern.test(sshName) || sshNames.has(sshName)) throw new Error(`duplicate or invalid fleet.sshName: ${sshName}`);
|
|
92
|
+
if (role !== 'worker' && role !== 'hq') throw new Error(`invalid fleet.role: ${role}`);
|
|
93
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'wsl') throw new Error(`invalid fleet.platform: ${platform}`);
|
|
94
|
+
if (!Number.isSafeInteger(fleet.cap) || (fleet.cap as number) < 0) throw new Error('invalid fleet.cap');
|
|
95
|
+
sshNames.add(sshName);
|
|
96
|
+
machines[index]!.fleet = { sshName, role, platform, podPool, cap: fleet.cap as number };
|
|
97
|
+
}
|
|
70
98
|
return {
|
|
71
99
|
title: text(data.title, 'title'),
|
|
72
100
|
policy: list(data.policy, 'policy'),
|
|
73
|
-
machines
|
|
101
|
+
machines,
|
|
74
102
|
devices: rows<DeviceRow>(data.devices, ['id', 'name', 'status', 'purpose'], 'devices'),
|
|
75
103
|
rules: list(data.rules, 'rules'),
|
|
76
104
|
changes: list(data.changes, 'changes'),
|
|
@@ -18,14 +18,24 @@ export function cliRootVisibleFor(name: string, role: Role, { showAll }: { showA
|
|
|
18
18
|
return grade === 'stable' || (role === 'contributor' && (grade === 'beta' || grade === 'tool'));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
21
|
+
/** Root --help first screen (SETUP-HELP-SLIM): the three stable onboarding commands. */
|
|
22
|
+
export const FIRST_SCREEN_COMMANDS: readonly string[] = ['start', 'setup', 'ask'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Only help is filtered: Commander still dispatches explicitly typed commands.
|
|
26
|
+
* firstScreen narrows the visible set to FIRST_SCREEN_COMMANDS and showAll wins over it;
|
|
27
|
+
* the caller opts in per role — the wired default (deferred slice) enables it for the
|
|
28
|
+
* general role only, so owner/contributor keep today's list.
|
|
29
|
+
*/
|
|
30
|
+
export function filterCliRootHelp(commands: readonly Command[], role: Role, showAll: boolean, firstScreen: boolean = false): { shown: number; hidden: number } {
|
|
31
|
+
const firstScreenActive = firstScreen && !showAll;
|
|
23
32
|
let shown = 0;
|
|
24
33
|
let hidden = 0;
|
|
25
34
|
for (const command of commands) {
|
|
26
35
|
const grade = cliRootMaturity(command.name(), commands);
|
|
27
36
|
if (!grade) throw new Error(`cliRoot 에 등급 없는 명령: ${command.name()}`);
|
|
28
|
-
const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands)
|
|
37
|
+
const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands)
|
|
38
|
+
&& (!firstScreenActive || FIRST_SCREEN_COMMANDS.includes(command.name()));
|
|
29
39
|
(command as Command & { _hidden: boolean })._hidden = !visible;
|
|
30
40
|
if (visible) shown++;
|
|
31
41
|
else hidden++;
|
|
@@ -62,7 +62,7 @@ export const FEATURE_MATURITY = {
|
|
|
62
62
|
preview: 'beta', pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', image: 'beta', 'agent-env': 'beta',
|
|
63
63
|
where: 'stable', bench: 'beta', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
|
|
64
64
|
pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
|
|
65
|
-
research: 'beta', release: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
65
|
+
research: 'beta', release: 'beta', rfc: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
66
66
|
control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
|
|
67
67
|
grounding: 'beta', graph: 'beta', loop: 'stable', resource: 'beta', card: 'stable', 'launch-head': 'beta',
|
|
68
68
|
a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { canonicalSeatId, type Message, type MsgStore } from './msg-store.js';
|
|
3
|
+
|
|
4
|
+
type Subscriber = { to: string; lastId: number; heartbeatAt: number; replaying: boolean; controller: ReadableStreamDefaultController<Uint8Array> };
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
|
|
7
|
+
function cursor(raw: string | null): number {
|
|
8
|
+
if (raw === null) return 0;
|
|
9
|
+
if (!/^(0|[1-9]\d*)$/.test(raw)) throw new Error('invalid message cursor');
|
|
10
|
+
const id = Number(raw);
|
|
11
|
+
if (!Number.isSafeInteger(id)) throw new Error('invalid message cursor');
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function authorized(request: Request, token: string): boolean {
|
|
16
|
+
const value = request.headers.get('authorization');
|
|
17
|
+
if (!value?.startsWith('Bearer ')) return false;
|
|
18
|
+
const received = Buffer.from(value.slice(7));
|
|
19
|
+
const expected = Buffer.from(token);
|
|
20
|
+
return expected.length > 0 && received.length === expected.length && timingSafeEqual(received, expected);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function addressed(message: Message, to: string): boolean {
|
|
24
|
+
return message.to === to || (message.to === 'ALL' && message.from !== to);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The SQLite ledger remains authoritative; this independent process only accelerates delivery. */
|
|
28
|
+
export function startMsgSseServer(input: {
|
|
29
|
+
store: MsgStore;
|
|
30
|
+
token: string;
|
|
31
|
+
port: number;
|
|
32
|
+
hostname?: string;
|
|
33
|
+
pollMs?: number;
|
|
34
|
+
}): { port: number; stats: () => { subscribers: number; maxQueuedEvents: number }; stop: () => void } {
|
|
35
|
+
if (!input.token) throw new Error('message SSE token is required');
|
|
36
|
+
if (!Number.isSafeInteger(input.port) || input.port < 0 || input.port > 65535) throw new Error('invalid SSE port');
|
|
37
|
+
const hostname = input.hostname ?? '127.0.0.1';
|
|
38
|
+
if (!['127.0.0.1', 'localhost', '::1', '[::1]'].includes(hostname)) throw new Error('message SSE must bind to loopback');
|
|
39
|
+
const pollMs = input.pollMs ?? 250;
|
|
40
|
+
if (!Number.isSafeInteger(pollMs) || pollMs < 1 || pollMs > 500) throw new Error('SSE ledger poll must be 1–500ms');
|
|
41
|
+
const subscribers = new Set<Subscriber>();
|
|
42
|
+
let maxQueuedEvents = 0;
|
|
43
|
+
let scanned = input.store.latestId();
|
|
44
|
+
const send = (subscriber: Subscriber, message: Message) => {
|
|
45
|
+
if (message.id <= subscriber.lastId || !addressed(message, subscriber.to)) return;
|
|
46
|
+
if ((subscriber.controller.desiredSize ?? 0) < -256) throw new Error('slow SSE subscriber; reconnect for ledger replay');
|
|
47
|
+
subscriber.controller.enqueue(encoder.encode(`id: ${message.id}\ndata: ${JSON.stringify(message)}\n\n`));
|
|
48
|
+
maxQueuedEvents = Math.max(maxQueuedEvents, Math.max(0, 1 - (subscriber.controller.desiredSize ?? 0)));
|
|
49
|
+
subscriber.lastId = message.id;
|
|
50
|
+
};
|
|
51
|
+
const replay = (subscriber: Subscriber) => {
|
|
52
|
+
if (!subscriber.replaying) return;
|
|
53
|
+
try {
|
|
54
|
+
while ((subscriber.controller.desiredSize ?? 0) > 0) {
|
|
55
|
+
const batch = input.store.listByRecipient(subscriber.to, subscriber.lastId, 1000);
|
|
56
|
+
if (!batch.length) { subscriber.replaying = false; break; }
|
|
57
|
+
for (const message of batch) {
|
|
58
|
+
if ((subscriber.controller.desiredSize ?? 0) <= 0) break;
|
|
59
|
+
send(subscriber, message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
subscribers.delete(subscriber);
|
|
64
|
+
subscriber.controller.error(error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const tick = () => {
|
|
68
|
+
try {
|
|
69
|
+
let batch: Message[];
|
|
70
|
+
let scans = 0;
|
|
71
|
+
do {
|
|
72
|
+
batch = input.store.listAfter(scanned);
|
|
73
|
+
for (const message of batch) {
|
|
74
|
+
scanned = message.id;
|
|
75
|
+
for (const subscriber of subscribers) {
|
|
76
|
+
if (subscriber.replaying) continue;
|
|
77
|
+
try { send(subscriber, message); } catch (error) {
|
|
78
|
+
subscribers.delete(subscriber);
|
|
79
|
+
try { subscriber.controller.error(error); } catch { /* already disconnected */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
scans++;
|
|
84
|
+
} while (batch.length === 1000 && scans < 4);
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
for (const subscriber of subscribers) {
|
|
87
|
+
if (subscriber.replaying || (subscriber.controller.desiredSize ?? 0) <= 0) continue;
|
|
88
|
+
if (now - subscriber.heartbeatAt < 1000) continue;
|
|
89
|
+
try {
|
|
90
|
+
subscriber.controller.enqueue(encoder.encode(': keepalive\n\n'));
|
|
91
|
+
subscriber.heartbeatAt = now;
|
|
92
|
+
} catch { subscribers.delete(subscriber); }
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// The next tick retries from the last scanned ID; the durable ledger is unchanged.
|
|
96
|
+
console.error(`msg SSE scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const server = Bun.serve({
|
|
100
|
+
port: input.port,
|
|
101
|
+
hostname,
|
|
102
|
+
fetch(request) {
|
|
103
|
+
const url = new URL(request.url);
|
|
104
|
+
if (url.pathname !== '/stream' || request.method !== 'GET') return new Response('Not Found', { status: 404 });
|
|
105
|
+
if (!authorized(request, input.token)) return new Response('Unauthorized', { status: 401 });
|
|
106
|
+
let to: string;
|
|
107
|
+
let after: number;
|
|
108
|
+
try {
|
|
109
|
+
to = canonicalSeatId(url.searchParams.get('to') ?? '');
|
|
110
|
+
if (to === 'ALL') throw new Error('ALL is not a recipient');
|
|
111
|
+
after = Math.max(cursor(url.searchParams.get('since')), cursor(request.headers.get('last-event-id')));
|
|
112
|
+
} catch (error) { return new Response(error instanceof Error ? error.message : String(error), { status: 400 }); }
|
|
113
|
+
let subscriber: Subscriber;
|
|
114
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
115
|
+
start(controller) {
|
|
116
|
+
subscriber = { to, lastId: after, heartbeatAt: Date.now(), replaying: true, controller };
|
|
117
|
+
subscribers.add(subscriber);
|
|
118
|
+
controller.enqueue(encoder.encode(': connected\n\n'));
|
|
119
|
+
},
|
|
120
|
+
pull() { replay(subscriber); },
|
|
121
|
+
cancel() { subscribers.delete(subscriber); },
|
|
122
|
+
});
|
|
123
|
+
request.signal.addEventListener('abort', () => subscribers.delete(subscriber), { once: true });
|
|
124
|
+
return new Response(stream, { headers: {
|
|
125
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
126
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
127
|
+
Connection: 'keep-alive',
|
|
128
|
+
} });
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const timer = setInterval(tick, pollMs);
|
|
132
|
+
timer.unref();
|
|
133
|
+
return { port: server.port ?? input.port,
|
|
134
|
+
stats: () => ({ subscribers: subscribers.size, maxQueuedEvents }),
|
|
135
|
+
stop: () => {
|
|
136
|
+
clearInterval(timer);
|
|
137
|
+
for (const subscriber of subscribers) { try { subscriber.controller.close(); } catch { /* disconnected */ } }
|
|
138
|
+
subscribers.clear();
|
|
139
|
+
server.stop(true);
|
|
140
|
+
} };
|
|
141
|
+
}
|