elanous 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +104 -11
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +92 -13
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +399 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +178 -12
- package/src/harness/harness-pod-dispatch.ts +36 -55
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +133 -0
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +16 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +168 -13
- package/src/intake-plane/digest.ts +85 -28
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +172 -21
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +107 -25
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +157 -8
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +9 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +6 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +59 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { releaseGitDiffPaths, releasePrFilePaths } from '../self-dev/release-path-guard.js';
|
|
5
|
+
|
|
6
|
+
const defaultExecFile = promisify(execFileCb);
|
|
7
|
+
const GIT_TIMEOUT = 20_000;
|
|
8
|
+
|
|
9
|
+
export type AppApiExecFile = (
|
|
10
|
+
file: string,
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
opts: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer?: number; env?: NodeJS.ProcessEnv },
|
|
13
|
+
) => Promise<{ stdout: string; stderr?: string }>;
|
|
14
|
+
|
|
15
|
+
let execFileImpl: AppApiExecFile = (file, args, opts) => defaultExecFile(file, [...args], opts);
|
|
16
|
+
|
|
17
|
+
/** Tests inject the `gh` runner. Production keeps `execFile`. */
|
|
18
|
+
export function setAppApiExecFileForTest(impl: AppApiExecFile | undefined): void {
|
|
19
|
+
execFileImpl = impl ?? ((file, args, opts) => defaultExecFile(file, [...args], opts));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** App-token `gh` calls only. Caller names are fixed strings; the token value is never logged. */
|
|
23
|
+
const GH_APP_CALLERS = {
|
|
24
|
+
'post-pr-comment-history': 'postPrComment',
|
|
25
|
+
'post-pr-comment': 'postPrComment',
|
|
26
|
+
'find-pr-comment': 'findPrComment',
|
|
27
|
+
'edit-pr-comment': 'editPrComment',
|
|
28
|
+
'add-pr-label-list': 'addPrLabel',
|
|
29
|
+
'add-pr-label-create': 'addPrLabel',
|
|
30
|
+
'add-pr-label': 'addPrLabel',
|
|
31
|
+
'read-pr-files': 'readPrFiles',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export type GhAppCaller = keyof typeof GH_APP_CALLERS;
|
|
35
|
+
|
|
36
|
+
export const APP_API_RATE_LIMIT_RETRY_CAP_MS = 15 * 60_000;
|
|
37
|
+
|
|
38
|
+
export class AppApiRateLimitError extends Error {
|
|
39
|
+
readonly rateLimited = true as const;
|
|
40
|
+
readonly status: 403 | 429;
|
|
41
|
+
readonly resetAtMs: number | undefined;
|
|
42
|
+
constructor(message: string, status: 403 | 429, resetAtMs: number | undefined) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = 'AppApiRateLimitError';
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.resetAtMs = resetAtMs;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isAppApiRateLimitFailure(status: number | undefined, text: string, remaining?: string): boolean {
|
|
51
|
+
if (remaining === '0') return true;
|
|
52
|
+
if (status !== undefined && status !== 403 && status !== 429) return false;
|
|
53
|
+
return /API rate limit exceeded|rate limit/i.test(text) && (status === 403 || status === 429 || status === undefined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ghHeader(stderr: string, name: string): string | undefined {
|
|
57
|
+
const match = new RegExp(`^${name}:\\s*(.*)$`, 'im').exec(stderr);
|
|
58
|
+
return match?.[1]?.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ghStatusCode(stderr: string, error: unknown): number | undefined {
|
|
62
|
+
const fromHeader = ghHeader(stderr, 'HTTP') ?? ghHeader(stderr, 'status');
|
|
63
|
+
const headerCode = fromHeader ? Number.parseInt(fromHeader, 10) : NaN;
|
|
64
|
+
if (Number.isFinite(headerCode)) return headerCode;
|
|
65
|
+
const text = `${stderr}\n${error instanceof Error ? error.message : String(error ?? '')}`;
|
|
66
|
+
const match = /\b(403|429)\b/.exec(text);
|
|
67
|
+
return match ? Number(match[1]) : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Wait until X-RateLimit-Reset, never longer than 15 minutes. Returns false when the reset is already past or unreadable. */
|
|
71
|
+
export async function waitForAppApiRateLimitReset(
|
|
72
|
+
resetAtMs: number | undefined,
|
|
73
|
+
now = Date.now(),
|
|
74
|
+
sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
if (resetAtMs === undefined || !Number.isFinite(resetAtMs)) return false;
|
|
77
|
+
const waitMs = Math.min(Math.max(0, resetAtMs - now), APP_API_RATE_LIMIT_RETRY_CAP_MS);
|
|
78
|
+
if (waitMs <= 0) return false;
|
|
79
|
+
await sleep(waitMs);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runGhApp(
|
|
84
|
+
caller: GhAppCaller,
|
|
85
|
+
args: readonly string[],
|
|
86
|
+
opts: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer?: number; env: NodeJS.ProcessEnv },
|
|
87
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
88
|
+
let stdout = '';
|
|
89
|
+
let stderr = '';
|
|
90
|
+
let status: number | undefined;
|
|
91
|
+
let error: unknown;
|
|
92
|
+
try {
|
|
93
|
+
const result = await execFileImpl('gh', args, opts);
|
|
94
|
+
stdout = result.stdout;
|
|
95
|
+
stderr = result.stderr ?? '';
|
|
96
|
+
status = 200;
|
|
97
|
+
} catch (caught) {
|
|
98
|
+
error = caught;
|
|
99
|
+
const failed = caught as { stdout?: string; stderr?: string };
|
|
100
|
+
stdout = typeof failed.stdout === 'string' ? failed.stdout : '';
|
|
101
|
+
stderr = typeof failed.stderr === 'string' ? failed.stderr : '';
|
|
102
|
+
status = ghStatusCode(stderr, caught);
|
|
103
|
+
}
|
|
104
|
+
const remaining = ghHeader(stderr, 'X-RateLimit-Remaining');
|
|
105
|
+
debug.log('github.app-api', 'call', {
|
|
106
|
+
caller: GH_APP_CALLERS[caller],
|
|
107
|
+
endpoint: args.filter((arg) => !arg.startsWith('-')).at(-1) ?? 'gh',
|
|
108
|
+
status: status ?? 0,
|
|
109
|
+
remaining: remaining ?? '',
|
|
110
|
+
});
|
|
111
|
+
if (error) {
|
|
112
|
+
const text = `${stderr}\n${error instanceof Error ? error.message : String(error)}`;
|
|
113
|
+
if (isAppApiRateLimitFailure(status, text, remaining)) {
|
|
114
|
+
const reset = ghHeader(stderr, 'X-RateLimit-Reset');
|
|
115
|
+
const resetSeconds = reset ? Number(reset) : NaN;
|
|
116
|
+
throw new AppApiRateLimitError(
|
|
117
|
+
text.trim() || 'API rate limit exceeded',
|
|
118
|
+
status === 429 ? 429 : 403,
|
|
119
|
+
Number.isFinite(resetSeconds) ? resetSeconds * 1000 : undefined,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return { stdout, stderr };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function readPrFilesWithRateLimitFallback(input: {
|
|
128
|
+
number: number;
|
|
129
|
+
cwd: string;
|
|
130
|
+
base?: string;
|
|
131
|
+
env: NodeJS.ProcessEnv;
|
|
132
|
+
sleep?: (ms: number) => Promise<void>;
|
|
133
|
+
now?: () => number;
|
|
134
|
+
}): Promise<string[]> {
|
|
135
|
+
const env = input.env;
|
|
136
|
+
const endpoint = [
|
|
137
|
+
'api', '--paginate', '--slurp', '--method', 'GET', '-f', 'per_page=100',
|
|
138
|
+
`repos/{owner}/{repo}/pulls/${input.number}/files`,
|
|
139
|
+
];
|
|
140
|
+
const ghOpts = { cwd: input.cwd, encoding: 'utf8' as const, timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env };
|
|
141
|
+
const fromApi = async () => releasePrFilePaths(JSON.parse((await runGhApp('read-pr-files', endpoint, ghOpts)).stdout));
|
|
142
|
+
const fromGit = async (): Promise<string[]> => {
|
|
143
|
+
const base = input.base ?? 'origin/main';
|
|
144
|
+
const { stdout } = await execFileImpl('git', ['diff', '--find-renames', '--name-status', '-z', `${base}...HEAD`], {
|
|
145
|
+
cwd: input.cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024,
|
|
146
|
+
});
|
|
147
|
+
return releaseGitDiffPaths(stdout);
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
return await fromApi();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!(error instanceof AppApiRateLimitError)) throw error;
|
|
153
|
+
try {
|
|
154
|
+
return await fromGit();
|
|
155
|
+
} catch (gitError) {
|
|
156
|
+
const waited = await waitForAppApiRateLimitReset(error.resetAtMs, input.now?.() ?? Date.now(), input.sleep);
|
|
157
|
+
if (!waited) throw error;
|
|
158
|
+
try {
|
|
159
|
+
return await fromApi();
|
|
160
|
+
} catch (retryError) {
|
|
161
|
+
if (retryError instanceof AppApiRateLimitError) throw retryError;
|
|
162
|
+
throw gitError;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { UsageSnapshot } from '../budget/types.js';
|
|
|
9
9
|
import { debug } from '../debug/log.js';
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_BUDGET_GATE_MAX_USED_PERCENT,
|
|
12
|
+
LAUNCH_GROK_USED_PERCENT_CAP,
|
|
12
13
|
type BudgetGateMaxUsedPercent,
|
|
13
14
|
type BudgetGateOnShortfall,
|
|
14
15
|
} from '../user-config.js';
|
|
@@ -137,16 +138,17 @@ function slotUsable(
|
|
|
137
138
|
return { usable: measuredUsable || unmeasured, reason: formatCodexReason(input.codexCandidates, cap), ...(unmeasured ? { unmeasured: true } : {}) };
|
|
138
139
|
}
|
|
139
140
|
if (provider === GROK_PROVIDER) {
|
|
140
|
-
const
|
|
141
|
+
const configured = capFor(input.maxUsedPercent, GROK_PROVIDER);
|
|
142
|
+
// 발사 관문은 `elanous usage` 와 같은 주간 사용률에 80% 상한을 실제로 적용한다.
|
|
143
|
+
// «못 쟀다»(undefined)는 통과가 아니다. decideBudget 은 설정 상한을 그대로 쓴다.
|
|
144
|
+
const cap = allowUnmeasured ? LAUNCH_GROK_USED_PERCENT_CAP : configured;
|
|
141
145
|
const known = typeof input.grokUsedPercent === 'number' && Number.isFinite(input.grokUsedPercent);
|
|
142
|
-
const
|
|
143
|
-
const usable = unmeasured || (cap === undefined
|
|
146
|
+
const usable = cap === undefined
|
|
144
147
|
? known
|
|
145
|
-
: known && (input.grokUsedPercent as number) < cap
|
|
148
|
+
: known && (input.grokUsedPercent as number) < cap;
|
|
146
149
|
return {
|
|
147
150
|
usable,
|
|
148
151
|
reason: formatGrokReason(input.grokUsedPercent, cap),
|
|
149
|
-
...(unmeasured ? { unmeasured: true } : {}),
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
return { usable: false, reason: `${provider}: 예산 판정 없음` };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// FOLLOWUP-LOOP① — after a harness PR merges, leftover findings become one
|
|
2
|
+
// follow-up goal draft. Default mode is shadow: record only. `live` also
|
|
3
|
+
// enqueues through the same function as `harness queue add`.
|
|
4
|
+
//
|
|
5
|
+
// The test process injects every store. Nothing here opens the operational
|
|
6
|
+
// ledger or queue unless the caller passes those seams.
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { addHarnessQueue, type QueueSeat } from '../harness/harness-queue.js';
|
|
12
|
+
import { debug } from '../debug/log.js';
|
|
13
|
+
import { withFileLockSync } from '../storage/file-lock.js';
|
|
14
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
15
|
+
import { devVersion, listChecklist, parseOwner } from '../release-loop/checklist.js';
|
|
16
|
+
import { getUserConfig } from '../user-config.js';
|
|
17
|
+
|
|
18
|
+
export const FOLLOW_UP_DEPTH_LIMIT = 3;
|
|
19
|
+
export const FOLLOW_UP_OP_CARD_REQUIRED = 'OP 카드 필요';
|
|
20
|
+
|
|
21
|
+
const REMAINING_SECTION = '남은 조각';
|
|
22
|
+
const SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
23
|
+
|
|
24
|
+
export type FollowUpMode = 'shadow' | 'live';
|
|
25
|
+
|
|
26
|
+
export interface FollowUpDraftRecord {
|
|
27
|
+
prNumber: number;
|
|
28
|
+
cellId: string;
|
|
29
|
+
depth: number;
|
|
30
|
+
draftHash: string;
|
|
31
|
+
kind: 'draft' | 'op-card-required';
|
|
32
|
+
originalAskFirstLine: string;
|
|
33
|
+
remainings: readonly string[];
|
|
34
|
+
draft?: string;
|
|
35
|
+
note?: string;
|
|
36
|
+
queued: boolean;
|
|
37
|
+
/** live 대기열 넣기가 실패했을 때의 이유(초안은 그대로 남는다). */
|
|
38
|
+
enqueueError?: string;
|
|
39
|
+
seat?: QueueSeat;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FollowUpMergeInput {
|
|
43
|
+
prNumber: number;
|
|
44
|
+
/** Result field set when the review budget is exhausted. */
|
|
45
|
+
followUpMustFix?: readonly string[];
|
|
46
|
+
/** Last review's non-blocking findings. */
|
|
47
|
+
shouldFix?: readonly string[];
|
|
48
|
+
/** Opened PR body — the «남은 조각» section is read verbatim. */
|
|
49
|
+
prBody?: string;
|
|
50
|
+
/** Original request. Only its first line enters the draft. */
|
|
51
|
+
feature: string;
|
|
52
|
+
/** Release-cell id. Absent means the goal file's GoalId, else the feature's first token. */
|
|
53
|
+
cellId?: string;
|
|
54
|
+
goalFile?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FollowUpSeams {
|
|
58
|
+
stateRoot?: string;
|
|
59
|
+
mode?: FollowUpMode;
|
|
60
|
+
/**
|
|
61
|
+
* Prior follow-up records for this cell, oldest first.
|
|
62
|
+
* Absent means the caller did not hand over a store — the test process writes nothing.
|
|
63
|
+
* Production `defaultSeams` always sets this to the state-root ledger.
|
|
64
|
+
*/
|
|
65
|
+
readDrafts?: (cellId: string) => readonly FollowUpDraftRecord[];
|
|
66
|
+
appendDraft?: (record: FollowUpDraftRecord) => void;
|
|
67
|
+
/** Same function `harness queue add` calls. Injected in tests so the real queue is never opened. */
|
|
68
|
+
enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<unknown> | unknown;
|
|
69
|
+
/** Release-cell owner (`TC`, `TC/rel`, …). Absent means look up the developing checklist. */
|
|
70
|
+
cellOwner?: (cellId: string) => string | undefined;
|
|
71
|
+
log?: (category: string, event: string, data?: unknown) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function followUpDraftsPath(stateRoot = elanousStateRoot()): string {
|
|
75
|
+
return join(stateRoot, 'follow-up', 'drafts.jsonl');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readFollowUpMode(read: () => { harness?: { followUp?: unknown } } = getUserConfig): FollowUpMode {
|
|
79
|
+
return read().harness?.followUp === 'live' ? 'live' : 'shadow';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Lines under a markdown heading «남은 조각», verbatim, without the bullet mark. */
|
|
83
|
+
export function remainingPiecesFromPrBody(body: string | undefined): string[] {
|
|
84
|
+
if (!body) return [];
|
|
85
|
+
const lines = body.split(/\r?\n/);
|
|
86
|
+
const start = lines.findIndex((line) => line.trim() === `## ${REMAINING_SECTION}`);
|
|
87
|
+
if (start < 0) return [];
|
|
88
|
+
const out: string[] = [];
|
|
89
|
+
for (const line of lines.slice(start + 1)) {
|
|
90
|
+
if (/^#{1,6}\s/.test(line)) break;
|
|
91
|
+
const item = line.match(/^\s*[-*]\s+(.+?)\s*$/)?.[1];
|
|
92
|
+
if (item) out.push(item);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function collectRemainings(input: Pick<FollowUpMergeInput, 'followUpMustFix' | 'shouldFix' | 'prBody'>): string[] {
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const out: string[] = [];
|
|
100
|
+
for (const item of [
|
|
101
|
+
...(input.followUpMustFix ?? []),
|
|
102
|
+
...(input.shouldFix ?? []),
|
|
103
|
+
...remainingPiecesFromPrBody(input.prBody),
|
|
104
|
+
]) {
|
|
105
|
+
const text = item.trim();
|
|
106
|
+
if (!text || seen.has(text)) continue;
|
|
107
|
+
seen.add(text);
|
|
108
|
+
out.push(text);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function followUpCellId(input: Pick<FollowUpMergeInput, 'cellId' | 'goalFile' | 'feature'>, readGoal: (path: string) => string = (path) => readFileSync(path, 'utf8')): string | undefined {
|
|
114
|
+
const explicit = input.cellId?.trim();
|
|
115
|
+
if (explicit) return explicit;
|
|
116
|
+
if (input.goalFile) {
|
|
117
|
+
try {
|
|
118
|
+
const goalId = readGoal(input.goalFile).match(/^- GoalId:\s*(\S+)\s*$/m)?.[1];
|
|
119
|
+
if (goalId) return goalId;
|
|
120
|
+
} catch { /* a missing goal file is not a cell id */ }
|
|
121
|
+
}
|
|
122
|
+
// 자리 발사 머리(`[MK 자리 · 0.2.16 체크리스트 칸 FOLLOWUP-LOOP · …]`)가 칸을 «명시»한 경우만 읽는다.
|
|
123
|
+
// 추정(요청문 첫 단어)으로는 기록하지 않는다 — 다른 골이 한 칸으로 합쳐져 깊이·담당이 틀린다.
|
|
124
|
+
// 자리 머리 줄만 본다 — 본문이 다른 칸을 언급해도 그 칸으로 기록하지 않는다.
|
|
125
|
+
// 저작 Pod 규칙(OP 10-06)에서 첫 줄은 `대상 경로:` 이고 머리는 둘째 줄이라, 앞의 `대상 경로:` 줄 하나는 건너뛴다.
|
|
126
|
+
const lines = input.feature.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
127
|
+
const header = /^대상 경로:/u.test(lines[0] ?? '') ? lines[1] : lines[0];
|
|
128
|
+
return header?.match(/^\[(?:OP|MK|TC|UX) 자리 · [^\]]*?체크리스트 칸 ([A-Z][A-Z0-9-]*[A-Z0-9])(?: ·|\])/u)?.[1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function originalAskFirstLine(feature: string): string {
|
|
132
|
+
return feature.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function followUpDraftText(firstLine: string, remainings: readonly string[]): string {
|
|
136
|
+
return [
|
|
137
|
+
firstLine,
|
|
138
|
+
...remainings,
|
|
139
|
+
'origin/main 에서 시작 · 남은 것만',
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function followUpDraftHash(text: string): string {
|
|
144
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Chain length of follow-up goals already recorded for this cell, before this one. */
|
|
148
|
+
export function followUpChainDepth(prior: readonly Pick<FollowUpDraftRecord, 'kind' | 'prNumber'>[]): number {
|
|
149
|
+
// 한 PR 이 «기록 → 대기열 확정» 두 줄을 남길 수 있으니 PR 번호로 센다.
|
|
150
|
+
return new Set(prior.filter((record) => record.kind === 'draft' || record.kind === 'op-card-required').map((record) => record.prNumber)).size;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Seat = the cell's owner. `TC/rel` keeps the seat `TC`. Unknown or absent owners go to OP (ONEDOOR). */
|
|
154
|
+
export function seatForCellOwner(owner: string | undefined): QueueSeat {
|
|
155
|
+
if (!owner) return 'OP';
|
|
156
|
+
try {
|
|
157
|
+
const seat = parseOwner(owner.trim()).seat;
|
|
158
|
+
if ((SEATS as readonly string[]).includes(seat)) return seat as QueueSeat;
|
|
159
|
+
} catch { /* not a seat */ }
|
|
160
|
+
const head = owner.trim().split(/[/\s·]/)[0] ?? '';
|
|
161
|
+
return (SEATS as readonly string[]).includes(head) ? head as QueueSeat : 'OP';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defaultReadDrafts(stateRoot: string, cellId: string): FollowUpDraftRecord[] {
|
|
165
|
+
let text = '';
|
|
166
|
+
try { text = readFileSync(followUpDraftsPath(stateRoot), 'utf8'); } catch { return []; }
|
|
167
|
+
const out: FollowUpDraftRecord[] = [];
|
|
168
|
+
for (const line of text.split('\n')) {
|
|
169
|
+
if (!line.trim()) continue;
|
|
170
|
+
try {
|
|
171
|
+
const record = JSON.parse(line) as FollowUpDraftRecord;
|
|
172
|
+
if (record.cellId === cellId) out.push(record);
|
|
173
|
+
} catch { /* a torn line does not invent a chain */ }
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 같은 PR 은 칸과 무관하게 한 번 — 칸 id 가 달리 들어와도 다시 만들지 않는다. */
|
|
179
|
+
function defaultPrRecorded(stateRoot: string, prNumber: number): boolean {
|
|
180
|
+
let text = '';
|
|
181
|
+
try { text = readFileSync(followUpDraftsPath(stateRoot), 'utf8'); } catch { return false; }
|
|
182
|
+
return text.split('\n').some((line) => {
|
|
183
|
+
if (!line.trim()) return false;
|
|
184
|
+
try { return (JSON.parse(line) as FollowUpDraftRecord).prNumber === prNumber; } catch { return false; }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function defaultAppendDraft(stateRoot: string, record: FollowUpDraftRecord): void {
|
|
189
|
+
const path = followUpDraftsPath(stateRoot);
|
|
190
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
191
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function defaultCellOwner(cellId: string): string | undefined {
|
|
195
|
+
try {
|
|
196
|
+
return listChecklist(devVersion()).items.find((item) => item.id === cellId)?.owner;
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Record one follow-up draft for a merged PR. Same PR is recorded once.
|
|
204
|
+
* Depth above {@link FOLLOW_UP_DEPTH_LIMIT} records «OP 카드 필요» instead of a draft.
|
|
205
|
+
* `live` enqueues; `shadow` (the default) only records. Returns null when nothing remains.
|
|
206
|
+
* Every failure is `debug.log` only — the merge result is the caller's to return unchanged.
|
|
207
|
+
*/
|
|
208
|
+
export async function recordFollowUpOnMerge(input: FollowUpMergeInput, seams: FollowUpSeams = {}): Promise<FollowUpDraftRecord | null> {
|
|
209
|
+
const log = seams.log ?? ((category, event, data) => { debug.log(category, event, data); });
|
|
210
|
+
try {
|
|
211
|
+
const remainings = collectRemainings(input);
|
|
212
|
+
if (remainings.length === 0) return null;
|
|
213
|
+
// No injected store and no explicit state root: this is the test process.
|
|
214
|
+
// Do not open the operational ledger or the harness queue.
|
|
215
|
+
// A test process handed the operational root (defaultSeams) is refused the same way.
|
|
216
|
+
const testProcess = process.env.NODE_ENV === 'test' || !!process.env.ELANOUS_TEST_HOME;
|
|
217
|
+
if (!seams.readDrafts && !seams.appendDraft && (!seams.stateRoot || (testProcess && seams.stateRoot === elanousStateRoot()))) return null;
|
|
218
|
+
const stateRoot = seams.stateRoot ?? elanousStateRoot();
|
|
219
|
+
const cellId = followUpCellId(input);
|
|
220
|
+
if (!cellId) {
|
|
221
|
+
log('self-implement.follow-up', 'draft-skipped-no-cell', { prNumber: input.prNumber });
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const prior = (seams.readDrafts ?? ((id) => defaultReadDrafts(stateRoot, id)))(cellId);
|
|
225
|
+
if (prior.some((record) => record.prNumber === input.prNumber)) return null;
|
|
226
|
+
if (!seams.readDrafts && defaultPrRecorded(stateRoot, input.prNumber)) return null;
|
|
227
|
+
const depth = followUpChainDepth(prior) + 1;
|
|
228
|
+
const firstLine = originalAskFirstLine(input.feature);
|
|
229
|
+
const mode = seams.mode ?? readFollowUpMode();
|
|
230
|
+
const overDepth = depth > FOLLOW_UP_DEPTH_LIMIT;
|
|
231
|
+
const draft = overDepth ? undefined : followUpDraftText(firstLine, remainings);
|
|
232
|
+
const record: FollowUpDraftRecord = {
|
|
233
|
+
prNumber: input.prNumber,
|
|
234
|
+
cellId,
|
|
235
|
+
depth,
|
|
236
|
+
draftHash: followUpDraftHash(draft ?? `${firstLine}\n${FOLLOW_UP_OP_CARD_REQUIRED}`),
|
|
237
|
+
kind: overDepth ? 'op-card-required' : 'draft',
|
|
238
|
+
originalAskFirstLine: firstLine,
|
|
239
|
+
remainings,
|
|
240
|
+
...(draft ? { draft } : {}),
|
|
241
|
+
...(overDepth ? { note: FOLLOW_UP_OP_CARD_REQUIRED } : {}),
|
|
242
|
+
queued: false,
|
|
243
|
+
};
|
|
244
|
+
// 운영 원장은 «같은 PR 확인 ⊕ 첫 기록»을 한 잠금 안에서 한다 — 동시 병합 둘이 초안 둘을 만들지 않게.
|
|
245
|
+
const append = seams.appendDraft ?? ((row: FollowUpDraftRecord) => defaultAppendDraft(stateRoot, row));
|
|
246
|
+
const appendFirst = seams.appendDraft
|
|
247
|
+
? (row: FollowUpDraftRecord) => { append(row); return true; }
|
|
248
|
+
: (row: FollowUpDraftRecord) => (mkdirSync(dirname(followUpDraftsPath(stateRoot)), { recursive: true }), withFileLockSync(`${followUpDraftsPath(stateRoot)}.lock`, () => {
|
|
249
|
+
if (defaultPrRecorded(stateRoot, row.prNumber)) return false;
|
|
250
|
+
defaultAppendDraft(stateRoot, row);
|
|
251
|
+
return true;
|
|
252
|
+
}));
|
|
253
|
+
if (!overDepth && mode === 'live' && draft) record.seat = seatForCellOwner((seams.cellOwner ?? defaultCellOwner)(cellId));
|
|
254
|
+
// 원장 기록을 먼저 확정한다 — 기록이 실패하면 대기열에 넣지 않는다(같은 PR 한 번 · 원장 밖 연결골 0).
|
|
255
|
+
if (!appendFirst(record)) return null;
|
|
256
|
+
if (!overDepth && mode === 'live' && draft && record.seat) {
|
|
257
|
+
const seat = record.seat;
|
|
258
|
+
if (testProcess && !seams.enqueue) return record;
|
|
259
|
+
try {
|
|
260
|
+
await (seams.enqueue ?? ((item) => addHarnessQueue({ seat: item.seat, say: item.say, idempotencyKey: item.idempotencyKey })))({
|
|
261
|
+
seat,
|
|
262
|
+
say: draft,
|
|
263
|
+
idempotencyKey: `follow-up:${input.prNumber}`,
|
|
264
|
+
});
|
|
265
|
+
record.queued = true;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// 대기열 넣기가 실패해도 초안은 남긴다 — 같은 PR 은 다시 오지 않으므로 여기서 잃으면 영영 없다.
|
|
268
|
+
// queued=false ⊕ enqueueError 로 원장에 남기고, 재투입은 같은 idempotencyKey 로 사람이·다음 수리 노드가 한다.
|
|
269
|
+
record.enqueueError = (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
270
|
+
log('self-implement.follow-up', 'enqueue-failed', { prNumber: input.prNumber, seat, error: record.enqueueError });
|
|
271
|
+
}
|
|
272
|
+
// 대기열 결과를 원장에 한 줄 더 — 이 쓰기의 실패는 대기열 실패와 다른 사건이다.
|
|
273
|
+
try {
|
|
274
|
+
append({ ...record });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
log('self-implement.follow-up', 'ledger-confirm-failed', {
|
|
277
|
+
prNumber: input.prNumber, seat, queued: record.queued, error: error instanceof Error ? error.message : String(error),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return record;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
log('self-implement.follow-up', 'draft-failed', {
|
|
284
|
+
prNumber: input.prNumber,
|
|
285
|
+
error: error instanceof Error ? error.message : String(error),
|
|
286
|
+
});
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -2,9 +2,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync,
|
|
|
2
2
|
import { dlopen, FFIType } from 'bun:ffi';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
|
-
import { beginLandingMerge,
|
|
7
|
+
import { beginLandingMerge, type LandingFreeze } from '../release-loop/landing-freeze.js';
|
|
8
8
|
|
|
9
9
|
/** `manual`: the owning repository could not be determined — a sweep never resumes it; a person does. */
|
|
10
10
|
export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true; /** Host goal document, so a resumed merge can still apply its «이 칸 완료» declaration (CL-AUTO). */ goalFile?: string }
|
|
@@ -182,8 +182,10 @@ export function admitLandingMerge(
|
|
|
182
182
|
root = effectiveInstanceRoot(),
|
|
183
183
|
hooks: { afterQueue?: () => void } = {},
|
|
184
184
|
claimKey?: { prNumber: number; repoRoot: string; headCommit?: string },
|
|
185
|
+
opts: { prodFreezeRoot?: string; forceReason?: string } = {},
|
|
185
186
|
): LandingAdmission {
|
|
186
187
|
let entry: FrozenMerge | null | undefined;
|
|
188
|
+
const prodFreezeRoot = opts.prodFreezeRoot ?? prodInstanceRoot();
|
|
187
189
|
// A held PR is merged by exactly one runner: the claim (repository + PR) is held from admission until the lander's
|
|
188
190
|
// merge has ended. Its queue entry stays until that merge is confirmed — `end(true)` removes this lander's own head;
|
|
189
191
|
// a failed or interrupted merge leaves it for the next resume sweep.
|
|
@@ -201,7 +203,7 @@ export function admitLandingMerge(
|
|
|
201
203
|
} finally { landing.end(); held?.release(); }
|
|
202
204
|
};
|
|
203
205
|
for (;;) {
|
|
204
|
-
const landing = beginLandingMerge(root);
|
|
206
|
+
const landing = beginLandingMerge(root, new Date(), prodFreezeRoot, opts.forceReason);
|
|
205
207
|
if (!landing.frozen) {
|
|
206
208
|
if (!claim && claimKey) {
|
|
207
209
|
const key = { prNumber: claimKey.prNumber, repoRoot: repoIdentity(claimKey.repoRoot), headCommit: claimKey.headCommit };
|
|
@@ -222,17 +224,19 @@ export function admitLandingMerge(
|
|
|
222
224
|
if (!entry) return { kind: 'held', freeze: landing.frozen };
|
|
223
225
|
queueFrozenMerge(entry, root);
|
|
224
226
|
hooks.afterQueue?.();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
const afterQueue = beginLandingMerge(root, new Date(), prodFreezeRoot, opts.forceReason);
|
|
228
|
+
if (afterQueue.frozen) return { kind: 'held', freeze: afterQueue.frozen };
|
|
229
|
+
try {
|
|
230
|
+
const mine = { ...entry, repoRoot: repoIdentity(entry.repoRoot) };
|
|
231
|
+
claim = claimFrozenMerge(mine, root);
|
|
232
|
+
if (!claim) return { kind: 'taken' };
|
|
233
|
+
// A resume sweep already merged and dequeued it between our queueing and our claim: nothing left to merge.
|
|
234
|
+
const stillHeld = readQueue(root).some((item) => sameEntry(item, mine));
|
|
235
|
+
if (!stillHeld) { claim.release(); return { kind: 'taken' }; }
|
|
236
|
+
ownHead = mine;
|
|
237
|
+
debug.log('harness.merge', 'freeze-lifted-while-queueing', { pr: entry.prNumber });
|
|
238
|
+
} catch (error) { claim?.release(); throw error; }
|
|
239
|
+
finally { afterQueue.end(); }
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
|
|
@@ -259,16 +263,17 @@ export async function sweepFrozenMerges(
|
|
|
259
263
|
},
|
|
260
264
|
root = effectiveInstanceRoot(),
|
|
261
265
|
alreadyMerged: (entry: FrozenMerge) => boolean | Promise<boolean> = githubMergedAt,
|
|
266
|
+
prodFreezeRoot = prodInstanceRoot(),
|
|
262
267
|
): Promise<{ pending: number; merged: number }> {
|
|
263
|
-
const frozen = readLandingFreeze(root);
|
|
264
|
-
if (frozen) return { pending: readQueue(root).length, merged: 0 };
|
|
265
268
|
const pending = readQueue(root);
|
|
266
269
|
let merged = 0;
|
|
267
270
|
for (const entry of pending) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
+
const guard = beginLandingMerge(root, new Date(), prodFreezeRoot);
|
|
272
|
+
if (guard.frozen) break;
|
|
273
|
+
let claim: { release: () => void } | null = null;
|
|
271
274
|
try {
|
|
275
|
+
claim = claimFrozenMerge(entry, root);
|
|
276
|
+
if (!claim) continue; // another sweep or the original lander is on it
|
|
272
277
|
if (!readQueue(root).some((item) => sameEntry(item, entry))) continue; // taken back by its lander
|
|
273
278
|
if (entry.manual) { debug.log('harness.merge', 'resume-manual', { pr: entry.prNumber, repoRoot: entry.repoRoot }); continue; }
|
|
274
279
|
if (!(await alreadyMerged(entry))) {
|
|
@@ -289,7 +294,7 @@ export async function sweepFrozenMerges(
|
|
|
289
294
|
});
|
|
290
295
|
} catch (error) {
|
|
291
296
|
debug.log('harness.merge', 'resume-failed', { pr: entry.prNumber, error: String(error) });
|
|
292
|
-
} finally { claim
|
|
297
|
+
} finally { guard.end(); claim?.release(); }
|
|
293
298
|
}
|
|
294
299
|
return { pending: readQueue(root).length, merged };
|
|
295
300
|
}
|
|
@@ -681,6 +681,87 @@ export function injectGoalDocumentClarificationOtherAnswer(document: string, que
|
|
|
681
681
|
* 「답이 한 번도 안 들어가서 효과를 못 쟀다」***일 수 있다 — 두 무리 다 «답이 없는» 무리였다.
|
|
682
682
|
* ⛔ 무인 계약은 유지한다 — 대화형이 아니면 종전대로 지나가되 «그 사실을 값으로» 낸다.
|
|
683
683
|
*/
|
|
684
|
+
export type GoalClarificationSelfAnswerPath = 'say-first' | 'ask' | 'rework';
|
|
685
|
+
export type GoalClarificationSelfResolver = NonNullable<GoalAuthorClarificationResolutionDeps['selfResolve']>;
|
|
686
|
+
|
|
687
|
+
/** Attempt only unanswered document questions; retain every unanswered line if evidence is insufficient. */
|
|
688
|
+
export async function selfAnswerGoalDocumentClarifications(
|
|
689
|
+
document: string,
|
|
690
|
+
options: {
|
|
691
|
+
path: GoalClarificationSelfAnswerPath;
|
|
692
|
+
ask?: string;
|
|
693
|
+
evidence?: readonly string[];
|
|
694
|
+
resolver?: GoalClarificationSelfResolver;
|
|
695
|
+
},
|
|
696
|
+
): Promise<{ document: string; asked: number; answered: number; unanswered: number; newlyAnswered: number }> {
|
|
697
|
+
const clarifications = parseGoalDocumentClarifications(document);
|
|
698
|
+
const pending = clarifications.filter((item) => !item.answered);
|
|
699
|
+
// The first say pass may already have self-answered some questions while authoring: count those,
|
|
700
|
+
// never re-ask them, and still attempt every question that is left unanswered.
|
|
701
|
+
const alreadySelfAnswered = options.path === 'say-first'
|
|
702
|
+
? clarifications.filter((item) => item.answered && item.provenanceSource === 'self-authored').length
|
|
703
|
+
: 0;
|
|
704
|
+
let updated = document;
|
|
705
|
+
let answered = 0;
|
|
706
|
+
const evidence = options.evidence ?? document.split(/\r?\n/)
|
|
707
|
+
.filter((line) => /^(?:\s*- (?:\[code:|\[document:|TRACED PATHS|evidence:|Evidence:)|\s*-?\s*`?(?:src|test|scripts)\/[^\s`]+:\d+)/i.test(line))
|
|
708
|
+
.slice(0, 40);
|
|
709
|
+
for (const item of pending) {
|
|
710
|
+
if (parseGoalDocumentClarifications(updated).filter((entry) => entry.questionId === item.questionId).length !== 1) continue;
|
|
711
|
+
let response: Awaited<ReturnType<typeof resolveGoalAuthorClarification>>;
|
|
712
|
+
try { response = await resolveGoalAuthorClarification({
|
|
713
|
+
questionId: item.questionId,
|
|
714
|
+
kind: 'scope',
|
|
715
|
+
header: item.header,
|
|
716
|
+
question: item.question,
|
|
717
|
+
options: item.options.map((option) => ({ label: option.label, description: option.description })),
|
|
718
|
+
blocking: false,
|
|
719
|
+
}, {
|
|
720
|
+
selfResolve: options.resolver ?? defaultGoalAuthorSelfResolve,
|
|
721
|
+
evidence,
|
|
722
|
+
ask: options.ask,
|
|
723
|
+
selfResolutionSelected: true,
|
|
724
|
+
}); } catch {
|
|
725
|
+
// A resolver failure leaves this question unanswered; the remaining questions are still attempted.
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (response.answer === null || response.provenance?.source !== 'self-authored'
|
|
729
|
+
|| response.answer.startsWith(DEFERRED_ANSWER_PREFIX)) continue;
|
|
730
|
+
try {
|
|
731
|
+
if (!response.answer.trim() || /[\r\n]/.test(response.answer)
|
|
732
|
+
|| (!item.includeOther && !item.options.some((option) => option.label === response.answer))) continue;
|
|
733
|
+
const current = findUnansweredGoalDocumentClarification(updated, item.questionId);
|
|
734
|
+
const lines = updated.split(/\r?\n/);
|
|
735
|
+
const provenanceLines = current.provenanceLines ?? [];
|
|
736
|
+
for (const line of [...provenanceLines].sort((left, right) => right - left)) lines.splice(line, 1);
|
|
737
|
+
const answerLine = current.answerLine - provenanceLines.filter((line) => line < current.answerLine).length;
|
|
738
|
+
lines[answerLine] = ` - answer: ${response.answer}`;
|
|
739
|
+
lines.splice(answerLine + 1, 0,
|
|
740
|
+
' - provenance.source: self-authored',
|
|
741
|
+
...response.provenance.evidence!.map((entry) => ` - evidence: ${entry.replace(/[\r\n]/g, ' ')}`));
|
|
742
|
+
updateWhatToBuildRenderAnswers(lines, item.questionId, response.answer);
|
|
743
|
+
if (hasWhatToBuildUnansweredRenderAnswer(lines, item.questionId)) continue;
|
|
744
|
+
const candidate = lines.join(document.includes('\r\n') ? '\r\n' : '\n');
|
|
745
|
+
if (!parseGoalDocumentClarifications(candidate).some((entry) => entry.questionId === item.questionId
|
|
746
|
+
&& entry.answered && entry.answer === response.answer && entry.provenanceSource === 'self-authored')) continue;
|
|
747
|
+
updated = candidate;
|
|
748
|
+
answered += 1;
|
|
749
|
+
} catch {
|
|
750
|
+
// Leave malformed or disallowed answers deferred.
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const asked = pending.length + alreadySelfAnswered;
|
|
754
|
+
const unanswered = pending.length - answered;
|
|
755
|
+
const newlyAnswered = answered;
|
|
756
|
+
answered += alreadySelfAnswered;
|
|
757
|
+
debug.log('self-implement.clarification-escalation', 'self-answer', {
|
|
758
|
+
path: options.path, asked, answered, unanswered,
|
|
759
|
+
reason: unanswered === 0 ? 'answered' : 'insufficient-evidence',
|
|
760
|
+
});
|
|
761
|
+
return { document: updated, asked, answered, unanswered, newlyAnswered };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
|
|
684
765
|
export type ClarificationIntakeMode = 'none' | 'deferred-noninteractive' | 'ask';
|
|
685
766
|
|
|
686
767
|
export interface ClarificationIntakePlan {
|