elanous 0.2.17 → 0.2.18
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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.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/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/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/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +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/loops/index.html +1 -1
- package/apps/pwa/out/loops/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/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/** GATE-REMOTE (0.2.18 · 10-06) — run a heavy pre-landing check (self gate · changed-file tsc · public-export test run)
|
|
2
|
+
* on a remote host at the SAME commit and bring back only its result (exit code · stdout · stderr).
|
|
3
|
+
* Reach: the release gate's bare mirror (`~/mirror/elanous-agent.git`, refs/elanous/gate/<sha>) — copied minimally from
|
|
4
|
+
* scripts/release-loop/gate-node.ts (RELGUARD path, not edited); gate-node could later switch to this module. */
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { loadavg } from 'node:os';
|
|
8
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { debug } from '../debug/log.js';
|
|
10
|
+
import type { GateRemoteConfig } from '../user-config.js';
|
|
11
|
+
|
|
12
|
+
export type GateRemoteTool = 'self-gate' | 'ci-typecheck-changed' | 'public-export-test-run';
|
|
13
|
+
|
|
14
|
+
export interface GateRemoteSettings {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
host: string;
|
|
17
|
+
loadThreshold: number;
|
|
18
|
+
hostCap: number;
|
|
19
|
+
mirror: string;
|
|
20
|
+
slotWaitSeconds: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MAX_HOST_CAP = 2;
|
|
24
|
+
|
|
25
|
+
export const GATE_REMOTE_DEFAULTS: GateRemoteSettings = {
|
|
26
|
+
enabled: true, host: 'node-b', loadThreshold: 20, hostCap: 2, mirror: '~/mirror/elanous-agent.git', slotWaitSeconds: 600,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const hostPattern = /^(?:[\w.-]+@)?[\w.-]+$/;
|
|
30
|
+
export const isValidGateHost = (host: string): boolean => hostPattern.test(host) && !host.startsWith('-');
|
|
31
|
+
const mirrorPattern = /^(?:\/|~\/)[\w./-]+$/;
|
|
32
|
+
const shaPattern = /^[0-9a-f]{40}$/;
|
|
33
|
+
|
|
34
|
+
/** config > env > default (user-config hierarchy). Env: ELANOUS_GATE_REMOTE=off|on · ELANOUS_GATE_REMOTE_HOST · ELANOUS_GATE_REMOTE_LOAD. */
|
|
35
|
+
export function resolveGateRemoteSettings(config: GateRemoteConfig | undefined, env: NodeJS.ProcessEnv = process.env): GateRemoteSettings {
|
|
36
|
+
const envEnabled = env.ELANOUS_GATE_REMOTE === 'off' || env.ELANOUS_GATE_REMOTE === '0' ? false
|
|
37
|
+
: env.ELANOUS_GATE_REMOTE === 'on' || env.ELANOUS_GATE_REMOTE === '1' ? true : undefined;
|
|
38
|
+
const envLoadRaw = env.ELANOUS_GATE_REMOTE_LOAD && /^\d+(?:\.\d+)?$/.test(env.ELANOUS_GATE_REMOTE_LOAD) ? Number(env.ELANOUS_GATE_REMOTE_LOAD) : undefined;
|
|
39
|
+
const envLoad = envLoadRaw !== undefined && Number.isFinite(envLoadRaw) ? envLoadRaw : undefined;
|
|
40
|
+
const host = config?.host ?? env.ELANOUS_GATE_REMOTE_HOST?.trim() ?? GATE_REMOTE_DEFAULTS.host;
|
|
41
|
+
const mirror = config?.mirror ?? GATE_REMOTE_DEFAULTS.mirror;
|
|
42
|
+
return {
|
|
43
|
+
enabled: config?.enabled ?? envEnabled ?? GATE_REMOTE_DEFAULTS.enabled,
|
|
44
|
+
host: isValidGateHost(host) ? host : GATE_REMOTE_DEFAULTS.host,
|
|
45
|
+
loadThreshold: config?.loadThreshold ?? envLoad ?? GATE_REMOTE_DEFAULTS.loadThreshold,
|
|
46
|
+
// The cell's ceiling is 2 per host (GATE-REMOTE acceptance); config may only lower it.
|
|
47
|
+
hostCap: Math.min(MAX_HOST_CAP, Math.max(1, Math.floor(config?.hostCap ?? GATE_REMOTE_DEFAULTS.hostCap))),
|
|
48
|
+
mirror: mirrorPattern.test(mirror) && !mirror.split('/').includes('..') ? mirror : GATE_REMOTE_DEFAULTS.mirror,
|
|
49
|
+
slotWaitSeconds: config?.slotWaitSeconds ?? GATE_REMOTE_DEFAULTS.slotWaitSeconds,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The token after a bare `--remote` is its host when it is a valid ssh destination that is not an existing path and does
|
|
54
|
+
* not look like a test/list file — so `--remote node-b.lan a.test.ts` takes the host and `--remote a.test.ts` keeps the file. */
|
|
55
|
+
export function looksLikeHost(token: string, exists: (path: string) => boolean = existsSync): boolean {
|
|
56
|
+
if (!isValidGateHost(token) || exists(token)) return false;
|
|
57
|
+
return !/\.(?:test\.)?(?:[cm]?[jt]sx?|txt|json|md)$/i.test(token);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GateRemoteFlags { remote?: string | true; local: boolean; rest: string[]; error?: string }
|
|
61
|
+
|
|
62
|
+
/** Pull `--remote [host]` / `--remote=<host>` / `--local` out of argv; everything else is left for the tool. */
|
|
63
|
+
export function extractGateRemoteFlags(argv: readonly string[]): GateRemoteFlags {
|
|
64
|
+
const out: GateRemoteFlags = { local: false, rest: [] };
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const arg = argv[i]!;
|
|
67
|
+
if (arg === '--local') out.local = true;
|
|
68
|
+
else if (arg.startsWith('--remote=')) out.remote = arg.slice('--remote='.length);
|
|
69
|
+
else if (arg === '--remote') {
|
|
70
|
+
const next = argv[i + 1];
|
|
71
|
+
if (next !== undefined && looksLikeHost(next)) { out.remote = next; i++; } else out.remote = true;
|
|
72
|
+
} else out.rest.push(arg);
|
|
73
|
+
}
|
|
74
|
+
if (typeof out.remote === 'string' && !isValidGateHost(out.remote)) out.error = `invalid --remote host: ${out.remote}`;
|
|
75
|
+
if (out.remote !== undefined && out.local) out.error = '--remote and --local cannot be combined';
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GateDispatchDecision { mode: 'local' | 'remote'; host?: string; reason: string; load1: number }
|
|
80
|
+
|
|
81
|
+
export function decideGateDispatch(input: { flags: Pick<GateRemoteFlags, 'remote' | 'local'>; settings: GateRemoteSettings; load1: number; env?: NodeJS.ProcessEnv }): GateDispatchDecision {
|
|
82
|
+
const { flags, settings, load1 } = input;
|
|
83
|
+
const env = input.env ?? process.env;
|
|
84
|
+
if (flags.local) return { mode: 'local', reason: 'flag-local', load1 };
|
|
85
|
+
if (env.ELANOUS_GATE_REMOTE_CHILD === '1') return { mode: 'local', reason: 'remote-child', load1 };
|
|
86
|
+
if (flags.remote !== undefined) return { mode: 'remote', host: flags.remote === true ? settings.host : flags.remote, reason: 'flag-remote', load1 };
|
|
87
|
+
if (!settings.enabled) return { mode: 'local', reason: 'config-off', load1 };
|
|
88
|
+
// Tests never reach a real host on their own (an explicit --remote with an injected runner still can).
|
|
89
|
+
if (env.NODE_ENV === 'test') return { mode: 'local', reason: 'test-env', load1 };
|
|
90
|
+
if (load1 > settings.loadThreshold) return { mode: 'remote', host: settings.host, reason: `load ${load1.toFixed(1)} > ${settings.loadThreshold}`, load1 };
|
|
91
|
+
return { mode: 'local', reason: `load ${load1.toFixed(1)} <= ${settings.loadThreshold}`, load1 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── host lock (on the remote host, cap N) ───────────────────────
|
|
95
|
+
// The slots live on the host itself, so every machine that dispatches there shares one cap. Each slot is a kernel file
|
|
96
|
+
// lock (flock(1) / BSD lockf(1)) held for exactly the life of the run — a crashed or killed run releases it, so there is
|
|
97
|
+
// no staleness guess and no reclaim race.
|
|
98
|
+
|
|
99
|
+
export const SLOT_DIR = '/tmp/elanous-gate-remote-slots';
|
|
100
|
+
const BUSY = '__GATE_REMOTE_BUSY';
|
|
101
|
+
|
|
102
|
+
/** Shell lines that run "$W/run.sh" under the first free of `cap` slot locks, or print the busy marker and exit 98.
|
|
103
|
+
* run.sh must create "$W/started" first: that is how a lock refusal is told apart from the run's own exit code. */
|
|
104
|
+
export type SlotLockTool = 'flock' | 'lockf';
|
|
105
|
+
const SLOT_LOCK = { flock: 'flock -n', lockf: 'lockf -k -s -t 0' } as const;
|
|
106
|
+
|
|
107
|
+
/** `tools` is the preference order (default: Linux flock, then BSD/macOS lockf); a test can pin one path. */
|
|
108
|
+
export function hostSlotScript(cap: number, dir = SLOT_DIR, tools: readonly SlotLockTool[] = ['flock', 'lockf']): string[] {
|
|
109
|
+
const n = Math.min(MAX_HOST_CAP, Math.max(1, Math.floor(cap)));
|
|
110
|
+
const pick = tools.map((tool, i) => `${i ? 'elif' : 'if'} command -v ${tool} >/dev/null 2>&1; then GL="${SLOT_LOCK[tool]}"; `).join('');
|
|
111
|
+
return [
|
|
112
|
+
`GS=${quote(dir)}; mkdir -p "$GS" || infra slots`,
|
|
113
|
+
`${pick}else infra lock-tool; fi`,
|
|
114
|
+
`n=1; while [ $n -le ${n} ]; do $GL "$GS/slot$n" sh "$W/run.sh"; s=$?; [ -f "$W/started" ] && exit $s; n=$((n+1)); done`,
|
|
115
|
+
`echo "${BUSY}" >&2; exit 98`,
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── remote execution ───────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export interface ExecResult { rc: number | null; stdout: string; stderr: string; error?: string }
|
|
122
|
+
/** Raw bytes from the host — the tool's output is passed on byte for byte (strings are accepted from test runners). */
|
|
123
|
+
export interface SshResult { rc: number | null; stdout: Buffer | string; stderr: Buffer | string; error?: string }
|
|
124
|
+
export interface GateRemoteRunner {
|
|
125
|
+
/** Run a command locally (git rev-parse · status · push). */
|
|
126
|
+
local(cmd: string, args: string[], cwd: string, options?: { timeoutMs?: number; env?: NodeJS.ProcessEnv }): ExecResult;
|
|
127
|
+
/** Run a shell script on the host over ssh. */
|
|
128
|
+
ssh(host: string, script: string, timeoutMs: number): SshResult;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const realGateRemoteRunner: GateRemoteRunner = {
|
|
132
|
+
local(cmd, args, cwd, options) {
|
|
133
|
+
const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
|
134
|
+
...(options?.env ? { env: options.env } : {}), ...(options?.timeoutMs ? { timeout: options.timeoutMs, killSignal: 'SIGKILL' as const } : {}) });
|
|
135
|
+
return { rc: run.status, stdout: run.stdout ?? '', stderr: run.stderr ?? '', ...(run.error ? { error: String(run.error) } : {}) };
|
|
136
|
+
},
|
|
137
|
+
ssh(host, script, timeoutMs) {
|
|
138
|
+
const run = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host, 'sh', '-s'], {
|
|
139
|
+
input: script, cwd: '/tmp', maxBuffer: 256 * 1024 * 1024, timeout: timeoutMs, killSignal: 'SIGKILL',
|
|
140
|
+
});
|
|
141
|
+
return { rc: run.status, stdout: run.stdout ?? Buffer.alloc(0), stderr: run.stderr ?? Buffer.alloc(0), ...(run.error ? { error: String(run.error) } : {}) };
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const quote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
146
|
+
const INFRA = '__GATE_REMOTE_INFRA=';
|
|
147
|
+
const RC = '__GATE_REMOTE_RC=';
|
|
148
|
+
export const PUSH_TIMEOUT_MS = 10 * 60_000;
|
|
149
|
+
|
|
150
|
+
export type RemoteRunOutcome =
|
|
151
|
+
| { kind: 'ran'; rc: number; stdout: Buffer; stderr: Buffer; commit: string; host: string; ms: number }
|
|
152
|
+
| { kind: 'busy' }
|
|
153
|
+
| { kind: 'infra'; reason: string };
|
|
154
|
+
|
|
155
|
+
export interface RemoteRunOptions {
|
|
156
|
+
repo: string;
|
|
157
|
+
host: string;
|
|
158
|
+
mirror: string;
|
|
159
|
+
/** argv run from the remote checkout root, e.g. ['bun', 'bin/elanous.mjs', 'self', 'gate', ...]; a function receives
|
|
160
|
+
* `sha(ref)` for each of `refs` so the remote command names the exact commit, not a ref the host may not have. */
|
|
161
|
+
argv: RemoteArgv;
|
|
162
|
+
hostCap?: number;
|
|
163
|
+
/** Host-side slot lock directory (default SLOT_DIR) — tests point it at a scratch dir. */
|
|
164
|
+
slotDir?: string;
|
|
165
|
+
/** Extra local refs whose commits must exist remotely (e.g. a --base ref). Resolved to shas. */
|
|
166
|
+
refs?: string[];
|
|
167
|
+
installPwa?: boolean;
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
/** Files the remote run needs from this machine (e.g. the private export redaction table): sent over ssh stdin, written
|
|
170
|
+
* mode 600 inside the scratch directory, exposed to the tool via `env`, removed with it. Never on argv. */
|
|
171
|
+
payloadFiles?: PayloadFile[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PayloadFile { env: string; content: string }
|
|
175
|
+
export type RemoteArgv = string[] | ((sha: (ref: string) => string) => string[]);
|
|
176
|
+
|
|
177
|
+
/** Build the remote shell script (exported for tests). Every setup failure ends in an INFRA marker, the tool's rc in an RC marker. */
|
|
178
|
+
export function remoteScript(input: { mirror: string; commit: string; fetchShas: string[]; mainSha?: string; argv: string[]; installPwa: boolean; payloadFiles?: PayloadFile[]; hostCap?: number; slotDir?: string }): string {
|
|
179
|
+
const mirror = input.mirror.startsWith('~/') ? `"$HOME"/${quote(input.mirror.slice(2))}` : quote(input.mirror);
|
|
180
|
+
const fetches = input.fetchShas.map((sha) => `refs/elanous/gate/${sha}:refs/elanous/gate/${sha}`).join(' ');
|
|
181
|
+
const payloads = (input.payloadFiles ?? []).filter((file) => /^[A-Z_][A-Z0-9_]*$/.test(file.env));
|
|
182
|
+
const payloadLines = payloads.flatMap((file, i) => {
|
|
183
|
+
const delimiter = `__GATE_REMOTE_PAYLOAD_${i}__`;
|
|
184
|
+
if (file.content.split('\n').includes(delimiter)) throw new Error('payload contains its delimiter');
|
|
185
|
+
return [`cat > "$W/payload-${i}" <<'${delimiter}' || infra payload`, file.content.replace(/\n$/, ''), delimiter];
|
|
186
|
+
});
|
|
187
|
+
const payloadEnv = payloads.map((file, i) => `${file.env}="$W/payload-${i}" `).join('');
|
|
188
|
+
// One brace group: sh parses the whole script off stdin before running anything, so no child can swallow the rest of it.
|
|
189
|
+
// The work itself is written to "$W/run.sh" and runs under a host slot lock.
|
|
190
|
+
const RUN = '__GATE_REMOTE_RUN__';
|
|
191
|
+
const work = [
|
|
192
|
+
`infra() { echo "${INFRA}$1" >&2; exit 97; }`,
|
|
193
|
+
': > "$W/started"',
|
|
194
|
+
`git clone -q --shared --no-checkout ${mirror} "$W/t" >"$W/setup.log" 2>&1 || infra clone`,
|
|
195
|
+
'cd "$W/t" || infra cd',
|
|
196
|
+
`git fetch -q origin ${fetches} >>"$W/setup.log" 2>&1 || infra fetch`,
|
|
197
|
+
`git checkout -q --detach ${input.commit} >>"$W/setup.log" 2>&1 || infra checkout`,
|
|
198
|
+
...(input.mainSha ? [`git update-ref refs/remotes/origin/main ${input.mainSha} || infra origin-main`] : []),
|
|
199
|
+
'bun install >>"$W/setup.log" 2>&1 || infra bun-install',
|
|
200
|
+
...(input.installPwa ? ['(cd apps/pwa && bun install) >>"$W/setup.log" 2>&1 || infra bun-install-pwa'] : []),
|
|
201
|
+
// A scratch universe: the remote run never touches the host's operational ~/.elanous state.
|
|
202
|
+
'mkdir -p "$W/state" || infra scratch',
|
|
203
|
+
`${payloadEnv}ELANOUS_STATE_DIR="$W/state" ELANOUS_GATE_REMOTE_CHILD=1 ${input.argv.map(quote).join(' ')} </dev/null`,
|
|
204
|
+
// The marker always starts on its own line, whatever the tool's stderr ended with; the parser strips that newline.
|
|
205
|
+
`rc=$?; printf '\\n%s%s\\n' '${RC}' "$rc" >&2`,
|
|
206
|
+
'exit 0',
|
|
207
|
+
];
|
|
208
|
+
// Any argument line equal to the delimiter would end run.sh early and run the rest outside the slot lock.
|
|
209
|
+
if (work.join('\n').split('\n').includes(RUN)) throw new Error('run script contains its delimiter');
|
|
210
|
+
return [
|
|
211
|
+
'{',
|
|
212
|
+
'umask 077',
|
|
213
|
+
'PATH="$HOME/.bun/bin:/opt/homebrew/bin:$PATH"; export PATH',
|
|
214
|
+
`infra() { echo "${INFRA}$1" >&2; exit 97; }`,
|
|
215
|
+
'W=$(mktemp -d /tmp/gate-remote-XXXXXXXX) || infra mktemp',
|
|
216
|
+
'export W',
|
|
217
|
+
'trap \'rm -rf "$W"\' EXIT',
|
|
218
|
+
...payloadLines,
|
|
219
|
+
`cat > "$W/run.sh" <<'${RUN}' || infra run-script`,
|
|
220
|
+
...work,
|
|
221
|
+
RUN,
|
|
222
|
+
...hostSlotScript(input.hostCap ?? GATE_REMOTE_DEFAULTS.hostCap, input.slotDir),
|
|
223
|
+
'}',
|
|
224
|
+
'',
|
|
225
|
+
].join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function revParse(runner: GateRemoteRunner, repo: string, ref: string): string | undefined {
|
|
229
|
+
const r = runner.local('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], repo);
|
|
230
|
+
const sha = r.stdout.trim();
|
|
231
|
+
return r.rc === 0 && shaPattern.test(sha) ? sha : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function runOnRemote(opts: RemoteRunOptions, runner: GateRemoteRunner = realGateRemoteRunner): RemoteRunOutcome {
|
|
235
|
+
const started = Date.now();
|
|
236
|
+
const status = runner.local('git', ['status', '--porcelain'], opts.repo);
|
|
237
|
+
if (status.rc !== 0) return { kind: 'infra', reason: 'git-status-failed' };
|
|
238
|
+
// A remote checkout can only reproduce a commit — uncommitted or untracked files would silently not be measured.
|
|
239
|
+
if (status.stdout.trim()) return { kind: 'infra', reason: 'dirty-tree' };
|
|
240
|
+
const commit = revParse(runner, opts.repo, 'HEAD');
|
|
241
|
+
if (!commit) return { kind: 'infra', reason: 'head-unresolved' };
|
|
242
|
+
// Every check measures against merge-base(origin/main); without the local one the host would fall back to its mirror's
|
|
243
|
+
// (possibly stale) branch — not the same measurement.
|
|
244
|
+
const mainSha = revParse(runner, opts.repo, 'origin/main');
|
|
245
|
+
if (!mainSha) return { kind: 'infra', reason: 'origin-main-unresolved' };
|
|
246
|
+
const resolved = new Map<string, string>();
|
|
247
|
+
for (const ref of opts.refs ?? []) {
|
|
248
|
+
const sha = revParse(runner, opts.repo, ref);
|
|
249
|
+
if (!sha) return { kind: 'infra', reason: `ref-unresolved:${ref}` };
|
|
250
|
+
resolved.set(ref, sha);
|
|
251
|
+
}
|
|
252
|
+
const extra = [...resolved.values()];
|
|
253
|
+
let argv: string[];
|
|
254
|
+
try {
|
|
255
|
+
argv = typeof opts.argv === 'function' ? opts.argv((ref) => {
|
|
256
|
+
const sha = resolved.get(ref);
|
|
257
|
+
if (!sha) throw new Error(`ref not declared: ${ref}`);
|
|
258
|
+
return sha;
|
|
259
|
+
}) : opts.argv;
|
|
260
|
+
} catch (error) { return { kind: 'infra', reason: `argv: ${error instanceof Error ? error.message : String(error)}` }; }
|
|
261
|
+
const shas = [...new Set([commit, mainSha, ...extra])];
|
|
262
|
+
const pushTarget = `${opts.host}:${opts.mirror.startsWith('~/') ? opts.mirror.slice(2) : opts.mirror}`;
|
|
263
|
+
// Non-interactive and time-bounded: a stuck connection or auth prompt must end in a local fallback, not a hang.
|
|
264
|
+
const push = runner.local('git', ['push', '--quiet', pushTarget, ...shas.map((sha) => `${sha}:refs/elanous/gate/${sha}`)], opts.repo, {
|
|
265
|
+
timeoutMs: PUSH_TIMEOUT_MS,
|
|
266
|
+
env: { ...process.env, GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o ConnectTimeout=10', GIT_TERMINAL_PROMPT: '0' },
|
|
267
|
+
});
|
|
268
|
+
if (push.rc !== 0 || push.error) return { kind: 'infra', reason: `push-failed rc=${push.rc}: ${(push.stderr || push.error || '').trim().split('\n').slice(-2).join(' | ')}` };
|
|
269
|
+
const script = remoteScript({ mirror: opts.mirror, commit, fetchShas: shas, mainSha, argv, installPwa: opts.installPwa ?? false, payloadFiles: opts.payloadFiles, hostCap: opts.hostCap, slotDir: opts.slotDir });
|
|
270
|
+
const run = runner.ssh(opts.host, script, opts.timeoutMs ?? 120 * 60_000);
|
|
271
|
+
// The script prints "\n<marker><rc>\n": everything before that leading newline is the tool's own stderr, byte for byte.
|
|
272
|
+
const stdoutBytes = Buffer.from(run.stdout);
|
|
273
|
+
const stderrBytes = Buffer.from(run.stderr);
|
|
274
|
+
const stderrText = stderrBytes.toString('utf8');
|
|
275
|
+
const marker = stderrBytes.lastIndexOf(Buffer.from(`\n${RC}`));
|
|
276
|
+
// Busy (98) and setup failure (97) are the script's own exits, each with its marker as the last stderr line. A completed
|
|
277
|
+
// run exits 0 after the rc marker, and its stderr belongs to the tool — never scanned for markers.
|
|
278
|
+
if (!(run.rc === 0 && marker >= 0)) {
|
|
279
|
+
const last = stderrText.trimEnd().split('\n').at(-1) ?? '';
|
|
280
|
+
if (run.rc === 98 && last === BUSY) return { kind: 'busy' };
|
|
281
|
+
if (run.rc === 97 && last.startsWith(INFRA)) return { kind: 'infra', reason: `remote-${last.slice(INFRA.length).trim()}` };
|
|
282
|
+
}
|
|
283
|
+
// No marker, or ssh did not end cleanly after it (the script always exits 0 once the marker is written): output may be cut.
|
|
284
|
+
if (marker < 0) return { kind: 'infra', reason: `ssh rc=${run.rc}${run.error ? ` ${run.error}` : ''}: ${stderrText.trim().split('\n').slice(-2).join(' | ')}` };
|
|
285
|
+
if (run.rc !== 0 || run.error) return { kind: 'infra', reason: `ssh-incomplete rc=${run.rc}${run.error ? ` ${run.error}` : ''}` };
|
|
286
|
+
// The marker is the last thing the script writes: exactly "\n<marker><digits>\n". Anything else (empty, cut) is infra.
|
|
287
|
+
const tail = /^\n__GATE_REMOTE_RC=(\d{1,3})\n$/.exec(stderrBytes.subarray(marker).toString('utf8'));
|
|
288
|
+
const rc = tail ? Number(tail[1]) : NaN;
|
|
289
|
+
if (!(rc >= 0 && rc <= 255)) return { kind: 'infra', reason: 'rc-unparsed' };
|
|
290
|
+
// The tool's stderr is exactly what came before the marker line.
|
|
291
|
+
return { kind: 'ran', rc, stdout: stdoutBytes, stderr: stderrBytes.subarray(0, marker), commit, host: opts.host, ms: Date.now() - started };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** An absolute argument outside the repo names a file the remote checkout does not have — such a run is not reproducible. */
|
|
295
|
+
export function outsideRepoArgs(args: readonly string[], repo: string): string[] {
|
|
296
|
+
// The tool resolves file arguments against the repository root; anything that lands outside it (absolute or ../) is not
|
|
297
|
+
// in the remote checkout.
|
|
298
|
+
return args.filter((arg) => {
|
|
299
|
+
if (arg.startsWith('-')) return false;
|
|
300
|
+
const rel = relative(repo, resolve(repo, arg));
|
|
301
|
+
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** File arguments the remote checkout cannot reproduce: outside the repo (by string or after symlinks resolve), or not
|
|
306
|
+
* tracked at HEAD (ignored/untracked files are not in the pushed commit). `tracked` defaults to `git ls-files`. */
|
|
307
|
+
export function unreproducibleArgs(paths: readonly string[], repo: string, tracked: (path: string) => boolean = (path) =>
|
|
308
|
+
spawnSync('git', ['ls-files', '--error-unmatch', '--', path], { cwd: repo, stdio: 'ignore', maxBuffer: 1024 * 1024 }).status === 0): string[] {
|
|
309
|
+
let realRepo = repo;
|
|
310
|
+
try { realRepo = realpathSync(repo); } catch { /* keep as given */ }
|
|
311
|
+
return paths.filter((arg) => {
|
|
312
|
+
if (outsideRepoArgs([arg], repo).length) return true;
|
|
313
|
+
const abs = resolve(repo, arg);
|
|
314
|
+
if (!tracked(relative(repo, abs))) return true;
|
|
315
|
+
// Conservative: any symlink on the way (the file itself or a directory above it, inside the repo) means local —
|
|
316
|
+
// a link's target may be untracked, outside, looped or dangling, and none of that is worth reproducing remotely.
|
|
317
|
+
const parts = relative(repo, abs).split(sep);
|
|
318
|
+
let walk = realRepo;
|
|
319
|
+
for (const part of parts) {
|
|
320
|
+
walk = join(walk, part);
|
|
321
|
+
try { if (lstatSync(walk).isSymbolicLink()) return true; } catch { return false; } // missing locally: tracked() already answered
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Rewrite absolute paths under the repo to repo-relative so they mean the same file in the remote checkout. */
|
|
328
|
+
export function relativizeArgs(args: readonly string[], repo: string): string[] {
|
|
329
|
+
return args.map((arg) => {
|
|
330
|
+
if (!isAbsolute(arg)) return arg;
|
|
331
|
+
const rel = relative(repo, arg);
|
|
332
|
+
return rel && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ? rel : arg;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** For a JSON object stdout: add `host` and `remote: true` by splicing them in before the closing brace — every other byte
|
|
337
|
+
* of the tool's output (spacing, escapes, key order, trailing newline) is kept. Anything else passes through unchanged. */
|
|
338
|
+
export function annotateJsonStdout(stdout: string, host: string): string {
|
|
339
|
+
const trimmed = stdout.trim();
|
|
340
|
+
if (!trimmed.startsWith('{')) return stdout;
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
343
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return stdout;
|
|
344
|
+
} catch { return stdout; }
|
|
345
|
+
const close = stdout.lastIndexOf('}');
|
|
346
|
+
const empty = Object.keys(JSON.parse(trimmed) as object).length === 0;
|
|
347
|
+
return `${stdout.slice(0, close)}${empty ? '' : ','}"host":${JSON.stringify(host)},"remote":true${stdout.slice(close)}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface DispatchOptions {
|
|
351
|
+
tool: GateRemoteTool;
|
|
352
|
+
repo: string;
|
|
353
|
+
flags: Pick<GateRemoteFlags, 'remote' | 'local'>;
|
|
354
|
+
/** argv for the remote run (the tool re-invoked with `--local` so it never recurses). */
|
|
355
|
+
remoteArgv: RemoteArgv;
|
|
356
|
+
refs?: string[];
|
|
357
|
+
installPwa?: boolean;
|
|
358
|
+
payloadFiles?: PayloadFile[];
|
|
359
|
+
json?: boolean;
|
|
360
|
+
/** Set when this invocation cannot be reproduced remotely (e.g. self gate without --base): a remote decision becomes a
|
|
361
|
+
* logged fallback with this reason instead of a silent local run. */
|
|
362
|
+
localOnlyReason?: string;
|
|
363
|
+
runLocal: () => number | Promise<number>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface DispatchDeps {
|
|
367
|
+
runner?: GateRemoteRunner;
|
|
368
|
+
settings?: GateRemoteSettings;
|
|
369
|
+
load1?: number;
|
|
370
|
+
env?: NodeJS.ProcessEnv;
|
|
371
|
+
sleep?: (ms: number) => Promise<void>;
|
|
372
|
+
now?: () => number;
|
|
373
|
+
write?: { out: (data: string | Uint8Array) => void; err: (data: string | Uint8Array) => void };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function loadSettings(): Promise<GateRemoteSettings> {
|
|
377
|
+
try {
|
|
378
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
379
|
+
return resolveGateRemoteSettings(getUserConfig().gateRemote);
|
|
380
|
+
} catch { return resolveGateRemoteSettings(undefined); }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Decide local/remote, run, and return the tool's exit code. Infra failure ⇒ local fallback (never a test result). */
|
|
384
|
+
export async function dispatchHeavyCheck(opts: DispatchOptions, deps: DispatchDeps = {}): Promise<number> {
|
|
385
|
+
const settings = deps.settings ?? await loadSettings();
|
|
386
|
+
const env = deps.env ?? process.env;
|
|
387
|
+
const write = deps.write ?? { out: (d: string | Uint8Array) => process.stdout.write(d), err: (d: string | Uint8Array) => process.stderr.write(d) };
|
|
388
|
+
const decision = decideGateDispatch({ flags: opts.flags, settings, load1: deps.load1 ?? loadavg()[0]!, env });
|
|
389
|
+
debug.log('gate.remote', 'decision', { tool: opts.tool, mode: decision.mode, host: decision.host, reason: decision.reason, load1: decision.load1 });
|
|
390
|
+
if (decision.mode === 'local') return opts.runLocal();
|
|
391
|
+
const host = decision.host!;
|
|
392
|
+
const fallback = (reason: string) => {
|
|
393
|
+
debug.log('gate.remote', 'fallback', { tool: opts.tool, host, reason });
|
|
394
|
+
write.err(`[gate.remote] fallback to local — ${reason}\n`);
|
|
395
|
+
return opts.runLocal();
|
|
396
|
+
};
|
|
397
|
+
if (opts.localOnlyReason) return fallback(opts.localOnlyReason);
|
|
398
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
399
|
+
const now = deps.now ?? Date.now;
|
|
400
|
+
const deadline = now() + settings.slotWaitSeconds * 1000;
|
|
401
|
+
debug.log('gate.remote', 'dispatch', { tool: opts.tool, host, reason: decision.reason });
|
|
402
|
+
let outcome: RemoteRunOutcome;
|
|
403
|
+
for (;;) {
|
|
404
|
+
try {
|
|
405
|
+
outcome = runOnRemote({ repo: opts.repo, host, mirror: settings.mirror, argv: opts.remoteArgv, refs: opts.refs, installPwa: opts.installPwa, payloadFiles: opts.payloadFiles, hostCap: settings.hostCap }, deps.runner);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
outcome = { kind: 'infra', reason: `exception: ${error instanceof Error ? error.message : String(error)}` };
|
|
408
|
+
}
|
|
409
|
+
if (outcome.kind !== 'busy') break;
|
|
410
|
+
debug.log('gate.remote', 'host-busy', { tool: opts.tool, host, cap: settings.hostCap });
|
|
411
|
+
// Never start an attempt past the deadline: an expired wait means local, even if a slot would free a moment later.
|
|
412
|
+
const left = deadline - now();
|
|
413
|
+
if (left <= 0) break;
|
|
414
|
+
await sleep(Math.min(15_000, left));
|
|
415
|
+
if (now() >= deadline) break;
|
|
416
|
+
}
|
|
417
|
+
if (outcome.kind === 'busy') return fallback(`host-busy (cap ${settings.hostCap} held for ${settings.slotWaitSeconds}s)`);
|
|
418
|
+
if (outcome.kind === 'infra') return fallback(outcome.reason);
|
|
419
|
+
// Parity: the tool's own stdout/stderr/rc pass through unchanged — only a JSON stdout gains host · remote:true.
|
|
420
|
+
debug.log('gate.remote', 'result', { tool: opts.tool, host: outcome.host, commit: outcome.commit, rc: outcome.rc, ms: outcome.ms });
|
|
421
|
+
// Bytes pass through untouched; only a JSON object stdout is re-serialised (to add host · remote:true).
|
|
422
|
+
const text = opts.json ? outcome.stdout.toString('utf8') : undefined;
|
|
423
|
+
const annotated = text !== undefined ? annotateJsonStdout(text, outcome.host) : undefined;
|
|
424
|
+
write.out(annotated !== undefined && annotated !== text ? annotated : outcome.stdout);
|
|
425
|
+
write.err(outcome.stderr);
|
|
426
|
+
return outcome.rc;
|
|
427
|
+
}
|
|
@@ -20,12 +20,17 @@ function observeGoalAuthor(
|
|
|
20
20
|
import { getUserConfig } from '../user-config.js';
|
|
21
21
|
|
|
22
22
|
export type GoalAuthorGrade = 'full' | 'lite';
|
|
23
|
-
export type GoalAuthorGradeSource = 'flag' | 'config' | 'default';
|
|
23
|
+
export type GoalAuthorGradeSource = 'flag' | 'env' | 'config' | 'default';
|
|
24
24
|
export interface GoalAuthorGradeSelection { grade: GoalAuthorGrade; source: GoalAuthorGradeSource }
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Precedence flag > env (`ELANOUS_AUTHOR_GRADE`) > config > default(full). Malformed env or
|
|
28
|
+
* configuration values are ignored and keep the full pipeline. The env seam lets the Pod carry the
|
|
29
|
+
* grade without a CLI flag an older in-Pod elanous would reject (AUTHOR-LITE2-POD).
|
|
30
|
+
*/
|
|
31
|
+
export function resolveGoalAuthorGrade(flag?: GoalAuthorGrade, configured?: unknown, env: unknown = process.env.ELANOUS_AUTHOR_GRADE): GoalAuthorGradeSelection {
|
|
28
32
|
if (flag !== undefined) return { grade: flag, source: 'flag' };
|
|
33
|
+
if (env === 'lite' || env === 'full') return { grade: env, source: 'env' };
|
|
29
34
|
if (configured === 'lite' || configured === 'full') return { grade: configured, source: 'config' };
|
|
30
35
|
return { grade: 'full', source: 'default' };
|
|
31
36
|
}
|
|
@@ -129,12 +129,33 @@ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}
|
|
|
129
129
|
if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
|
|
130
130
|
verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
|
|
131
131
|
const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
|
|
132
|
+
let autoResolvedNextMd = false;
|
|
132
133
|
if (merge.status !== 0) {
|
|
133
|
-
const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp);
|
|
134
|
-
if (!unresolved) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp).split('\n').filter(Boolean);
|
|
135
|
+
if (!unresolved.length) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
|
|
136
|
+
if (unresolved.length === 1 && unresolved[0] === NEXT_MD_PATH) {
|
|
137
|
+
const stage = (n: number): string => {
|
|
138
|
+
const result = execute('git', ['show', `:${n}:${NEXT_MD_PATH}`], temp!);
|
|
139
|
+
if (result.status !== 0) throw new Error(`release/next.md stage ${n} unavailable: ${result.stderr}`);
|
|
140
|
+
return result.stdout;
|
|
141
|
+
};
|
|
142
|
+
// Stage 2 is main in this checkout; preserve the incoming PR (stage 3) and union main's additions.
|
|
143
|
+
const resolved = resolveNextMdConflict(stage(1), stage(2), stage(3));
|
|
144
|
+
if (resolved === null) {
|
|
145
|
+
verdict.verdict = 'conflict';
|
|
146
|
+
verdict.detail = 'release/next.md conflict is not append-only';
|
|
147
|
+
} else {
|
|
148
|
+
writeFileSync(join(temp, NEXT_MD_PATH), resolved);
|
|
149
|
+
run('git', ['add', '--', NEXT_MD_PATH], temp);
|
|
150
|
+
run('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'commit', '-m', 'Integrate PR for L8 shadow evaluation'], temp);
|
|
151
|
+
autoResolvedNextMd = true;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
verdict.verdict = 'conflict';
|
|
155
|
+
verdict.detail = unresolved.join('\n');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (merge.status === 0 || autoResolvedNextMd) {
|
|
138
159
|
if (verdict.tests.length) {
|
|
139
160
|
if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
|
|
140
161
|
run('bun', ['install', '--frozen-lockfile'], temp);
|
|
@@ -153,6 +174,9 @@ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}
|
|
|
153
174
|
} else {
|
|
154
175
|
verdict.verdict = 'pass';
|
|
155
176
|
}
|
|
177
|
+
if (autoResolvedNextMd) {
|
|
178
|
+
verdict.detail = verdict.detail ? `release/next.md auto-resolved; ${verdict.detail}` : 'release/next.md auto-resolved';
|
|
179
|
+
}
|
|
156
180
|
}
|
|
157
181
|
} finally {
|
|
158
182
|
try {
|
|
@@ -1078,6 +1078,9 @@ export interface SelfImplementSeams {
|
|
|
1078
1078
|
openPr: (opts: { title: string; body: string; head: string; base?: string; draft?: boolean; labels?: string[]; cwd: string }) => Promise<{ url: string; number: number }>;
|
|
1079
1079
|
/** PR 생성 뒤 런의 라운드 대화를 게시한다. 실패는 PR 개설을 되돌리지 않는다. */
|
|
1080
1080
|
postPrComment?: (opts: { number: number; body: string; cwd: string }) => Promise<void>;
|
|
1081
|
+
/** Read and edit the single marked status comment; missing seams fall back to individual comments. */
|
|
1082
|
+
findPrComment?: (opts: { number: number; marker: string; cwd: string; comments?: unknown }) => Promise<{ id: number; body: string } | undefined>;
|
|
1083
|
+
editPrComment?: (opts: { id: number; body: string; cwd: string }) => Promise<void>;
|
|
1081
1084
|
addPrLabel?: (opts: { number: number; label: string; cwd: string }) => Promise<void>;
|
|
1082
1085
|
readPrFiles?: (opts: { number: number; cwd: string; base?: string }) => Promise<string[]>;
|
|
1083
1086
|
/** PR의 base/head SHA와 관측된 PR base를 한 응답에서 고정한다. 미주입·실패·빈 SHA면 검사 대상을 증명할 수 없어 fail-closed로 PR을 열어 둔다. */
|
|
@@ -3185,6 +3188,20 @@ function autoMergeEnabled(opts: Pick<SelfImplementOptions, 'completion' | 'autoM
|
|
|
3185
3188
|
return completionIntentOf(opts) === 'auto-merge';
|
|
3186
3189
|
}
|
|
3187
3190
|
|
|
3191
|
+
/**
|
|
3192
|
+
* FREEZE-POD — a Pod child never merges its own PR. The host's landing freeze is invisible inside the Pod, so an
|
|
3193
|
+
* auto-merge run there always stops at merge-ready and the launching host re-gates and merges (or holds it).
|
|
3194
|
+
* Local (non-Pod) runs are returned unchanged.
|
|
3195
|
+
*/
|
|
3196
|
+
export function routePodMergeToHost<T extends Pick<SelfImplementOptions, 'completion' | 'autoMerge' | 'mergeByHost'>>(
|
|
3197
|
+
opts: T, env: NodeJS.ProcessEnv = process.env,
|
|
3198
|
+
): { opts: T; reason: 'requested' | 'pod-child-auto-merge' | null } {
|
|
3199
|
+
const podChild = env.ELANOUS_SUBSTRATE === 'pod' && Boolean(env.ELANOUS_POD_NAME?.trim());
|
|
3200
|
+
if (!podChild || !autoMergeEnabled(opts)) return { opts, reason: null };
|
|
3201
|
+
if (opts.mergeByHost === true) return { opts, reason: 'requested' };
|
|
3202
|
+
return { opts: { ...opts, mergeByHost: true }, reason: 'pod-child-auto-merge' };
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3188
3205
|
const SELF_IMPLEMENT_LOG_CATEGORY = 'self-implement';
|
|
3189
3206
|
|
|
3190
3207
|
/** Emits a run-aware auxiliary log without admitting it to the run ledger. */
|
|
@@ -3468,6 +3485,12 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
|
|
|
3468
3485
|
}
|
|
3469
3486
|
const identity = resolveRunIdentity({ explicit: opts.runId });
|
|
3470
3487
|
const { runId } = identity;
|
|
3488
|
+
const podMerge = routePodMergeToHost(opts);
|
|
3489
|
+
if (podMerge.reason) {
|
|
3490
|
+
opts = podMerge.opts;
|
|
3491
|
+
try { debug.log('self-implement.pod', 'merge-routed-to-host', { job: process.env.ELANOUS_POD_NAME ?? null, runId, reason: podMerge.reason }); }
|
|
3492
|
+
catch { /* observation must not block the run */ }
|
|
3493
|
+
}
|
|
3471
3494
|
const branch = opts.branchName ?? plannedSelfImplBranch(opts.feature, opts.goalId, runId);
|
|
3472
3495
|
try {
|
|
3473
3496
|
debug.log('self-implement.branch', 'planned', {
|
|
@@ -5444,9 +5467,11 @@ async function runSelfImplementInner(
|
|
|
5444
5467
|
const omission = `\n\n[${omittedChars} characters omitted; original body length ${body.length}]`;
|
|
5445
5468
|
prCommentBuffer.push(`${body.slice(0, MAX_PR_COMMENT_BODY_CHARS - omission.length)}${omission}`);
|
|
5446
5469
|
};
|
|
5447
|
-
|
|
5470
|
+
let statusCommentCreateAttempted = false;
|
|
5471
|
+
const flushPrComments = async (pr: { number: number }, salvageAction?: 'launched'): Promise<void> => {
|
|
5448
5472
|
if (!prCommentBuffer.length) return;
|
|
5449
5473
|
if (!s.postPrComment) {
|
|
5474
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'seam-missing', buffered: prCommentBuffer.length });
|
|
5450
5475
|
observe('pr.comment.seam-missing', { number: pr.number, buffered: prCommentBuffer.length }, { level: 'warn' });
|
|
5451
5476
|
return;
|
|
5452
5477
|
}
|
|
@@ -5454,24 +5479,39 @@ async function runSelfImplementInner(
|
|
|
5454
5479
|
if (s.findPrComment && s.editPrComment) {
|
|
5455
5480
|
try {
|
|
5456
5481
|
const existing = await s.findPrComment({ number: pr.number, marker, cwd: wt.path });
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5482
|
+
if (!existing && statusCommentCreateAttempted) {
|
|
5483
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'individual-fallback', reason: 'status-creation-uncertain', buffered: prCommentBuffer.length });
|
|
5484
|
+
} else {
|
|
5485
|
+
const history = prCommentBuffer.join('\n\n');
|
|
5486
|
+
const body = existing
|
|
5487
|
+
? existing.body.includes('</details>')
|
|
5488
|
+
? existing.body.replace(/<\/details>(?![\s\S]*<\/details>)/, `\n\n${history}\n</details>`)
|
|
5489
|
+
: `${existing.body}\n\n<details>\n<summary>Round history</summary>\n\n${history}\n</details>`
|
|
5490
|
+
: `${marker}\n<details>\n<summary>Round history</summary>\n\n${history}\n</details>`;
|
|
5491
|
+
if (existing) await s.editPrComment({ id: existing.id, body, cwd: wt.path });
|
|
5492
|
+
else {
|
|
5493
|
+
statusCommentCreateAttempted = true;
|
|
5494
|
+
await s.postPrComment({ number: pr.number, body, cwd: wt.path });
|
|
5495
|
+
}
|
|
5496
|
+
observe('pr.comment.mode', { number: pr.number, mode: existing ? 'status-updated' : 'status-created', buffered: prCommentBuffer.length });
|
|
5497
|
+
prCommentBuffer.length = 0;
|
|
5498
|
+
}
|
|
5465
5499
|
} catch (error) {
|
|
5466
5500
|
// Unknown lookup/edit outcome is not proof of absence: never create a second status comment.
|
|
5467
5501
|
observe('pr.comment.status-failed', { number: pr.number, error: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
|
|
5502
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'individual-fallback', reason: 'status-failed', buffered: prCommentBuffer.length });
|
|
5468
5503
|
}
|
|
5504
|
+
} else {
|
|
5505
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'individual-fallback', reason: 'status-seam-missing', buffered: prCommentBuffer.length });
|
|
5469
5506
|
}
|
|
5470
|
-
|
|
5507
|
+
if (!prCommentBuffer.length) return;
|
|
5508
|
+
const pending = prCommentBuffer.splice(0);
|
|
5509
|
+
for (const body of pending) {
|
|
5471
5510
|
try {
|
|
5472
5511
|
await s.postPrComment({ number: pr.number, body, cwd: wt.path });
|
|
5473
5512
|
} catch (error) {
|
|
5474
|
-
|
|
5513
|
+
// Posting may have succeeded remotely even if the response failed; do not retry an uncertain comment.
|
|
5514
|
+
observe('pr.comment.post-failed', { number: pr.number, ...(salvageAction && body.includes('Rework salvage status: launched.') ? { salvageAction } : {}), error: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
|
|
5475
5515
|
}
|
|
5476
5516
|
}
|
|
5477
5517
|
};
|
|
@@ -5822,7 +5862,13 @@ async function runSelfImplementInner(
|
|
|
5822
5862
|
};
|
|
5823
5863
|
const postSalvageStatus = async (pr: { number: number } | undefined, action: 'launched' | 'parked', reason?: string): Promise<void> => {
|
|
5824
5864
|
if (!pr) return;
|
|
5865
|
+
if (action === 'launched') {
|
|
5866
|
+
bufferPrComment(round, 'author', 'Rework salvage status: launched.', reason ? [`reason: ${reason}`] : []);
|
|
5867
|
+
await flushPrComments(pr, 'launched');
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5825
5870
|
if (!s.postPrComment) {
|
|
5871
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'seam-missing', salvageAction: action });
|
|
5826
5872
|
observe('pr.comment.seam-missing', { number: pr.number, salvageAction: action }, { level: 'warn' });
|
|
5827
5873
|
return;
|
|
5828
5874
|
}
|
|
@@ -5831,6 +5877,7 @@ async function runSelfImplementInner(
|
|
|
5831
5877
|
`Rework salvage status: ${action}.`,
|
|
5832
5878
|
...(reason ? ['', `- reason: ${reason}`] : []),
|
|
5833
5879
|
].join('\n');
|
|
5880
|
+
observe('pr.comment.mode', { number: pr.number, mode: 'individual', salvageAction: action });
|
|
5834
5881
|
try {
|
|
5835
5882
|
await s.postPrComment({ number: pr.number, body, cwd: wt.path });
|
|
5836
5883
|
} catch (error) {
|
|
@@ -6000,7 +6047,7 @@ async function runSelfImplementInner(
|
|
|
6000
6047
|
if (!s.launchReworkSalvage) throw new Error('launch seam unavailable');
|
|
6001
6048
|
await s.launchReworkSalvage({ goalFile: opts.goalFile!, base: wt.branch, salvageAttempt: (opts.salvageAttempt ?? 0) + 1 });
|
|
6002
6049
|
observe('rework-salvage', { action: 'launched', ...launchObservation });
|
|
6003
|
-
await postSalvageStatus(pr, 'launched');
|
|
6050
|
+
await postSalvageStatus(pr, 'launched', environmentReason ?? (hardCapBlockedExtend ? 'hard-cap-extend' : undefined));
|
|
6004
6051
|
return 'launched';
|
|
6005
6052
|
} catch (error) {
|
|
6006
6053
|
const reason = 'launch-failed';
|