elanous 0.2.15 → 0.2.17
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/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +104 -11
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +92 -13
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +399 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +178 -12
- package/src/harness/harness-pod-dispatch.ts +36 -55
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +133 -0
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +16 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +168 -13
- package/src/intake-plane/digest.ts +85 -28
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +172 -21
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +107 -25
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +157 -8
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +9 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +6 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +59 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -22,6 +22,7 @@ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
|
22
22
|
export type QueueItem = {
|
|
23
23
|
id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
|
|
24
24
|
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
|
|
25
|
+
launchArgs?: string[]; launchCwd?: string; cellId?: string;
|
|
25
26
|
};
|
|
26
27
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
27
28
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
@@ -53,6 +54,10 @@ export interface HarnessQueueDeps {
|
|
|
53
54
|
cap?: (seat: QueueSeat) => number;
|
|
54
55
|
pool?: () => QueuePool;
|
|
55
56
|
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
57
|
+
/** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
|
|
58
|
+
afterEnqueue?: (item: QueueItem) => void | Promise<void>;
|
|
59
|
+
/** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
|
|
60
|
+
launchCommand?: string;
|
|
56
61
|
alive?: (pid: number) => boolean;
|
|
57
62
|
processes?: () => readonly QueueProcess[];
|
|
58
63
|
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
@@ -143,6 +148,9 @@ function read(path: string): QueueItem[] {
|
|
|
143
148
|
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
144
149
|
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
145
150
|
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
151
|
+
&& (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
|
|
152
|
+
&& (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
|
|
153
|
+
&& (row.cellId === undefined || typeof row.cellId === 'string')
|
|
146
154
|
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
147
155
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
148
156
|
}
|
|
@@ -207,7 +215,24 @@ function seat(value: string): QueueSeat {
|
|
|
207
215
|
return value as QueueSeat;
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
/** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
|
|
219
|
+
export function queueCellId(text: string): string | undefined {
|
|
220
|
+
return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
|
|
224
|
+
function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
|
|
225
|
+
if (kind === 'say') return queueCellId(input);
|
|
226
|
+
try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class HarnessQueueDuplicateError extends Error {
|
|
230
|
+
constructor(existing: QueueItem, key: string) {
|
|
231
|
+
super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
211
236
|
const assigned = seat(input.seat);
|
|
212
237
|
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
213
238
|
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
@@ -228,8 +253,24 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
|
|
|
228
253
|
return existing;
|
|
229
254
|
}
|
|
230
255
|
}
|
|
256
|
+
// Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
|
|
257
|
+
const cellId = queueRowCellId(kind, value);
|
|
258
|
+
if (input.refuseDuplicate) {
|
|
259
|
+
const cell = cellId;
|
|
260
|
+
const existing = rows.find((row) => row.status !== 'finished'
|
|
261
|
+
&& !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
|
|
262
|
+
&& ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
|
|
263
|
+
if (existing) {
|
|
264
|
+
const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
|
|
265
|
+
observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
|
|
266
|
+
throw new HarnessQueueDuplicateError(existing, key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
231
269
|
const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
|
|
232
270
|
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
|
|
271
|
+
...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
|
|
272
|
+
...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
|
|
273
|
+
...(cellId === undefined ? {} : { cellId }),
|
|
233
274
|
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
|
|
234
275
|
save(path, [...rows, item]);
|
|
235
276
|
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
@@ -318,6 +359,7 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
|
|
|
318
359
|
}
|
|
319
360
|
|
|
320
361
|
export function queueLaunchArgs(item: QueueItem): string[] {
|
|
362
|
+
if (item.launchArgs) return item.launchArgs;
|
|
321
363
|
return ['harness', item.kind, item.input, '--substrate', 'pod',
|
|
322
364
|
...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
|
|
323
365
|
}
|
|
@@ -468,15 +510,19 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
|
|
|
468
510
|
}
|
|
469
511
|
}
|
|
470
512
|
|
|
471
|
-
|
|
513
|
+
function defaultLaunch(command?: string) {
|
|
514
|
+
return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
|
|
472
518
|
const repo = resolve(import.meta.dir, '../..');
|
|
473
519
|
const logPath = join(root, 'harness', `${item.id}.log`);
|
|
474
520
|
const fd = openSync(logPath, 'a', 0o600);
|
|
475
521
|
try {
|
|
476
522
|
const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
|
|
477
|
-
harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
|
|
523
|
+
harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
|
|
478
524
|
...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
|
|
479
|
-
cwd: repo, detached: true, stdio: ['ignore', fd, fd],
|
|
525
|
+
cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
|
|
480
526
|
env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
|
|
481
527
|
ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
|
|
482
528
|
});
|
|
@@ -675,7 +721,7 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
|
|
|
675
721
|
}
|
|
676
722
|
}
|
|
677
723
|
|
|
678
|
-
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
|
|
724
|
+
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
|
|
679
725
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
680
726
|
return locked(root, async (path) => {
|
|
681
727
|
try {
|
|
@@ -699,10 +745,20 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
699
745
|
}
|
|
700
746
|
const heads = new Map<QueueSeat, QueueItem>();
|
|
701
747
|
for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
|
|
748
|
+
if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
|
|
749
|
+
const row = items.find((item) => item.id === requestedId);
|
|
750
|
+
if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
|
|
751
|
+
const reason = '앞선 대기열 항목 차례';
|
|
752
|
+
if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
|
|
753
|
+
observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
|
|
754
|
+
return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
|
|
755
|
+
}
|
|
702
756
|
if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
703
757
|
const lastSeat = lastLaunchedSeat(path);
|
|
704
758
|
const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
|
|
705
759
|
: queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
|
|
760
|
+
// A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
|
|
761
|
+
// head with room; an earlier seat's head with room means the requested row waits for its turn.
|
|
706
762
|
const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
|
|
707
763
|
.flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
|
|
708
764
|
const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
|
|
@@ -729,7 +785,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
729
785
|
...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
|
|
730
786
|
attributed, unattributed }, deps);
|
|
731
787
|
}
|
|
732
|
-
|
|
788
|
+
const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
|
|
789
|
+
const subjectReason = blocked.get(subject.seat)!.reason;
|
|
790
|
+
return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
|
|
733
791
|
};
|
|
734
792
|
for (const item of candidates) {
|
|
735
793
|
const injectedCap = deps.cap?.(item.seat);
|
|
@@ -799,11 +857,32 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
799
857
|
+ current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
|
|
800
858
|
}
|
|
801
859
|
if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
|
|
860
|
+
if (requestedId && item.id !== requestedId) {
|
|
861
|
+
const requested = current.find((row) => row.id === requestedId)!;
|
|
862
|
+
// The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
|
|
863
|
+
// evaluated yet; «another seat's turn» only when the requested seat itself has room.
|
|
864
|
+
const own = blocked.get(requested.seat)?.reason ?? (() => {
|
|
865
|
+
const ownCap = deps.cap?.(requested.seat);
|
|
866
|
+
const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
|
|
867
|
+
const charged = runs.filter((run) => !run.seat || run.seat === requested.seat);
|
|
868
|
+
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
869
|
+
const active = charged.length + current.filter((row) => row.seat === requested.seat &&
|
|
870
|
+
(row.status === 'launching' || row.status === 'launched')
|
|
871
|
+
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
872
|
+
if (active >= details.cap) return seatCapReason(requested.seat, active, details);
|
|
873
|
+
const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
|
|
874
|
+
return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
|
|
875
|
+
})();
|
|
876
|
+
const reason = own ?? `다른 자리 차례 — ${item.seat}`;
|
|
877
|
+
if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
|
|
878
|
+
observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
|
|
879
|
+
return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
|
|
880
|
+
}
|
|
802
881
|
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
|
|
803
882
|
delete launching.waitingReason;
|
|
804
883
|
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
805
884
|
try {
|
|
806
|
-
const pid = await (deps.launch ??
|
|
885
|
+
const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
|
|
807
886
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
808
887
|
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
809
888
|
save(path, current.map((row) => row.id === item.id ? launched : row));
|
|
@@ -727,10 +727,13 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
|
|
|
727
727
|
let inspectionError: string | undefined;
|
|
728
728
|
try {
|
|
729
729
|
if (!deps.seams.readPrFiles) throw new Error('readPrFiles seam unavailable');
|
|
730
|
-
const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd });
|
|
730
|
+
const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd, ...(deps.base ? { base: deps.base } : {}) });
|
|
731
731
|
if (!Array.isArray(prFiles) || !prFiles.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
|
|
732
732
|
holdPath = releasePathHold(prFiles);
|
|
733
|
-
} catch (error) {
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
|
|
735
|
+
inspectionError = rateLimited ? `rate-limit: ${String(error)}` : String(error);
|
|
736
|
+
}
|
|
734
737
|
if (holdPath) {
|
|
735
738
|
const protectedPath = holdPath;
|
|
736
739
|
const prCwd = cwd;
|
|
@@ -745,7 +748,14 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
|
|
|
745
748
|
observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-hold' });
|
|
746
749
|
} else if (inspectionError) {
|
|
747
750
|
observe('release-path-inspection-failed', { number: pr.number, error: inspectionError });
|
|
748
|
-
observe('deployed-pr', {
|
|
751
|
+
observe('deployed-pr', {
|
|
752
|
+
number: pr.number,
|
|
753
|
+
url: pr.url,
|
|
754
|
+
autoReview: false,
|
|
755
|
+
mergeReason: inspectionError.startsWith('rate-limit:')
|
|
756
|
+
? 'release-path-inspection-failed: rate-limit'
|
|
757
|
+
: 'release-path-inspection-failed',
|
|
758
|
+
});
|
|
749
759
|
} else {
|
|
750
760
|
let autoReviewLabeled = false;
|
|
751
761
|
if (labels?.includes(AUTO_REVIEW_LABEL)) {
|
|
@@ -2,6 +2,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
|
+
import { releasePathHold, releasePrFilePaths } from '../self-dev/release-path-guard.js';
|
|
6
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
7
|
+
import type { QueueItem, QueueSeat } from './harness-queue.js';
|
|
5
8
|
import type { RunGh, ScannedStoppedPr, StoppedPrCategory } from './helper-scan.js';
|
|
6
9
|
|
|
7
10
|
export interface RepairGoalInput {
|
|
@@ -62,26 +65,154 @@ export function repairInputsFromBody(body: string): { originalRequest: string; m
|
|
|
62
65
|
|
|
63
66
|
export type RepairShadowRow = { pr: number; category: StoppedPrCategory; goalHash: string; at: string };
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
type RepairLedgerResult = 'shadow' | 'launched' | 'cap' | 'needs-human' | 'launch-failed';
|
|
69
|
+
|
|
70
|
+
type RepairLedgerLine = {
|
|
71
|
+
pr: number; category: StoppedPrCategory; goalHash: string; at: string;
|
|
72
|
+
mode: 'shadow' | 'live'; goal: string; result: RepairLedgerResult;
|
|
73
|
+
/** Harness queue item id — the run itself starts on a later queue tick. */ queueId?: string; reason?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const RELEASE_FILE_PAGE = 100;
|
|
77
|
+
|
|
78
|
+
/** Changed files of the original PR. A failed read is a reason to hold, never a reason to launch. */
|
|
79
|
+
async function changedFiles(pr: number, runGh: RunGh): Promise<string[]> {
|
|
80
|
+
const pages: unknown[] = [];
|
|
81
|
+
for (let page = 1; ; page++) {
|
|
82
|
+
const raw = await runGh(['api', '--method', 'GET', '--raw-field', `per_page=${RELEASE_FILE_PAGE}`, '--raw-field', `page=${page}`, `repos/{owner}/{repo}/pulls/${pr}/files`]);
|
|
83
|
+
const parsed: unknown = JSON.parse(raw);
|
|
84
|
+
if (!Array.isArray(parsed)) throw new Error('gh api PR files returned an invalid file list');
|
|
85
|
+
pages.push(parsed);
|
|
86
|
+
if (parsed.length < RELEASE_FILE_PAGE) break;
|
|
87
|
+
}
|
|
88
|
+
return releasePrFilePaths(pages);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const QUEUE_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
92
|
+
|
|
93
|
+
/** 원 PR 의 자리 라벨(elanous:seat-<자리> 또는 자리 이름 라벨). 없거나 둘 이상이면 MK. */
|
|
94
|
+
function seatOf(labels: readonly { name?: string }[] | undefined): QueueSeat {
|
|
95
|
+
const found = new Set<QueueSeat>();
|
|
96
|
+
for (const label of labels ?? []) {
|
|
97
|
+
const name = label.name ?? '';
|
|
98
|
+
const prefixed = /^elanous:seat-(OP|TC|MK|UX)$/.exec(name)?.[1];
|
|
99
|
+
const bare = (QUEUE_SEATS as readonly string[]).includes(name) ? name : undefined;
|
|
100
|
+
const seat = prefixed ?? bare;
|
|
101
|
+
if (seat) found.add(seat as QueueSeat);
|
|
102
|
+
}
|
|
103
|
+
return found.size === 1 ? [...found][0]! : 'MK';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function helperSettings(config: UserConfig | undefined): { mode: 'shadow' | 'live'; perDay: number } {
|
|
107
|
+
const helper = config?.harness?.helper;
|
|
108
|
+
return {
|
|
109
|
+
mode: helper?.repair === 'live' ? 'live' : 'shadow',
|
|
110
|
+
perDay: typeof helper?.repairPerDay === 'number' && Number.isSafeInteger(helper.repairPerDay) && helper.repairPerDay >= 0
|
|
111
|
+
? helper.repairPerDay : 3,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 그림자(기본): 수리 골을 원장에만 남긴다. live: 같은 함수로 대기열에 넣고 원 PR 을 superseded 로 표시한다. 같은 PR 은 평생 한 번. */
|
|
116
|
+
export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: {
|
|
117
|
+
runGh: RunGh; root: string; now?: Date;
|
|
118
|
+
config?: UserConfig;
|
|
119
|
+
/** 대기열 추가. 생략하면 addHarnessQueue — 실제 발사는 기존 대기열 틱이 한다. */
|
|
120
|
+
enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<Pick<QueueItem, 'id'>>;
|
|
121
|
+
}): Promise<RepairShadowRow[]> {
|
|
67
122
|
const ledger = join(deps.root, 'helper', 'repairs.jsonl');
|
|
68
|
-
const
|
|
123
|
+
const prior: RepairLedgerLine[] = [];
|
|
69
124
|
if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
|
|
70
125
|
if (!line.trim()) continue;
|
|
71
|
-
try {
|
|
126
|
+
try { prior.push(JSON.parse(line) as RepairLedgerLine); } catch { /* malformed line is skipped */ }
|
|
127
|
+
}
|
|
128
|
+
const { mode, perDay } = helperSettings(deps.config ?? getUserConfig());
|
|
129
|
+
const day = (deps.now ?? new Date()).toISOString().slice(0, 10);
|
|
130
|
+
// Terminal results end a PR for good. A shadow line blocks only while still in shadow (live must still launch it),
|
|
131
|
+
// and a cap or failed launch blocks only for the same UTC day so the PR is retried tomorrow (ACP must-fix).
|
|
132
|
+
// Ledger lines written before results existed have no `result`; they were shadow records.
|
|
133
|
+
for (const line of prior) if (!line.result) line.result = 'shadow';
|
|
134
|
+
const seen = new Set(prior.filter((line) => line.result === 'launched' || line.result === 'needs-human'
|
|
135
|
+
|| (line.result === 'shadow' && mode === 'shadow')
|
|
136
|
+
|| ((line.result === 'cap' || line.result === 'launch-failed') && line.at.slice(0, 10) === day)).map((line) => line.pr));
|
|
137
|
+
// Distinct PRs: a follow-up repair line for an already launched PR is not a second launch.
|
|
138
|
+
let launchedToday = new Set(prior.filter((line) => line.result === 'launched' && line.at.slice(0, 10) === day).map((line) => line.pr)).size;
|
|
139
|
+
// A launched line whose label/comment failed is repaired here — the follow-up only, never a second enqueue.
|
|
140
|
+
if (mode === 'live') {
|
|
141
|
+
const latest = new Map<number, RepairLedgerLine>();
|
|
142
|
+
for (const line of prior) latest.set(line.pr, line);
|
|
143
|
+
for (const line of latest.values()) {
|
|
144
|
+
if (line.result !== 'launched' || !line.reason?.startsWith('후속 실패') || !line.queueId) continue;
|
|
145
|
+
const followups: string[] = [];
|
|
146
|
+
if (line.reason.includes('label ·')) {
|
|
147
|
+
try { await deps.runGh(['pr', 'edit', String(line.pr), '--add-label', 'elanous:superseded']); }
|
|
148
|
+
catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
|
|
149
|
+
}
|
|
150
|
+
if (line.reason.includes('comment ·')) {
|
|
151
|
+
try { await deps.runGh(['pr', 'comment', String(line.pr), '--body', `수리 대기열 항목 ${line.queueId} 로 대체(런은 대기열 틱에서 시작)`]); }
|
|
152
|
+
catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
|
|
153
|
+
}
|
|
154
|
+
const repaired: RepairLedgerLine = { ...line, at: (deps.now ?? new Date()).toISOString(), ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : { reason: '후속 복구' }) };
|
|
155
|
+
appendFileSync(ledger, JSON.stringify(repaired) + '\n');
|
|
156
|
+
debug.log('harness.helper', 'repair-followup-retried', { pr: line.pr, queueId: line.queueId, remaining: followups.length });
|
|
157
|
+
}
|
|
72
158
|
}
|
|
159
|
+
const enqueue = deps.enqueue ?? (async (input) => {
|
|
160
|
+
const { addHarnessQueue } = await import('./harness-queue.js');
|
|
161
|
+
return addHarnessQueue(input, { root: deps.root });
|
|
162
|
+
});
|
|
73
163
|
const written: RepairShadowRow[] = [];
|
|
74
164
|
for (const row of rows) {
|
|
75
165
|
if ((row.category !== 'review-budget' && row.category !== 'review-oscillation') || seen.has(row.pr)) continue;
|
|
76
166
|
const view = JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'body'])) as { body: string };
|
|
167
|
+
// 자리 라벨은 본문 조회와 분리한다 — 기존 본문 조회 계약(--json body)을 그대로 둔다. 못 읽으면 MK.
|
|
168
|
+
let labels: { name?: string }[] | undefined;
|
|
169
|
+
if (mode === 'live') {
|
|
170
|
+
try { labels = (JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'labels'])) as { labels?: { name?: string }[] }).labels; }
|
|
171
|
+
catch { labels = undefined; }
|
|
172
|
+
}
|
|
77
173
|
const goal = authorRepairGoal({ pr: row.pr, category: row.category, ...repairInputsFromBody(view.body) });
|
|
78
174
|
if (!goal) continue;
|
|
79
|
-
const
|
|
175
|
+
const at = (deps.now ?? new Date()).toISOString();
|
|
176
|
+
const base = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at, goal };
|
|
177
|
+
let record: RepairLedgerLine = { ...base, mode: 'shadow', result: 'shadow' };
|
|
178
|
+
if (mode === 'live') {
|
|
179
|
+
let files: string[] | undefined;
|
|
180
|
+
try { files = await changedFiles(row.pr, deps.runGh); }
|
|
181
|
+
catch (error) { files = undefined; debug.log('harness.helper', 'repair-files-unreadable', { pr: row.pr, reason: String(error) }); }
|
|
182
|
+
const release = files && releasePathHold(files);
|
|
183
|
+
// 상한은 «발사한 수»의 상한이다. 사람 승인이 필요한 PR 은 그 수를 쓰지 않으므로 상한보다 먼저 가른다.
|
|
184
|
+
if (files && !release && launchedToday >= perDay) {
|
|
185
|
+
record = { ...base, mode: 'live', result: 'cap', reason: '상한' };
|
|
186
|
+
} else if (!files) {
|
|
187
|
+
// An unreadable file list is transient: hold today, retry on a later day (never a permanent needs-human) (ACP must-fix).
|
|
188
|
+
record = { ...base, mode: 'live', result: 'launch-failed', reason: '변경 파일 조회 실패 · 다음 날 재시도' };
|
|
189
|
+
} else if (release) {
|
|
190
|
+
record = { ...base, mode: 'live', result: 'needs-human', reason: '사람 승인 필요' };
|
|
191
|
+
} else {
|
|
192
|
+
let item: Pick<QueueItem, 'id'> | undefined;
|
|
193
|
+
try {
|
|
194
|
+
item = await enqueue({ seat: seatOf(labels), say: goal, idempotencyKey: `helper-repair-${row.pr}` });
|
|
195
|
+
// The queue item exists from here on: a later label/comment failure is recorded on the launched line,
|
|
196
|
+
// never as launch-failed (which would let the next scan enqueue a second repair) (ACP must-fix).
|
|
197
|
+
const followups: string[] = [];
|
|
198
|
+
try { await deps.runGh(['pr', 'edit', String(row.pr), '--add-label', 'elanous:superseded']); }
|
|
199
|
+
catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
|
|
200
|
+
try { await deps.runGh(['pr', 'comment', String(row.pr), '--body', `수리 대기열 항목 ${item.id} 로 대체(런은 대기열 틱에서 시작)`]); }
|
|
201
|
+
catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
|
|
202
|
+
record = { ...base, mode: 'live', result: 'launched', queueId: item.id, ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : {}) };
|
|
203
|
+
launchedToday += 1;
|
|
204
|
+
debug.log('harness.helper', 'repair-launched', { pr: row.pr, queueId: item.id, mode: 'live', followupFailures: followups.length });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
record = { ...base, mode: 'live', result: 'launch-failed', reason: `발사 실패 · ${error instanceof Error ? error.message : String(error)}` };
|
|
207
|
+
debug.log('harness.helper', 'repair-launch-failed', { pr: row.pr, reason: record.reason });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
80
211
|
mkdirSync(dirname(ledger), { recursive: true });
|
|
81
|
-
appendFileSync(ledger, JSON.stringify(
|
|
82
|
-
debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
|
|
212
|
+
appendFileSync(ledger, JSON.stringify(record) + '\n');
|
|
213
|
+
if (record.result === 'shadow') debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
|
|
83
214
|
seen.add(row.pr);
|
|
84
|
-
written.push(
|
|
215
|
+
written.push({ pr: base.pr, category: base.category, goalHash: base.goalHash, at: base.at });
|
|
85
216
|
}
|
|
86
217
|
return written;
|
|
87
218
|
}
|
|
@@ -139,8 +139,19 @@ if (import.meta.main) {
|
|
|
139
139
|
// 명시적 그림자 기록에서만 추가 PR 조회와 헬퍼 원장 쓰기를 수행한다(발사 0).
|
|
140
140
|
const { recordRepairShadows } = await import('./helper-repair.js');
|
|
141
141
|
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
142
|
-
const
|
|
143
|
-
|
|
142
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
143
|
+
const root = effectiveInstanceRoot();
|
|
144
|
+
const config = getUserConfig();
|
|
145
|
+
const live = config.harness?.helper?.repair === 'live';
|
|
146
|
+
const shadows = await recordRepairShadows(rows, {
|
|
147
|
+
runGh, root, config,
|
|
148
|
+
// live 만 대기열에 넣는다. 실제 발사는 기존 대기열 틱이 한다(새 발사 경로 0).
|
|
149
|
+
...(live ? { enqueue: async (input) => {
|
|
150
|
+
const { addHarnessQueue } = await import('./harness-queue.js');
|
|
151
|
+
return addHarnessQueue(input, { root });
|
|
152
|
+
} } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (shadows.length) console.log(live ? `수리 골 ${shadows.length}건 → helper/repairs.jsonl` : `수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
|
|
144
155
|
}
|
|
145
156
|
if (json) console.log(JSON.stringify(rows, null, 2));
|
|
146
157
|
else for (const row of rows) {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Nested elanous launch policy.
|
|
2
|
+
//
|
|
3
|
+
// Codex children drop parent env under shell_environment_policy inherit=core, so a
|
|
4
|
+
// depth marker that only lives in the environment never arrives. Depth is therefore
|
|
5
|
+
// carried on the codex argv (`-c shell_environment_policy.set.ELANOUS_NESTED_DEPTH=<n>`).
|
|
6
|
+
//
|
|
7
|
+
// Unset ELANOUS_NESTED_DEPTH is depth 0 (the outer launcher). A missing marker is
|
|
8
|
+
// not "depth unknown" and does not refuse an ordinary mission spawn.
|
|
9
|
+
// Refusal for a backend that cannot carry the marker applies only when a nested
|
|
10
|
+
// elanous launch was actually requested (allow flag or config allow).
|
|
11
|
+
//
|
|
12
|
+
// Only depth 0 (the outer launcher) may allow a nested elanous launch. At depth >= 1
|
|
13
|
+
// an allow flag is ignored and the launch stays refused.
|
|
14
|
+
|
|
15
|
+
import { debug } from '../debug/log.js';
|
|
16
|
+
import { getUserConfig } from '../user-config.js';
|
|
17
|
+
|
|
18
|
+
export const ELANOUS_NESTED_DEPTH_ENV = 'ELANOUS_NESTED_DEPTH';
|
|
19
|
+
|
|
20
|
+
/** Codex config key that sets the depth marker inside the child's shell policy. */
|
|
21
|
+
export const CODEX_NESTED_DEPTH_CONFIG_KEY = `shell_environment_policy.set.${ELANOUS_NESTED_DEPTH_ENV}`;
|
|
22
|
+
|
|
23
|
+
export type NestedElanousBackendName = 'codex' | 'claude' | 'gemini' | 'grok' | 'aside' | (string & {});
|
|
24
|
+
|
|
25
|
+
export interface NestedElanousDecision {
|
|
26
|
+
readonly allowed: boolean;
|
|
27
|
+
readonly depth: number;
|
|
28
|
+
/** True when depth >= 1 supplied allow and it was ignored. */
|
|
29
|
+
readonly allowIgnored: boolean;
|
|
30
|
+
readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored' | 'depth-cap';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Current process depth from the nested-depth marker. Unset or unreadable = 0 (outer launcher). */
|
|
34
|
+
export function readNestedElanousDepth(env: Record<string, string | undefined> = process.env): number {
|
|
35
|
+
const raw = env[ELANOUS_NESTED_DEPTH_ENV];
|
|
36
|
+
if (raw === undefined || raw.trim() === '') return 0;
|
|
37
|
+
if (!/^\d+$/.test(raw.trim())) return 0;
|
|
38
|
+
return Number(raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when this backend can carry the depth marker to its child. Only codex can. */
|
|
42
|
+
export function backendCarriesNestedDepth(backendName: string): boolean {
|
|
43
|
+
return backendName === 'codex';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Codex argv fragment that sets the child's depth marker.
|
|
48
|
+
* `depth` is the child's depth (parent + 1). Non-codex backends return [] — they cannot carry it.
|
|
49
|
+
*/
|
|
50
|
+
export function nestedDepthCodexArgs(backendName: string, childDepth: number): string[] {
|
|
51
|
+
if (backendName !== 'codex') return [];
|
|
52
|
+
if (!Number.isInteger(childDepth) || childDepth < 0) return [];
|
|
53
|
+
return ['-c', `${CODEX_NESTED_DEPTH_CONFIG_KEY}=${childDepth}`];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Allow decision. Depth 0 allows only when `allow` is true.
|
|
58
|
+
* Depth >= 1 always refuses; a supplied allow is ignored and logged.
|
|
59
|
+
*/
|
|
60
|
+
/** Nested depth cap: explicit value, else `harness.nestedElanousMaxDepth`, else 2. */
|
|
61
|
+
export function resolveNestedDepthCap(maxDepth?: number): number {
|
|
62
|
+
const configured = maxDepth ?? getUserConfig().harness?.nestedElanousMaxDepth;
|
|
63
|
+
return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function decideNestedElanousLaunch(input: {
|
|
67
|
+
readonly depth: number;
|
|
68
|
+
readonly allow?: boolean;
|
|
69
|
+
readonly maxDepth?: number;
|
|
70
|
+
/** False for an ordinary mission spawn that did not request a nested launch — no cap log. */
|
|
71
|
+
readonly logCap?: boolean;
|
|
72
|
+
}): NestedElanousDecision {
|
|
73
|
+
const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
|
|
74
|
+
const cap = resolveNestedDepthCap(input.maxDepth);
|
|
75
|
+
if (depth >= cap) {
|
|
76
|
+
if (input.logCap !== false) debug.log('agent-mission.nested', 'depth-cap', { depth, cap });
|
|
77
|
+
return { allowed: false, depth, allowIgnored: input.allow === true, reason: 'depth-cap' };
|
|
78
|
+
}
|
|
79
|
+
if (depth >= 1) {
|
|
80
|
+
if (input.allow === true) {
|
|
81
|
+
debug.log('agent-mission.nested', 'allow-ignored', { depth });
|
|
82
|
+
return { allowed: false, depth, allowIgnored: true, reason: 'allow-ignored' };
|
|
83
|
+
}
|
|
84
|
+
return { allowed: false, depth, allowIgnored: false, reason: 'nested-refused' };
|
|
85
|
+
}
|
|
86
|
+
if (input.allow === true) {
|
|
87
|
+
return { allowed: true, depth, allowIgnored: false, reason: 'depth-0-allow' };
|
|
88
|
+
}
|
|
89
|
+
return { allowed: false, depth, allowIgnored: false, reason: 'depth-0' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Config `harness.nestedElanous`. Only `'allow'` counts, and only at depth 0. */
|
|
93
|
+
export function configAllowsNestedElanous(
|
|
94
|
+
config: { harness?: { nestedElanous?: string } } | undefined,
|
|
95
|
+
depth: number,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (depth !== 0) return false;
|
|
98
|
+
return config?.harness?.nestedElanous === 'allow';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Gate a nested elanous launch.
|
|
103
|
+
*
|
|
104
|
+
* Ordinary mission spawn is not this gate: call it only when a nested elanous
|
|
105
|
+
* launch was requested. A backend that cannot carry the depth marker is then
|
|
106
|
+
* refused (`depth-unknown`) — including depth 0 with allow — because the child
|
|
107
|
+
* would not know its own depth. Unset depth is 0, not unknown.
|
|
108
|
+
*
|
|
109
|
+
* `allow` is the flag. Config allow counts only at depth 0 and never overrides
|
|
110
|
+
* a depth >= 1 refusal.
|
|
111
|
+
*/
|
|
112
|
+
export function gateNestedElanousLaunch(input: {
|
|
113
|
+
readonly backendName: string;
|
|
114
|
+
readonly depth: number;
|
|
115
|
+
/** True only when this call is gating a requested nested elanous launch. */
|
|
116
|
+
readonly nestedLaunchRequested: boolean;
|
|
117
|
+
readonly allow?: boolean;
|
|
118
|
+
readonly configAllow?: boolean;
|
|
119
|
+
readonly maxDepth?: number;
|
|
120
|
+
}): NestedElanousDecision {
|
|
121
|
+
const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
|
|
122
|
+
const allow = input.nestedLaunchRequested && (input.allow === true || (depth === 0 && input.configAllow === true));
|
|
123
|
+
if (!input.nestedLaunchRequested) {
|
|
124
|
+
return decideNestedElanousLaunch({ depth, allow: false, logCap: false, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
|
|
125
|
+
}
|
|
126
|
+
if (!backendCarriesNestedDepth(input.backendName)) {
|
|
127
|
+
// The depth-unknown refusal stands; reaching the cap is still recorded.
|
|
128
|
+
const cap = resolveNestedDepthCap(input.maxDepth);
|
|
129
|
+
if (depth >= cap) debug.log('agent-mission.nested', 'depth-cap', { depth, cap, backend: input.backendName });
|
|
130
|
+
return { allowed: false, depth, allowIgnored: false, reason: 'depth-unknown' };
|
|
131
|
+
}
|
|
132
|
+
return decideNestedElanousLaunch({ depth, allow, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
|
|
133
|
+
}
|