elanous 0.2.17 → 0.2.19
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/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- 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 +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +25 -24
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +24 -23
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +28 -27
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- 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/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +6 -3
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/lib/pch-regress-map.ts +23 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- 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 +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +436 -59
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- 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 +8 -1
- 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 +174 -0
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +45 -7
- package/scripts/release-loop/version-node.ts +145 -40
- 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/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- 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/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -0
- package/src/briefing/brief-items.ts +86 -17
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +85 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +128 -19
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +403 -4
- 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-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +244 -40
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- 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/graph-runner/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +12 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +185 -18
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- 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 -15
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- 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/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +106 -4
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +450 -54
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +352 -23
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/nexus/index.ts +7 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +106 -6
- package/src/release-loop/feature-store.ts +64 -16
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +64 -20
- package/src/release-loop/rubric.ts +136 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-cli.ts +36 -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/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +61 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +101 -7
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +203 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +49 -5
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +449 -0
- package/src/task-agent/shadow.ts +258 -0
- package/src/task-agent/task-hand.ts +337 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
- package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/telegram.ts +5 -20
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +196 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.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/4795-f372490b178583fc.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/5360-be1f1d8f9fcc9755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.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/8060-cc008cc6fedfa818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, 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';
|
|
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
|
+
import { rubricPriority } from '../../release-loop/rubric.js';
|
|
9
10
|
import { debug } from '../../debug/log.js';
|
|
10
11
|
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';
|
|
12
|
+
import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
|
|
13
|
+
import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
|
|
14
|
+
import { placeOnGrid, type GridHost } from './grid-place.js';
|
|
15
|
+
import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
|
|
16
|
+
import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
|
|
17
|
+
import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
|
|
13
18
|
|
|
14
19
|
export type Window = '08' | '12' | '18';
|
|
15
20
|
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
|
|
16
21
|
export type Mode = 'off' | 'shadow' | 'live';
|
|
17
22
|
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[] }
|
|
23
|
+
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' | 'P3' | 'P4'; predecessors?: string[] }
|
|
19
24
|
export interface TickState {
|
|
20
25
|
runId: string; window: Window; mode: Mode; day: string;
|
|
21
26
|
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
27
|
reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
|
|
23
28
|
runSummaries?: string[];
|
|
29
|
+
degradation?: { level: 1 | 2; reason: string };
|
|
30
|
+
/** The highest degradation actually applied in this run — the report keeps it even if the signal recovers before report. */
|
|
31
|
+
degradationPeak?: { level: 1 | 2; reason: string };
|
|
32
|
+
deferredCells?: Cell[];
|
|
33
|
+
/** place ran at L2 (skipped) in this run — its unplaced cells are carried even if the board is back by delegate. */
|
|
34
|
+
placeDegraded?: boolean;
|
|
35
|
+
delegatedKeys?: string[];
|
|
24
36
|
launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
|
|
37
|
+
/** ORCH-TA-HAND — cells handed to TASK-AGENT this run (card id · mode · launched). */
|
|
38
|
+
handed?: Array<{ key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean }>;
|
|
25
39
|
queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
|
|
26
40
|
steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
|
|
27
41
|
nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
|
|
28
42
|
missing: string[];
|
|
29
43
|
}
|
|
30
44
|
/** 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 }>;
|
|
45
|
+
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
46
|
/** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
|
|
33
47
|
export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
|
|
34
48
|
export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
@@ -36,12 +50,51 @@ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown
|
|
|
36
50
|
export interface TickDeps {
|
|
37
51
|
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
38
52
|
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
39
|
-
|
|
53
|
+
degradation?: () => { llm: boolean; board: boolean };
|
|
54
|
+
gridHosts?: readonly GridHost[];
|
|
55
|
+
seatTurn?: (seat: string, root: string, host?: string, options?: Pick<SeatDeps, 'enqueue'>) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
|
|
40
56
|
queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
|
|
57
|
+
/** 발사 전 대기열 항목의 풀 힌트 갱신(주입 안 하면 harness-queue `setQueuedPoolHint`). */
|
|
58
|
+
setPoolHint?: (queueId: string, poolHint: string | undefined, root: string) => Promise<boolean>;
|
|
41
59
|
loadAdapter?: (path: string, name: string) => Promise<unknown>;
|
|
42
60
|
checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
|
|
43
61
|
observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
|
|
44
62
|
print?: (line: string) => void;
|
|
63
|
+
/** ORCH-TA-HAND seams — absent = config `loops.orchestrator.handToTaskAgent(Cell)` · real `handTask` · `defaultTaskLauncher`. */
|
|
64
|
+
handToTaskAgent?: 'off' | 'shadow' | 'live';
|
|
65
|
+
handToTaskAgentCell?: string;
|
|
66
|
+
handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
|
|
67
|
+
taskLauncher?: TaskLauncher;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
|
|
71
|
+
function handedLedger(root: string): string { return join(root, 'loop', 'orchestrator', 'handed.jsonl'); }
|
|
72
|
+
type HandedRow = { key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean; runId?: string };
|
|
73
|
+
/** Completed hands (`status: 'handed'`) by ledger key — a re-entered run restores its `handed[]` from these. */
|
|
74
|
+
function handedRows(path: string): Map<string, HandedRow> {
|
|
75
|
+
const rows = new Map<string, HandedRow>();
|
|
76
|
+
if (!existsSync(path)) return rows;
|
|
77
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
78
|
+
if (!line.trim()) continue;
|
|
79
|
+
let row: Partial<HandedRow> & { status?: unknown };
|
|
80
|
+
try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
|
|
81
|
+
if (row.status === 'handed' && typeof row.key === 'string' && typeof row.cardId === 'string' && typeof row.cell === 'string'
|
|
82
|
+
&& (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 } : {}) });
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|
|
86
|
+
function handedKeys(path: string): Set<string> {
|
|
87
|
+
if (!existsSync(path)) return new Set();
|
|
88
|
+
const keys = new Set<string>();
|
|
89
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
90
|
+
if (!line.trim()) continue;
|
|
91
|
+
let row: { key?: unknown; status?: unknown };
|
|
92
|
+
try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
|
|
93
|
+
if (typeof row.key !== 'string') continue;
|
|
94
|
+
// A released claim (a shadow hand that failed before its card) frees the key again — the last row decides.
|
|
95
|
+
if (row.status === 'released') keys.delete(row.key); else keys.add(row.key);
|
|
96
|
+
}
|
|
97
|
+
return keys;
|
|
45
98
|
}
|
|
46
99
|
|
|
47
100
|
export function kstWindow(now: Date): Window {
|
|
@@ -88,7 +141,9 @@ export function reportLine(state: TickState): string {
|
|
|
88
141
|
const r = state.reconciled;
|
|
89
142
|
const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
|
|
90
143
|
const details = [...(state.rebalanceBlocked ?? []).map(row => `⛔ 이월 거부 ${row.id}: ${row.reason}`), ...(state.runSummaries ?? [])];
|
|
91
|
-
|
|
144
|
+
const body = details.length ? `${line}\n${details.join('\n')}` : line;
|
|
145
|
+
const applied = state.degradationPeak ?? state.degradation;
|
|
146
|
+
return applied ? `강등 L${applied.level}: ${applied.reason}\n${body}` : body;
|
|
92
147
|
}
|
|
93
148
|
function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
|
|
94
149
|
const dir = taskCardsDir(root);
|
|
@@ -122,10 +177,138 @@ function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id'
|
|
|
122
177
|
try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
|
|
123
178
|
finally { db.close(); }
|
|
124
179
|
}
|
|
180
|
+
/** 명시 인벤토리: 없으면 undefined, «있는데 못 읽으면» null — null 은 풀 명부로 대체하지 않는다. */
|
|
181
|
+
function readGridHosts(): readonly GridHost[] | null | undefined {
|
|
182
|
+
const supplied = process.env.ELANOUS_GRID_HOSTS;
|
|
183
|
+
if (supplied === undefined) return undefined;
|
|
184
|
+
try {
|
|
185
|
+
const inventory: unknown = JSON.parse(supplied);
|
|
186
|
+
if (!Array.isArray(inventory) || !inventory.every(row => row && typeof row === 'object' &&
|
|
187
|
+
typeof row.name === 'string' && row.name.trim() !== '' && Array.isArray(row.capabilities) &&
|
|
188
|
+
row.capabilities.every((capability: unknown) => typeof capability === 'string') &&
|
|
189
|
+
typeof row.available === 'boolean')) return null;
|
|
190
|
+
return inventory.map(row => ({ name: row.name as string, capabilities: new Set<string>(row.capabilities), available: row.available as boolean }));
|
|
191
|
+
} catch { return null; }
|
|
192
|
+
}
|
|
193
|
+
/** 주입 > 명시 인벤토리 > 풀 명부. 명시 인벤토리를 못 읽으면 제안하지 않는다(기존 경로 그대로). */
|
|
194
|
+
function resolveGridHosts(deps: TickDeps, observe: (event: string, reason: string, count: number) => void): readonly GridHost[] | undefined {
|
|
195
|
+
if (deps.gridHosts) return deps.gridHosts;
|
|
196
|
+
const supplied = readGridHosts();
|
|
197
|
+
if (supplied === null) { observe('node-missing', 'grid-inventory-unreadable', 0); return undefined; }
|
|
198
|
+
return supplied ?? gridHostsFromPodPool();
|
|
199
|
+
}
|
|
200
|
+
function preferredPoolSpec(host: string): string | undefined {
|
|
201
|
+
const config = getUserConfig();
|
|
202
|
+
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
203
|
+
if (!spec) return undefined;
|
|
204
|
+
try {
|
|
205
|
+
const members = parsePodPool(spec);
|
|
206
|
+
const member = members.find(candidate => (candidate.sshHost ?? candidate.context) === host);
|
|
207
|
+
if (!member) return undefined;
|
|
208
|
+
const parts = spec.split(',').map(part => part.trim());
|
|
209
|
+
const preferred = parts.find(part => part === member.context ||
|
|
210
|
+
['@', ':', '#'].some(separator => part.startsWith(`${member.context}${separator}`)));
|
|
211
|
+
return preferred ? [preferred, ...parts.filter(part => part !== preferred)].join(',') : undefined;
|
|
212
|
+
} catch { return undefined; }
|
|
213
|
+
}
|
|
214
|
+
function gridHostsFromPodPool(): readonly GridHost[] | undefined {
|
|
215
|
+
try {
|
|
216
|
+
const config = getUserConfig();
|
|
217
|
+
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
218
|
+
if (!spec) return undefined;
|
|
219
|
+
const members = parsePodPool(spec);
|
|
220
|
+
const checked = checkPodPool(members, leaseKubectl);
|
|
221
|
+
const ready = new Set(checked.ready.map(member => member.context));
|
|
222
|
+
return members.map(member => ({
|
|
223
|
+
name: member.sshHost ?? member.context,
|
|
224
|
+
capabilities: new Set(['pod']),
|
|
225
|
+
available: ready.has(member.context),
|
|
226
|
+
}));
|
|
227
|
+
} catch { return undefined; }
|
|
228
|
+
}
|
|
125
229
|
function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
|
|
126
|
-
function readRequests(root: string): Array<{ key: string; status: string }> {
|
|
230
|
+
function readRequests(root: string): Array<{ key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }> {
|
|
127
231
|
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 }) : [];
|
|
232
|
+
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 }) : [];
|
|
233
|
+
}
|
|
234
|
+
function refreshGridHosts(cells: Cell[], hosts: readonly GridHost[] | undefined, mode: Mode,
|
|
235
|
+
root: string, observe: (event: string, reason: string, count: number, targetLoopId?: string) => void): void {
|
|
236
|
+
if (hosts === undefined) return;
|
|
237
|
+
for (const cell of cells) {
|
|
238
|
+
if (mode !== 'live' || cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) {
|
|
239
|
+
observe('exchange', `would-host:${cell.id}:${placeOnGrid(cell, hosts).host ?? 'null'}`, 1);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const path = journal(root);
|
|
243
|
+
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
244
|
+
// 의뢰가 이미 대기열을 떠났으면(발사·완료) 제안만 관측하고 칸의 호스트는 그 행의 확정 호스트로 되돌린다.
|
|
245
|
+
const keepLaunched = (row: { host?: string; gridHost?: boolean }): void => {
|
|
246
|
+
if (row.host) cell.host = row.host; else delete cell.host;
|
|
247
|
+
if (row.gridHost) cell.gridHost = true; else delete cell.gridHost;
|
|
248
|
+
};
|
|
249
|
+
let changedFrom: string | undefined;
|
|
250
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
251
|
+
// 상태 확인 · 칸 변경 · 행 갱신은 모두 같은 잠금 안의 한 결정이다 — 다른 틱이 그 사이 행을 만들거나 바꿔도 덮지 않는다.
|
|
252
|
+
withFileLockSync(`${path}.lock`, () => {
|
|
253
|
+
const current = readRequests(root).filter(row => row.key === key).at(-1);
|
|
254
|
+
// 호스트 결정도 잠금 안 — 상태 확인과 같은 순간의 명부로 고른다.
|
|
255
|
+
const choice = placeOnGrid(cell, hosts);
|
|
256
|
+
observe('exchange', `would-host:${cell.id}:${choice.host ?? 'null'}`, 1);
|
|
257
|
+
if (current && current.status !== 'queued') { keepLaunched(current); return; }
|
|
258
|
+
if (choice.host) { cell.host = choice.host; cell.gridHost = true; }
|
|
259
|
+
else if (cell.gridHost || current?.gridHost) { delete cell.host; delete cell.gridHost; }
|
|
260
|
+
if (!current || (current.host === cell.host && current.gridHost === cell.gridHost && (current.kind === cell.kind || !cell.kind))) return;
|
|
261
|
+
if (current.host !== cell.host) changedFrom = current.host;
|
|
262
|
+
const updated = { ...current };
|
|
263
|
+
delete updated.host;
|
|
264
|
+
delete updated.gridHost;
|
|
265
|
+
if (cell.host) updated.host = cell.host;
|
|
266
|
+
if (cell.gridHost) updated.gridHost = true;
|
|
267
|
+
if (cell.kind) updated.kind = cell.kind;
|
|
268
|
+
appendFileSync(path, `${JSON.stringify(updated)}\n`);
|
|
269
|
+
});
|
|
270
|
+
if (changedFrom !== undefined) observe('exchange', `rehost:${cell.id}:${changedFrom}→${cell.host ?? 'none'}`, 1, SEAT_LOOP_IDS[cell.seat]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 재배치된 의뢰가 앞 틱에서 이미 대기열에 들어갔다면(아직 발사 전) 그 항목의 풀 힌트도 의뢰 행의 새 호스트로 맞춘다.
|
|
275
|
+
* 의뢰 키 ↔ 대기열 id 는 live 틱 상태들의 `launched` 에서 찾는다(날짜를 넘겨 대기 중인 항목도). 의뢰 행이 아직 queued 일 때만 옮긴다.
|
|
276
|
+
*/
|
|
277
|
+
async function syncQueuedPoolHints(root: string, current: NonNullable<TickState['launched']>, deps: TickDeps,
|
|
278
|
+
observe: (event: string, reason: string, count: number) => void): Promise<void> {
|
|
279
|
+
const dir = join(root, 'loop', 'orchestrator');
|
|
280
|
+
const turns = [...current];
|
|
281
|
+
if (existsSync(dir)) for (const name of readdirSync(dir).filter(file => /^[a-zA-Z0-9._-]+\.json$/.test(file))) {
|
|
282
|
+
try {
|
|
283
|
+
const other = JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState;
|
|
284
|
+
if (other.mode === 'live') turns.push(...(other.launched ?? []));
|
|
285
|
+
} catch { /* 깨진 상태 파일은 건너뛴다 */ }
|
|
286
|
+
}
|
|
287
|
+
const latest = new Map(readRequests(root).map(row => [row.key, row]));
|
|
288
|
+
const seen = new Set<string>();
|
|
289
|
+
for (const turn of turns) {
|
|
290
|
+
if (!turn.queueId || !turn.key || seen.has(turn.queueId)) continue;
|
|
291
|
+
seen.add(turn.queueId);
|
|
292
|
+
const row = latest.get(turn.key);
|
|
293
|
+
if (!row || row.kind !== 'pod' || row.status !== 'queued') continue;
|
|
294
|
+
// GRID 가 고른 행이면 그 호스트 쪽 힌트로, GRID 가 호스트를 거둔 행(둘 다 막힘)이면 GRID 가 넣었던 힌트만 지운다.
|
|
295
|
+
// GRID 와 무관한 기존 pod 의뢰의 힌트(ORCH1·명시 풀)는 건드리지 않는다.
|
|
296
|
+
const gridChosen = !!(row.gridHost && row.host);
|
|
297
|
+
if (!gridChosen && row.host) continue;
|
|
298
|
+
const hint = gridChosen ? preferredPoolSpec(row.host!) : undefined;
|
|
299
|
+
if (gridChosen && !hint) continue;
|
|
300
|
+
const key = turn.key;
|
|
301
|
+
// 갱신 직전 대기열 잠금 안에서: 의뢰가 아직 queued 이고 행의 호스트가 계산 때와 같으며,
|
|
302
|
+
// 지우는 경우엔 지금 대기열 힌트가 GRID 가 만든 꼴일 때만.
|
|
303
|
+
const stillQueued = (item: { poolHint?: string; poolHintSource?: 'grid' }): boolean => {
|
|
304
|
+
const now = readRequests(root).filter(r => r.key === key).at(-1);
|
|
305
|
+
if (now?.status !== 'queued' || now.host !== row.host || now.gridHost !== row.gridHost) return false;
|
|
306
|
+
// 지우는 쪽은 GRID 가 소유한 힌트만(소유 기록 확인) — 같은 문자열이라도 사람이 넣은 힌트는 남긴다.
|
|
307
|
+
return gridChosen || item.poolHintSource === 'grid';
|
|
308
|
+
};
|
|
309
|
+
if (await (deps.setPoolHint ?? ((id: string, value: string | undefined, stateRoot: string) => setQueuedPoolHint(id, value, { root: stateRoot }, stillQueued)))(turn.queueId, hint, root))
|
|
310
|
+
observe('exchange', `rehost-queue:${turn.key}:${hint ?? 'none'}`, 1);
|
|
311
|
+
}
|
|
129
312
|
}
|
|
130
313
|
function stateFile(root: string, runId: string): string {
|
|
131
314
|
if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
|
|
@@ -149,6 +332,22 @@ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState,
|
|
|
149
332
|
}
|
|
150
333
|
return { cards: [...cards.values()], cells: [...cells.values()], launched: [...launched.values()] };
|
|
151
334
|
}
|
|
335
|
+
function previousDelegations(root: string, day: string, mode: Mode, runId: string): { deferred: Cell[]; keys: string[] } {
|
|
336
|
+
const dir = join(root, 'loop', 'orchestrator');
|
|
337
|
+
if (!existsSync(dir)) return { deferred: [], keys: [] };
|
|
338
|
+
const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
|
|
339
|
+
.flatMap(name => {
|
|
340
|
+
try {
|
|
341
|
+
const path = join(dir, name);
|
|
342
|
+
const state = JSON.parse(readFileSync(path, 'utf8')) as TickState;
|
|
343
|
+
return state.runId !== runId && state.day <= day && state.mode === mode && state.window === '08' &&
|
|
344
|
+
state.nodes?.delegate === 'ok' && Array.isArray(state.deferredCells)
|
|
345
|
+
? [{ name, state, mtime: statSync(path).mtimeMs }] : [];
|
|
346
|
+
} catch { return []; }
|
|
347
|
+
});
|
|
348
|
+
states.sort((a, b) => a.state.day.localeCompare(b.state.day) || a.mtime - b.mtime || a.name.localeCompare(b.name));
|
|
349
|
+
return { deferred: states.at(-1)?.state.deferredCells ?? [], keys: states.flatMap(({ state }) => Array.isArray(state.delegatedKeys) ? state.delegatedKeys : []) };
|
|
350
|
+
}
|
|
152
351
|
function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
|
|
153
352
|
return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
|
|
154
353
|
reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, steps: [], nodes: {}, missing: [] };
|
|
@@ -177,11 +376,20 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
177
376
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
178
377
|
const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
|
|
179
378
|
const path = stateFile(root, runId);
|
|
180
|
-
const
|
|
379
|
+
const orchestratorConfig = getUserConfig().loops?.orchestrator;
|
|
380
|
+
const configured = deps.mode ?? orchestratorConfig?.mode;
|
|
181
381
|
const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
|
|
182
382
|
const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
|
|
183
383
|
state.steps ??= [];
|
|
184
384
|
if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
|
|
385
|
+
if (deps.degradation) {
|
|
386
|
+
const signal = deps.degradation();
|
|
387
|
+
state.degradation = !signal.board ? { level: 2, reason: signal.llm ? 'board 불가' : 'LLM·board 불가' }
|
|
388
|
+
: !signal.llm ? { level: 1, reason: 'LLM 불가' } : undefined;
|
|
389
|
+
// Only nodes that act on the degradation (split · place · delegate · launch) count toward the reported peak.
|
|
390
|
+
const applies = (['split', 'place', 'delegate', 'launch'] as Node[]).includes(node) && activeNode(node, window);
|
|
391
|
+
if (applies && state.degradation && (!state.degradationPeak || state.degradation.level >= state.degradationPeak.level)) state.degradationPeak = state.degradation;
|
|
392
|
+
}
|
|
185
393
|
const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', launch: 'delegate', reconcile: 'launch', report: 'reconcile' };
|
|
186
394
|
const preceding = predecessor[node];
|
|
187
395
|
if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
|
|
@@ -208,14 +416,17 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
208
416
|
observe('exchange', 'wish-cards', state.cards.length);
|
|
209
417
|
} else if (node === 'split') {
|
|
210
418
|
// Shadow also asks the splitter (it only proposes cells); only live records `orch:split` on the card.
|
|
211
|
-
const
|
|
212
|
-
|
|
419
|
+
const ruleOnly = (state.degradation?.level ?? 0) >= 1;
|
|
420
|
+
const split = ruleOnly ? undefined : deps.split ?? await adapter<SplitAdapter>('../../flow-loop/split.js', 'splitCard');
|
|
421
|
+
if (!split && !ruleOnly) { state.missing.push('flow1a-absent'); observe('node-missing', 'flow1a-absent', state.cards.length); }
|
|
213
422
|
state.cells = [];
|
|
214
423
|
for (const card of state.cards) {
|
|
215
|
-
const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
|
|
424
|
+
const cells = ruleOnly ? [{ id: card.id, title: card.title }] : split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
|
|
216
425
|
for (const cell of cells) {
|
|
217
426
|
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',
|
|
427
|
+
state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate',
|
|
428
|
+
...(ruleOnly ? { priority: rubricPriority(card.sections.map(section => section.content).join('\n')) } : {}),
|
|
429
|
+
...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
|
|
219
430
|
}
|
|
220
431
|
observe('exchange', `split:${card.id}`, cells.length);
|
|
221
432
|
if (split && cells.length && mode === 'live') {
|
|
@@ -226,63 +437,213 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
226
437
|
}
|
|
227
438
|
}
|
|
228
439
|
} else if (node === 'place') {
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
440
|
+
// Carry unfinished assignments into the same place/delegate path before applying this tick's cap.
|
|
441
|
+
const carry = previousDelegations(root, state.day, mode, runId).deferred;
|
|
442
|
+
if (carry.length) {
|
|
443
|
+
// Carried cells keep their place ahead of this tick's new cells (deduplicated) — a cell deferred by the cap is not
|
|
444
|
+
// pushed back again by a newly split cell.
|
|
445
|
+
const carried = new Set(carry.map(cell => `orch:${cell.cardId}:${cell.id}`));
|
|
446
|
+
state.cells = [...carry, ...state.cells.filter(cell => !carried.has(`orch:${cell.cardId}:${cell.id}`))];
|
|
236
447
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
448
|
+
if (state.degradation?.level === 2) {
|
|
449
|
+
state.placeDegraded = true;
|
|
450
|
+
observe('degraded-skip', 'place', 0);
|
|
451
|
+
} else {
|
|
452
|
+
const gridHosts = resolveGridHosts(deps, observe);
|
|
453
|
+
refreshGridHosts(state.cells, gridHosts, mode, root, observe);
|
|
454
|
+
if (mode === 'live') await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
|
|
455
|
+
const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
|
|
456
|
+
const available = typeof place === 'function';
|
|
457
|
+
if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
|
|
458
|
+
if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
|
|
459
|
+
// 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
|
|
460
|
+
for (const cell of state.cells) {
|
|
461
|
+
if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
|
|
462
|
+
}
|
|
463
|
+
if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
|
|
464
|
+
if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
|
|
465
|
+
// RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
|
|
466
|
+
try {
|
|
467
|
+
const priority = cell.priority === 'P3' || cell.priority === 'P4' ? 'P2' : cell.priority ?? 'P2';
|
|
468
|
+
const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority, predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
|
|
469
|
+
if (validVersion(placed?.version)) {
|
|
470
|
+
cell.version = placed.version;
|
|
471
|
+
state.placed++;
|
|
472
|
+
observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
|
|
473
|
+
} else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
|
|
474
|
+
} catch (error) {
|
|
475
|
+
state.unplaced = (state.unplaced ?? 0) + 1;
|
|
476
|
+
observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
|
|
477
|
+
}
|
|
250
478
|
}
|
|
251
479
|
}
|
|
252
480
|
} else if (node === 'delegate') {
|
|
481
|
+
if (state.degradation?.level === 2) observe('degraded-skip', 'delegate', 0);
|
|
482
|
+
const cap = orchestratorConfig?.maxDelegatePerTick;
|
|
483
|
+
const previous = previousDelegations(root, state.day, mode, runId);
|
|
484
|
+
if (cap !== undefined || state.degradation?.level === 2 || state.placeDegraded || previous.deferred.length) {
|
|
485
|
+
state.deferredCells = [];
|
|
486
|
+
state.delegatedKeys = [];
|
|
487
|
+
}
|
|
253
488
|
const path = journal(root);
|
|
254
|
-
|
|
489
|
+
// 모드와 칸 필터는 따로 푼다 — 주입으로 모드만 줘도 설정의 칸 필터는 그대로 먹는다.
|
|
490
|
+
const handConfig: Partial<OrchestratorLoopConfig> = deps.handToTaskAgent !== undefined && deps.handToTaskAgentCell !== undefined ? {} : getUserConfig().loops?.orchestrator ?? {};
|
|
491
|
+
// live 로 넘긴 칸은 지금 설정(off·칸 필터)과 무관하게 자리 경로로 다시 안 간다 — 발사 주체가 둘이 되지 않게.
|
|
492
|
+
// ⭐ 넘김 claim 과 자리 저널 기록은 «같은 잠금»(자리 저널 잠금) 아래에서 서로를 다시 읽는다 — 동시 틱이 낡은 값을 보지 않게.
|
|
493
|
+
const ledger = handedLedger(root);
|
|
494
|
+
const liveHanded = handedKeys(ledger);
|
|
495
|
+
// Earlier runs' (would-)assignments only count while a cap is configured — they are what the cap carried over.
|
|
496
|
+
// With no cap the tick behaves as before: shadow would-delegates every cell again, live dedups on the seat journal.
|
|
497
|
+
const keys = new Set([...readRequests(root).map(row => row.key), ...(cap !== undefined ? previous.keys : [])]);
|
|
498
|
+
// 넘김 뒤 상태 저장 전에 죽은 같은 run 이 다시 돌면 원장에서 handed[] 를 되살린다.
|
|
499
|
+
const ledgerHanded = handedRows(ledger);
|
|
500
|
+
const restoreHanded = (handKey: string, key: string, rows: Map<string, HandedRow> = handedRows(ledger)): void => {
|
|
501
|
+
const row = rows.get(handKey);
|
|
502
|
+
// 이 run 이 넘긴 것만 되살린다 — 다른 run 의 넘김을 «이 run 에서 넘겼다»로 적지 않게.
|
|
503
|
+
if (!row || row.runId !== runId || (state.handed ?? []).some(entry => entry.key === key && entry.mode === row.mode)) return;
|
|
504
|
+
(state.handed ??= []).push({ key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched });
|
|
505
|
+
};
|
|
506
|
+
// A retried delegate (died after assigning, before saving state) rebuilds this run's cap usage from what it already
|
|
507
|
+
// wrote: seat rows tagged with this run and live hands claimed by this run.
|
|
508
|
+
if (cap !== undefined && state.delegatedKeys) {
|
|
509
|
+
const used = new Set(state.delegatedKeys);
|
|
510
|
+
if (existsSync(path)) for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
511
|
+
if (!line) continue;
|
|
512
|
+
let row: { key?: unknown; source?: unknown; orchRunId?: unknown };
|
|
513
|
+
try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid seat request journal'); }
|
|
514
|
+
if (row.source === 'orchestrator' && row.orchRunId === runId && typeof row.key === 'string') used.add(row.key);
|
|
515
|
+
}
|
|
516
|
+
for (const row of handedRows(ledger).values()) if (row.runId === runId && row.mode === 'live') used.add(row.key);
|
|
517
|
+
if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
|
|
518
|
+
if (!line) continue;
|
|
519
|
+
try {
|
|
520
|
+
const claim = JSON.parse(line) as { key?: unknown; runId?: unknown; mode?: unknown; status?: unknown };
|
|
521
|
+
if (claim.runId === runId && claim.mode === 'live' && claim.status === 'claimed' && typeof claim.key === 'string') used.add(claim.key);
|
|
522
|
+
} catch { /* handedKeys already rejects a broken ledger */ }
|
|
523
|
+
}
|
|
524
|
+
state.delegatedKeys = [...used];
|
|
525
|
+
}
|
|
526
|
+
const journalHas = (key: string): boolean => {
|
|
527
|
+
if (!existsSync(path)) return false;
|
|
528
|
+
return readFileSync(path, 'utf8').split('\n').some(line => {
|
|
529
|
+
if (!line) return false;
|
|
530
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
|
|
531
|
+
});
|
|
532
|
+
};
|
|
255
533
|
for (const cell of state.cells) {
|
|
256
534
|
if (cell.origin !== 'flow1a' || !cell.seat) continue;
|
|
257
535
|
const targetLoopId = SEAT_LOOP_IDS[cell.seat];
|
|
258
536
|
if (!targetLoopId) continue;
|
|
259
537
|
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
538
|
+
// 저널 키로 건너뛰기 «전에» 원장에서 복구한다 — shadow 넘김 ⊕ 자리 기록 뒤 상태 저장 전에 죽은 run 도 handed[] 를 되찾게.
|
|
539
|
+
restoreHanded(key, key, ledgerHanded);
|
|
540
|
+
restoreHanded(`shadow:${key}`, key, ledgerHanded);
|
|
260
541
|
if (keys.has(key)) continue;
|
|
542
|
+
if (liveHanded.has(key)) { keys.add(key); observe('exchange', `handed-to-task-agent:${key}`, 1, 'task-agent'); continue; }
|
|
543
|
+
// COORD-HA ②: a live tick skips unplaced cells for assignment; L2 assigns nothing new (live hands included) and the
|
|
544
|
+
// per-tick cap defers the rest to the next tick — both before the TASK-AGENT hand so a hand never bypasses them.
|
|
545
|
+
const unplacedLive = mode === 'live' && !validVersion(cell.version);
|
|
546
|
+
// L2 carries every cell — placed or not — to the next tick: split already marked the card, so a cell skipped here
|
|
547
|
+
// would never be split again.
|
|
548
|
+
if (state.degradation?.level === 2 || (unplacedLive && state.placeDegraded)) {
|
|
549
|
+
state.deferredCells?.push(cell);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
// The cap limits assignments (a live hand or a seat row); a shadow card is not an assignment and is still written.
|
|
553
|
+
const capped = !unplacedLive && cap !== undefined && state.delegatedKeys!.length >= cap;
|
|
554
|
+
// ORCH-TA-HAND: hand the picked cell to TASK-AGENT. shadow = card only (the seat path below is unchanged);
|
|
555
|
+
// live (tick live ⊕ hand live) = launch through TASK-AGENT and skip the seat journal so the cell launches once.
|
|
556
|
+
const handMode = deps.handToTaskAgent ?? handConfig.handToTaskAgent ?? 'off';
|
|
557
|
+
const handCell = deps.handToTaskAgentCell ?? handConfig.handToTaskAgentCell;
|
|
558
|
+
if (handMode !== 'off' && (!handCell || handCell === cell.id) && (TASK_SEATS as readonly string[]).includes(cell.seat)) {
|
|
559
|
+
// live 는 tick 도 live 이고 배치(유효한 판)가 된 칸만 — 나머지는 카드만(shadow · 배치 전 칸도 표본이 된다).
|
|
560
|
+
const handLive = handMode === 'live' && mode === 'live' && validVersion(cell.version) && !capped;
|
|
561
|
+
// shadow 표본은 따로 센다 — 자리 경로로 아직 안 간 칸(shadow 틱·배치 전)이 나중에 live 로 바뀌면 그때 뜬다.
|
|
562
|
+
// ⚠️ 이미 자리 저널에 들어간 칸은 live 로 바꿔도 다시 안 넘긴다(keys.has 에서 끝) — 한 칸의 발사 주체는 하나다(의도).
|
|
563
|
+
const handKey = handLive ? key : `shadow:${key}`;
|
|
564
|
+
mkdirSync(dirname(ledger), { recursive: true });
|
|
565
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
566
|
+
// Claim under the seat-journal lock — the same lock the seat append takes, so a live claim and a seat row never both land.
|
|
567
|
+
const claimed = withFileLockSync(`${path}.lock`, () => {
|
|
568
|
+
if (handedKeys(ledger).has(handKey)) return false;
|
|
569
|
+
if (handLive && journalHas(key)) return false;
|
|
570
|
+
appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, seat: cell.seat, mode: handLive ? 'live' : 'shadow', status: 'claimed', runId, at: now.toISOString() })}\n`);
|
|
571
|
+
return true;
|
|
572
|
+
});
|
|
573
|
+
if (claimed) {
|
|
574
|
+
try {
|
|
575
|
+
const result = await (deps.handTask ?? (async (opts: HandTaskOptions) => (await import('../../task-agent/task-hand.js')).handTask(opts)))({
|
|
576
|
+
text: cell.title, seat: cell.seat as TaskSeat, checklistId: cell.id, live: handLive, now: () => now,
|
|
577
|
+
statePath: join(root, 'task-agent-actions.json'),
|
|
578
|
+
...(handLive ? { launcher: deps.taskLauncher ?? (async (args: string[], _cwd?: string, context?: Parameters<TaskLauncher>[2]) => (await import('../../cli/tasks-cli.js')).defaultTaskLauncher(args, undefined, context)) } : {}),
|
|
579
|
+
});
|
|
580
|
+
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`));
|
|
581
|
+
(state.handed ??= []).push({ key, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched });
|
|
582
|
+
observe('hand-to-task-agent', `${key}:${result.card.id}:${result.mode}${result.launched ? ':launched' : ''}`, 1, 'task-agent');
|
|
583
|
+
// A live hand is an assignment: it counts toward maxDelegatePerTick like a seat row (COORD-HA ②).
|
|
584
|
+
if (handLive) { keys.add(key); state.delegatedKeys?.push(key); continue; }
|
|
585
|
+
} catch (error) {
|
|
586
|
+
observe('hand-to-task-agent-failed', `${key}:${String(error).slice(0, 160)}`, 1, 'task-agent');
|
|
587
|
+
// A failed live hand stays claimed (no relaunch loop) and does not fall back to the seat path — one launcher per cell.
|
|
588
|
+
if (handLive) { keys.add(key); continue; }
|
|
589
|
+
// A failed shadow hand launched nothing — release the claim so a later tick can write the card,
|
|
590
|
+
// but only when no card for this cell was written (a failure after the card must not make a second one).
|
|
591
|
+
let cardWritten = true;
|
|
592
|
+
try {
|
|
593
|
+
const storePath = join(root, 'task-agent-actions.json');
|
|
594
|
+
if (!existsSync(storePath)) cardWritten = false;
|
|
595
|
+
else {
|
|
596
|
+
// 상태를 믿을 수 있을 때만 «카드 0장»으로 읽는다 — tasks 가 없거나 꼴이 틀리면 claim 을 남긴다.
|
|
597
|
+
const tasks = readTaskAgentState<{ tasks?: unknown }>(storePath).tasks;
|
|
598
|
+
if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)
|
|
599
|
+
&& Object.values(tasks).every(card => card && typeof card === 'object' && !Array.isArray(card))) {
|
|
600
|
+
cardWritten = Object.values(tasks as Record<string, TaskAgentCard>).some(card => card.checklistId === cell.id && card.text === cell.title);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch { cardWritten = true; /* unreadable store → keep the claim (never risk a duplicate) */ }
|
|
604
|
+
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`));
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
restoreHanded(handKey, key);
|
|
608
|
+
if (handLive) { keys.add(key); continue; }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (capped) {
|
|
612
|
+
state.deferredCells!.push(cell);
|
|
613
|
+
observe('cap-deferred', key, 1, targetLoopId);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (unplacedLive) continue;
|
|
261
617
|
if (!validVersion(cell.version)) {
|
|
262
|
-
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
|
|
618
|
+
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; state.delegatedKeys?.push(key); observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
|
|
263
619
|
continue;
|
|
264
620
|
}
|
|
265
|
-
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', key, 1, targetLoopId); continue; }
|
|
621
|
+
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; state.delegatedKeys?.push(key); observe('would-delegate', key, 1, targetLoopId); continue; }
|
|
266
622
|
keys.add(key);
|
|
267
623
|
mkdirSync(dirname(path), { recursive: true });
|
|
268
624
|
// TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
|
|
269
625
|
// 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`);
|
|
626
|
+
const appended = withFileLockSync(`${path}.lock`, (): boolean | 'handed' => {
|
|
627
|
+
if (journalHas(key)) return false;
|
|
628
|
+
// 잠금 안에서 다시 본다 — 동시 틱이 이 칸을 방금 live 로 넘겼으면 자리 경로로 보내지 않는다.
|
|
629
|
+
if (handedKeys(ledger).has(key)) return 'handed';
|
|
630
|
+
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, orchRunId: runId })}\n`);
|
|
277
631
|
return true;
|
|
278
632
|
});
|
|
633
|
+
if (appended === 'handed') { observe('exchange', `seat-suppressed-handed:${key}`, 1, 'task-agent'); continue; }
|
|
279
634
|
if (!appended) continue;
|
|
280
635
|
state.delegated++;
|
|
636
|
+
state.delegatedKeys?.push(key);
|
|
281
637
|
observe('exchange', 'queued', 1, targetLoopId);
|
|
282
638
|
}
|
|
283
639
|
} else if (node === 'launch') {
|
|
284
|
-
|
|
285
|
-
|
|
640
|
+
if (state.degradation?.level === 2) observe('degraded-skip', 'launch', 0);
|
|
641
|
+
// shadow 는 저널을 읽지 않는다 — 깨진 live 저널이 관측 전용 틱을 넘어뜨리지 않게.
|
|
642
|
+
const latestRequests = new Map(mode === 'live' && state.degradation?.level !== 2 ? readRequests(root).map(row => [row.key, row]) : []);
|
|
643
|
+
const eligible = state.cells.filter(cell => state.degradation?.level !== 2 && cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
|
|
644
|
+
&& validVersion(cell.version) && (mode === 'shadow' || latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued')
|
|
645
|
+
&& (orchestratorConfig?.maxDelegatePerTick === undefined || state.delegatedKeys?.includes(`orch:${cell.cardId}:${cell.id}`)
|
|
646
|
+
|| (mode === 'live' && latestRequests.has(`orch:${cell.cardId}:${cell.id}`))));
|
|
286
647
|
const seats = [...new Set(eligible.map(cell => cell.seat!))];
|
|
287
648
|
state.launched = [];
|
|
288
649
|
for (const seat of seats) {
|
|
@@ -293,9 +654,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
293
654
|
state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
|
|
294
655
|
observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
|
|
295
656
|
} else {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
657
|
+
const selected = eligible.find(cell => cell.seat === seat && latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued');
|
|
658
|
+
const host = selected && latestRequests.get(`orch:${selected.cardId}:${selected.id}`)?.host;
|
|
659
|
+
const pools = new Map<string, string>([...latestRequests].flatMap(([key, row]) => {
|
|
660
|
+
if (row.status !== 'queued' || row.seat !== seat || row.kind !== 'pod' || !row.gridHost || !row.host || !key.startsWith('orch:')) return [];
|
|
661
|
+
const pool = preferredPoolSpec(row.host);
|
|
662
|
+
// 풀 힌트는 «설정된 풀»의 순서 바꾸기다 — 고른 호스트가 풀에 없으면 힌트를 지어내지 않고 그 사실을 남긴다.
|
|
663
|
+
if (!pool) observe('exchange', `grid-host-unmapped:${key}:${row.host}`, 1, SEAT_LOOP_IDS[seat]);
|
|
664
|
+
return pool ? [[key, pool] as const] : [];
|
|
665
|
+
}));
|
|
666
|
+
const enqueue: SeatDeps['enqueue'] | undefined = pools.size
|
|
667
|
+
? (queuedSeat, text, queuedRoot, idempotencyKey, item) => addHarnessQueue({ seat: queuedSeat, say: text, idempotencyKey,
|
|
668
|
+
...(item.source === 'request' && pools.has(item.id) ? { poolHint: pools.get(item.id), poolHintSource: 'grid' as const } : {}) }, { root: queuedRoot })
|
|
669
|
+
: undefined;
|
|
670
|
+
const options = enqueue ? { enqueue } : undefined;
|
|
671
|
+
const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string, _preferred?: string, overrides?: Pick<SeatDeps, 'enqueue'>) =>
|
|
672
|
+
runSeatLoopOnce(assigned, { root: stateRoot,
|
|
673
|
+
config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] }, ...overrides })))(seat, root, host, options);
|
|
299
674
|
// 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
|
|
300
675
|
const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
|
|
301
676
|
const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
|
|
@@ -318,6 +693,10 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
318
693
|
state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
|
|
319
694
|
state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
|
|
320
695
|
}
|
|
696
|
+
if (mode === 'live') {
|
|
697
|
+
refreshGridHosts(state.cells, resolveGridHosts(deps, observe), mode, root, observe);
|
|
698
|
+
await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
|
|
699
|
+
}
|
|
321
700
|
}
|
|
322
701
|
state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
|
|
323
702
|
const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
|
|
@@ -362,19 +741,36 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
362
741
|
return state;
|
|
363
742
|
}
|
|
364
743
|
|
|
744
|
+
/** COORD-HA — the production supply of `TickDeps.degradation` (config `loops.orchestrator.degradationSignal`). Shadow (the
|
|
745
|
+
* default) probes and logs what the tick would have done; only live hands the signal to the tick. */
|
|
746
|
+
export async function degradationDeps(node: Node, deps: { config?: () => Partial<OrchestratorLoopConfig> | undefined; root?: string;
|
|
747
|
+
probe?: (root: string) => { llm: boolean; board: boolean; why: string[] } } = {}): Promise<Pick<TickDeps, 'degradation'>> {
|
|
748
|
+
const mode = (deps.config ?? (() => getUserConfig().loops?.orchestrator))()?.degradationSignal ?? 'shadow';
|
|
749
|
+
if (mode === 'off') return {};
|
|
750
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
751
|
+
const signal = (deps.probe ?? (await import('./degradation.js')).probeDegradation)(root);
|
|
752
|
+
const level = !signal.board ? 2 : !signal.llm ? 1 : 0;
|
|
753
|
+
debug.log('loop.orchestrator', 'degradation-signal', { node, mode, llm: signal.llm, board: signal.board, level, why: signal.why });
|
|
754
|
+
return mode === 'live' ? { degradation: () => ({ llm: signal.llm, board: signal.board }) } : {};
|
|
755
|
+
}
|
|
756
|
+
|
|
365
757
|
if (import.meta.main) {
|
|
366
758
|
const node = process.argv[2];
|
|
367
759
|
const index = process.argv.indexOf('--window');
|
|
368
|
-
|
|
760
|
+
// ORCH-LIVE-1008: `graph run` recipes pass no flags, so a manual/rehearsal tick picks its window through the
|
|
761
|
+
// environment (`ELANOUS_ORCH_WINDOW=08 elanous graph run graphs/orchestrator/orchestrator.yaml`). The flag wins.
|
|
762
|
+
const window = index < 0 ? process.env.ELANOUS_ORCH_WINDOW : process.argv[index + 1];
|
|
763
|
+
// A given flag (even without a value) or a set variable must name a real window — never fall back to «now».
|
|
764
|
+
const windowGiven = index >= 0 || process.env.ELANOUS_ORCH_WINDOW !== undefined;
|
|
369
765
|
if (!['intake', 'split', 'place', 'delegate', 'launch', 'reconcile', 'report'].includes(node ?? '') ||
|
|
370
|
-
(
|
|
371
|
-
console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18]');
|
|
766
|
+
(windowGiven && !['08', '12', '18'].includes(window ?? ''))) {
|
|
767
|
+
console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18] (or ELANOUS_ORCH_WINDOW=08|12|18)');
|
|
372
768
|
process.exitCode = 2;
|
|
373
769
|
} else {
|
|
374
770
|
void (async () => {
|
|
375
771
|
try { await (await import('../../domains/standalone-log-sink.js')).registerStandaloneLogSink('orchestrator'); }
|
|
376
772
|
catch { /* local execution can still use the per-run file */ }
|
|
377
|
-
await runOrchestratorNode(node as Node, { window: window as Window | undefined });
|
|
773
|
+
await runOrchestratorNode(node as Node, { window: window as Window | undefined, ...await degradationDeps(node as Node) });
|
|
378
774
|
})().catch(error => { console.error(`orchestrator ${node}: ${String(error)}`); process.exitCode = 1; });
|
|
379
775
|
}
|
|
380
776
|
}
|