elanous 0.2.13 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- 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/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 +23 -23
- 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 +22 -22
- 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 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- 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/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +6 -0
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +1 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +188 -0
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/coord-post.sh +11 -1
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +169 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/standby-verify.ts +11 -7
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +112 -0
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +89 -14
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +428 -0
- package/scripts/release-story/draft.ts +220 -0
- package/scripts/rhythm/daily.ts +570 -0
- package/scripts/rhythm/weekly.ts +315 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/scripts/test-deterministic.ts +4 -8
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +3 -3
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +73 -10
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/briefing/brief-items.ts +121 -0
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +166 -0
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +44 -3
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +29 -1
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +190 -33
- package/src/cli/rfc-status.ts +91 -0
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +15 -20
- package/src/context-bus/context-now-public.ts +96 -0
- package/src/context-bus/context-now-surfaces.ts +55 -2
- package/src/context-bus/context-now.ts +6 -3
- package/src/context-bus/coord-events.ts +6 -4
- package/src/context-bus/long-term-memory.ts +92 -11
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/decision-ledger.ts +40 -1
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -0
- package/src/graph-runner/graph-tick.ts +4 -0
- package/src/graph-runner/runner.ts +30 -5
- package/src/harness/harness-cli-command.ts +259 -38
- package/src/harness/harness-incidents.ts +45 -12
- package/src/harness/harness-pod-dispatch.ts +102 -11
- package/src/harness/harness-queue-child.ts +3 -3
- package/src/harness/harness-queue.ts +583 -65
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +151 -0
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +63 -5
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +332 -0
- package/src/index.ts +159 -27
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/linear-wish.ts +39 -11
- package/src/intake-plane/route.ts +145 -1
- package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +63 -0
- package/src/loops/checker.ts +5 -3
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +36 -4
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +10 -0
- package/src/loops/orchestrator/author-depth.ts +220 -0
- package/src/loops/orchestrator/author-input-check.ts +82 -0
- package/src/loops/orchestrator/author-ledger.ts +232 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +51 -12
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +309 -0
- package/src/loops/orchestrator/traffic.ts +134 -13
- package/src/loops/registry.ts +535 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +17 -5
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +9 -5
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/http-server.ts +33 -3
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/schedules-read.ts +18 -1
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +51 -0
- package/src/release-loop/checklist.ts +22 -4
- package/src/release-loop/feature-store.ts +71 -32
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +302 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +16 -3
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +531 -40
- package/src/self-dev/dev-pipeline.ts +14 -6
- package/src/self-dev/draft-sweep.ts +162 -6
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +57 -2
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +295 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +197 -0
- package/src/self-implement/host-regate.ts +32 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +299 -48
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +97 -0
- package/src/self-implement/run-ledger.ts +3 -1
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +29 -9
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session/index.ts +2 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-cards/card-cli.ts +10 -2
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +146 -18
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -1,34 +1,126 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
|
-
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
8
|
-
import {
|
|
7
|
+
import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
8
|
+
import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
9
|
+
import { listSchedules } from '../release-loop/release-schedule.js';
|
|
10
|
+
import { withFileLockSync } from '../storage/file-lock.js';
|
|
11
|
+
import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
|
|
12
|
+
import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
|
|
13
|
+
import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
|
|
14
|
+
import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
|
|
9
15
|
import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
|
|
10
16
|
import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
11
17
|
import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
12
18
|
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
19
|
+
import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
|
|
13
20
|
|
|
14
21
|
export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
15
22
|
export type QueueItem = {
|
|
16
23
|
id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
|
|
17
|
-
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string;
|
|
24
|
+
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
|
|
18
25
|
};
|
|
19
26
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
20
27
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
21
|
-
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
|
|
28
|
+
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number };
|
|
29
|
+
type ProcessProbe = {
|
|
30
|
+
run?: typeof spawnSync; platform?: NodeJS.Platform;
|
|
31
|
+
cwd?: (pid: number) => string;
|
|
32
|
+
seatTrees?: Partial<Record<QueueSeat, string[]>>;
|
|
33
|
+
runsDir?: string;
|
|
34
|
+
environ?: (pid: number) => string;
|
|
35
|
+
birthId?: (pid: number) => string | undefined;
|
|
36
|
+
};
|
|
37
|
+
const queueSeatNames = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
38
|
+
|
|
39
|
+
export function queueSeatForCwd(cwd: string, trees: Partial<Record<QueueSeat, string[]>> = {}): QueueSeat | undefined {
|
|
40
|
+
if (!cwd.startsWith('/')) return undefined;
|
|
41
|
+
const current = resolve(cwd);
|
|
42
|
+
const matches = queueSeatNames.filter((s) => trees[s]?.some((tree) => {
|
|
43
|
+
if (!tree.startsWith('/')) return false;
|
|
44
|
+
const prefix = resolve(tree);
|
|
45
|
+
return current === prefix || current.startsWith(`${prefix}/`);
|
|
46
|
+
}));
|
|
47
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
export interface HarnessQueueDeps {
|
|
24
51
|
root?: string;
|
|
52
|
+
configPath?: string;
|
|
25
53
|
cap?: (seat: QueueSeat) => number;
|
|
26
54
|
pool?: () => QueuePool;
|
|
27
55
|
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
28
56
|
alive?: (pid: number) => boolean;
|
|
29
57
|
processes?: () => readonly QueueProcess[];
|
|
30
58
|
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
59
|
+
now?: () => Date;
|
|
60
|
+
authorShadow?: (root: string, now: Date) => void | Promise<void>;
|
|
61
|
+
idleRequest?: (root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps) => void | Promise<void>;
|
|
62
|
+
idleCells?: (now: Date) => { current: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[];
|
|
63
|
+
next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
|
|
64
|
+
idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
31
65
|
log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
|
|
66
|
+
/** FINISH-RATE metrics source; a test process without it skips the real measurement. */
|
|
67
|
+
finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const FINISH_CACHE_MS = 10 * 60 * 1_000;
|
|
71
|
+
const FINISH_KEYS = ['launched', 'landed', 'landingRate', 'staleDrafts', 'conflictRatio', 'unknownMergeable'] as const;
|
|
72
|
+
|
|
73
|
+
/** A cache row missing any metric or its reasons map is re-measured, never fed to finishAdvice. */
|
|
74
|
+
function validFinishMetrics(value: unknown): value is FinishMetrics {
|
|
75
|
+
if (!value || typeof value !== 'object') return false;
|
|
76
|
+
const row = value as Record<string, unknown>;
|
|
77
|
+
return FINISH_KEYS.every((key) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key])))
|
|
78
|
+
&& !!row.reasons && typeof row.reasons === 'object' && !Array.isArray(row.reasons);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Reads finish metrics at most once per 10 minutes per root; unreadable or missing → null (never a hold). */
|
|
82
|
+
async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDeps): Promise<FinishMetrics | null> {
|
|
83
|
+
// Checked before the cache: a cached real measurement must not reach an uninjected test tick either.
|
|
84
|
+
if (!deps.finishMetrics && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
|
|
85
|
+
debug.log('loop.orchestrator', 'finish-skipped-test', {});
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const path = join(root, 'harness', 'finish-metrics.json');
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
const cached = JSON.parse(readFileSync(path, 'utf8')) as { at?: unknown; metrics?: unknown };
|
|
92
|
+
const at = typeof cached.at === 'string' ? Date.parse(cached.at) : NaN;
|
|
93
|
+
if (Number.isFinite(at) && now.getTime() - at >= 0 && now.getTime() - at < FINISH_CACHE_MS
|
|
94
|
+
&& validFinishMetrics(cached.metrics)) return cached.metrics;
|
|
95
|
+
}
|
|
96
|
+
} catch { /* A broken cache is re-measured. */ }
|
|
97
|
+
const measure = deps.finishMetrics ?? (() => measureFinish());
|
|
98
|
+
const metrics = await measure(now);
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(join(root, 'harness'), { recursive: true });
|
|
101
|
+
writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
|
|
102
|
+
} catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
|
|
103
|
+
return metrics;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** FINISH-RATE: a backlogged board holds this launch when the pool already fills the launch share. */
|
|
107
|
+
async function finishGateReason(root: string, now: Date, mode: 'off' | 'shadow' | 'on', running: number, totalSlots: number,
|
|
108
|
+
deps: HarnessQueueDeps): Promise<string | null> {
|
|
109
|
+
if (mode === 'off') return null;
|
|
110
|
+
let metrics: FinishMetrics | null;
|
|
111
|
+
try { metrics = await cachedFinishMetrics(root, now, deps); }
|
|
112
|
+
catch (error) {
|
|
113
|
+
debug.log('loop.orchestrator', 'finish-measure-failed', { reason: String(error) });
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!metrics) return null;
|
|
117
|
+
const advice = finishAdvice(metrics, totalSlots);
|
|
118
|
+
if (advice.state !== 'backlogged' || running < advice.launchSlots) return null;
|
|
119
|
+
debug.log('loop.orchestrator', 'finish-rebalanced', {
|
|
120
|
+
finishSlots: advice.finishSlots, launchSlots: advice.launchSlots, running, reasons: advice.reasons, mode,
|
|
121
|
+
});
|
|
122
|
+
if (mode === 'shadow') return null;
|
|
123
|
+
return `마무리 우선 — finish=${advice.finishSlots}/${totalSlots} · 사유 ${advice.reasons.join(', ')}`;
|
|
32
124
|
}
|
|
33
125
|
|
|
34
126
|
export function harnessQueuePath(root = effectiveInstanceRoot()): string {
|
|
@@ -49,7 +141,9 @@ function read(path: string): QueueItem[] {
|
|
|
49
141
|
&& typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
|
|
50
142
|
&& ['queued', 'launching', 'launched', 'finished'].includes(row.status)
|
|
51
143
|
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
52
|
-
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
144
|
+
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
145
|
+
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
146
|
+
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
53
147
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
54
148
|
}
|
|
55
149
|
return value as QueueItem[];
|
|
@@ -66,6 +160,29 @@ function save(path: string, items: QueueItem[]): void {
|
|
|
66
160
|
}
|
|
67
161
|
}
|
|
68
162
|
|
|
163
|
+
function lastLaunchedSeat(path: string): QueueSeat | undefined {
|
|
164
|
+
const marker = `${path}.round-robin.json`;
|
|
165
|
+
if (!existsSync(marker)) return undefined;
|
|
166
|
+
const value: unknown = JSON.parse(readFileSync(marker, 'utf8'));
|
|
167
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
168
|
+
|| !queueSeatNames.some((name) => name === (value as { lastSeat?: unknown }).lastSeat)) {
|
|
169
|
+
throw new Error('harness queue: invalid round-robin state (no launch)');
|
|
170
|
+
}
|
|
171
|
+
return (value as { lastSeat: QueueSeat }).lastSeat;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function saveLastLaunchedSeat(path: string, lastSeat: QueueSeat): void {
|
|
175
|
+
const marker = `${path}.round-robin.json`;
|
|
176
|
+
const temporary = `${marker}.${randomUUID()}.tmp`;
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(temporary, JSON.stringify({ lastSeat }), { flag: 'wx', mode: 0o600 });
|
|
179
|
+
renameSync(temporary, marker);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
try { unlinkSync(temporary); } catch { /* write did not finish */ }
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
69
186
|
// SQLite OS locks release on process death, including death while awaiting a launch.
|
|
70
187
|
async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
|
|
71
188
|
const path = harnessQueuePath(root);
|
|
@@ -90,7 +207,7 @@ function seat(value: string): QueueSeat {
|
|
|
90
207
|
return value as QueueSeat;
|
|
91
208
|
}
|
|
92
209
|
|
|
93
|
-
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
210
|
+
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
94
211
|
const assigned = seat(input.seat);
|
|
95
212
|
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
96
213
|
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
@@ -100,9 +217,21 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
|
|
|
100
217
|
if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
|
|
101
218
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
102
219
|
return locked(root, async (path) => {
|
|
220
|
+
const rows = read(path);
|
|
221
|
+
if (input.idempotencyKey !== undefined) {
|
|
222
|
+
if (!input.idempotencyKey.trim()) throw new Error('harness queue add: empty idempotency key');
|
|
223
|
+
const existing = rows.find((row) => row.idempotencyKey === input.idempotencyKey
|
|
224
|
+
&& (row.status === 'queued' || row.status === 'launching' || row.status === 'launched')
|
|
225
|
+
&& queueRowOutcome(row, root) !== 'retryable');
|
|
226
|
+
if (existing) {
|
|
227
|
+
if (existing.seat !== assigned || existing.kind !== kind || existing.input !== value) throw new Error('harness queue: idempotency key collision');
|
|
228
|
+
return existing;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
103
231
|
const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
|
|
104
|
-
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued'
|
|
105
|
-
|
|
232
|
+
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
|
|
233
|
+
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
|
|
234
|
+
save(path, [...rows, item]);
|
|
106
235
|
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
107
236
|
return item;
|
|
108
237
|
});
|
|
@@ -112,6 +241,60 @@ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
|
|
|
112
241
|
return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
|
|
113
242
|
}
|
|
114
243
|
|
|
244
|
+
/** A missing row is not proof of failure unless the queue left a cancellation marker. */
|
|
245
|
+
export function harnessQueueOutcome(id: string, deps: HarnessQueueDeps = {}): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
|
|
246
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
247
|
+
const row = listHarnessQueue(deps).find((item) => item.id === id);
|
|
248
|
+
if (!row) {
|
|
249
|
+
const marker = join(root, 'harness', `${id}.outcome`);
|
|
250
|
+
if (!existsSync(marker)) return 'unknown';
|
|
251
|
+
const value = readFileSync(marker, 'utf8');
|
|
252
|
+
return value === 'succeeded' || value === 'retryable' ? value : 'unknown';
|
|
253
|
+
}
|
|
254
|
+
return queueRowOutcome(row, root);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const keyMarkerPath = (root: string, key: string) => join(root, 'harness', 'by-key', `${createHash('sha256').update(key).digest('hex')}.id`);
|
|
258
|
+
|
|
259
|
+
function recordQueueOutcome(root: string, id: string, outcome: 'succeeded' | 'retryable', key?: string): void {
|
|
260
|
+
const path = join(root, 'harness', `${id}.outcome`);
|
|
261
|
+
if (!existsSync(path)) writeFileSync(path, outcome, { flag: 'wx', mode: 0o600 });
|
|
262
|
+
// The row is about to leave the queue; keep «which id last held this key» so a caller that crashed before
|
|
263
|
+
// recording the id can still find the outcome (LOOP-LIVE1 · seat loop recovery).
|
|
264
|
+
if (key) {
|
|
265
|
+
mkdirSync(join(root, 'harness', 'by-key'), { recursive: true });
|
|
266
|
+
writeFileSync(keyMarkerPath(root, key), id, { mode: 0o600 });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Last queue id that settled under this idempotency key, or undefined when none was recorded. */
|
|
271
|
+
export function harnessQueueIdForKey(key: string, deps: HarnessQueueDeps = {}): string | undefined {
|
|
272
|
+
try {
|
|
273
|
+
const id = readFileSync(keyMarkerPath(deps.root ?? effectiveInstanceRoot(), key), 'utf8').trim();
|
|
274
|
+
return /^hq-[0-9a-f-]{36}$/i.test(id) ? id : undefined;
|
|
275
|
+
} catch { return undefined; }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
|
|
279
|
+
if (row.status === 'queued') return 'pending';
|
|
280
|
+
if (row.status === 'finished') {
|
|
281
|
+
const marker = join(root, 'harness', `${row.id}.outcome`);
|
|
282
|
+
if (existsSync(marker)) {
|
|
283
|
+
const value = readFileSync(marker, 'utf8');
|
|
284
|
+
if (value === 'succeeded' || value === 'retryable') return value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!row.launchId) return 'unknown';
|
|
288
|
+
const receiptPath = harnessQueueReceiptPath(root, row.launchId);
|
|
289
|
+
if (!existsSync(receiptPath)) return 'pending';
|
|
290
|
+
const record: unknown = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
291
|
+
if (!record || typeof record !== 'object') return 'unknown';
|
|
292
|
+
const { state, exitCode } = record as { state?: unknown; exitCode?: unknown };
|
|
293
|
+
if (state === 'not-started') return 'retryable';
|
|
294
|
+
if (state === 'finished') return exitCode === 0 ? 'succeeded' : typeof exitCode === 'number' ? 'retryable' : 'unknown';
|
|
295
|
+
return state === 'started' ? 'pending' : 'unknown';
|
|
296
|
+
}
|
|
297
|
+
|
|
115
298
|
export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
|
|
116
299
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
117
300
|
return locked(root, async (path) => {
|
|
@@ -124,6 +307,10 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
|
|
|
124
307
|
if (state !== 'finished' && state !== 'not-started'
|
|
125
308
|
|| (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
|
|
126
309
|
}
|
|
310
|
+
const outcome = item.status === 'queued' ? 'retryable' : queueRowOutcome(item, root);
|
|
311
|
+
if (item.idempotencyKey && (outcome === 'retryable' || outcome === 'succeeded')) {
|
|
312
|
+
recordQueueOutcome(root, id, outcome, item.idempotencyKey);
|
|
313
|
+
}
|
|
127
314
|
save(path, rows.filter((row) => row.id !== id));
|
|
128
315
|
observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
|
|
129
316
|
return true;
|
|
@@ -159,31 +346,109 @@ function alive(pid: number): boolean {
|
|
|
159
346
|
}
|
|
160
347
|
|
|
161
348
|
/** Host inventory includes harness authoring before it has a run ledger. Unattributed processes count for every seat. */
|
|
162
|
-
export function readHarnessQueueProcesses(): readonly QueueProcess[] {
|
|
163
|
-
const
|
|
349
|
+
export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly QueueProcess[] {
|
|
350
|
+
const run = probe.run ?? spawnSync;
|
|
351
|
+
const platform = probe.platform ?? process.platform;
|
|
352
|
+
const result = run('ps', ['-eo', 'pid=,ppid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
|
|
164
353
|
if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
|
|
165
|
-
const
|
|
354
|
+
const candidates: { pid: number; ppid: number; command: string }[] = [];
|
|
166
355
|
for (const line of result.stdout.split('\n')) {
|
|
167
|
-
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
168
|
-
if (!match)
|
|
169
|
-
|
|
356
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
357
|
+
if (!match) {
|
|
358
|
+
if (/(?:elanous\.mjs|harness-queue-child\.ts)/.test(line)) throw new Error('invalid harness process inventory row');
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const command = match[3]!;
|
|
170
362
|
if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
|
|
171
363
|
|| !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
|
|
172
364
|
|| !/(?:\bharness\s+(?:say|ask)\b|\bdev\s+.*(?:--say|--ask|--file|--implement)\b|\bself\s+(?:implement|orchestrate)\b|harness-queue-child\.ts)/.test(command)) continue;
|
|
173
|
-
const pid = Number(match[1]);
|
|
174
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('invalid harness pid');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
365
|
+
const pid = Number(match[1]), ppid = Number(match[2]);
|
|
366
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(ppid) || ppid < 0) throw new Error('invalid harness pid');
|
|
367
|
+
candidates.push({ pid, ppid, command });
|
|
368
|
+
}
|
|
369
|
+
const byPid = new Map(candidates.map((row) => [row.pid, row]));
|
|
370
|
+
const children = new Map<number, typeof candidates>();
|
|
371
|
+
for (const candidate of candidates) {
|
|
372
|
+
const siblings = children.get(candidate.ppid) ?? [];
|
|
373
|
+
siblings.push(candidate);
|
|
374
|
+
children.set(candidate.ppid, siblings);
|
|
375
|
+
}
|
|
376
|
+
const trees = probe.seatTrees ?? getUserConfig().loops?.orchestrator?.seatTrees;
|
|
377
|
+
const ledgerSeats = new Map<number, QueueSeat>();
|
|
378
|
+
const ambiguousPids = new Set<number>();
|
|
379
|
+
const candidatePids = new Set(candidates.map(({ pid }) => pid));
|
|
380
|
+
const births = new Map<number, string | undefined>();
|
|
381
|
+
for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
|
|
382
|
+
const pid = record.pid;
|
|
383
|
+
if (!pid || !candidatePids.has(pid) || !record.pidStart || !queueSeatNames.some((name) => name === record.seat)) continue;
|
|
384
|
+
if (!births.has(pid)) {
|
|
385
|
+
try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
|
|
386
|
+
catch { births.set(pid, undefined); }
|
|
387
|
+
}
|
|
388
|
+
if (births.get(pid) !== record.pidStart || ambiguousPids.has(pid)) continue;
|
|
389
|
+
const prior = ledgerSeats.get(pid);
|
|
390
|
+
if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
|
|
391
|
+
else ledgerSeats.set(pid, record.seat!);
|
|
392
|
+
}
|
|
393
|
+
const rows: QueueProcess[] = [];
|
|
394
|
+
// Attribute a run after grouping: the orchestrator may switch working trees while its launcher stays in HQ.
|
|
395
|
+
for (const root of candidates.filter((row) => !byPid.has(row.ppid))) {
|
|
396
|
+
const members: { process: typeof root; depth: number }[] = [];
|
|
397
|
+
const visit = (process: typeof root, depth: number): void => {
|
|
398
|
+
members.push({ process, depth });
|
|
399
|
+
for (const child of children.get(process.pid) ?? []) visit(child, depth + 1);
|
|
400
|
+
};
|
|
401
|
+
visit(root, 0);
|
|
402
|
+
const attributed: { seat: QueueSeat; depth: number; source: 'env' | 'flag' | 'ledger' | 'cwd'; priority: number }[] = [];
|
|
403
|
+
const worktrees: { seat: QueueSeat; depth: number }[] = [];
|
|
404
|
+
let launchId: string | undefined;
|
|
405
|
+
let runId: string | undefined;
|
|
406
|
+
for (const { process: { pid, command }, depth } of members) {
|
|
407
|
+
let envSeat: string | undefined;
|
|
408
|
+
if (platform === 'linux') {
|
|
409
|
+
try {
|
|
410
|
+
const env = (probe.environ ?? ((id: number) => readFileSync(`/proc/${id}/environ`, 'utf8')))(pid).split('\0');
|
|
411
|
+
envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
|
|
412
|
+
launchId ??= env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
|
|
413
|
+
runId ??= env.find((entry) => entry.startsWith('ELANOUS_RUN_ID='))?.slice('ELANOUS_RUN_ID='.length);
|
|
414
|
+
} catch { /* Unknown attribution is charged to all seats. */ }
|
|
415
|
+
}
|
|
416
|
+
const flag = /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1] as QueueSeat | undefined;
|
|
417
|
+
const envAssigned = envSeat && /^(OP|TC|MK|UX)$/.test(envSeat) ? envSeat as QueueSeat : undefined;
|
|
418
|
+
if (envAssigned) attributed.push({ seat: envAssigned, depth, source: 'env', priority: 0 });
|
|
419
|
+
if (flag) attributed.push({ seat: flag, depth, source: 'flag', priority: 1 });
|
|
420
|
+
const fromLedger = ledgerSeats.get(pid);
|
|
421
|
+
if (fromLedger) attributed.push({ seat: fromLedger, depth, source: 'ledger', priority: 2 });
|
|
422
|
+
if (!trees || !queueSeatNames.some((s) => trees[s]?.length)) continue;
|
|
178
423
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
424
|
+
let cwd: string;
|
|
425
|
+
if (probe.cwd) cwd = probe.cwd(pid);
|
|
426
|
+
else if (platform === 'linux') cwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
427
|
+
else {
|
|
428
|
+
const lsof = run('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 5_000 });
|
|
429
|
+
if (lsof.error || lsof.status !== 0 || typeof lsof.stdout !== 'string') throw new Error('cwd unavailable');
|
|
430
|
+
cwd = lsof.stdout.split('\n').find((line) => line.startsWith('n/'))?.slice(1) ?? '';
|
|
431
|
+
}
|
|
432
|
+
if (!cwd.startsWith('/')) continue;
|
|
433
|
+
const found = queueSeatForCwd(cwd, trees);
|
|
434
|
+
if (found) worktrees.push({ seat: found, depth });
|
|
182
435
|
} catch { /* Unknown attribution is charged to all seats. */ }
|
|
183
436
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
437
|
+
const deepest = Math.max(...members.map((entry) => entry.depth));
|
|
438
|
+
const inferred = new Set(worktrees.filter((entry) => entry.depth === deepest).map((entry) => entry.seat));
|
|
439
|
+
const complete = worktrees.filter((entry) => entry.depth === deepest).length
|
|
440
|
+
=== members.filter((entry) => entry.depth === deepest).length;
|
|
441
|
+
if (complete && inferred.size === 1) attributed.push({ seat: [...inferred][0]!, depth: deepest, source: 'cwd', priority: 3 });
|
|
442
|
+
const priority = Math.min(...attributed.map((entry) => entry.priority));
|
|
443
|
+
const eligible = attributed.filter((entry) => entry.priority === priority);
|
|
444
|
+
const depth = Math.max(...eligible.map((entry) => entry.depth));
|
|
445
|
+
const top = eligible.filter((entry) => entry.depth === depth);
|
|
446
|
+
const seats = new Set(top.map((entry) => entry.seat));
|
|
447
|
+
const assigned = seats.size === 1 ? top[0] : undefined;
|
|
448
|
+
try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
|
|
449
|
+
catch { /* Observation does not block inventory. */ }
|
|
450
|
+
rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
|
|
451
|
+
...(runId ? { runId } : {}) });
|
|
187
452
|
}
|
|
188
453
|
return rows;
|
|
189
454
|
}
|
|
@@ -242,6 +507,8 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
|
|
|
242
507
|
if (state !== 'finished' && state !== 'not-started') return 'unknown';
|
|
243
508
|
// A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
|
|
244
509
|
// Keep completed launches in the ledger, but never charge their seat again.
|
|
510
|
+
const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(item, root);
|
|
511
|
+
if (item.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, id, outcome, item.idempotencyKey);
|
|
245
512
|
save(path, state === 'finished' && item.status === 'launched'
|
|
246
513
|
? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
|
|
247
514
|
: rows.filter((row) => row.id !== id));
|
|
@@ -250,57 +517,308 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
|
|
|
250
517
|
});
|
|
251
518
|
}
|
|
252
519
|
|
|
520
|
+
const warnedLegacySeatCapPaths = new Set<string>();
|
|
521
|
+
|
|
522
|
+
function warnLegacySeatCap(path: string): void {
|
|
523
|
+
if (warnedLegacySeatCapPaths.has(path)) return;
|
|
524
|
+
try {
|
|
525
|
+
const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
526
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
527
|
+
const harness = (raw as Record<string, unknown>).harness;
|
|
528
|
+
if (!harness || typeof harness !== 'object' || Array.isArray(harness)) return;
|
|
529
|
+
const queue = (harness as Record<string, unknown>).queue;
|
|
530
|
+
if (!queue || typeof queue !== 'object' || Array.isArray(queue)
|
|
531
|
+
|| !Object.hasOwn(queue, 'seatCap')) return;
|
|
532
|
+
warnedLegacySeatCapPaths.add(path);
|
|
533
|
+
debug.log('harness.queue', 'legacy-seat-cap-ignored', {
|
|
534
|
+
key: 'harness.queue.seatCap', replacement: 'loops.orchestrator.seatCaps', path,
|
|
535
|
+
}, { level: 'warn' });
|
|
536
|
+
} catch { /* An unreadable config cannot affect dispatch or observation. */ }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat; launchIds: Set<string> }[] {
|
|
540
|
+
const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string> }[] = [];
|
|
541
|
+
for (const row of processes) {
|
|
542
|
+
const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
|
|
543
|
+
row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
|
|
544
|
+
const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
|
|
545
|
+
const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
|
|
546
|
+
launchIds: new Set<string>(), keys: new Set<string>() };
|
|
547
|
+
for (const other of matching.slice(1)) {
|
|
548
|
+
for (const key of other.keys) group.keys.add(key);
|
|
549
|
+
for (const seat of other.seats) group.seats.add(seat);
|
|
550
|
+
for (const id of other.launchIds) group.launchIds.add(id);
|
|
551
|
+
group.unknown ||= other.unknown;
|
|
552
|
+
groups.splice(groups.indexOf(other), 1);
|
|
553
|
+
}
|
|
554
|
+
for (const key of keys) group.keys.add(key);
|
|
555
|
+
if (row.seat) group.seats.add(row.seat);
|
|
556
|
+
else group.unknown = true;
|
|
557
|
+
if (row.launchId) group.launchIds.add(row.launchId);
|
|
558
|
+
if (!matching.length) groups.push(group);
|
|
559
|
+
}
|
|
560
|
+
return groups.map((group) => ({
|
|
561
|
+
...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
|
|
562
|
+
launchIds: group.launchIds,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
|
|
567
|
+
const IDLE_REQUEST_MINUTES = 30;
|
|
568
|
+
type IdleRequestState = { checkedAt: string; idleSince: Partial<Record<QueueSeat, string>>; notified?: string[] };
|
|
569
|
+
|
|
570
|
+
function idleRequestCells(now: Date, root: string): ReturnType<NonNullable<HarnessQueueDeps['idleCells']>> {
|
|
571
|
+
const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
|
|
572
|
+
if (!existsSync(join(ledger, 'release', 'features.sqlite'))) throw new Error('release checklist ledger unavailable');
|
|
573
|
+
const upcoming = listSchedules(ledger).filter(row => Date.parse(row.cutAt) > now.getTime())
|
|
574
|
+
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
|
|
575
|
+
if (!upcoming[0]) throw new Error('current release schedule unavailable');
|
|
576
|
+
return { current: listChecklist(upcoming[0].version, ledger).items,
|
|
577
|
+
next: upcoming[1] ? listChecklist(upcoming[1].version, ledger).items : [] };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function hasRunEvidence(cell: Pick<ChecklistItem, 'evidence'>): boolean {
|
|
581
|
+
return /(?:\brun[-_][a-z0-9-]+\b|\b(?:런|run)(?:\s*(?:근거|id|ID))?\s*[::#]\s*\S+)/i.test(cell.evidence ?? '');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** The queue lock serializes the observation marker; request journal lock also covers external traffic writers. */
|
|
585
|
+
export function requestIdleSeats(root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps = {}): void {
|
|
586
|
+
const config: OrchestratorLoopConfig | undefined = (deps.configPath ? getUserConfig(deps.configPath) : getUserConfig()).loops?.orchestrator;
|
|
587
|
+
const mode = config?.idleRequest ?? 'shadow';
|
|
588
|
+
if (mode === 'off') return;
|
|
589
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid idle request clock');
|
|
590
|
+
const marker = join(root, 'orchestrator', 'idle-request.json');
|
|
591
|
+
const previous: IdleRequestState = existsSync(marker) ? JSON.parse(readFileSync(marker, 'utf8')) as IdleRequestState
|
|
592
|
+
: { checkedAt: new Date(now.getTime() - IDLE_REQUEST_INTERVAL_MS).toISOString(), idleSince: {} };
|
|
593
|
+
if (!previous || typeof previous !== 'object' || !previous.idleSince || typeof previous.idleSince !== 'object'
|
|
594
|
+
|| !Number.isFinite(Date.parse(previous.checkedAt)) || (previous.notified !== undefined
|
|
595
|
+
&& (!Array.isArray(previous.notified) || previous.notified.some(key => typeof key !== 'string')))
|
|
596
|
+
|| Object.entries(previous.idleSince).some(([seat, value]) =>
|
|
597
|
+
!TRAFFIC_SEATS.some(name => name === seat) || typeof value !== 'string' || !Number.isFinite(Date.parse(value)))) {
|
|
598
|
+
throw new Error('invalid idle request marker');
|
|
599
|
+
}
|
|
600
|
+
const processes = (deps.processes ?? readHarnessQueueProcesses)();
|
|
601
|
+
const groups = groupQueueRuns(processes);
|
|
602
|
+
const reservations = items.filter(item => item.status === 'launching' || item.status === 'launched').filter(item => {
|
|
603
|
+
if (item.launchId && groups.some(group => group.launchIds.has(item.launchId!))) return false;
|
|
604
|
+
const state = item.launchId ? (deps.receipt ?? receipt)(root, item.launchId) : null;
|
|
605
|
+
return state !== 'finished' && state !== 'not-started';
|
|
606
|
+
});
|
|
607
|
+
const counts = Object.fromEntries(TRAFFIC_SEATS.map(seat => [seat,
|
|
608
|
+
groups.filter(group => !group.seat || group.seat === seat).length
|
|
609
|
+
+ reservations.filter(item => item.seat === seat).length])) as Record<QueueSeat, number>;
|
|
610
|
+
const idleSince = { ...previous.idleSince };
|
|
611
|
+
for (const seat of TRAFFIC_SEATS) {
|
|
612
|
+
if (counts[seat] > 0) delete idleSince[seat];
|
|
613
|
+
else if (!idleSince[seat]) idleSince[seat] = now.toISOString();
|
|
614
|
+
}
|
|
615
|
+
const due = now.getTime() - Date.parse(previous.checkedAt) >= IDLE_REQUEST_INTERVAL_MS;
|
|
616
|
+
if (!due && Object.keys(idleSince).length === Object.keys(previous.idleSince).length) return;
|
|
617
|
+
const day = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
618
|
+
const state: IdleRequestState = { checkedAt: due ? now.toISOString() : previous.checkedAt, idleSince,
|
|
619
|
+
notified: (previous.notified ?? []).filter(key => key.startsWith('orch-idle:') && key.split(':')[2] === day) };
|
|
620
|
+
mkdirSync(join(root, 'orchestrator'), { recursive: true });
|
|
621
|
+
const temporary = `${marker}.${randomUUID()}.tmp`;
|
|
622
|
+
try {
|
|
623
|
+
writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
|
|
624
|
+
renameSync(temporary, marker);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
try { unlinkSync(temporary); } catch { /* no temporary file */ }
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
if (!due) return;
|
|
630
|
+
const cells = (deps.idleCells ?? ((clock: Date) => idleRequestCells(clock, root)))(now);
|
|
631
|
+
const eligible = (rows: typeof cells.current, seat: QueueSeat) => rows.filter(cell =>
|
|
632
|
+
cell.owner?.split('/')[0] === seat && cell.status === 'yellow' && !hasRunEvidence(cell));
|
|
633
|
+
const candidates: TrafficCell[] = TRAFFIC_SEATS.flatMap(seat => {
|
|
634
|
+
const current = eligible(cells.current, seat);
|
|
635
|
+
return (current.length ? current : eligible(cells.next, seat)).slice(0, 1);
|
|
636
|
+
});
|
|
637
|
+
const measured = trafficTick({ now, processes: TRAFFIC_SEATS.flatMap(seat => Array.from({ length: counts[seat] }, () =>
|
|
638
|
+
({ seat, command: 'bun bin/elanous.mjs harness say observed', elapsedSeconds: 0 }))),
|
|
639
|
+
caps: config?.seatCaps ?? { OP: 4, TC: 8, MK: 6, UX: 6 }, idleSince: Object.fromEntries(TRAFFIC_SEATS.map(seat =>
|
|
640
|
+
[seat, idleSince[seat] ? new Date(idleSince[seat]) : null])), openCells: candidates, idleMinutes: IDLE_REQUEST_MINUTES });
|
|
641
|
+
const log = deps.idleLog ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
642
|
+
for (const row of measured.seats) {
|
|
643
|
+
if (!row.idle || row.running !== 0) continue;
|
|
644
|
+
const cell = row.nextCell ? `${row.nextCell.id} ${row.nextCell.title.replace(/\s+/g, ' ').trim()}` : '칸 없음';
|
|
645
|
+
let outcome: 'shadow' | 'queued' | 'duplicate' | 'no-cell' = mode === 'live' ? 'no-cell' : 'shadow';
|
|
646
|
+
const key = `orch-idle:${row.seat}:${day}:${row.nextCell?.id ?? 'no-cell'}`;
|
|
647
|
+
if (state.notified!.includes(key)) continue;
|
|
648
|
+
if (mode === 'live' && row.nextCell) {
|
|
649
|
+
const path = join(root, 'seat-requests', 'requests.jsonl');
|
|
650
|
+
mkdirSync(join(root, 'seat-requests'), { recursive: true });
|
|
651
|
+
outcome = withFileLockSync(`${path}.lock`, () => {
|
|
652
|
+
if (existsSync(path) && readFileSync(path, 'utf8').split('\n').some(line => {
|
|
653
|
+
if (!line) return false;
|
|
654
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; }
|
|
655
|
+
catch { throw new Error('invalid seat request journal'); }
|
|
656
|
+
})) return 'duplicate';
|
|
657
|
+
appendFileSync(path, JSON.stringify({ key, receiptId: key, seat: row.seat,
|
|
658
|
+
text: `다음 칸: ${cell} — 30분 놀았다`, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator-idle' }) + '\n');
|
|
659
|
+
return 'queued';
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (outcome !== 'duplicate') {
|
|
663
|
+
state.notified!.push(key);
|
|
664
|
+
try { log('loop.orchestrator', 'idle-requested', {
|
|
665
|
+
seat: row.seat, idleMinutes: IDLE_REQUEST_MINUTES, cell, mode, outcome }); }
|
|
666
|
+
catch { /* Observation cannot change the request or queue decision. */ }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
|
|
671
|
+
renameSync(temporary, marker);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
try { unlinkSync(temporary); } catch { /* no temporary file */ }
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
253
678
|
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
|
|
254
679
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
255
680
|
return locked(root, async (path) => {
|
|
681
|
+
try {
|
|
682
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
683
|
+
// A test process must not run the real author shadow — it reads operational ledgers and blew the 5s test budget (10-05 P0).
|
|
684
|
+
if (!deps.authorShadow && process.env.ELANOUS_AUTHOR_SHADOW_LIVE !== '1' && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
|
|
685
|
+
debug.log('loops.author-depth', 'shadow-skipped-test', {});
|
|
686
|
+
} else {
|
|
687
|
+
const shadow = deps.authorShadow ?? (await import('../loops/orchestrator/author-depth.js')).runAuthorDepthShadow;
|
|
688
|
+
await shadow(root, now);
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
try { debug.log('loops.author-depth', 'shadow-failed', { reason: String(error) }); }
|
|
692
|
+
catch { /* Shadow observation cannot affect queue dispatch. */ }
|
|
693
|
+
}
|
|
256
694
|
const items = read(path);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
695
|
+
try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
|
|
696
|
+
catch (error) {
|
|
697
|
+
try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
|
|
698
|
+
catch { /* Idle observation cannot affect queue dispatch. */ }
|
|
699
|
+
}
|
|
700
|
+
const heads = new Map<QueueSeat, QueueItem>();
|
|
701
|
+
for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
|
|
702
|
+
if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
703
|
+
const lastSeat = lastLaunchedSeat(path);
|
|
704
|
+
const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
|
|
705
|
+
: queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
|
|
706
|
+
const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
|
|
707
|
+
.flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
|
|
708
|
+
const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
|
|
709
|
+
warnLegacySeatCap(deps.configPath ?? userConfigPath());
|
|
710
|
+
const caps = config.loops?.orchestrator?.seatCaps;
|
|
711
|
+
const gate = config.loops?.orchestrator?.releaseGate;
|
|
712
|
+
const attributed: Record<QueueSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
|
|
713
|
+
let unattributed = 0;
|
|
714
|
+
const blocked = new Map<QueueSeat, { reason: string; active: number }>();
|
|
715
|
+
const waiting = (): QueueTick => {
|
|
716
|
+
const reason = candidates.map((item) => blocked.get(item.seat)!.reason).join('\n');
|
|
717
|
+
const updated = current.map((row) => {
|
|
718
|
+
const block = blocked.get(row.seat);
|
|
719
|
+
return heads.get(row.seat)?.id === row.id && block && row.waitingReason !== block.reason
|
|
720
|
+
? { ...row, waitingReason: block.reason } : row;
|
|
721
|
+
});
|
|
722
|
+
if (updated.some((row, index) => row !== current[index])) save(path, updated);
|
|
723
|
+
for (const item of candidates) {
|
|
724
|
+
const { reason: seatReason, active } = blocked.get(item.seat)!;
|
|
725
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
726
|
+
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
727
|
+
observe('waiting', { id: item.id, seat: item.seat, reason: seatReason, active, cap: details.cap,
|
|
728
|
+
seatCaps: details.seatCaps, releaseGate: details.releaseGate,
|
|
729
|
+
...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
|
|
730
|
+
attributed, unattributed }, deps);
|
|
731
|
+
}
|
|
732
|
+
return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
|
|
264
733
|
};
|
|
734
|
+
for (const item of candidates) {
|
|
735
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
736
|
+
if (injectedCap !== undefined && (!Number.isSafeInteger(injectedCap) || injectedCap < 0)) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
|
|
737
|
+
}
|
|
738
|
+
let current = items;
|
|
265
739
|
let processes: readonly QueueProcess[];
|
|
266
740
|
try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
|
|
267
|
-
catch (error) {
|
|
268
|
-
|
|
741
|
+
catch (error) {
|
|
742
|
+
for (const item of candidates) {
|
|
743
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
744
|
+
const budget = decideSpawn({ seat: item.seat, running: null, caps, gate, injectedCap });
|
|
745
|
+
blocked.set(item.seat, { reason: `${budget.reason}: harness process inventory unavailable: ${String(error)}`, active: 0 });
|
|
746
|
+
}
|
|
747
|
+
return waiting();
|
|
748
|
+
}
|
|
749
|
+
const runs = groupQueueRuns(processes);
|
|
750
|
+
for (const run of runs) {
|
|
751
|
+
if (run.seat) attributed[run.seat]++;
|
|
752
|
+
else unattributed++;
|
|
753
|
+
}
|
|
754
|
+
current = items.flatMap((row) => {
|
|
269
755
|
if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
|
|
270
756
|
const state = (deps.receipt ?? receipt)(root, row.launchId);
|
|
271
757
|
if (state !== 'finished' && state !== 'not-started'
|
|
272
758
|
|| processes.some((process) => process.launchId === row.launchId)) return [row];
|
|
273
|
-
|
|
759
|
+
if (state === 'finished' && row.status === 'launched') return [{ ...row, status: 'finished' as const }];
|
|
760
|
+
const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(row, root);
|
|
761
|
+
if (row.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, row.id, outcome, row.idempotencyKey);
|
|
762
|
+
return [];
|
|
274
763
|
});
|
|
275
764
|
if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
765
|
+
let pool: QueuePool | undefined;
|
|
766
|
+
let poolReason: string | undefined;
|
|
767
|
+
let finishHeld: string | null | undefined;
|
|
768
|
+
for (const item of candidates) {
|
|
769
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
770
|
+
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
771
|
+
const charged = runs.filter((run) => !run.seat || run.seat === item.seat);
|
|
772
|
+
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
773
|
+
const active = charged.length + current.filter((row) => row.seat === item.seat &&
|
|
774
|
+
(row.status === 'launching' || row.status === 'launched')
|
|
775
|
+
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
776
|
+
if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
|
|
777
|
+
if (!pool && !poolReason) {
|
|
778
|
+
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|
|
779
|
+
catch (error) { poolReason = `pod lease status unavailable: ${String(error)}`; }
|
|
780
|
+
if (pool && (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1)) {
|
|
781
|
+
poolReason = 'pod lease status incomplete';
|
|
782
|
+
}
|
|
783
|
+
if (pool && !poolReason) {
|
|
784
|
+
const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
|
|
785
|
+
if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
|
|
786
|
+
poolReason = `pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (poolReason) { blocked.set(item.seat, { reason: poolReason, active }); continue; }
|
|
791
|
+
const budget = decideSpawn({ seat: item.seat, running: active, caps, gate, injectedCap });
|
|
792
|
+
if (!budget.allow) {
|
|
793
|
+
blocked.set(item.seat, { reason: `${budget.reason}: ${seatCapReason(item.seat, active, details)}`, active });
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (finishHeld === undefined) {
|
|
797
|
+
finishHeld = await finishGateReason(root, (deps.now ?? (() => new Date()))(), config.loops?.orchestrator?.finishGate ?? 'shadow',
|
|
798
|
+
pool!.running + pool!.pending + pool!.reserved
|
|
799
|
+
+ current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
|
|
800
|
+
}
|
|
801
|
+
if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
|
|
802
|
+
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
|
|
803
|
+
delete launching.waitingReason;
|
|
804
|
+
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
805
|
+
try {
|
|
806
|
+
const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
|
|
807
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
808
|
+
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
809
|
+
save(path, current.map((row) => row.id === item.id ? launched : row));
|
|
810
|
+
try { saveLastLaunchedSeat(path, item.seat); }
|
|
811
|
+
catch (error) {
|
|
812
|
+
try { debug.log('harness.queue', 'round-robin-save-failed', { id: item.id, seat: item.seat, reason: String(error) }, { level: 'warn' }); }
|
|
813
|
+
catch { /* Observation cannot change an already persisted launch. */ }
|
|
814
|
+
}
|
|
815
|
+
observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
|
|
816
|
+
return { outcome: 'launched', item: launched, reason: 'spawned' };
|
|
817
|
+
} catch (error) {
|
|
818
|
+
observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
304
821
|
}
|
|
822
|
+
return waiting();
|
|
305
823
|
});
|
|
306
824
|
}
|