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
package/src/cli/self-update.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync, writeFileSync, lstatSync } from 'node:fs';
|
|
2
2
|
import { resolve, join, basename, dirname } from 'node:path';
|
|
3
3
|
import { homedir, platform, userInfo } from 'node:os';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { decideRestartNeeded, type RestartNeededResult } from './nexus-restart-needed.js';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
7
|
import { envLiteral } from '../platform/env-literal.js';
|
|
8
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
9
|
+
import { getUserConfig } from '../user-config.js';
|
|
10
|
+
import { planEdgeRail, type EdgeRailInput, type EdgeRailPlan } from './self-update-canary.js';
|
|
11
|
+
import { loadRunLedger } from '../self-implement/run-ledger.js';
|
|
8
12
|
|
|
9
13
|
export interface SelfUpdateOptions {
|
|
10
14
|
from?: string;
|
|
@@ -18,6 +22,18 @@ export interface SelfUpdateOptions {
|
|
|
18
22
|
skipPwaBuild?: boolean;
|
|
19
23
|
/** 설치된 릴리스에서만 사용한다. 체크아웃 갱신에는 적용하지 않는다. */
|
|
20
24
|
version?: string;
|
|
25
|
+
/** 판 사이 dev 빌드 설치 — versions/<판>-dev.<sha12> · install.json channel:'dev'. 사람 승인 명령. */
|
|
26
|
+
dev?: boolean;
|
|
27
|
+
/** install.json 의 이전 versionDir 로 current 를 되돌린다. 이전이 없으면 거부. */
|
|
28
|
+
rollback?: boolean;
|
|
29
|
+
/** Canary rollout action; legacy --auto on|off|status is handled by the CLI scheduler. */
|
|
30
|
+
auto?: boolean;
|
|
31
|
+
dryRun?: boolean;
|
|
32
|
+
seat?: string;
|
|
33
|
+
quietWindow?: boolean;
|
|
34
|
+
failureRate?: EdgeRailInput['failureRate'];
|
|
35
|
+
/** Unique ID of an independently successful canary run; no id means no new run. */
|
|
36
|
+
canaryRunId?: string;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
export interface SelfUpdateDeps {
|
|
@@ -26,6 +42,8 @@ export interface SelfUpdateDeps {
|
|
|
26
42
|
decide?: (opts: { to: string; cwd: string; out: { log: (s: string) => void; error: (s: string) => void } }) => Promise<RestartNeededResult>;
|
|
27
43
|
run?: (command: string, args: string[], cwd: string, options?: { timeout?: number }) => { status: number | null; stderr: string; stdout?: string };
|
|
28
44
|
installedVersion?: () => string;
|
|
45
|
+
/** 캐너리 시작 «뒤»에 시작해 `completed` 로 끝난 런인가(주입 안 하면 런 원장 `canaryRunSucceededInLedger`). */
|
|
46
|
+
canaryRunSucceeded?: (runId: string, sinceIso: string) => boolean;
|
|
29
47
|
/** 재시작 뒤 데몬이 이 커밋으로 떴나(주입 안 하면 `/v1/health` 를 최대 90초 폴링). */
|
|
30
48
|
verifyRestart?: (expectedCommit: string) => Promise<VerifyRestartResult>;
|
|
31
49
|
/** 판 목록(이름) — 되돌릴 판 찾기. */
|
|
@@ -45,6 +63,22 @@ export interface SelfUpdateDeps {
|
|
|
45
63
|
out?: { log: (s: string) => void; error: (s: string) => void };
|
|
46
64
|
/** 판올림 성공 알림(주입 안 하면 sendOutbound 'report' · 시험에선 안 보냄). */
|
|
47
65
|
notice?: (text: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* 설치 루트(versions · current · install.json). 주입하지 않으면 ~/.local/share/elanous.
|
|
68
|
+
* 시험은 임시 루트만 넘긴다 — 실물 심링크·재시작을 만지지 않는다.
|
|
69
|
+
*/
|
|
70
|
+
installRoot?: string;
|
|
71
|
+
/** 패키지 버전(package.json). 주입하지 않으면 체크아웃 package.json. */
|
|
72
|
+
packageVersion?: (checkout: string) => string;
|
|
73
|
+
/** 재시작을 실제로 호출해도 되는지. NODE_ENV=test · ELANOUS_TEST_HOME 이면 기본 false. */
|
|
74
|
+
allowLiveRestart?: boolean;
|
|
75
|
+
/** Automatic edge-rail inputs; injected in tests, never a service registration. */
|
|
76
|
+
now?: () => Date;
|
|
77
|
+
stateDir?: string;
|
|
78
|
+
edgeRailConfig?: () => Pick<EdgeRailInput, 'canaryOkRuns' | 'failureMultiplier' | 'minSamples'>;
|
|
79
|
+
installEdgeRail?: (options: SelfUpdateOptions, deps: SelfUpdateDeps) => Promise<SelfUpdateResult>;
|
|
80
|
+
rollbackEdgeRail?: (deps: Pick<SelfUpdateDeps, 'installRoot'>) => ReturnType<typeof rollbackDevInstall>;
|
|
81
|
+
restartEdgeRail?: () => Promise<boolean>;
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
export interface SelfUpdateResult {
|
|
@@ -63,11 +97,21 @@ export interface SelfUpdateResult {
|
|
|
63
97
|
prune?: VersionPruneOutcome;
|
|
64
98
|
/** 재시작 뒤 건강 — ok · rolled-back(직전 판으로 되돌려 회복) · rollback-failed · no-rollback-target. */
|
|
65
99
|
health?: 'ok' | 'rolled-back' | 'rollback-failed' | 'no-rollback-target' | 'unmeasured';
|
|
100
|
+
/** --dev 설치 또는 --rollback 이 옮긴 current 대상(versions/<name>). */
|
|
101
|
+
current?: string;
|
|
102
|
+
/** install.json channel. dev 설치면 'dev'. */
|
|
103
|
+
channel?: string;
|
|
104
|
+
/** --dev 가 남긴 이전 versionDir. */
|
|
105
|
+
previous?: string;
|
|
66
106
|
rolledBackTo?: string;
|
|
67
107
|
/** relay(`com.elanous.openai-relay`) 판정 — 넥서스와 «따로» 본다(T5 · 2026-09-24). */
|
|
68
108
|
relay?: RelayUpdateOutcome;
|
|
69
109
|
/** 텔레그램 러너(`com.elanous.telegram` · `elanous-telegram.service`) 판정 — 🆕 2026-09-25. */
|
|
70
110
|
telegramRunner?: RelayUpdateOutcome;
|
|
111
|
+
edgeRail?: EdgeRailPlan;
|
|
112
|
+
missingFixes?: string;
|
|
113
|
+
/** 실패율 측정 원천이 없으면 자동 되돌림이 판단되지 않는다 — 측정 원천은 다음 조각(EDGE-RAIL2 범위 밖). */
|
|
114
|
+
autoRollback?: 'active' | 'inactive-no-failure-rate';
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
export interface RelayUpdateOutcome {
|
|
@@ -182,6 +226,142 @@ export function rollbackTarget(versions: readonly string[], daemonSha: string, i
|
|
|
182
226
|
|
|
183
227
|
const VERSIONS_DIR = () => join(homedir(), '.local/share/elanous/versions');
|
|
184
228
|
|
|
229
|
+
/** 시험·ELANOUS_TEST_HOME 에서는 실물 재시작을 호출하지 않는다. */
|
|
230
|
+
export function liveRestartBlocked(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
231
|
+
return env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME?.trim());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface InstallRecord {
|
|
235
|
+
version?: string;
|
|
236
|
+
versionDir?: string;
|
|
237
|
+
source?: string;
|
|
238
|
+
installedAt?: string;
|
|
239
|
+
commit?: string;
|
|
240
|
+
channel?: string;
|
|
241
|
+
previous?: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function defaultInstallRoot(): string {
|
|
245
|
+
return join(homedir(), '.local/share/elanous');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function readInstallRecord(root: string): InstallRecord | null {
|
|
249
|
+
try {
|
|
250
|
+
const parsed: unknown = JSON.parse(readFileSync(join(root, 'install.json'), 'utf8'));
|
|
251
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
252
|
+
return parsed as InstallRecord;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** install.json 의 version·commit. 못 읽으면 둘 다 «unknown». */
|
|
259
|
+
export function readInstalledIdentity(root: string = defaultInstallRoot()): { installedVersion: string; installedCommit: string } {
|
|
260
|
+
const record = readInstallRecord(root);
|
|
261
|
+
const version = typeof record?.version === 'string' && record.version.trim() ? record.version.trim() : '';
|
|
262
|
+
const commit = typeof record?.commit === 'string' && record.commit.trim() ? record.commit.trim() : '';
|
|
263
|
+
return { installedVersion: version || 'unknown', installedCommit: commit || 'unknown' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const VERSION_DIR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
267
|
+
|
|
268
|
+
function versionDirName(versionDir: string | undefined): string | null {
|
|
269
|
+
if (typeof versionDir !== 'string') return null;
|
|
270
|
+
if (!/^versions\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(versionDir) || versionDir.includes('..')) return null;
|
|
271
|
+
return basename(versionDir);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 판 디렉터리에 실행 진입 파일이 있나(설치기가 실제로 내용을 깔았나). */
|
|
275
|
+
export function installEntryExists(root: string, versionName: string): boolean {
|
|
276
|
+
return existsSync(join(root, 'versions', versionName, 'node_modules', 'elanous', 'bin', 'elanous.mjs'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 판 이름 «<판>-<커밋12>» · «<판>-dev.<커밋12>» 에서 판·커밋을 읽는다(되돌림 메타 일치용). */
|
|
280
|
+
export function parseVersionDirName(name: string): { version: string; commit: string } | null {
|
|
281
|
+
const m = /^(.+?)-(?:dev\.)?([0-9a-f]{12})(?:-dirty)?$/.exec(name);
|
|
282
|
+
return m ? { version: m[1]!, commit: m[2]! } : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** current 심링크를 versions/<name> 으로 원자적으로 옮긴다. 대상 폴더가 없으면 거부. */
|
|
286
|
+
export function relinkInstallCurrent(root: string, versionName: string): void {
|
|
287
|
+
if (!VERSION_DIR_NAME.test(versionName) || versionName.includes('..')) throw new Error(`잘못된 판 이름: ${versionName}`);
|
|
288
|
+
if (!existsSync(join(root, 'versions', versionName))) throw new Error(`판 디렉터리 없음: ${versionName}`);
|
|
289
|
+
// EDGE-RAIL-VERIFY(10-06 05:30 사고): 진입 파일이 없는 판으로는 current 를 옮기지 않는다(빈 판 → 전역 elanous·크론 전부 실패).
|
|
290
|
+
if (!installEntryExists(root, versionName)) throw new Error(`빈 판(진입 파일 없음): ${versionName} — current 를 옮기지 않는다`);
|
|
291
|
+
const tmp = join(root, `.current-dev-${process.pid}`);
|
|
292
|
+
rmSync(tmp, { force: true });
|
|
293
|
+
symlinkSync(`versions/${versionName}`, tmp);
|
|
294
|
+
renameSync(tmp, join(root, 'current'));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function checkoutPackageVersion(checkout: string): string {
|
|
298
|
+
const parsed = JSON.parse(readFileSync(join(checkout, 'package.json'), 'utf8')) as { version?: unknown };
|
|
299
|
+
if (typeof parsed.version !== 'string' || !parsed.version.trim()) throw new Error('package.json version 없음');
|
|
300
|
+
return parsed.version.trim();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 판 사이 dev 빌드 — versions/<판>-dev.<sha12> 를 만들고 current 를 옮긴다.
|
|
305
|
+
* install.json 에 channel:'dev' 와 이전 versionDir(previous) 를 남긴다. 실제 설치기·재시작은 부르지 않는다.
|
|
306
|
+
*/
|
|
307
|
+
export function installDevBuild(checkout: string, commit: string, deps: Pick<SelfUpdateDeps, 'installRoot' | 'packageVersion'> & { runInstaller?: () => { status: number | null; stderr: string } } = {}): { versionName: string; versionDir: string; previous: string; record: InstallRecord } {
|
|
308
|
+
const sha12 = commit.trim().toLowerCase().slice(0, 12);
|
|
309
|
+
if (!/^[0-9a-f]{12}$/.test(sha12)) throw new Error(`dev 설치 커밋이 12자가 아님: ${commit}`);
|
|
310
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
311
|
+
const prior = readInstallRecord(root);
|
|
312
|
+
const previous = versionDirName(prior?.versionDir) ? prior!.versionDir! : (typeof prior?.previous === 'string' ? prior.previous : '');
|
|
313
|
+
// EDGE-RAIL-VERIFY: 빈 디렉터리를 만들고 current 만 옮기던 길(10-06 05:30 사고)을 버리고, 정식 길과 같은 설치기로 내용을 깐다.
|
|
314
|
+
if (!deps.runInstaller) throw new Error('dev 설치기 없음 — 빈 판을 깔지 않는다');
|
|
315
|
+
// install.sh 는 current 를 «먼저» 옮기고 뒤에서 실패할 수 있다 — 실패·빈 판 어느 쪽이든 이전 판·install.json 으로 되돌린다.
|
|
316
|
+
const restore = (why: string, versionName: string | null): never => {
|
|
317
|
+
const previousName = versionDirName(previous);
|
|
318
|
+
if (previousName && installEntryExists(root, previousName)) relinkInstallCurrent(root, previousName);
|
|
319
|
+
if (prior) writeFileSync(join(root, 'install.json'), `${JSON.stringify(prior, null, 2)}\n`);
|
|
320
|
+
try { debug.log('self-update', 'dev-install-refused', { why, versionName, kept: previousName ?? null }); } catch { /* 관측 실패가 막지 않는다 */ }
|
|
321
|
+
throw new Error(`${why}: ${versionName ?? '?'} — current 를 이전 판(${previousName ?? '없음'})으로 유지`);
|
|
322
|
+
};
|
|
323
|
+
let result: { status: number | null; stderr: string };
|
|
324
|
+
try { result = deps.runInstaller(); }
|
|
325
|
+
catch (error) { return restore(`설치기 예외(${String(error).slice(0, 200)})`, null); }
|
|
326
|
+
const after = readInstallRecord(root);
|
|
327
|
+
const versionName = versionDirName(after?.versionDir);
|
|
328
|
+
if (result.status !== 0) return restore(`설치기 실패(rc=${result.status}: ${result.stderr.slice(0, 200)})`, versionName);
|
|
329
|
+
if (!versionName || !installEntryExists(root, versionName)) return restore('빈 판(진입 파일 없음)', versionName);
|
|
330
|
+
const versionDir = `versions/${versionName}`;
|
|
331
|
+
const record: InstallRecord = {
|
|
332
|
+
...(after ?? {}),
|
|
333
|
+
versionDir,
|
|
334
|
+
channel: 'dev',
|
|
335
|
+
...(previous && previous !== versionDir ? { previous } : {}),
|
|
336
|
+
};
|
|
337
|
+
writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
|
|
338
|
+
return { versionName, versionDir, previous, record };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** install.json.previous 의 판으로 current 를 되돌린다. previous 가 없으면 거부(exit 2). */
|
|
342
|
+
export function rollbackDevInstall(deps: Pick<SelfUpdateDeps, 'installRoot'> = {}): { ok: true; versionName: string; record: InstallRecord } | { ok: false; reason: string } {
|
|
343
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
344
|
+
const prior = readInstallRecord(root);
|
|
345
|
+
const previousName = versionDirName(prior?.previous);
|
|
346
|
+
if (!prior || !previousName) return { ok: false, reason: '이전 판(install.json previous)이 없다 — rollback 거부' };
|
|
347
|
+
if (!existsSync(join(root, 'versions', previousName))) return { ok: false, reason: `이전 판 디렉터리 없음: ${previousName}` };
|
|
348
|
+
const currentName = versionDirName(prior.versionDir);
|
|
349
|
+
relinkInstallCurrent(root, previousName);
|
|
350
|
+
const parsed = parseVersionDirName(previousName);
|
|
351
|
+
const record: InstallRecord = {
|
|
352
|
+
...prior,
|
|
353
|
+
...(parsed ? { version: parsed.version, commit: parsed.commit } : {}),
|
|
354
|
+
versionDir: `versions/${previousName}`,
|
|
355
|
+
channel: prior.channel === 'dev' ? undefined : prior.channel,
|
|
356
|
+
previous: currentName ? `versions/${currentName}` : undefined,
|
|
357
|
+
installedAt: new Date().toISOString(),
|
|
358
|
+
};
|
|
359
|
+
if (!record.channel) delete record.channel;
|
|
360
|
+
if (!record.previous) delete record.previous;
|
|
361
|
+
writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
|
|
362
|
+
return { ok: true, versionName: previousName, record };
|
|
363
|
+
}
|
|
364
|
+
|
|
185
365
|
/** `unmeasured` = 건강을 «잴 수» 없었다(REST 주소를 한 번도 못 얻음) — 「건강하지 않다」와 다르다. 되돌리지 않는다. */
|
|
186
366
|
export interface VerifyRestartResult { ok: boolean; daemonSha?: string; reason?: string; unmeasured?: boolean }
|
|
187
367
|
|
|
@@ -399,9 +579,14 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
399
579
|
}
|
|
400
580
|
let previous = '';
|
|
401
581
|
let previousBuild = '';
|
|
582
|
+
let replacedDev: { from: string; to: string } | null = null;
|
|
402
583
|
try {
|
|
403
584
|
const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
|
|
404
585
|
previousBuild = installedBuild(metadata);
|
|
586
|
+
if (typeof metadata === 'object' && metadata !== null && (metadata as { channel?: unknown }).channel === 'dev') {
|
|
587
|
+
const fromDir = typeof (metadata as { versionDir?: unknown }).versionDir === 'string' ? (metadata as { versionDir: string }).versionDir : '';
|
|
588
|
+
replacedDev = { from: fromDir, to: '' };
|
|
589
|
+
}
|
|
405
590
|
if (typeof metadata === 'object' && metadata !== null && 'versionDir' in metadata && typeof metadata.versionDir === 'string') {
|
|
406
591
|
const dir = metadata.versionDir;
|
|
407
592
|
if (/^versions\/[a-zA-Z0-9._-]+$/.test(dir) && !dir.includes('..')) previous = basename(dir);
|
|
@@ -418,6 +603,12 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
418
603
|
const installedVersion = typeof metadata === 'object' && metadata !== null && 'version' in metadata && typeof metadata.version === 'string'
|
|
419
604
|
? metadata.version : null;
|
|
420
605
|
if (!installedVersion) return fail(1, '설치판 확인 실패: install.json version 없음');
|
|
606
|
+
if (replacedDev) {
|
|
607
|
+
const toDir = typeof metadata === 'object' && metadata !== null && typeof (metadata as { versionDir?: unknown }).versionDir === 'string'
|
|
608
|
+
? (metadata as { versionDir: string }).versionDir : installedVersion;
|
|
609
|
+
replacedDev = { from: replacedDev.from, to: toDir };
|
|
610
|
+
try { debug.log('self-update', 'dev-replaced', replacedDev); } catch { /* 관측 실패가 갱신을 막지 않는다 */ }
|
|
611
|
+
}
|
|
421
612
|
const internalRevision = isTailnet(source) && version?.match(/^(.*)-([0-9a-f]{12})$/);
|
|
422
613
|
if (version && installedVersion !== version && (!internalRevision || installedVersion !== internalRevision[1])) {
|
|
423
614
|
return fail(1, `설치판 버전 불일치: 요청 ${version}, 설치 ${installedVersion}`);
|
|
@@ -462,12 +653,186 @@ function readInstallSource(cliRoot: string, exists: (path: string) => boolean, r
|
|
|
462
653
|
}
|
|
463
654
|
}
|
|
464
655
|
|
|
656
|
+
/** Output only; never uses a title to decide whether to install. */
|
|
657
|
+
export function missingFixCommitTitles(checkout: string, installed: string, main: string, git: NonNullable<SelfUpdateDeps['git']>): string {
|
|
658
|
+
if (!/^[a-f0-9]{12,40}$/i.test(installed) || !/^[a-f0-9]{12,40}$/i.test(main)) return 'main 엔 있고 운영엔 없는 수리: 판정 불가';
|
|
659
|
+
const result = git(checkout, ['log', '--format=%s', '--no-merges', `${installed}..${main}`]);
|
|
660
|
+
if (result.status !== 0) return 'main 엔 있고 운영엔 없는 수리: 판정 불가';
|
|
661
|
+
const titles = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => /\bfix(?:es|ed)?\b|(?:^|[(:])fix[:)]/i.test(line))
|
|
662
|
+
.map((line) => line.replace(/[\r\n\t]+/g, ' ').slice(0, 160)).slice(0, 10);
|
|
663
|
+
return `main 엔 있고 운영엔 없는 수리: ${titles.length ? titles.join(' · ') : '없음'}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
interface EdgeRailState { canary: NonNullable<EdgeRailInput['canary']>; mainCommit: string; lastRunId?: string; countedRunIds?: string[]; failureRate?: NonNullable<EdgeRailInput['failureRate']>; promoted?: boolean; rolledBack?: boolean }
|
|
667
|
+
|
|
668
|
+
function saveEdgeRailState(file: string, state: EdgeRailState): void {
|
|
669
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
670
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
671
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
672
|
+
renameSync(tmp, file);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** 런 원장에서: `start` 가 캐너리 시작 이후이고 마지막 `run-status` 가 `completed` 인 런만 참. 못 읽으면 거짓. */
|
|
676
|
+
export function canaryRunSucceededInLedger(runId: string, sinceIso: string, dir?: string): boolean {
|
|
677
|
+
try {
|
|
678
|
+
const entries = (dir ? loadRunLedger(runId, dir) : loadRunLedger(runId)) ?? [];
|
|
679
|
+
const started = Date.parse(entries.find((entry) => entry.event === 'start')?.timestamp ?? '');
|
|
680
|
+
const last = entries.filter((entry) => entry.event === 'run-status').at(-1);
|
|
681
|
+
return Number.isFinite(started) && started >= Date.parse(sinceIso) && last?.data.runStatus === 'completed';
|
|
682
|
+
} catch { return false; }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function restartEdgeRail(deps: SelfUpdateDeps, checkout: string): Promise<boolean> {
|
|
686
|
+
if (deps.restartEdgeRail) return deps.restartEdgeRail();
|
|
687
|
+
if (deps.allowLiveRestart === false || (deps.allowLiveRestart !== true && liveRestartBlocked())) return false;
|
|
688
|
+
const os = deps.os ?? platform();
|
|
689
|
+
const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
|
|
690
|
+
if (!command) return false;
|
|
691
|
+
const args = os === 'darwin' ? ['kickstart', '-k', `gui/${deps.uid ?? process.getuid?.() ?? userInfo().uid}/com.elanous.nexus`] : ['--user', 'restart', 'elanous-nexus'];
|
|
692
|
+
return (deps.run ?? execute)(command, args, checkout).status === 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export async function runEdgeRailUpdate(options: SelfUpdateOptions, deps: SelfUpdateDeps = {}): Promise<SelfUpdateResult> {
|
|
696
|
+
const out = deps.out ?? console;
|
|
697
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
698
|
+
const record = readInstallRecord(root);
|
|
699
|
+
const checkout = resolve(options.from ?? record?.source ?? deps.cliRoot ?? resolve(import.meta.dir, '../..'));
|
|
700
|
+
const stateFile = join(deps.stateDir ?? elanousStateRoot(), 'self-update-edge-rail.json');
|
|
701
|
+
const git = deps.git ?? ((cwd: string, args: string[]) => execute('git', args, cwd));
|
|
702
|
+
const fail = (reason: string): SelfUpdateResult => {
|
|
703
|
+
const result: SelfUpdateResult = { exitCode: 2, installedVersion: null, decision: null, restarted: false, reason };
|
|
704
|
+
out.log(options.json ? JSON.stringify(result) : `self-update --auto: hold — ${reason}`);
|
|
705
|
+
return result;
|
|
706
|
+
};
|
|
707
|
+
if (options.dev || options.rollback || options.version || options.restart) return fail('--auto cannot combine with --dev, --rollback, --version or --restart');
|
|
708
|
+
if (options.seat !== 'OP' && options.seat !== 'TC' && options.seat !== 'MK' && options.seat !== 'UX') return fail('--auto requires a known --seat (OP|TC|MK|UX)');
|
|
709
|
+
if (!record?.commit) return fail('installed commit unknown');
|
|
710
|
+
const currentName = versionDirName(record.versionDir);
|
|
711
|
+
if (!currentName || !installEntryExists(root, currentName)) return fail('installed entry missing');
|
|
712
|
+
try {
|
|
713
|
+
if (readlinkSync(join(root, 'current')) !== record.versionDir) return fail('current does not match installed record');
|
|
714
|
+
} catch { return fail('current is not a version link'); }
|
|
715
|
+
if (!options.from && (!record.source || /^(?:https?:|file:)/.test(record.source))) return fail('local checkout source unknown — use --from');
|
|
716
|
+
if (!existsSync(join(checkout, 'scripts', 'install.sh'))) return fail('checkout installer missing');
|
|
717
|
+
const verifiedRoot = git(checkout, ['rev-parse', '--show-toplevel']);
|
|
718
|
+
try {
|
|
719
|
+
if (verifiedRoot.status !== 0 || !verifiedRoot.stdout.trim() || realpathSync(verifiedRoot.stdout.trim()) !== realpathSync(checkout)) return fail('checkout git root mismatch');
|
|
720
|
+
} catch { return fail('checkout git root mismatch'); }
|
|
721
|
+
const head = git(checkout, ['rev-parse', '--verify', 'main']);
|
|
722
|
+
if (head.status !== 0 || !/^[a-f0-9]{12,40}$/i.test(head.stdout.trim())) return fail('main commit unknown');
|
|
723
|
+
if (!options.dryRun) {
|
|
724
|
+
const tracked = git(checkout, ['diff', '--quiet', 'HEAD', '--']);
|
|
725
|
+
const working = git(checkout, ['rev-parse', '--verify', 'HEAD']);
|
|
726
|
+
if (tracked.status !== 0 || working.status !== 0 || working.stdout.trim() !== head.stdout.trim()) return fail('main is not the clean checkout HEAD');
|
|
727
|
+
const untracked = git(checkout, ['ls-files', '--others', '--exclude-standard']);
|
|
728
|
+
if (untracked.status !== 0 || untracked.stdout.trim()) return fail('checkout contains untracked files');
|
|
729
|
+
}
|
|
730
|
+
let state: EdgeRailState | null = null;
|
|
731
|
+
if (existsSync(stateFile)) {
|
|
732
|
+
try {
|
|
733
|
+
const value: unknown = JSON.parse(readFileSync(stateFile, 'utf8'));
|
|
734
|
+
if (value && typeof value === 'object' && 'canary' in value && 'mainCommit' in value) state = value as EdgeRailState;
|
|
735
|
+
else return fail('invalid edge-rail state');
|
|
736
|
+
} catch { return fail('invalid edge-rail state'); }
|
|
737
|
+
}
|
|
738
|
+
const mainCommit = head.stdout.trim();
|
|
739
|
+
const config = deps.edgeRailConfig?.() ?? getUserConfig().harness?.edgeRail ?? {};
|
|
740
|
+
const canary = state?.mainCommit === mainCommit ? state.canary : null;
|
|
741
|
+
const observed = (deps.now ?? (() => new Date()))();
|
|
742
|
+
if (!options.dryRun && canary && canary.okRuns < (config.canaryOkRuns ?? 3) && options.seat === 'OP' && (record.commit.startsWith(mainCommit) || mainCommit.startsWith(record.commit))
|
|
743
|
+
&& !options.failureRate && options.canaryRunId?.trim() && options.canaryRunId !== state?.lastRunId
|
|
744
|
+
// 같은 런을 두 번 세지 않고, 캐너리 시작 뒤 «성공으로 끝난» 런만 센다(임의·과거·실패 id 로 승격을 열지 못하게).
|
|
745
|
+
&& !(state?.countedRunIds ?? []).includes(options.canaryRunId.trim())
|
|
746
|
+
&& (deps.canaryRunSucceeded ?? canaryRunSucceededInLedger)(options.canaryRunId.trim(), canary.startedAt)
|
|
747
|
+
&& Number.isFinite(observed.getTime()) && observed.getTime() >= Date.parse(canary.startedAt)) {
|
|
748
|
+
const health = await (deps.verifyRestart ?? defaultVerifyRestart)(mainCommit);
|
|
749
|
+
if (health.ok && health.daemonSha && (health.daemonSha.startsWith(mainCommit) || mainCommit.startsWith(health.daemonSha)) && state) {
|
|
750
|
+
state = { ...state, canary: { ...canary, okRuns: canary.okRuns + 1 }, lastRunId: options.canaryRunId, countedRunIds: [...(state.countedRunIds ?? []), options.canaryRunId.trim()] };
|
|
751
|
+
saveEdgeRailState(stateFile, state);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const input: EdgeRailInput = {
|
|
755
|
+
// 설정에서는 세 수치만 받는다 — 여분 키가 판단 입력(seat·failureRate·canary…)을 덮지 못하게.
|
|
756
|
+
...(config.canaryOkRuns !== undefined ? { canaryOkRuns: config.canaryOkRuns } : {}),
|
|
757
|
+
...(config.failureMultiplier !== undefined ? { failureMultiplier: config.failureMultiplier } : {}),
|
|
758
|
+
...(config.minSamples !== undefined ? { minSamples: config.minSamples } : {}),
|
|
759
|
+
now: observed, mainCommit, installed: record.commit, seat: options.seat,
|
|
760
|
+
canary: state?.mainCommit === mainCommit ? state.canary : null,
|
|
761
|
+
failureRate: options.failureRate ?? (state?.mainCommit === mainCommit ? state.failureRate : null) ?? null,
|
|
762
|
+
quietWindow: options.quietWindow === true, promoted: state?.mainCommit === mainCommit ? state.promoted : false,
|
|
763
|
+
};
|
|
764
|
+
// 이 main 은 이미 되돌렸다 — 새 main 이 올 때까지 다시 깔거나 승격하지 않는다.
|
|
765
|
+
const plan = state?.mainCommit === mainCommit && state.rolledBack
|
|
766
|
+
? { decision: 'hold' as const, reason: 'this main was rolled back; waiting for a new main' }
|
|
767
|
+
: planEdgeRail(input);
|
|
768
|
+
const fixes = missingFixCommitTitles(checkout, record.commit, mainCommit, git);
|
|
769
|
+
if (!options.dryRun) try { debug.log('self-update.edge-rail', 'plan', { decision: plan.decision, reason: plan.reason, seat: options.seat, mainCommit, installed: record.commit, fixes }); } catch { /* log sink optional */ }
|
|
770
|
+
const emit = (result: SelfUpdateResult): SelfUpdateResult => {
|
|
771
|
+
const autoRollback = input.failureRate ? 'active' as const : 'inactive-no-failure-rate' as const;
|
|
772
|
+
const merged = { ...result, edgeRail: plan, missingFixes: fixes, autoRollback };
|
|
773
|
+
out.log(options.json ? JSON.stringify(merged) : `self-update --auto: ${plan.decision} — ${plan.reason} · ${fixes}${autoRollback === 'active' ? '' : ' · 자동 되돌림 비활성 — 실패율 측정 원천 없음'}`);
|
|
774
|
+
return merged;
|
|
775
|
+
};
|
|
776
|
+
if (options.dryRun || plan.decision === 'hold' || plan.decision === 'wait-quiet') return emit({ exitCode: 0, installedVersion: null, decision: null, restarted: false, reason: plan.reason });
|
|
777
|
+
if (plan.decision === 'rollback' && (!record.previous || !versionDirName(record.previous))) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'no previous version for rollback' });
|
|
778
|
+
if (plan.decision === 'rollback' && !deps.restartEdgeRail && liveRestartBlocked()) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'live restart blocked' });
|
|
779
|
+
if (plan.decision === 'rollback') {
|
|
780
|
+
const rolled = (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps);
|
|
781
|
+
if (!rolled.ok) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: rolled.reason });
|
|
782
|
+
// 되돌린 main 은 표시해 둔다 — 캐너리 OK 수가 남아 있어도 다음 실행이 다시 승격하지 못하게.
|
|
783
|
+
if (state?.mainCommit === mainCommit) saveEdgeRailState(stateFile, { ...state, promoted: false, rolledBack: true });
|
|
784
|
+
else saveEdgeRailState(stateFile, { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, rolledBack: true });
|
|
785
|
+
const restarted = await restartEdgeRail(deps, checkout);
|
|
786
|
+
const health = restarted ? await (deps.verifyRestart ?? defaultVerifyRestart)(rolled.record.commit ?? '') : null;
|
|
787
|
+
const confirmed = !!(health?.ok && health.daemonSha && rolled.record.commit
|
|
788
|
+
&& (health.daemonSha.startsWith(rolled.record.commit) || rolled.record.commit.startsWith(health.daemonSha)));
|
|
789
|
+
return emit({ exitCode: confirmed ? 0 : 1, installedVersion: rolled.versionName, decision: null, restarted, current: `versions/${rolled.versionName}`, reason: confirmed ? plan.reason : `${plan.reason}; rollback health not confirmed` });
|
|
790
|
+
}
|
|
791
|
+
if (!deps.restartEdgeRail && liveRestartBlocked()) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'live restart blocked' });
|
|
792
|
+
const installer = deps.installEdgeRail ?? ((next: SelfUpdateOptions, injected: SelfUpdateDeps) => runUpdateForInstallation(next, { cliRoot: injected.cliRoot, checkout: injected }));
|
|
793
|
+
// The existing --from dev installer checks entry existence and restores current even when install.sh fails after relinking.
|
|
794
|
+
const installed = await installer({ from: checkout, dev: true, restart: false }, { ...deps, notice: () => {}, out: { log: () => {}, error: () => {} } });
|
|
795
|
+
if (installed.exitCode !== 0) return emit(installed);
|
|
796
|
+
const nextRecord = readInstallRecord(root);
|
|
797
|
+
const nextName = versionDirName(nextRecord?.versionDir);
|
|
798
|
+
let currentLink = '';
|
|
799
|
+
try { currentLink = readlinkSync(join(root, 'current')); } catch { /* no valid installed current */ }
|
|
800
|
+
if (!nextName || !installEntryExists(root, nextName) || nextRecord?.commit !== mainCommit || currentLink !== nextRecord.versionDir) {
|
|
801
|
+
const rolled = record.versionDir && nextRecord?.previous === record.versionDir ? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
802
|
+
return emit({ ...installed, exitCode: 1, reason: `installed entry/commit not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
803
|
+
}
|
|
804
|
+
const restarted = await restartEdgeRail(deps, checkout);
|
|
805
|
+
// 설치한 판이 재시작·건강에서 실패해 되돌렸으면 같은 main 을 다시 깔지 않게 표시한다.
|
|
806
|
+
const markRolledBack = (): void => saveEdgeRailState(stateFile, { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, rolledBack: true });
|
|
807
|
+
if (!restarted) {
|
|
808
|
+
const rolled = record.versionDir && readInstallRecord(root)?.previous === record.versionDir
|
|
809
|
+
? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
810
|
+
if (rolled?.ok) markRolledBack();
|
|
811
|
+
return emit({ ...installed, exitCode: 1, restarted: false, reason: `${plan.reason}; restart not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
812
|
+
}
|
|
813
|
+
const verified = await (deps.verifyRestart ?? defaultVerifyRestart)(mainCommit);
|
|
814
|
+
if (!verified.ok || !verified.daemonSha || !(verified.daemonSha.startsWith(mainCommit) || mainCommit.startsWith(verified.daemonSha))) {
|
|
815
|
+
const rolled = record.versionDir && readInstallRecord(root)?.previous === record.versionDir
|
|
816
|
+
? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
817
|
+
if (rolled?.ok) { markRolledBack(); await restartEdgeRail(deps, checkout); }
|
|
818
|
+
return emit({ ...installed, exitCode: 1, restarted: true, reason: `${plan.reason}; restarted build health not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
819
|
+
}
|
|
820
|
+
if (plan.decision === 'canary-install') {
|
|
821
|
+
const next: EdgeRailState = { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, ...(options.failureRate ? { failureRate: options.failureRate } : {}) };
|
|
822
|
+
saveEdgeRailState(stateFile, next);
|
|
823
|
+
} else if (state?.mainCommit === mainCommit && plan.decision === 'promote-all') {
|
|
824
|
+
saveEdgeRailState(stateFile, { ...state, promoted: true });
|
|
825
|
+
}
|
|
826
|
+
return emit({ ...installed, restarted: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
465
829
|
/** Route an installed CLI by where it was installed from: a release URL → release installer, a local checkout → git update. */
|
|
466
830
|
export async function runUpdateForInstallation(
|
|
467
831
|
options: SelfUpdateOptions = {},
|
|
468
832
|
deps: { cliRoot?: string; exists?: (path: string) => boolean; readText?: (path: string) => string; release?: ReleaseUpdateDeps; checkout?: SelfUpdateDeps } = {},
|
|
469
833
|
): Promise<SelfUpdateResult> {
|
|
470
834
|
const cliRoot = resolve(deps.cliRoot ?? resolve(import.meta.dir, '../..'));
|
|
835
|
+
if (options.auto) return runEdgeRailUpdate(options, { cliRoot, ...deps.checkout });
|
|
471
836
|
const exists = deps.exists ?? existsSync;
|
|
472
837
|
const holder = resolve(cliRoot, '..', '..');
|
|
473
838
|
const parent = resolve(holder, '..');
|
|
@@ -491,6 +856,9 @@ export async function runUpdateForInstallation(
|
|
|
491
856
|
out.log(options.json ? JSON.stringify(result) : `self-update: ${result.reason}`);
|
|
492
857
|
return result;
|
|
493
858
|
}
|
|
859
|
+
if (options.dev || options.rollback) {
|
|
860
|
+
return runSelfUpdate(options, { cliRoot, ...deps.checkout });
|
|
861
|
+
}
|
|
494
862
|
if (options.version !== undefined) {
|
|
495
863
|
const result: SelfUpdateResult = { exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: '--version 은 릴리스 설치본에서만 사용 가능' };
|
|
496
864
|
const out = deps.checkout?.out ?? console;
|
|
@@ -537,6 +905,15 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
537
905
|
return result;
|
|
538
906
|
};
|
|
539
907
|
const reject = (reason: string): SelfUpdateResult => emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason });
|
|
908
|
+
if (options.rollback) {
|
|
909
|
+
const rolled = rollbackDevInstall(deps);
|
|
910
|
+
if (!rolled.ok) return reject(rolled.reason);
|
|
911
|
+
return emit({
|
|
912
|
+
exitCode: 0, installedVersion: rolled.versionName, decision: null, restarted: false,
|
|
913
|
+
current: `versions/${rolled.versionName}`, previous: rolled.record.previous, channel: rolled.record.channel,
|
|
914
|
+
reason: `rollback: current → ${rolled.versionName}`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
540
917
|
try {
|
|
541
918
|
if (!existsSync(join(checkout, 'scripts/install.sh'))) return reject(`체크아웃 없음 또는 설치기 없음: ${checkout}`);
|
|
542
919
|
const root = git(checkout, ['rev-parse', '--show-toplevel']);
|
|
@@ -546,6 +923,24 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
546
923
|
const dirty = git(checkout, ['diff', '--quiet', 'HEAD', '--']);
|
|
547
924
|
if (dirty.status !== 0) return reject(dirty.status === 1 ? '추적 파일 변경: 체크아웃이 더러움' : `git diff 실패: ${dirty.stderr}`);
|
|
548
925
|
|
|
926
|
+
if (options.dev) {
|
|
927
|
+
try {
|
|
928
|
+
const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path', ...(deps.installRoot ? ['--prefix', deps.installRoot] : [])], checkout) });
|
|
929
|
+
return emit({
|
|
930
|
+
exitCode: 0,
|
|
931
|
+
installedVersion: installed.versionName,
|
|
932
|
+
decision: null,
|
|
933
|
+
restarted: false,
|
|
934
|
+
current: installed.versionDir,
|
|
935
|
+
channel: 'dev',
|
|
936
|
+
previous: installed.previous || undefined,
|
|
937
|
+
reason: `dev 설치: current → ${installed.versionName}`,
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
return emit({ exitCode: 1, installedVersion: null, decision: null, restarted: false, reason: `dev 설치 실패: ${error instanceof Error ? error.message : String(error)}` });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
549
944
|
const silence = { log: (_s: string) => {}, error: (_s: string) => {} };
|
|
550
945
|
let decision: RestartNeededResult;
|
|
551
946
|
try {
|
|
@@ -628,6 +1023,9 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
628
1023
|
const os = deps.os ?? platform();
|
|
629
1024
|
const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
|
|
630
1025
|
if (!command) return emit({ exitCode: 1, installedVersion, decision, restarted: false, prune, reason: `지원하지 않는 플랫폼: ${os}` });
|
|
1026
|
+
if (deps.allowLiveRestart === false || (deps.allowLiveRestart !== true && liveRestartBlocked())) {
|
|
1027
|
+
return emit({ exitCode: 0, installedVersion, decision, restarted: false, prune, reason: '테스트 가드: 실물 재시작 0' });
|
|
1028
|
+
}
|
|
631
1029
|
const args = os === 'darwin' ? ['kickstart', '-k', `gui/${deps.uid ?? process.getuid?.() ?? userInfo().uid}/com.elanous.nexus`] : ['--user', 'restart', 'elanous-nexus'];
|
|
632
1030
|
try {
|
|
633
1031
|
const restarted = run(command, args, checkout);
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
|
|
|
10
10
|
import { collectUnifiedUsage, formatUnifiedUsage } from '../budget/unified-usage.js';
|
|
11
11
|
import { consumeCodexResetCredits, listCodexResetCredits } from '../budget/codex-reset-credits.js';
|
|
12
12
|
import { rollupRunUsage, type RunUsageInput } from '../budget/run-usage-rollup.js';
|
|
13
|
+
import { formatUsageOutcomes, outcomeTableHasCodexGrokUnknown, queryUsageOutcomes, type UsageOutcomesDeps, type UsageOutcomesReport } from '../budget/usage-outcomes.js';
|
|
13
14
|
import { LogStore, logsDbPath, type LogQuery, type LogStoreRow } from '../mss/logging/log-store.js';
|
|
14
15
|
import { LOGS_SINCE_OPTION, parseSince } from './logs-cli.js';
|
|
15
16
|
import { formatCodexCreditPlan, type CodexCreditPlan } from '../budget/codex-credit-plan.js';
|
|
@@ -28,6 +29,9 @@ export interface UsageCliDeps {
|
|
|
28
29
|
/** 계정 이름 → CODEX_HOME. ⛔ 여기 없는 이름은 «거부»한다(모르는 곳에 쓰지 않는다). */
|
|
29
30
|
readonly homes?: Readonly<Record<string, string>>;
|
|
30
31
|
readonly exit?: (code: number) => never;
|
|
32
|
+
/** 런 원장 결과 표. 시험은 디렉터리를 주입한다. */
|
|
33
|
+
readonly outcomes?: (sinceDays: number) => UsageOutcomesReport;
|
|
34
|
+
readonly outcomeDeps?: UsageOutcomesDeps;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/** 계정 이름 → CODEX_HOME 기본 표.
|
|
@@ -134,6 +138,28 @@ export function registerUsageCommand(program: Command, deps: UsageCliDeps = {}):
|
|
|
134
138
|
// 🩸 그래서 ⑴ `--yes` 없이는 «안 쓴다» ⑵ 쓰기 «전»에 남은 수를 보여 준다
|
|
135
139
|
// ⑶ 쓴 «뒤»에도 다시 조회해 「줄었나」를 값으로 낸다.
|
|
136
140
|
// 📏 2026-09-24 현황 예: default 100%·team 100%·third 46% ⇒ third 에 쓰면 54% 가 버려진다.
|
|
141
|
+
const outcomes = deps.outcomes ?? ((sinceDays: number) => queryUsageOutcomes(sinceDays, deps.outcomeDeps));
|
|
142
|
+
usage
|
|
143
|
+
.command('outcomes')
|
|
144
|
+
.description('프로바이더별 무인 착지율 · 라운드 수 · 막힘 사유 분포 (런 원장 childProvider · 미기록은 불명)')
|
|
145
|
+
.option('--days <n>', '최근 N일', '2')
|
|
146
|
+
.option('--json', 'JSON 출력')
|
|
147
|
+
.action((opts: { days?: string; json?: boolean }, command: Command) => {
|
|
148
|
+
const json = opts.json || command.optsWithGlobals<{ json?: boolean }>().json;
|
|
149
|
+
const days = Number(opts.days);
|
|
150
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
151
|
+
out.log(`⛔ --days 는 양의 수: '${opts.days}'`);
|
|
152
|
+
exit(2);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const report = outcomes(days);
|
|
156
|
+
if (json) {
|
|
157
|
+
out.log(JSON.stringify({ ...report, hasCodexGrokUnknown: outcomeTableHasCodexGrokUnknown(report) }, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
out.log(formatUsageOutcomes(report));
|
|
161
|
+
});
|
|
162
|
+
|
|
137
163
|
const list = deps.list ?? listCodexResetCredits;
|
|
138
164
|
const consume = deps.consume ?? consumeCodexResetCredits;
|
|
139
165
|
const homes = deps.homes ?? defaultHomes();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A rebuildable view of the ledger and code, never an authoritative record. */
|
|
2
|
+
export interface ContextCard {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly kind: 'task' | 'goal' | 'run' | 'pr' | 'cell' | 'doc';
|
|
5
|
+
readonly conclusion: string;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
readonly verdict: string;
|
|
8
|
+
readonly remaining: readonly string[];
|
|
9
|
+
readonly pointers: readonly string[];
|
|
10
|
+
readonly source: string;
|
|
11
|
+
readonly updatedAt: string;
|
|
12
|
+
readonly supersedes?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Serialized code points, including JSON escaping; callers may supply a smaller screen budget. */
|
|
16
|
+
export const CONTEXT_CARD_SCREEN_CHARS = 1_600;
|
|
17
|
+
const length = (text: string): number => [...text].length;
|
|
18
|
+
|
|
19
|
+
/** Preserve the decision and provenance; fold only the two expandable lists. Never mutate the card. */
|
|
20
|
+
export function serializeContextCard(card: ContextCard, maxChars = CONTEXT_CARD_SCREEN_CHARS): string {
|
|
21
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new RangeError('maxChars must be a positive safe integer');
|
|
22
|
+
if (/[\r\n\u2028\u2029]/u.test(card.conclusion)) throw new Error('conclusion must be one line');
|
|
23
|
+
const remaining = [...card.remaining];
|
|
24
|
+
const pointers = [...card.pointers];
|
|
25
|
+
const render = () => JSON.stringify({
|
|
26
|
+
id: card.id, kind: card.kind, conclusion: card.conclusion, why: card.why,
|
|
27
|
+
verdict: card.verdict, remaining: remaining.length < card.remaining.length
|
|
28
|
+
? [...remaining, `외 ${card.remaining.length - remaining.length}개`] : remaining,
|
|
29
|
+
pointers: pointers.length < card.pointers.length
|
|
30
|
+
? [...pointers, `외 ${card.pointers.length - pointers.length}개`] : pointers,
|
|
31
|
+
source: card.source, updatedAt: card.updatedAt,
|
|
32
|
+
...(card.supersedes === undefined ? {} : { supersedes: card.supersedes }),
|
|
33
|
+
});
|
|
34
|
+
let text = render();
|
|
35
|
+
while (length(text) > maxChars && (remaining.length || pointers.length)) {
|
|
36
|
+
// Remove the larger last entry first, then break ties by preferring remaining.
|
|
37
|
+
const remainingCost = remaining.length ? length(JSON.stringify(remaining.at(-1))) : -1;
|
|
38
|
+
const pointerCost = pointers.length ? length(JSON.stringify(pointers.at(-1))) : -1;
|
|
39
|
+
if (remainingCost >= pointerCost) remaining.pop();
|
|
40
|
+
else pointers.pop();
|
|
41
|
+
text = render();
|
|
42
|
+
}
|
|
43
|
+
if (length(text) > maxChars) throw new RangeError('card core and omission counts exceed maxChars');
|
|
44
|
+
return text;
|
|
45
|
+
}
|