elanous 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -24,11 +24,12 @@ import { debug } from '../debug/log.js';
|
|
|
24
24
|
import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
|
|
25
25
|
import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
|
|
26
26
|
import { PR_LABELS } from '../github/pr-labels.js';
|
|
27
|
-
import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
|
|
27
|
+
import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, decideLaunchBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
|
|
28
28
|
import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
|
|
29
29
|
import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
|
|
30
30
|
import { getUserConfig } from '../user-config.js';
|
|
31
31
|
import { installHarnessCliSinkHook } from './harness-cli-sink.js';
|
|
32
|
+
import { addHarnessQueue, listHarnessQueue, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps } from './harness-queue.js';
|
|
32
33
|
import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
|
|
33
34
|
import { runHarnessPlanRfc } from './harness-plan-rfc.js';
|
|
34
35
|
import { classifyGarbage, isGarbageProcessTarget, type GarbageProcess } from './process-garbage.js';
|
|
@@ -338,6 +339,8 @@ function resolveLaunchSubstrate(opts: HarnessSubstrateOpts): ResolvedHarnessSubs
|
|
|
338
339
|
}
|
|
339
340
|
type PodExit = { status: number | null; signal?: string | null };
|
|
340
341
|
type PodExitReason = 'signal' | 'human-stop' | 'supervisor' | 'no-launch' | 'unknown';
|
|
342
|
+
/** GitHub lists at most this many files for one pull request; a list that reaches it is not provably complete. */
|
|
343
|
+
export const GITHUB_PR_FILES_LIST_CAP = 3000;
|
|
341
344
|
const STOPPING_SUPERVISOR_VERDICTS = new Set(['UNCONVERGEABLE', 'CONTRACT-CONFLICT']);
|
|
342
345
|
// Ledger events that only exist once the child actually ran — any of them rules out «never reached the Pod».
|
|
343
346
|
const LAUNCH_EVIDENCE_EVENTS = new Set(['job-applied', 'pod-child-run', 'reviewed', 'rework-budget', 'pr-opened']);
|
|
@@ -578,8 +581,12 @@ const LAUNCH_BUDGET_PROVIDER: Readonly<Record<string, string>> = { 'codex-rotate
|
|
|
578
581
|
export function harnessLaunchBudgetDecision(
|
|
579
582
|
inputs: BudgetInputs,
|
|
580
583
|
opts: { readonly childLlmProvider?: string; readonly childLlmModel?: string } = {},
|
|
581
|
-
): { readonly decision: BudgetDecision; readonly warning?: string } {
|
|
584
|
+
): { readonly decision: BudgetDecision; readonly warning?: string; readonly unmeasuredProvider?: string } {
|
|
582
585
|
const explicit = opts.childLlmProvider?.trim();
|
|
586
|
+
const judged = (selected: BudgetInputs) => {
|
|
587
|
+
const { decision, unmeasuredProvider } = decideLaunchBudget(selected);
|
|
588
|
+
return { decision, ...(unmeasuredProvider ? { unmeasuredProvider, warning: `budget: ${unmeasuredProvider} usage unmeasured — launching` } : {}) };
|
|
589
|
+
};
|
|
583
590
|
if (explicit) {
|
|
584
591
|
if (explicit !== CODEX_PROVIDER && explicit !== GROK_PROVIDER) {
|
|
585
592
|
return {
|
|
@@ -588,13 +595,14 @@ export function harnessLaunchBudgetDecision(
|
|
|
588
595
|
};
|
|
589
596
|
}
|
|
590
597
|
const chain = [{ provider: explicit, ...(opts.childLlmModel ? { model: opts.childLlmModel } : {}) }];
|
|
591
|
-
return
|
|
598
|
+
return judged({ ...inputs, preference: { ...inputs.preference, chain } });
|
|
592
599
|
}
|
|
593
|
-
if (inputs.preference.chain.length > 0) return
|
|
600
|
+
if (inputs.preference.chain.length > 0) return judged(inputs);
|
|
594
601
|
const chain = DEFAULT_FALLBACK_CHAIN.map((step) => ({ provider: LAUNCH_BUDGET_PROVIDER[step] ?? step }));
|
|
602
|
+
const result = judged({ ...inputs, preference: { ...inputs.preference, chain } });
|
|
595
603
|
return {
|
|
596
|
-
|
|
597
|
-
warning: `budget: no chain in this universe's config — judged with the code default ${DEFAULT_FALLBACK_CHAIN.join(',')}`,
|
|
604
|
+
...result,
|
|
605
|
+
warning: `budget: no chain in this universe's config — judged with the code default ${DEFAULT_FALLBACK_CHAIN.join(',')}${result.warning ? ` · ${result.warning}` : ''}`,
|
|
598
606
|
};
|
|
599
607
|
}
|
|
600
608
|
|
|
@@ -602,10 +610,13 @@ async function readHarnessLaunchBudget(
|
|
|
602
610
|
opts: { readonly childLlmProvider?: string; readonly childLlmModel?: string } = {},
|
|
603
611
|
): Promise<BudgetDecision | 'unknown'> {
|
|
604
612
|
try {
|
|
605
|
-
const { decision, warning } = harnessLaunchBudgetDecision(await readBudgetInputsLive(), opts);
|
|
613
|
+
const { decision, warning, unmeasuredProvider } = harnessLaunchBudgetDecision(await readBudgetInputsLive(), opts);
|
|
606
614
|
if (warning) {
|
|
607
615
|
console.error(`⚠️ launch gate: ${warning}`);
|
|
608
|
-
try {
|
|
616
|
+
try {
|
|
617
|
+
if (unmeasuredProvider) debug.log('execution-loop.launch-gate', 'budget-unmeasured', { provider: unmeasuredProvider });
|
|
618
|
+
if (!opts.childLlmProvider?.trim() && warning.includes('code default')) debug.log('execution-loop.launch-gate', 'budget-chain-defaulted', { warning, action: decision.action, provider: decision.provider });
|
|
619
|
+
} catch { /* observation is fail-soft */ }
|
|
609
620
|
}
|
|
610
621
|
return decision;
|
|
611
622
|
}
|
|
@@ -644,8 +655,8 @@ async function dispatchHarnessAskSay(
|
|
|
644
655
|
}
|
|
645
656
|
goalId ??= launchRequestId(identity);
|
|
646
657
|
// Under bun test with no injected gate deps, skip the live reads (≈5 s of codex rotation + run scans) —
|
|
647
|
-
// CLI wiring tests time out on them. Gate tests inject deps
|
|
648
|
-
if (process.env.NODE_ENV === 'test' && Object.keys(gate).length === 0) {
|
|
658
|
+
// CLI wiring tests time out on them. Gate tests inject deps or opt in with ELANOUS_LAUNCH_GATE_LIVE=1.
|
|
659
|
+
if (process.env.NODE_ENV === 'test' && Object.keys(gate).length === 0 && process.env.ELANOUS_LAUNCH_GATE_LIVE !== '1') {
|
|
649
660
|
try { debug.log('execution-loop.launch-gate', 'skipped-test-env', { goalId }); } catch { /* observation is fail-soft */ }
|
|
650
661
|
await dispatch(resolved);
|
|
651
662
|
return;
|
|
@@ -1461,7 +1472,7 @@ export function readProcessOwnership(
|
|
|
1461
1472
|
}
|
|
1462
1473
|
}
|
|
1463
1474
|
|
|
1464
|
-
function defaultListHarnessProcesses(): HarnessProcessListObservation {
|
|
1475
|
+
export function defaultListHarnessProcesses(): HarnessProcessListObservation {
|
|
1465
1476
|
let out: string;
|
|
1466
1477
|
try {
|
|
1467
1478
|
out = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime=,command='], {
|
|
@@ -1577,10 +1588,13 @@ type GithubPull = {
|
|
|
1577
1588
|
draft: boolean;
|
|
1578
1589
|
state: 'open' | 'closed';
|
|
1579
1590
|
head: { ref: string };
|
|
1591
|
+
base?: { ref: string };
|
|
1580
1592
|
labels: Array<{ name: string }>;
|
|
1581
1593
|
created_at: string;
|
|
1582
1594
|
updated_at?: string;
|
|
1583
1595
|
merged_at: string | null;
|
|
1596
|
+
body?: string | null;
|
|
1597
|
+
merge_commit_sha?: string | null;
|
|
1584
1598
|
};
|
|
1585
1599
|
|
|
1586
1600
|
type GhExecute = (args: string[]) => string;
|
|
@@ -1629,9 +1643,25 @@ export function draftSweepRunStatus(ledgerMatches: readonly RunLedgerMatch[], ru
|
|
|
1629
1643
|
['completed', 'failed', 'cancelled', 'abandoned'].includes(terminal) ? terminal : undefined);
|
|
1630
1644
|
if (status && status !== 'unknown') statuses.add(status);
|
|
1631
1645
|
}
|
|
1646
|
+
if (statuses.has('running')) return 'running';
|
|
1647
|
+
if (statuses.has('probable-running')) return 'probable-running';
|
|
1632
1648
|
return statuses.size === 1 ? [...statuses][0] : undefined;
|
|
1633
1649
|
}
|
|
1634
1650
|
|
|
1651
|
+
function currentDraftSweepRunId(matches: readonly RunLedgerMatch[], repository: string, number: number): string | undefined {
|
|
1652
|
+
const owners = matches.flatMap((match) => {
|
|
1653
|
+
const events = match.entries.filter((entry) => entry.event === 'pr-opened' && entry.data.number === number);
|
|
1654
|
+
if (events.length === 0 || events.some((entry) => ledgerPrRepository(entry.data, number) !== repository)) return [];
|
|
1655
|
+
const times = events.map((entry) => Date.parse(entry.timestamp ?? ''));
|
|
1656
|
+
return [{ runId: match.runId, time: times.every(Number.isFinite) ? Math.max(...times) : NaN }];
|
|
1657
|
+
});
|
|
1658
|
+
if (owners.length === 1) return owners[0]!.runId;
|
|
1659
|
+
if (owners.some((owner) => !Number.isFinite(owner.time))) return undefined;
|
|
1660
|
+
owners.sort((a, b) => b.time - a.time);
|
|
1661
|
+
if (owners.length > 1 && owners[0]!.time === owners[1]!.time) return undefined;
|
|
1662
|
+
return owners[0]?.runId;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1635
1665
|
type DraftSweepGitExecute = (cwd: string, args: string[]) => Pick<ReturnType<typeof runGitCommand>, 'status' | 'stdout' | 'stderr'>;
|
|
1636
1666
|
|
|
1637
1667
|
function githubRemoteRepository(url: string): string | undefined {
|
|
@@ -1669,24 +1699,117 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
|
|
|
1669
1699
|
if (!cache.has(repository)) cache.set(repository, inventory(repository, state));
|
|
1670
1700
|
return cache.get(repository)!;
|
|
1671
1701
|
};
|
|
1702
|
+
// GitHub stops listing a pull request's files at 3,000, and the cut looks like a normal last page.
|
|
1703
|
+
// A list that reaches the cap cannot prove it is complete, so it is reported as unknown (undefined):
|
|
1704
|
+
// a draft without a provable file list never qualifies for the all-files-landed close.
|
|
1705
|
+
const filesFor = (repository: string, number: number): string[] | undefined => {
|
|
1706
|
+
const files: string[] = [];
|
|
1707
|
+
for (let page = 1; page <= 100; page++) {
|
|
1708
|
+
const batch = ghJson<Array<{ filename: string }>>(['api', `repos/${repository}/pulls/${number}/files?per_page=100&page=${page}`], execute);
|
|
1709
|
+
if (!Array.isArray(batch) || batch.length > 100 || batch.some((file) => typeof file.filename !== 'string')) throw new Error('Incomplete PR file inventory');
|
|
1710
|
+
files.push(...batch.map((file) => file.filename));
|
|
1711
|
+
if (files.length >= GITHUB_PR_FILES_LIST_CAP) {
|
|
1712
|
+
debug.log('drafts.cleanup', 'file-inventory-capped', { number, listed: files.length });
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
if (batch.length < 100) return files;
|
|
1716
|
+
}
|
|
1717
|
+
throw new Error('PR file inventory exceeded pagination limit');
|
|
1718
|
+
};
|
|
1719
|
+
const latestFileChangesFor = (draft: SweepDraft, repository: string): Record<string, string> | undefined => {
|
|
1720
|
+
if (!draft.changedFiles?.length) return undefined;
|
|
1721
|
+
const changes: Record<string, string> = {};
|
|
1722
|
+
for (let page = 1; page <= 100; page++) {
|
|
1723
|
+
const batch = ghJson<Array<{ sha: string; commit: { committer: { date: string } } }>>(
|
|
1724
|
+
['api', `repos/${repository}/pulls/${draft.number}/commits?per_page=100&page=${page}`], execute);
|
|
1725
|
+
if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete draft commit inventory');
|
|
1726
|
+
for (const commit of batch) {
|
|
1727
|
+
const changedAt = commit.commit?.committer?.date;
|
|
1728
|
+
if (!commit.sha || !changedAt || !Number.isFinite(Date.parse(changedAt))) return undefined;
|
|
1729
|
+
const detail = ghJson<{ files?: Array<{ filename: string; previous_filename?: string }> }>(
|
|
1730
|
+
['api', `repos/${repository}/commits/${commit.sha}?per_page=100`], execute);
|
|
1731
|
+
// GitHub truncates very large commit file lists; never infer coverage from a partial list.
|
|
1732
|
+
if (!Array.isArray(detail.files) || detail.files.length >= 100) return undefined;
|
|
1733
|
+
for (const file of detail.files) {
|
|
1734
|
+
for (const name of [file.filename, file.previous_filename]) {
|
|
1735
|
+
if (!name || !draft.changedFiles.includes(name)) continue;
|
|
1736
|
+
if (!changes[name] || Date.parse(changes[name]) < Date.parse(changedAt)) changes[name] = changedAt;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (batch.length < 100) return draft.changedFiles.every((file) => changes[file]) ? changes : undefined;
|
|
1741
|
+
}
|
|
1742
|
+
return undefined;
|
|
1743
|
+
};
|
|
1744
|
+
const landingCommentsFor = (repository: string, number: number): string[] => {
|
|
1745
|
+
const comments: string[] = [];
|
|
1746
|
+
for (let page = 1; page <= 100; page++) {
|
|
1747
|
+
const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`], execute);
|
|
1748
|
+
if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete landing comment inventory');
|
|
1749
|
+
comments.push(...batch.filter((comment) => /\blanding-verified\b/i.test(comment.body ?? '')).map((comment) => comment.body!));
|
|
1750
|
+
if (batch.length < 100) return comments;
|
|
1751
|
+
}
|
|
1752
|
+
throw new Error('Landing comment inventory exceeded pagination limit');
|
|
1753
|
+
};
|
|
1672
1754
|
let ledgerMatches: readonly RunLedgerMatch[] | undefined;
|
|
1673
1755
|
let running: ReadonlyMap<string, string> | undefined;
|
|
1674
|
-
const runStatusFor = (
|
|
1756
|
+
const runStatusFor = (draft: SweepDraft, repository: string): string | undefined => {
|
|
1675
1757
|
ledgerMatches ??= listRunLedgers().matches;
|
|
1676
1758
|
running ??= new Map(queryRunningRuns().entries.map((entry) => [entry.runId, entry.status]));
|
|
1677
|
-
|
|
1759
|
+
if (draft.runId) {
|
|
1760
|
+
const match = ledgerMatches.find((entry) => entry.runId === draft.runId);
|
|
1761
|
+
return match ? draftSweepRunStatus([match], running, repository, draft.number) : undefined;
|
|
1762
|
+
}
|
|
1763
|
+
return draftSweepRunStatus(ledgerMatches, running, repository, draft.number);
|
|
1678
1764
|
};
|
|
1679
1765
|
return {
|
|
1680
1766
|
listDrafts: async (page, perPage, repository): Promise<SweepDraft[]> =>
|
|
1681
1767
|
(await listed(open, repository, 'open')).filter((pr) => pr.draft)
|
|
1682
1768
|
.slice((page - 1) * perPage, page * perPage)
|
|
1683
1769
|
.map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref,
|
|
1684
|
-
labels: pr.labels.map((item) => item.name), createdAt: pr.created_at,
|
|
1770
|
+
labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, body: pr.body ?? undefined,
|
|
1771
|
+
changedFiles: filesFor(repository, pr.number), ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
|
|
1772
|
+
runId: currentDraftSweepRunId(ledgerMatches ??= listRunLedgers().matches, repository, pr.number) })),
|
|
1685
1773
|
listMerged: async (page, perPage, repository): Promise<SweepMergedPr[]> =>
|
|
1686
|
-
(await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null)
|
|
1774
|
+
(await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null && pr.base?.ref === 'main')
|
|
1687
1775
|
.slice((page - 1) * perPage, page * perPage)
|
|
1688
|
-
.map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref
|
|
1689
|
-
|
|
1776
|
+
.map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref, body: pr.body ?? undefined,
|
|
1777
|
+
mergedAt: pr.merged_at ?? undefined, changedFiles: filesFor(repository, pr.number),
|
|
1778
|
+
mergeCommitMessage: pr.merge_commit_sha ? ghJson<{ commit: { message: string } }>(
|
|
1779
|
+
['api', `repos/${repository}/commits/${pr.merge_commit_sha}`], execute).commit.message : undefined,
|
|
1780
|
+
landingVerifiedComments: landingCommentsFor(repository, pr.number) })),
|
|
1781
|
+
getRunStatus: async (draft, repository) => runStatusFor(draft, repository),
|
|
1782
|
+
getLatestFileChanges: async (draft, repository) => latestFileChangesFor(draft, repository),
|
|
1783
|
+
hasFinalRunResult: async (draft, repository) => {
|
|
1784
|
+
ledgerMatches ??= listRunLedgers().matches;
|
|
1785
|
+
const ownedRuns = new Set(ledgerMatches.filter((match) => {
|
|
1786
|
+
const prEvents = match.entries.filter((entry) => entry.event === 'pr-opened' && entry.data.number === draft.number);
|
|
1787
|
+
return prEvents.length > 0 && prEvents.every((entry) => ledgerPrRepository(entry.data, draft.number) === repository);
|
|
1788
|
+
}).map((match) => match.runId));
|
|
1789
|
+
if (ownedRuns.size === 0) return false;
|
|
1790
|
+
// A previous owner's final does not establish finality for a later owner of this PR.
|
|
1791
|
+
if (draft.runId && !ownedRuns.has(draft.runId)) return false;
|
|
1792
|
+
if (!draft.runId && ownedRuns.size > 1) return false;
|
|
1793
|
+
const currentOwners = draft.runId ? new Set([draft.runId]) : ownedRuns;
|
|
1794
|
+
const path = logsDbPath();
|
|
1795
|
+
if (!existsSync(path)) return false;
|
|
1796
|
+
const store = LogStore.openReadOnly(path);
|
|
1797
|
+
try {
|
|
1798
|
+
let beforeId: number | undefined;
|
|
1799
|
+
for (;;) {
|
|
1800
|
+
const rows = store.query({ exactCategories: ['self-implement.result'], events: ['final'], grep: String(draft.number),
|
|
1801
|
+
limit: 1_000, ...(beforeId === undefined ? {} : { beforeId }) });
|
|
1802
|
+
for (const row of rows) {
|
|
1803
|
+
try {
|
|
1804
|
+
const data = JSON.parse(row.data ?? '{}') as { runId?: string; prNumber?: number };
|
|
1805
|
+
if (data.prNumber === draft.number && data.runId && currentOwners.has(data.runId)) return true;
|
|
1806
|
+
} catch { /* A malformed result cannot establish finality. */ }
|
|
1807
|
+
}
|
|
1808
|
+
if (rows.length < 1_000) return false;
|
|
1809
|
+
beforeId = rows.at(-1)!.id;
|
|
1810
|
+
}
|
|
1811
|
+
} finally { store.close(); }
|
|
1812
|
+
},
|
|
1690
1813
|
listLiveBranches: async (repository) => {
|
|
1691
1814
|
const cwd = process.cwd();
|
|
1692
1815
|
const remote = git(cwd, ['config', '--get', 'remote.origin.url']);
|
|
@@ -1982,6 +2105,7 @@ export interface HarnessCliCommandDeps {
|
|
|
1982
2105
|
deliverableVerify?: InstallDeliverableVerifyCliDeps;
|
|
1983
2106
|
processObservation?: HarnessProcessObservationDeps;
|
|
1984
2107
|
draftSweep?: HarnessDraftSweepDeps;
|
|
2108
|
+
queue?: HarnessQueueDeps;
|
|
1985
2109
|
ask?: HarnessAskHandler;
|
|
1986
2110
|
say?: HarnessSayHandler;
|
|
1987
2111
|
/** Observe-only Pod host dispatch seam. */
|
|
@@ -2034,6 +2158,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2034
2158
|
installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
|
|
2035
2159
|
installHarnessBudgetCommand(harnessCmd);
|
|
2036
2160
|
installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
|
|
2161
|
+
const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
|
|
2162
|
+
const queueDeps = deps.queue ?? {};
|
|
2163
|
+
const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
|
|
2164
|
+
queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
|
|
2165
|
+
.option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
|
|
2166
|
+
.option('--hold', '자동 병합 금지').option('--heavy', 'Pod high 메모리')
|
|
2167
|
+
.action((opts: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }) => queueAction(async () => {
|
|
2168
|
+
const row = await addHarnessQueue(opts, queueDeps);
|
|
2169
|
+
console.log(`${row.id} ${row.seat} ${row.kind} queued`);
|
|
2170
|
+
}));
|
|
2171
|
+
queue.command('list').description('대기·발사 원장 조회').action(() => queueAction(async () => {
|
|
2172
|
+
for (const row of listHarnessQueue(queueDeps)) console.log(`${row.id} ${row.seat} ${row.status} ${row.kind} ${row.input}`);
|
|
2173
|
+
}));
|
|
2174
|
+
queue.command('remove <id>').description('대기 중 또는 종료 확인된 항목 제거').action((id: string) => queueAction(async () => {
|
|
2175
|
+
if (!await removeHarnessQueue(id, queueDeps)) throw new Error(`queue item active or not found: ${id}`);
|
|
2176
|
+
console.log(`${id} removed`);
|
|
2177
|
+
}));
|
|
2178
|
+
queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
|
|
2179
|
+
console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
|
|
2180
|
+
}));
|
|
2181
|
+
queue.command('tick').description('맨 앞 한 건의 자리 몫과 Pod 풀 여유를 확인하고 발사').action(() => queueAction(async () => {
|
|
2182
|
+
const result = await tickHarnessQueue(queueDeps);
|
|
2183
|
+
console.log(`${result.outcome}${result.item ? ` ${result.item.id}` : ''}: ${result.reason}`);
|
|
2184
|
+
}));
|
|
2037
2185
|
|
|
2038
2186
|
const ask = deps.ask;
|
|
2039
2187
|
if (ask) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/** Only a confirmed terminal observation allows reconciliation to release an uncertain launch. */
|
|
6
|
+
export function writeHarnessQueueReceipt(receiptPath: string, state: 'started' | 'finished' | 'not-started'): void {
|
|
7
|
+
const temp = `${receiptPath}.${randomUUID()}.tmp`;
|
|
8
|
+
writeFileSync(temp, JSON.stringify({ state, at: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
|
|
9
|
+
renameSync(temp, receiptPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runHarnessQueueChild(
|
|
13
|
+
receiptPath: string, command: string, args: string[],
|
|
14
|
+
start: typeof spawn = spawn,
|
|
15
|
+
): Promise<number> {
|
|
16
|
+
const record = (state: 'started' | 'finished' | 'not-started') => writeHarnessQueueReceipt(receiptPath, state);
|
|
17
|
+
let child: ReturnType<typeof spawn>;
|
|
18
|
+
try { child = start(process.execPath, [command, ...args], { stdio: 'inherit', env: process.env }); }
|
|
19
|
+
catch { record('not-started'); return 1; }
|
|
20
|
+
return await new Promise<number>((done) => {
|
|
21
|
+
let started = false;
|
|
22
|
+
child.once('spawn', () => {
|
|
23
|
+
started = true;
|
|
24
|
+
record('started');
|
|
25
|
+
child.once('exit', (code) => { record('finished'); done(code ?? 1); });
|
|
26
|
+
});
|
|
27
|
+
child.once('error', () => {
|
|
28
|
+
if (!started) { record('not-started'); done(1); }
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (import.meta.main) {
|
|
34
|
+
const [receiptPath, command, ...args] = process.argv.slice(2);
|
|
35
|
+
if (!receiptPath || !command) throw new Error('queue child requires receipt path and command');
|
|
36
|
+
process.exitCode = await runHarnessQueueChild(receiptPath, command, args);
|
|
37
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
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';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { Database } from 'bun:sqlite';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
8
|
+
import { getUserConfig } from '../user-config.js';
|
|
9
|
+
import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
|
|
10
|
+
import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
11
|
+
import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
12
|
+
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
13
|
+
|
|
14
|
+
export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
15
|
+
export type QueueItem = {
|
|
16
|
+
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;
|
|
18
|
+
};
|
|
19
|
+
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
20
|
+
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
21
|
+
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
|
|
22
|
+
|
|
23
|
+
export interface HarnessQueueDeps {
|
|
24
|
+
root?: string;
|
|
25
|
+
cap?: (seat: QueueSeat) => number;
|
|
26
|
+
pool?: () => QueuePool;
|
|
27
|
+
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
28
|
+
alive?: (pid: number) => boolean;
|
|
29
|
+
processes?: () => readonly QueueProcess[];
|
|
30
|
+
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
31
|
+
log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function harnessQueuePath(root = effectiveInstanceRoot()): string {
|
|
35
|
+
return join(root, 'harness', 'queue.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function observe(event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>, deps: HarnessQueueDeps): void {
|
|
39
|
+
try { (deps.log ?? ((name, payload) => debug.log('harness.queue', name, payload)))(event, data); }
|
|
40
|
+
catch { /* Logging cannot affect dispatch. */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function read(path: string): QueueItem[] {
|
|
44
|
+
if (!existsSync(path)) return [];
|
|
45
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
46
|
+
if (!Array.isArray(value) || !value.every((row) => row && typeof row === 'object'
|
|
47
|
+
&& typeof row.id === 'string' && /^(OP|TC|MK|UX)$/.test(row.seat)
|
|
48
|
+
&& (row.kind === 'say' || row.kind === 'ask') && typeof row.input === 'string'
|
|
49
|
+
&& typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
|
|
50
|
+
&& ['queued', 'launching', 'launched', 'finished'].includes(row.status)
|
|
51
|
+
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
52
|
+
&& (row.launchId === undefined || typeof row.launchId === 'string'))) {
|
|
53
|
+
throw new Error('harness queue: invalid queue file (no launch)');
|
|
54
|
+
}
|
|
55
|
+
return value as QueueItem[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function save(path: string, items: QueueItem[]): void {
|
|
59
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
60
|
+
try {
|
|
61
|
+
writeFileSync(temporary, `${JSON.stringify(items, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
62
|
+
renameSync(temporary, path);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
try { unlinkSync(temporary); } catch { /* write did not finish */ }
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// SQLite OS locks release on process death, including death while awaiting a launch.
|
|
70
|
+
async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
|
|
71
|
+
const path = harnessQueuePath(root);
|
|
72
|
+
mkdirSync(join(root, 'harness'), { recursive: true, mode: 0o700 });
|
|
73
|
+
const db = new Database(`${path}.mutex.sqlite`, { create: true, strict: true });
|
|
74
|
+
try {
|
|
75
|
+
const deadline = Date.now() + 30_000;
|
|
76
|
+
for (;;) {
|
|
77
|
+
try { db.exec('BEGIN IMMEDIATE'); break; }
|
|
78
|
+
catch (error) {
|
|
79
|
+
if ((error as { code?: string }).code !== 'SQLITE_BUSY' || Date.now() >= deadline) throw error;
|
|
80
|
+
await Bun.sleep(25);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
try { return await action(path); }
|
|
84
|
+
finally { db.exec('ROLLBACK'); }
|
|
85
|
+
} finally { db.close(); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function seat(value: string): QueueSeat {
|
|
89
|
+
if (!/^(OP|TC|MK|UX)$/.test(value)) throw new Error(`harness queue: unknown seat ${value}`);
|
|
90
|
+
return value as QueueSeat;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
94
|
+
const assigned = seat(input.seat);
|
|
95
|
+
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
96
|
+
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
97
|
+
const text = kind === 'say' ? input.say! : input.ask!;
|
|
98
|
+
if (!text.trim()) throw new Error('harness queue add: 빈 입력');
|
|
99
|
+
const value = kind === 'ask' ? resolve(text) : text;
|
|
100
|
+
if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
|
|
101
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
102
|
+
return locked(root, async (path) => {
|
|
103
|
+
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
|
+
save(path, [...read(path), item]);
|
|
106
|
+
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
107
|
+
return item;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
|
|
112
|
+
return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
|
|
116
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
117
|
+
return locked(root, async (path) => {
|
|
118
|
+
const rows = read(path);
|
|
119
|
+
const item = rows.find((row) => row.id === id);
|
|
120
|
+
if (!item) return false;
|
|
121
|
+
if (item.status !== 'queued' && item.status !== 'finished') {
|
|
122
|
+
if ((item.status !== 'launched' && item.status !== 'launching') || !item.launchId) return false;
|
|
123
|
+
const state = (deps.receipt ?? receipt)(root, item.launchId);
|
|
124
|
+
if (state !== 'finished' && state !== 'not-started'
|
|
125
|
+
|| (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
|
|
126
|
+
}
|
|
127
|
+
save(path, rows.filter((row) => row.id !== id));
|
|
128
|
+
observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function queueLaunchArgs(item: QueueItem): string[] {
|
|
134
|
+
return ['harness', item.kind, item.input, '--substrate', 'pod',
|
|
135
|
+
...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readHarnessQueuePool(): QueuePool {
|
|
139
|
+
const config = getUserConfig();
|
|
140
|
+
const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
|
|
141
|
+
const current = spec ? null : leaseKubectl(['config', 'current-context']);
|
|
142
|
+
const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
|
|
143
|
+
if (!context) throw new Error('pod lease status: no pool/context');
|
|
144
|
+
const members = parsePodPool(context);
|
|
145
|
+
const observed = measurePoolLease(members);
|
|
146
|
+
if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
|
|
147
|
+
const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
|
|
148
|
+
const records = podPoolHostLease(members).live();
|
|
149
|
+
return {
|
|
150
|
+
running: observed.members.reduce((sum, member) => sum + member.running!, 0),
|
|
151
|
+
pending: observed.members.reduce((sum, member) => sum + member.pending!, 0),
|
|
152
|
+
reserved: hostLeaseCounts(records).reserved + records.filter((row) => row.stage === 'job' && !leaseHasPendingPod(row, pendingJobs)).length,
|
|
153
|
+
limit: members.reduce((sum, member) => sum + member.capacity, 0),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function alive(pid: number): boolean {
|
|
158
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 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 result = spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
|
|
164
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
|
|
165
|
+
const rows: QueueProcess[] = [];
|
|
166
|
+
for (const line of result.stdout.split('\n')) {
|
|
167
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
168
|
+
if (!match) continue;
|
|
169
|
+
const command = match[2]!;
|
|
170
|
+
if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
|
|
171
|
+
|| !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
|
|
172
|
+
|| !/(?:\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
|
+
let envSeat: string | undefined;
|
|
176
|
+
let envLaunch: string | undefined;
|
|
177
|
+
if (process.platform === 'linux') {
|
|
178
|
+
try {
|
|
179
|
+
const env = readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
|
|
180
|
+
envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
|
|
181
|
+
envLaunch = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
|
|
182
|
+
} catch { /* Unknown attribution is charged to all seats. */ }
|
|
183
|
+
}
|
|
184
|
+
const assigned = envSeat ?? /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1];
|
|
185
|
+
rows.push({ pid, ...(assigned && /^(OP|TC|MK|UX)$/.test(assigned) ? { seat: assigned as QueueSeat } : {}),
|
|
186
|
+
...(envLaunch ? { launchId: envLaunch } : {}) });
|
|
187
|
+
}
|
|
188
|
+
return rows;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function harnessQueueReceiptPath(root: string, launchId: string): string {
|
|
192
|
+
if (!/^hq-[0-9a-f-]{36}$/.test(launchId)) throw new Error('invalid queue launch id');
|
|
193
|
+
return join(root, 'harness', `${launchId}.receipt.json`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-started' | null {
|
|
197
|
+
try {
|
|
198
|
+
const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { state?: string };
|
|
199
|
+
return record.state === 'started' || record.state === 'finished' || record.state === 'not-started' ? record.state : null;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
|
|
207
|
+
const repo = resolve(import.meta.dir, '../..');
|
|
208
|
+
const logPath = join(root, 'harness', `${item.id}.log`);
|
|
209
|
+
const fd = openSync(logPath, 'a', 0o600);
|
|
210
|
+
try {
|
|
211
|
+
const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
|
|
212
|
+
harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
|
|
213
|
+
...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
|
|
214
|
+
cwd: repo, detached: true, stdio: ['ignore', fd, fd],
|
|
215
|
+
env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
|
|
216
|
+
ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
|
|
217
|
+
});
|
|
218
|
+
const pid = await new Promise<number>((done, reject) => {
|
|
219
|
+
child.once('spawn', () => done(child.pid!));
|
|
220
|
+
child.once('error', (error) => {
|
|
221
|
+
try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
|
|
222
|
+
catch { /* A failed receipt write leaves the reservation safely indeterminate. */ }
|
|
223
|
+
reject(error);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
child.unref();
|
|
227
|
+
return pid;
|
|
228
|
+
} finally { closeSync(fd); }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Only a wrapper receipt proving no active child can clear an uncertain reservation. */
|
|
232
|
+
export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<'released' | 'running' | 'unknown'> {
|
|
233
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
234
|
+
return locked(root, async (path) => {
|
|
235
|
+
const rows = read(path);
|
|
236
|
+
const item = rows.find((row) => row.id === id);
|
|
237
|
+
if (!item || !['launching', 'launched'].includes(item.status) || !item.launchId) return 'unknown';
|
|
238
|
+
const state = (deps.receipt ?? receipt)(root, item.launchId);
|
|
239
|
+
const processes = (deps.processes ?? readHarnessQueueProcesses)();
|
|
240
|
+
if (processes.some((row) => row.launchId === item.launchId)
|
|
241
|
+
|| (item.pid !== undefined && (deps.alive ?? alive)(item.pid) && state !== 'finished' && state !== 'not-started')) return 'running';
|
|
242
|
+
if (state !== 'finished' && state !== 'not-started') return 'unknown';
|
|
243
|
+
// A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
|
|
244
|
+
// Keep completed launches in the ledger, but never charge their seat again.
|
|
245
|
+
save(path, state === 'finished' && item.status === 'launched'
|
|
246
|
+
? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
|
|
247
|
+
: rows.filter((row) => row.id !== id));
|
|
248
|
+
observe('skipped', { id, seat: item.seat, reason: `reconciled ${state}`, launchId: item.launchId }, deps);
|
|
249
|
+
return 'released';
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
|
|
254
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
255
|
+
return locked(root, async (path) => {
|
|
256
|
+
const items = read(path);
|
|
257
|
+
const item = items.find((row) => row.status === 'queued');
|
|
258
|
+
if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
259
|
+
const cap = (deps.cap ?? ((s: QueueSeat) => getUserConfig().harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
|
|
260
|
+
if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
|
|
261
|
+
const wait = (reason: string, active: number): QueueTick => {
|
|
262
|
+
observe('waiting', { id: item.id, seat: item.seat, reason, active, cap }, deps);
|
|
263
|
+
return { outcome: 'waiting', item, reason };
|
|
264
|
+
};
|
|
265
|
+
let processes: readonly QueueProcess[];
|
|
266
|
+
try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
|
|
267
|
+
catch (error) { return wait(`harness process inventory unavailable: ${String(error)}`, 0); }
|
|
268
|
+
const current = items.flatMap((row) => {
|
|
269
|
+
if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
|
|
270
|
+
const state = (deps.receipt ?? receipt)(root, row.launchId);
|
|
271
|
+
if (state !== 'finished' && state !== 'not-started'
|
|
272
|
+
|| processes.some((process) => process.launchId === row.launchId)) return [row];
|
|
273
|
+
return state === 'finished' && row.status === 'launched' ? [{ ...row, status: 'finished' as const }] : [];
|
|
274
|
+
});
|
|
275
|
+
if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
|
|
276
|
+
const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
|
|
277
|
+
const launchIds = new Set(charged.map((row) => row.launchId));
|
|
278
|
+
const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
|
|
279
|
+
(row.status === 'launching' || row.status === 'launched')
|
|
280
|
+
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
281
|
+
if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
|
|
282
|
+
let pool: QueuePool;
|
|
283
|
+
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|
|
284
|
+
catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
|
|
285
|
+
if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
|
|
286
|
+
return wait('pod lease status incomplete', active);
|
|
287
|
+
}
|
|
288
|
+
const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
|
|
289
|
+
if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
|
|
290
|
+
return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
|
|
291
|
+
}
|
|
292
|
+
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
|
|
293
|
+
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
294
|
+
try {
|
|
295
|
+
const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
|
|
296
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
297
|
+
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
298
|
+
save(path, current.map((row) => row.id === item.id ? launched : row));
|
|
299
|
+
observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
|
|
300
|
+
return { outcome: 'launched', item: launched, reason: 'spawned' };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|