elanous 0.2.25 → 0.2.28
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/{1454-37fd16f479f5554a.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-24453f6a302816b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-21050928a697ed7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +119 -7
- package/scripts/coord-msg-bridge.ts +209 -0
- package/scripts/coord-post.sh +40 -7
- package/scripts/coord-requests-bridge.ts +10 -5
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/expose-rubric-suggest.ts +1 -1
- package/scripts/measure-tsc-affected-roots.ts +10 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/export-check-node.ts +8 -5
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +10 -3
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +32 -2
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/review-observation.ts +9 -2
- package/src/agent-substrate/self-review-cli.ts +62 -16
- package/src/boot/daemon-tools/self-implement.ts +72 -4
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +30 -3
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/control-cli.ts +29 -0
- package/src/cli/doctor-cli.ts +90 -8
- package/src/cli/msg-cli.ts +177 -11
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +133 -18
- package/src/cli/pr-land-overlap-evidence.ts +18 -10
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +34 -5
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/seat-cli.ts +121 -0
- package/src/cli/self-utterances-cli.ts +140 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +528 -31
- package/src/cli/usage-cli.ts +9 -4
- package/src/context-card/cell.ts +40 -0
- package/src/dashboard/boot-timing.ts +35 -0
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +94 -27
- package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
- package/src/debug/log.ts +1 -1
- package/src/domains/morning-report.ts +3 -1
- package/src/domains/release-tool.ts +29 -12
- package/src/domains/surface-events.ts +16 -0
- package/src/elanous-config-dir.ts +2 -2
- package/src/fleet/host-doctor.ts +73 -2
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +104 -15
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +68 -10
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/harness/harness-stop.ts +11 -7
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +69 -9
- package/src/instance/current.ts +2 -6
- package/src/instance/resolve.ts +5 -6
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/llm/openai-sampling-fields.ts +89 -0
- package/src/llm/runtime-model-family.ts +61 -0
- package/src/llm/zai-coding-plan.ts +27 -1
- package/src/llm.ts +13 -89
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/machines/machine-ledger.ts +31 -3
- package/src/maturity/cli-maturity.ts +13 -3
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +149 -17
- package/src/ops/overview-collect.ts +3 -2
- package/src/ops/overview-owner.ts +29 -0
- package/src/patents/ledger.ts +128 -0
- package/src/plugin-hooks/shell-hook.ts +17 -2
- package/src/plugin-missions/evaluator-runner.ts +18 -2
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/plugins/install/plugin-install.ts +11 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/prompt-enhance/enhance.ts +11 -0
- package/src/release-loop/checklist.ts +52 -4
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-loop.ts +5 -1
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/draft-sweep.ts +23 -14
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-dev/orchestrate.ts +6 -2
- package/src/self-dev/run-supervisor.ts +35 -1
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +5 -28
- package/src/self-implement/goal-command-execution.ts +26 -0
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +5 -11
- package/src/self-implement/pwa-import-graph.ts +194 -7
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/self-implement-runtime.ts +126 -21
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/session/index.ts +14 -4
- package/src/session-runtime/index.ts +55 -11
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +105 -19
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +56 -17
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +321 -25
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +66 -2
- package/src/task-agent/shadow.ts +150 -29
- package/src/task-agent/task-hand.ts +133 -8
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
- package/src/telegram.ts +30 -1
- package/src/tool-runtime/tool-search-runtime.ts +25 -0
- package/src/tui.ts +20 -1
- package/src/user-config.ts +11 -60
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
package/src/cli/msg-cli.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { ensureAuthToken } from '../auth/acp-token.js';
|
|
3
|
+
import { canonicalSeatId, openMsgStore, openMsgStoreReadOnly, type Message, type MsgStore } from '../msg/msg-store.js';
|
|
4
|
+
import { startMsgSseServer } from '../msg/msg-sse-server.js';
|
|
3
5
|
|
|
4
6
|
export interface MsgCliDeps {
|
|
5
7
|
openStore?: () => MsgStore;
|
|
8
|
+
openReadStore?: () => MsgStore;
|
|
6
9
|
out?: Pick<Console, 'log' | 'error'>;
|
|
7
10
|
readStdin?: () => Promise<string>;
|
|
8
11
|
sleep?: (ms: number) => Promise<void>;
|
|
9
12
|
setExitCode?: (code: number) => void;
|
|
10
13
|
signal?: AbortSignal;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
authToken?: () => string;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
type CommonOptions = { json?: boolean };
|
|
@@ -33,6 +38,7 @@ function formatMessage(message: Message): string {
|
|
|
33
38
|
/** Register the durable, recipient-addressed CLI. Reading never advances the acknowledgement cursor. */
|
|
34
39
|
export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): void {
|
|
35
40
|
const open = deps.openStore ?? (() => openMsgStore());
|
|
41
|
+
const openRead = deps.openReadStore ?? (() => openMsgStoreReadOnly());
|
|
36
42
|
const out = deps.out ?? console;
|
|
37
43
|
const readStdin = deps.readStdin ?? (() => Bun.stdin.text());
|
|
38
44
|
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
@@ -57,13 +63,20 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
msg.command('post [body]').description('Post a message; use - or omit body to read stdin')
|
|
60
|
-
.option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>')
|
|
61
|
-
.
|
|
66
|
+
.option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>')
|
|
67
|
+
.option('--reply-to <id>', 'point this reply at a question message ID')
|
|
68
|
+
.option('--origin-key <key>', 'deduplicate a channel mirror by coord:<commentId>:<recipient>').option('--json')
|
|
69
|
+
.action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string; replyTo?: string; originKey?: string }) => run(async () => {
|
|
62
70
|
const from = seat(opts.from, '--from');
|
|
63
71
|
const to = seat(opts.to, '--to');
|
|
64
72
|
if (body !== undefined && body !== '-' && opts.body !== undefined) throw new Error('provide body once (argument or --body)');
|
|
65
73
|
const text = opts.body ?? (body !== undefined && body !== '-' ? body : await readStdin());
|
|
66
|
-
const
|
|
74
|
+
const envelope = { from, to, body: text,
|
|
75
|
+
...(opts.kind === undefined ? {} : { kind: opts.kind }),
|
|
76
|
+
...(opts.replyTo === undefined ? {} : { replyTo: integer(opts.replyTo, '--reply-to', 0, 1, Number.MAX_SAFE_INTEGER) }) };
|
|
77
|
+
const posted = withStore(store => opts.originKey === undefined
|
|
78
|
+
? store.post(envelope)
|
|
79
|
+
: store.appendWithProvenance(envelope, opts.originKey));
|
|
67
80
|
emit(posted, opts.json, () => out.log(formatMessage(posted)));
|
|
68
81
|
})());
|
|
69
82
|
|
|
@@ -95,25 +108,32 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
95
108
|
.option('--to <seat>').option('--json')
|
|
96
109
|
.action((opts: CommonOptions & { to?: string }) => run(() => {
|
|
97
110
|
const to = opts.to === undefined ? undefined : seat(opts.to, '--to');
|
|
98
|
-
const rows = withStore(store =>
|
|
111
|
+
const rows = withStore(store => {
|
|
112
|
+
if (to === undefined) return store.unread();
|
|
113
|
+
const count = store.unreadCount(to);
|
|
114
|
+
return count ? [{ recipient: to, count }] : [];
|
|
115
|
+
});
|
|
99
116
|
emit(rows, opts.json, () => {
|
|
100
117
|
if (!rows.length) out.log('No unread messages.');
|
|
101
118
|
for (const row of rows) out.log(`${row.recipient}: ${row.count} unread`);
|
|
102
119
|
});
|
|
103
120
|
})());
|
|
104
121
|
|
|
105
|
-
msg.command('watch').description('
|
|
122
|
+
msg.command('watch').description('Watch durable messages; SSE accelerates delivery and polling covers outages')
|
|
106
123
|
.option('--to <seat>').option('--after <id>', 'exclusive message ID (default: acknowledged cursor)')
|
|
124
|
+
.option('--since-ack', 'start from the recipient seat’s acknowledged cursor')
|
|
107
125
|
.option('--interval <ms>', 'poll interval in milliseconds', '1000')
|
|
126
|
+
.option('--sse <url>', 'post office base URL (fall back to ledger polling on disconnect)')
|
|
108
127
|
.option('--once', 'poll once and exit').option('--json')
|
|
109
|
-
.action((opts: RecipientOptions & { interval?: string; once?: boolean }) => run(async () => {
|
|
128
|
+
.action((opts: RecipientOptions & { interval?: string; once?: boolean; sinceAck?: boolean; sse?: string }) => run(async () => {
|
|
129
|
+
if (opts.sinceAck && opts.after !== undefined) throw new Error('--since-ack cannot be used with --after');
|
|
110
130
|
const to = seat(opts.to, '--to');
|
|
111
131
|
const interval = integer(opts.interval, '--interval', 1000, 1, 60000);
|
|
112
132
|
const after = opts.after === undefined ? undefined : integer(opts.after, '--after', 0, 0, Number.MAX_SAFE_INTEGER);
|
|
113
133
|
const store = open();
|
|
114
134
|
try {
|
|
115
135
|
let cursor = after ?? store.getCursor(to);
|
|
116
|
-
|
|
136
|
+
const poll = () => {
|
|
117
137
|
let messages: Message[];
|
|
118
138
|
do {
|
|
119
139
|
messages = store.list(to, cursor, 1000);
|
|
@@ -122,12 +142,158 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
122
142
|
cursor = message.id;
|
|
123
143
|
}
|
|
124
144
|
} while (messages.length === 1000 && !deps.signal?.aborted);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
};
|
|
146
|
+
if (!opts.sse || opts.once) {
|
|
147
|
+
do {
|
|
148
|
+
poll();
|
|
149
|
+
if (opts.once || deps.signal?.aborted) break;
|
|
150
|
+
await sleep(interval);
|
|
151
|
+
} while (!deps.signal?.aborted);
|
|
152
|
+
} else {
|
|
153
|
+
const url = new URL('/stream', opts.sse);
|
|
154
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('--sse requires an HTTP URL');
|
|
155
|
+
if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
156
|
+
throw new Error('--sse requires HTTPS outside localhost');
|
|
157
|
+
url.searchParams.set('to', to);
|
|
158
|
+
const token = (deps.authToken ?? (() => ensureAuthToken().token))();
|
|
159
|
+
let outage = false;
|
|
160
|
+
while (!deps.signal?.aborted) {
|
|
161
|
+
poll();
|
|
162
|
+
url.searchParams.set('since', String(cursor));
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const onAbort = () => controller.abort();
|
|
165
|
+
deps.signal?.addEventListener('abort', onAbort, { once: true });
|
|
166
|
+
try {
|
|
167
|
+
let connectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
168
|
+
const response = await Promise.race([
|
|
169
|
+
(deps.fetch ?? fetch)(url, { headers: {
|
|
170
|
+
Authorization: `Bearer ${token}`, 'Last-Event-ID': String(cursor),
|
|
171
|
+
}, signal: controller.signal }),
|
|
172
|
+
new Promise<never>((_, reject) => { connectTimer = setTimeout(() => {
|
|
173
|
+
controller.abort();
|
|
174
|
+
reject(new Error('SSE connect timeout'));
|
|
175
|
+
}, Math.max(1000, Math.min(5000, interval * 2))); }),
|
|
176
|
+
]).finally(() => clearTimeout(connectTimer));
|
|
177
|
+
if (response.status === 401) throw new Error('SSE unauthorized (401)');
|
|
178
|
+
if (!response.ok || !response.body) throw new Error(`SSE unavailable (${response.status})`);
|
|
179
|
+
outage = false;
|
|
180
|
+
const reader = response.body.getReader();
|
|
181
|
+
const decoder = new TextDecoder();
|
|
182
|
+
let pending = '';
|
|
183
|
+
while (!deps.signal?.aborted) {
|
|
184
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
185
|
+
const chunk = await Promise.race([
|
|
186
|
+
reader.read(),
|
|
187
|
+
new Promise<never>((_, reject) => { idleTimer = setTimeout(() => {
|
|
188
|
+
controller.abort();
|
|
189
|
+
reject(new Error('SSE idle timeout'));
|
|
190
|
+
}, Math.max(1000, Math.min(5000, interval * 2))); }),
|
|
191
|
+
]).finally(() => clearTimeout(idleTimer));
|
|
192
|
+
if (chunk.done) break;
|
|
193
|
+
pending += decoder.decode(chunk.value, { stream: true });
|
|
194
|
+
pending = pending.replace(/\r\n/g, '\n');
|
|
195
|
+
if (pending.length > 1_000_000) throw new Error('SSE event exceeds limit');
|
|
196
|
+
let boundary: number;
|
|
197
|
+
while ((boundary = pending.indexOf('\n\n')) >= 0) {
|
|
198
|
+
const event = pending.slice(0, boundary);
|
|
199
|
+
pending = pending.slice(boundary + 2);
|
|
200
|
+
if (event.split('\n').some((line) => line.startsWith('id: '))) poll();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof Error && error.message === 'SSE unauthorized (401)') throw error;
|
|
205
|
+
if (!outage && !deps.signal?.aborted) out.error(`msg: SSE disconnected; polling ledger (${error instanceof Error ? error.message : String(error)})`);
|
|
206
|
+
outage = true;
|
|
207
|
+
} finally { deps.signal?.removeEventListener('abort', onAbort); controller.abort(); }
|
|
208
|
+
if (deps.signal?.aborted) break;
|
|
209
|
+
poll();
|
|
210
|
+
await sleep(interval);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
128
213
|
} finally { store.close(); }
|
|
129
214
|
})());
|
|
130
215
|
|
|
216
|
+
msg.command('ask <recipient> <question>').description('Post a question and wait for its first reply')
|
|
217
|
+
.option('--from <seat>', 'asking seat (required)').option('--wait <seconds>', 'seconds to wait for a reply (default 60)', '60')
|
|
218
|
+
.option('--json')
|
|
219
|
+
.action((recipient: string, question: string, opts: CommonOptions & { from?: string; wait?: string }) => run(async () => {
|
|
220
|
+
const from = seat(opts.from, '--from');
|
|
221
|
+
const to = canonicalSeatId(recipient);
|
|
222
|
+
const waitSeconds = integer(opts.wait, '--wait', 60, 0, 86400);
|
|
223
|
+
const interrupt = deps.signal === undefined ? new AbortController() : undefined;
|
|
224
|
+
const signal = deps.signal ?? interrupt!.signal;
|
|
225
|
+
const onInterrupt = () => interrupt?.abort();
|
|
226
|
+
if (interrupt) {
|
|
227
|
+
process.once('SIGINT', onInterrupt);
|
|
228
|
+
process.once('SIGTERM', onInterrupt);
|
|
229
|
+
}
|
|
230
|
+
let store: MsgStore | undefined;
|
|
231
|
+
try {
|
|
232
|
+
const asked = withStore(ledger => ledger.post({ from, to, body: question }));
|
|
233
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
234
|
+
let reply: Message | undefined;
|
|
235
|
+
store = open();
|
|
236
|
+
do {
|
|
237
|
+
reply = store.listReplies(asked.id, 1)[0];
|
|
238
|
+
if (reply !== undefined || signal.aborted || Date.now() >= deadline) break;
|
|
239
|
+
await sleep(1000);
|
|
240
|
+
} while (true);
|
|
241
|
+
if (reply === undefined && signal.aborted) throw new Error(`ask interrupted after posting #${asked.id}`);
|
|
242
|
+
if (reply === undefined) throw new Error(`no reply to #${asked.id} within ${waitSeconds}s`);
|
|
243
|
+
emit(reply, opts.json, () => out.log(formatMessage(reply)));
|
|
244
|
+
} finally {
|
|
245
|
+
store?.close();
|
|
246
|
+
if (interrupt) {
|
|
247
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
248
|
+
process.removeListener('SIGTERM', onInterrupt);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})());
|
|
252
|
+
|
|
253
|
+
msg.command('thread <id>').description('Show one question and its replies in time order')
|
|
254
|
+
.option('--json')
|
|
255
|
+
.action((id: string, opts: CommonOptions) => run(() => {
|
|
256
|
+
const target = integer(id, 'message ID', 0, 1, Number.MAX_SAFE_INTEGER);
|
|
257
|
+
const thread = withStore(store => {
|
|
258
|
+
const question = store.getMessage(target);
|
|
259
|
+
if (question === null) throw new Error(`message not found: #${target}`);
|
|
260
|
+
// Keep paging until a short page so a thread longer than one store page is never truncated.
|
|
261
|
+
const replies: Message[] = [];
|
|
262
|
+
let after = 0;
|
|
263
|
+
let page: Message[];
|
|
264
|
+
do {
|
|
265
|
+
page = store.listReplies(target, 100, after);
|
|
266
|
+
replies.push(...page);
|
|
267
|
+
after = page.at(-1)?.id ?? after;
|
|
268
|
+
} while (page.length === 100);
|
|
269
|
+
return { question, replies };
|
|
270
|
+
});
|
|
271
|
+
emit(thread, opts.json, () => {
|
|
272
|
+
out.log(formatMessage(thread.question));
|
|
273
|
+
for (const reply of thread.replies) out.log(formatMessage(reply));
|
|
274
|
+
});
|
|
275
|
+
})());
|
|
276
|
+
|
|
277
|
+
msg.command('serve').description('Run the independent, read-only post office SSE service')
|
|
278
|
+
.option('--sse', 'serve GET /stream')
|
|
279
|
+
.option('--port <port>', 'port (or ELANOUS_MSG_SSE_PORT)')
|
|
280
|
+
.option('--host <host>', 'bind address', '127.0.0.1')
|
|
281
|
+
.action((opts: { sse?: boolean; port?: string; host?: string }) => run(() => {
|
|
282
|
+
if (!opts.sse) throw new Error('serve requires --sse');
|
|
283
|
+
const rawPort = opts.port ?? process.env.ELANOUS_MSG_SSE_PORT;
|
|
284
|
+
if (rawPort === undefined) throw new Error('--port or ELANOUS_MSG_SSE_PORT is required');
|
|
285
|
+
const port = integer(rawPort, '--port', 0, 0, 65535);
|
|
286
|
+
const store = openRead();
|
|
287
|
+
try {
|
|
288
|
+
const service = startMsgSseServer({ store, token: (deps.authToken ?? (() => ensureAuthToken().token))(),
|
|
289
|
+
port, hostname: opts.host });
|
|
290
|
+
out.log(`msg SSE listening on ${opts.host}:${service.port}`);
|
|
291
|
+
const stop = () => { service.stop(); store.close(); };
|
|
292
|
+
process.once('SIGINT', stop);
|
|
293
|
+
process.once('SIGTERM', stop);
|
|
294
|
+
} catch (error) { store.close(); throw error; }
|
|
295
|
+
})());
|
|
296
|
+
|
|
131
297
|
// Commander reports option/argument errors before action handlers run. Scope their exit code
|
|
132
298
|
// to msg so unrelated commands keep their own error and help behavior.
|
|
133
299
|
for (const command of [msg, ...msg.commands]) {
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
6
6
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
7
7
|
import { userConfigPath } from '../user-config.js';
|
|
8
8
|
import { debug, redactSecretText } from '../debug/log.js';
|
|
9
|
-
import { addMarket, ensureInternalMarketIndex, listMarkets, MarketFetchError, updateMarket } from '../plugins/install/market-fetch.js';
|
|
9
|
+
import { addMarket, codexPluginPackagePath, ensureInternalMarketIndex, listMarkets, MarketFetchError, updateMarket } from '../plugins/install/market-fetch.js';
|
|
10
|
+
import { readCodexPluginList } from '../acp/codex-plugins.js';
|
|
10
11
|
import { emitDecision } from '../live/detail-switch.js';
|
|
11
12
|
import { OFFICIAL_INDEX_KEYS } from '../market/official-keys.js';
|
|
12
13
|
import { installPlugin, listInstalledPlugins, PluginInstallError, removePlugin, type InstallEvent } from '../plugins/install/plugin-install.js';
|
|
@@ -118,15 +119,22 @@ export function registerPluginCommands(program: Command): void {
|
|
|
118
119
|
}
|
|
119
120
|
});
|
|
120
121
|
plugin.command('add <spec>').description('Install a local path, pinned git plugin or signed market plugin')
|
|
122
|
+
.option('--from-codex', 'Import an installed Codex plugin by name@marketplace')
|
|
121
123
|
.option('--yes', 'Accept requested capabilities')
|
|
122
124
|
.option('--json', 'NDJSON progress events')
|
|
123
125
|
.option('--allow-unsigned', 'Allow unsigned marketplace indices')
|
|
124
126
|
.option('--refresh', 'Refresh the signed marketplace index before installing')
|
|
125
|
-
.action(async (spec: string, opts: { yes?: boolean; json?: boolean; allowUnsigned?: boolean; refresh?: boolean }) => {
|
|
127
|
+
.action(async (spec: string, opts: { yes?: boolean; json?: boolean; allowUnsigned?: boolean; refresh?: boolean; fromCodex?: boolean }) => {
|
|
126
128
|
try {
|
|
127
129
|
const root = elanousStateRoot();
|
|
128
130
|
const nodeCount = { value: 0 };
|
|
129
|
-
|
|
131
|
+
let source = spec;
|
|
132
|
+
if (opts.fromCodex) {
|
|
133
|
+
const match = /^([a-z0-9][a-z0-9-]{1,39})@([a-z0-9][a-z0-9-]{1,39})$/.exec(spec);
|
|
134
|
+
if (!match) throw new PluginInstallError('io', 'Codex import requires name@marketplace');
|
|
135
|
+
source = codexPluginPackagePath(await readCodexPluginList(), match[1]!, match[2]!);
|
|
136
|
+
}
|
|
137
|
+
await installPlugin(source, {
|
|
130
138
|
root, marketDir: join(root, 'plugins', 'markets'), trustedKeys: keys(),
|
|
131
139
|
yes: opts.yes, allowUnsigned: opts.allowUnsigned, refresh: opts.refresh,
|
|
132
140
|
onEvent: event => outputEvent(event, !!opts.json, nodeCount),
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -58,7 +58,9 @@ import { parseReleaseNoteSection } from '../release-loop/release-note.js';
|
|
|
58
58
|
import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
|
|
59
59
|
import { admitLandingMerge } from '../self-implement/frozen-merges.js';
|
|
60
60
|
import { verifyAncestorAutoPass, verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
|
|
61
|
+
import { observePrLandRefusal } from './pr-land-refusal.js';
|
|
61
62
|
import { taskAgentStatePath } from '../task-agent/task-hand.js';
|
|
63
|
+
import { PR_LABEL_NAMES } from '../github/pr-labels.js';
|
|
62
64
|
import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
|
|
63
65
|
|
|
64
66
|
export { branchLineageSlug, findSiblingPrs };
|
|
@@ -1143,8 +1145,36 @@ export function runPrGranularity(opts: PrGranularityOpts = {}, deps: PrLandDeps
|
|
|
1143
1145
|
const GITHUB_MERGEABLE = new Set(['MERGEABLE', 'CONFLICTING', 'UNKNOWN']);
|
|
1144
1146
|
const GITHUB_MERGE_STATE = new Set(['BEHIND', 'BLOCKED', 'CLEAN', 'DIRTY', 'DRAFT', 'HAS_HOOKS', 'UNKNOWN', 'UNSTABLE']);
|
|
1145
1147
|
|
|
1148
|
+
/** Read-only identity check shared by pinned landing and its dry-run. Later landing gates still run only on a real landing. */
|
|
1149
|
+
function inspectPinnedPr(run: CmdRunner, cwd: string, pr: number, expectedHead: string) {
|
|
1150
|
+
const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,headRefName,state,isDraft,url'], { cwd });
|
|
1151
|
+
let value: { headRefOid?: string; headRefName?: string; state?: string; isDraft?: boolean; url?: string } = {};
|
|
1152
|
+
try {
|
|
1153
|
+
if (viewed.ok) {
|
|
1154
|
+
const parsed: unknown = JSON.parse(viewed.out);
|
|
1155
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) value = parsed as typeof value;
|
|
1156
|
+
}
|
|
1157
|
+
} catch { /* unreadable is not approval */ }
|
|
1158
|
+
const local = run('git', ['rev-parse', 'HEAD'], { cwd });
|
|
1159
|
+
const dirty = run('git', ['status', '--porcelain'], { cwd });
|
|
1160
|
+
const reasons: string[] = [];
|
|
1161
|
+
if (value.state !== 'OPEN') reasons.push('PR is not OPEN or its state is unavailable');
|
|
1162
|
+
if (value.isDraft !== false) reasons.push(value.isDraft === true ? 'PR is draft' : 'PR ready status is unavailable');
|
|
1163
|
+
if (value.headRefOid?.toLowerCase() !== expectedHead.toLowerCase()) reasons.push('PR head differs from --expected-head or is unavailable');
|
|
1164
|
+
if (!value.url?.endsWith(`/pull/${pr}`) || !value.headRefName) reasons.push('PR identity or head branch is unavailable');
|
|
1165
|
+
if (!local.ok || local.out.trim().toLowerCase() !== expectedHead.toLowerCase()) reasons.push('local HEAD differs from --expected-head or is unavailable');
|
|
1166
|
+
if (!dirty.ok || dirty.out.trim()) reasons.push(dirty.ok ? 'local worktree is dirty' : 'local worktree status is unavailable');
|
|
1167
|
+
return { ok: reasons.length === 0, reasons, branch: value.headRefName, url: value.url };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1146
1170
|
export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): Promise<number> {
|
|
1147
1171
|
const out = deps.out ?? liveOut;
|
|
1172
|
+
// First slice: observe pinned identity/branch, overlap decision, and GitHub merge refusals.
|
|
1173
|
+
// Option syntax and prerequisite gate errors do not describe a merge recovery edge.
|
|
1174
|
+
const refuse = (message: string, context: { isDraft?: boolean } = {}): void => {
|
|
1175
|
+
out.error(message);
|
|
1176
|
+
observePrLandRefusal(message, context);
|
|
1177
|
+
};
|
|
1148
1178
|
if (opts.title !== undefined && opts.titleFile !== undefined) {
|
|
1149
1179
|
out.error('✗ title: --title과 --title-file은 함께 사용할 수 없습니다.');
|
|
1150
1180
|
return 1;
|
|
@@ -1195,6 +1225,84 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1195
1225
|
const cwd = opts.cwd ?? process.cwd();
|
|
1196
1226
|
const run = deps.run ?? defaultCmdRunner;
|
|
1197
1227
|
const pinned = opts.pr !== undefined || opts.expectedHead !== undefined;
|
|
1228
|
+
if (opts.dryRun && opts.pr !== undefined && opts.expectedHead === undefined) {
|
|
1229
|
+
if (opts.overlapEvidence !== undefined || opts.hold) {
|
|
1230
|
+
out.error('✗ dry-run: --overlap-evidence 와 --hold 는 번호 지정 읽기 전용 미리보기에서 사용할 수 없습니다.');
|
|
1231
|
+
return 1;
|
|
1232
|
+
}
|
|
1233
|
+
const pr = Number(opts.pr);
|
|
1234
|
+
if (!/^[1-9]\d*$/.test(opts.pr) || !Number.isSafeInteger(pr)) {
|
|
1235
|
+
out.error('✗ dry-run: --pr 에는 양의 정수를 지정해야 합니다.');
|
|
1236
|
+
return 1;
|
|
1237
|
+
}
|
|
1238
|
+
const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,state,headRefName,isDraft,mergeable,mergeStateStatus,url'], { cwd });
|
|
1239
|
+
if (!viewed.ok) {
|
|
1240
|
+
out.error(`✗ dry-run: PR #${pr} 를 조회하지 못했습니다 — 존재하지 않거나 GitHub 조회가 실패했습니다.`);
|
|
1241
|
+
return 1;
|
|
1242
|
+
}
|
|
1243
|
+
let value: { headRefOid?: string; state?: string; headRefName?: string; isDraft?: boolean; mergeable?: string; mergeStateStatus?: string; url?: string };
|
|
1244
|
+
try {
|
|
1245
|
+
const parsed: unknown = JSON.parse(viewed.out);
|
|
1246
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
|
|
1247
|
+
value = parsed as typeof value;
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
out.error(`✗ dry-run: PR #${pr} 조회 결과를 읽을 수 없습니다.`);
|
|
1251
|
+
return 1;
|
|
1252
|
+
}
|
|
1253
|
+
if (typeof value.headRefOid !== 'string' || !/^[a-f0-9]{40}$/i.test(value.headRefOid) || value.state !== 'OPEN') {
|
|
1254
|
+
out.error(`✗ dry-run: PR #${pr} 가 열려 있지 않거나 head 정보가 없습니다 (state=${value.state ?? 'unmeasured'}).`);
|
|
1255
|
+
return 1;
|
|
1256
|
+
}
|
|
1257
|
+
out.log(`PR #${pr} · head ${value.headRefOid} · state ${value.state}${value.headRefName ? ` · branch ${value.headRefName}` : ''}`);
|
|
1258
|
+
out.log('Landing plan (read-only preview):');
|
|
1259
|
+
out.log(`- GitHub PR state/head: PASS (${value.state}, ${value.headRefOid})`);
|
|
1260
|
+
if (value.isDraft === true) out.log('- Draft status: GitHub reports DRAFT; landing eligibility UNMEASURED.');
|
|
1261
|
+
else if (value.isDraft === false) out.log('- Draft status: observed — ready for review.');
|
|
1262
|
+
else if (value.isDraft !== false) out.log('- Draft status: UNMEASURED');
|
|
1263
|
+
if (value.mergeable === 'CONFLICTING') out.log('- Warning: GitHub reports merge conflicts; landing eligibility UNMEASURED.');
|
|
1264
|
+
else if (value.mergeable === 'MERGEABLE') out.log('- Mergeability: GitHub reports MERGEABLE; repository landing gates remain UNMEASURED.');
|
|
1265
|
+
else out.log(`- Mergeability: UNMEASURED${value.mergeable ? ` (${value.mergeable})` : ''}`);
|
|
1266
|
+
if (value.mergeStateStatus === 'BLOCKED' || value.mergeStateStatus === 'DIRTY' || value.mergeStateStatus === 'DRAFT') {
|
|
1267
|
+
out.log(`- Warning: GitHub merge state is ${value.mergeStateStatus}; landing eligibility UNMEASURED.`);
|
|
1268
|
+
} else if (value.mergeStateStatus && value.mergeStateStatus !== 'CLEAN') {
|
|
1269
|
+
out.log(`- GitHub merge state: ${value.mergeStateStatus} (additional requirements may apply; landing eligibility UNMEASURED).`);
|
|
1270
|
+
}
|
|
1271
|
+
out.log('- Local HEAD/worktree, branch/base, and same-branch guard: UNMEASURED — local git is not run');
|
|
1272
|
+
out.log('- Changed-file scope, active-run files, untracked files, plan artifacts, and deletion advisory: UNMEASURED — local files are not read');
|
|
1273
|
+
out.log('- Typecheck, isolation, mock-module restore, model-hardcode, daemon-port, test-interference, and global checks: UNMEASURED — gates are not run');
|
|
1274
|
+
out.log('- Public export/import, public leak, and docs CLI checks: UNMEASURED — gates are not run');
|
|
1275
|
+
out.log('- Android, iOS, and PWA gates: CONDITIONAL / UNMEASURED — changed-file applicability and builds are not checked');
|
|
1276
|
+
out.log('- Freeze, PR base/bench guards, sibling PRs, recent landings, overlap, and approval: UNMEASURED — not queried or approved');
|
|
1277
|
+
out.log('- GitHub draft/mergeability status: observed above; this preview does not approve or merge the PR.');
|
|
1278
|
+
out.log('- Mutations (ready/edit/create/merge, git commit/push, worktree): NOT RUN');
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
if (opts.dryRun && pinned) {
|
|
1282
|
+
const pr = Number(opts.pr);
|
|
1283
|
+
if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '')) {
|
|
1284
|
+
out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold');
|
|
1285
|
+
return 1;
|
|
1286
|
+
}
|
|
1287
|
+
if (opts.hold) {
|
|
1288
|
+
out.error('✗ dry-run: --hold 는 머리 고정 읽기 전용 예행과 함께 사용할 수 없습니다.');
|
|
1289
|
+
return 1;
|
|
1290
|
+
}
|
|
1291
|
+
if (opts.overlapEvidence !== undefined) {
|
|
1292
|
+
out.error('✗ dry-run: --overlap-evidence 는 머리 고정 읽기 전용 예행에서 사용할 수 없습니다.');
|
|
1293
|
+
return 1;
|
|
1294
|
+
}
|
|
1295
|
+
const inspected = inspectPinnedPr(run, cwd, pr, opts.expectedHead!);
|
|
1296
|
+
const branch = (deps.currentBranch ?? liveCurrentBranch)(cwd);
|
|
1297
|
+
const reasons = [...inspected.reasons];
|
|
1298
|
+
if (!branch || branch !== inspected.branch) reasons.push('current branch differs from the PR head branch or is unavailable');
|
|
1299
|
+
out.log(`[dry-run] pinned PR #${pr} · expected head ${opts.expectedHead}`);
|
|
1300
|
+
for (const reason of reasons) out.log(`[dry-run] ✗ preflight: ${reason}`);
|
|
1301
|
+
if (reasons.includes('PR is draft')) out.log(`[dry-run] 선행: eln gh pr ready ${pr}`);
|
|
1302
|
+
if (reasons.length === 0) out.log('[dry-run] ✓ preflight: PR OPEN non-draft, matching PR/local HEAD and branch, clean worktree');
|
|
1303
|
+
out.log('[dry-run] Remaining landing gates, freeze, overlap and merge: UNMEASURED; no PR or git state changed.');
|
|
1304
|
+
return reasons.length === 0 ? 0 : 1;
|
|
1305
|
+
}
|
|
1198
1306
|
if (opts.overlapEvidence !== undefined && (!pinned || !opts.overlapEvidence.trim())) {
|
|
1199
1307
|
out.error('✗ overlap-evidence: --overlap-evidence <file> 는 --pr ⊕ --expected-head 머리 고정 착지에서만 쓴다.');
|
|
1200
1308
|
return 1;
|
|
@@ -1203,30 +1311,26 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1203
1311
|
let pinnedUrl: string | undefined;
|
|
1204
1312
|
if (pinned) {
|
|
1205
1313
|
const pr = Number(opts.pr);
|
|
1206
|
-
if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '') || opts.hold
|
|
1207
|
-
out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold
|
|
1314
|
+
if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '') || opts.hold) {
|
|
1315
|
+
out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold');
|
|
1208
1316
|
return 1;
|
|
1209
1317
|
}
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
if (value.state !== 'OPEN' || value.isDraft !== false || value.headRefOid?.toLowerCase() !== opts.expectedHead!.toLowerCase()
|
|
1216
|
-
|| !value.url?.endsWith(`/pull/${pr}`) || !value.headRefName || !local.ok || local.out.trim().toLowerCase() !== opts.expectedHead!.toLowerCase()
|
|
1217
|
-
|| !dirty.ok || dirty.out.trim()) {
|
|
1218
|
-
out.error('✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required');
|
|
1318
|
+
const inspected = inspectPinnedPr(run, cwd, pr, opts.expectedHead!);
|
|
1319
|
+
if (!inspected.ok) {
|
|
1320
|
+
refuse('✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required', {
|
|
1321
|
+
isDraft: inspected.reasons.includes('PR is draft'),
|
|
1322
|
+
});
|
|
1219
1323
|
return 1;
|
|
1220
1324
|
}
|
|
1221
|
-
pinnedBranch =
|
|
1222
|
-
pinnedUrl =
|
|
1325
|
+
pinnedBranch = inspected.branch;
|
|
1326
|
+
pinnedUrl = inspected.url;
|
|
1223
1327
|
}
|
|
1224
1328
|
const resolveBase = deps.resolveBase ?? resolveDeliverableBase;
|
|
1225
1329
|
const branch = (deps.currentBranch ?? liveCurrentBranch)(cwd);
|
|
1226
1330
|
if (!branch || (pinned && branch !== pinnedBranch)) {
|
|
1227
1331
|
const detail = pinned ? 'PR head branch와 현재 브랜치가 다릅니다.' : '현재 브랜치를 해석하지 못했습니다.';
|
|
1228
1332
|
record('branch', false);
|
|
1229
|
-
|
|
1333
|
+
refuse(`✗ branch: ${detail}`);
|
|
1230
1334
|
return 1;
|
|
1231
1335
|
}
|
|
1232
1336
|
record('branch', true);
|
|
@@ -1611,6 +1715,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1611
1715
|
const hold = !!opts.hold || overlapDecision?.outcome === 'user-hold';
|
|
1612
1716
|
// TA-LAND-OVERLAP — 비대화형 «승인 불가»만 증거로 대신한다(사람의 hold 는 증거로 뒤집지 않는다) · 증거가 없거나 어긋나면 종전 거부 그대로.
|
|
1613
1717
|
let overlapEvidenceAccepted = false;
|
|
1718
|
+
let ancestorResyncNeeded = false;
|
|
1614
1719
|
if (pinned && overlapDecision?.outcome === 'unavailable' && opts.overlapEvidence !== undefined) {
|
|
1615
1720
|
const ref = resolveRemoteBranchRef(run, cwd, base);
|
|
1616
1721
|
const verdict = verifyOverlapEvidence({
|
|
@@ -1625,7 +1730,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1625
1730
|
? `✓ overlap-evidence: base-advanced-disjoint — gate base ${verdict.gateBase.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} 까지 ${verdict.windowCommits}개 커밋이 PR 파일을 안 건드린다 · review pass|warn must-fix 0 (${verdict.reviewResult})`
|
|
1626
1731
|
: `✓ overlap-evidence: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · gate passed on that base · review pass must-fix 0 (${verdict.reviewResult})`);
|
|
1627
1732
|
}
|
|
1628
|
-
else
|
|
1733
|
+
else refuse(`✗ overlap-evidence: ${verdict.reason}`);
|
|
1629
1734
|
} else if (pinned && overlapDecision?.outcome === 'unavailable') {
|
|
1630
1735
|
// 증거 파일이 없을 때 — 겹친 착지가 전부 머리의 조상이고 표준 자리의 리뷰가 pass|warn ⊕ must-fix 0 이면 통과(아니면 종전 거부).
|
|
1631
1736
|
const verdict = verifyAncestorAutoPass({
|
|
@@ -1637,9 +1742,19 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1637
1742
|
if (verdict.ok) {
|
|
1638
1743
|
out.log(`✓ overlap-evidence: already-ancestor-auto — 겹친 최근 착지 ${overlappingLandings.length}개가 전부 head ${opts.expectedHead!.slice(0, 12)} 의 조상 · review pass|warn must-fix 0 (${verdict.reviewResult})`);
|
|
1639
1744
|
}
|
|
1640
|
-
else
|
|
1745
|
+
else {
|
|
1746
|
+
refuse(`✗ ancestor-auto: ${verdict.reason}`);
|
|
1747
|
+
ancestorResyncNeeded = verdict.reason.startsWith('ancestor-auto-not-ancestor:');
|
|
1748
|
+
}
|
|
1641
1749
|
}
|
|
1642
1750
|
if (pinned && overlapDecision && overlapDecision.outcome !== 'user-merge' && !overlapEvidenceAccepted) {
|
|
1751
|
+
// A clean GitHub PR can still be behind a recently landed overlapping head. Preserve the cue
|
|
1752
|
+
// across hosts; the current steward tick acts only if it can also prove local run ownership.
|
|
1753
|
+
if (ancestorResyncNeeded && !opts.dryRun) {
|
|
1754
|
+
const marked = run('gh', ['pr', 'edit', String(opts.pr), '--add-label', PR_LABEL_NAMES.needsRebase], { cwd });
|
|
1755
|
+
record('resync-marker', marked.ok, { pr: Number(opts.pr), reason: 'ancestor-auto-not-ancestor' });
|
|
1756
|
+
if (!marked.ok) out.error('⚠ resync-marker: needs-rebase 라벨을 남기지 못했습니다');
|
|
1757
|
+
}
|
|
1643
1758
|
out.error('✗ expected-head: overlap merge approval unavailable for the reviewed head');
|
|
1644
1759
|
return 1;
|
|
1645
1760
|
}
|
|
@@ -1700,7 +1815,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1700
1815
|
if (opts.hold) {
|
|
1701
1816
|
out.log(`[dry-run] ${existing.url ? '기존 PR을 갱신' : 'PR을 생성'}하고 --hold 로 병합하지 않고 멈춥니다.`);
|
|
1702
1817
|
} else {
|
|
1703
|
-
out.log(`[dry-run] ${existing.url ? '기존 PR을 갱신하고 ready 상태를 보장' : 'PR을 생성하고 ready 상태를 보장'}한 뒤 squash merge 합니다.`);
|
|
1818
|
+
out.log(`[dry-run] 머리 고정 없는 일반 착지: ${existing.url ? '기존 PR을 갱신하고 ready 상태를 보장' : 'PR을 생성하고 ready 상태를 보장'}한 뒤 squash merge 합니다.`);
|
|
1704
1819
|
}
|
|
1705
1820
|
return 0;
|
|
1706
1821
|
}
|
|
@@ -1874,7 +1989,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1874
1989
|
...(mergeable ? { mergeable } : {}),
|
|
1875
1990
|
...(mergeStateStatus ? { mergeStateStatus } : {}),
|
|
1876
1991
|
});
|
|
1877
|
-
|
|
1992
|
+
refuse(`✗ merge: ${detail}`);
|
|
1878
1993
|
if (mergeable === 'CONFLICTING') {
|
|
1879
1994
|
out.error(' ↳ 충돌 — 이 브랜치가 기준 브랜치의 최근 착지와 같은 줄을 고쳤다. `bun bin/elanous.mjs git fetch origin` → `bun bin/elanous.mjs git rebase origin/<기준>` 로 풀고 다시 pr land.');
|
|
1880
1995
|
}
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* ⓐ `gate.baseCommit` 이 고정 머리의 조상 ⊕ 지금 끝의 조상이고 ⓑ 창 `gate.baseCommit..지금 끝`의 커밋이 «하나도»
|
|
12
12
|
* PR 의 바뀐 파일(머리 vs merge-base)을 안 건드리면 통과(관측 reason `base-advanced-disjoint`).
|
|
13
13
|
* 겹치면 `base-stale-overlap`(REJUDGE 가 재게이트를 청한다) · 못 재면 `base-window-unmeasured` · ⓐ 가 아니면 종전 사유.
|
|
14
|
-
* ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<
|
|
15
|
-
* 같은 PR
|
|
14
|
+
* ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head접두 10~40>.json`(TA 가 받는 `self review --json` stdout)이
|
|
15
|
+
* 파일명에서 같은 PR·머리 접두이고 내용에서 전체 PR·머리가 정확히 같으며 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
|
|
16
16
|
* (자동 병합 canAuto · TA-LAND-WARN-MUSTFIX0 과 같은 규칙 — must-fix 를 못 셌으면 거부).
|
|
17
17
|
* ⓐ 이미 조상(TC #26019 실측) — 겹친 최근 착지 커밋이 «전부» 고정 머리의 조상이면 그 겹침은 이미 그 머리의 게이트·리뷰에
|
|
18
18
|
* 반영됐다 ⇒ ①② 없이 ③ 만으로 통과(관측 reason `already-ancestor`). 하나라도 조상이 아니면 ①②③ 전부.
|
|
19
|
-
* ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<
|
|
19
|
+
* ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head접두 10~40>.json` 만으로 잰다
|
|
20
20
|
* (관측 reason `already-ancestor-auto`). 조상이 아닌 착지가 하나라도 있거나 리뷰 결과가 없으면 거부.
|
|
21
21
|
* 하나라도 못 재거나 어긋나면 거부 — 호출부는 종전 거부를 그대로 낸다(fail-closed).
|
|
22
22
|
* - 관측: `pr.land` · `overlap-evidence` (decision · reason · 잰 값).
|
|
@@ -33,7 +33,7 @@ interface OverlapEvidenceFile {
|
|
|
33
33
|
head: string;
|
|
34
34
|
/** ①② 경로에서만 필수 — 겹친 착지가 전부 조상(already-ancestor)이면 생략해도 된다. */
|
|
35
35
|
gate?: { head: string; baseCommit: string; passed: boolean };
|
|
36
|
-
/** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<
|
|
36
|
+
/** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<head접두 10~40>.json`); 내용의 전체 head도 확인한다. */
|
|
37
37
|
reviewResult: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -65,9 +65,14 @@ const COMMIT_MARK = '\u0001';
|
|
|
65
65
|
|
|
66
66
|
function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
|
|
67
67
|
if (typeof e.reviewResult !== 'string' || !e.reviewResult) return { ok: false, reason: 'review-missing', baseTip };
|
|
68
|
-
// TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<
|
|
68
|
+
// TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head접두 10~40>.json` · reviewResultPath)만; 내용의 전체 head도 확인한다.
|
|
69
|
+
const resultName = basename(e.reviewResult);
|
|
70
|
+
const resultMatch = /^([A-Za-z0-9][A-Za-z0-9._-]*)-(\d+)-([a-f0-9]{10,40})\.json$/i.exec(resultName);
|
|
69
71
|
if (basename(dirname(e.reviewResult)) !== 'review-results'
|
|
70
|
-
|| !
|
|
72
|
+
|| !resultMatch
|
|
73
|
+
|| Number(resultMatch[2]) !== input.pr
|
|
74
|
+
|| resultMatch[3]!.length < 10
|
|
75
|
+
|| !head.startsWith(resultMatch[3]!.toLowerCase())) {
|
|
71
76
|
return { ok: false, reason: 'review-path-not-for-this-pr-head', baseTip };
|
|
72
77
|
}
|
|
73
78
|
let text: string;
|
|
@@ -206,7 +211,7 @@ export interface VerifyAncestorAutoInput {
|
|
|
206
211
|
cwd: string;
|
|
207
212
|
run: CmdRunner;
|
|
208
213
|
readFile: (path: string) => string;
|
|
209
|
-
/** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<
|
|
214
|
+
/** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head접두 10~40>.json`만 후보로 보고 내용의 전체 head를 확인한다. */
|
|
210
215
|
reviewResultsDir: string;
|
|
211
216
|
listDir: (dir: string) => readonly string[];
|
|
212
217
|
/** 겹침 관문이 찾은 «겹친» 최근 착지 커밋들. */
|
|
@@ -222,10 +227,13 @@ function checkAncestorAuto(input: VerifyAncestorAutoInput): AncestorAutoVerdict
|
|
|
222
227
|
if (notAncestor !== undefined) return { ok: false, reason: `ancestor-auto-not-ancestor: ${notAncestor.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}` };
|
|
223
228
|
let names: readonly string[];
|
|
224
229
|
try { names = input.listDir(input.reviewResultsDir); } catch { return { ok: false, reason: 'review-missing' }; }
|
|
225
|
-
const
|
|
226
|
-
// 같은
|
|
230
|
+
const filename = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]*-${input.pr}-([a-f0-9]{10,40})\\.json$`, 'i');
|
|
231
|
+
// 같은 PR·머리 접두의 리뷰가 여럿이면 «전부» 통과해야 한다 — 내용 headCommit 이 정확히 맞아야 승인한다.
|
|
227
232
|
let accepted: string | undefined;
|
|
228
|
-
for (const name of [...names].filter((n) =>
|
|
233
|
+
for (const name of [...names].filter((n) => {
|
|
234
|
+
const match = filename.exec(n);
|
|
235
|
+
return Boolean(match && match[1]!.length >= 10 && head.startsWith(match[1]!.toLowerCase()));
|
|
236
|
+
}).sort()) {
|
|
229
237
|
const review = checkReview(input, { reviewResult: join(input.reviewResultsDir, name) }, head);
|
|
230
238
|
if (!review.ok) return { ok: false, reason: review.reason };
|
|
231
239
|
accepted = review.reviewResult;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readJourneyEdge, recordJourneyEdge } from '../self-dev/graph-journey-route.js';
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
|
|
4
|
+
/** The five outcomes of an observed `pr land` refusal. This function never retries a landing. */
|
|
5
|
+
export type PrLandRefusal = 'draft' | 'head-branch' | 'not-ancestor' | 'review-missing' | 'other';
|
|
6
|
+
|
|
7
|
+
export function classifyPrLandRefusal(message: string, context: { isDraft?: boolean } = {}): PrLandRefusal {
|
|
8
|
+
if (context.isDraft === true && message.startsWith('✗ expected-head: PR OPEN non-draft')) return 'draft';
|
|
9
|
+
if (message.startsWith('✗ branch: PR head branch와 현재 브랜치가 다릅니다.')) return 'head-branch';
|
|
10
|
+
if (message.startsWith('✗ ancestor-auto: ancestor-auto-not-ancestor:')) return 'not-ancestor';
|
|
11
|
+
if (/^✗ (?:ancestor-auto|overlap-evidence): (?:review-missing|review-not-reviewed|review-error|review-no-result|review-unreadable|review-pr-mismatch|review-head-mismatch|review-path-not-for-this-pr-head|review-verdict-null|review-must-fix-unknown)(?:\b|$)/.test(message)) return 'review-missing';
|
|
12
|
+
return 'other';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read the declared recovery edge and observe the refusal. No recovery command runs here. */
|
|
16
|
+
export function observePrLandRefusal(message: string, context: { isDraft?: boolean } = {}): void {
|
|
17
|
+
const route = readJourneyEdge('merge', classifyPrLandRefusal(message, context));
|
|
18
|
+
if (route.to !== null) recordJourneyEdge(route, route.to, 'pr-land-refused; recovery-execution=none');
|
|
19
|
+
else debug.log('self-implement', 'graph-edge-unresolved', { from: 'merge', label: route.label, reason: 'pr-land-refused; recovery-execution=none' });
|
|
20
|
+
}
|