elanous 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/driver.ts +53 -10
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/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 +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- 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 +93 -12
- 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/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.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/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
package/src/cli/self-update.ts
CHANGED
|
@@ -5,6 +5,10 @@ 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;
|
|
@@ -22,6 +26,14 @@ export interface SelfUpdateOptions {
|
|
|
22
26
|
dev?: boolean;
|
|
23
27
|
/** install.json 의 이전 versionDir 로 current 를 되돌린다. 이전이 없으면 거부. */
|
|
24
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;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export interface SelfUpdateDeps {
|
|
@@ -30,6 +42,8 @@ export interface SelfUpdateDeps {
|
|
|
30
42
|
decide?: (opts: { to: string; cwd: string; out: { log: (s: string) => void; error: (s: string) => void } }) => Promise<RestartNeededResult>;
|
|
31
43
|
run?: (command: string, args: string[], cwd: string, options?: { timeout?: number }) => { status: number | null; stderr: string; stdout?: string };
|
|
32
44
|
installedVersion?: () => string;
|
|
45
|
+
/** 캐너리 시작 «뒤»에 시작해 `completed` 로 끝난 런인가(주입 안 하면 런 원장 `canaryRunSucceededInLedger`). */
|
|
46
|
+
canaryRunSucceeded?: (runId: string, sinceIso: string) => boolean;
|
|
33
47
|
/** 재시작 뒤 데몬이 이 커밋으로 떴나(주입 안 하면 `/v1/health` 를 최대 90초 폴링). */
|
|
34
48
|
verifyRestart?: (expectedCommit: string) => Promise<VerifyRestartResult>;
|
|
35
49
|
/** 판 목록(이름) — 되돌릴 판 찾기. */
|
|
@@ -58,6 +72,13 @@ export interface SelfUpdateDeps {
|
|
|
58
72
|
packageVersion?: (checkout: string) => string;
|
|
59
73
|
/** 재시작을 실제로 호출해도 되는지. NODE_ENV=test · ELANOUS_TEST_HOME 이면 기본 false. */
|
|
60
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>;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export interface SelfUpdateResult {
|
|
@@ -87,6 +108,10 @@ export interface SelfUpdateResult {
|
|
|
87
108
|
relay?: RelayUpdateOutcome;
|
|
88
109
|
/** 텔레그램 러너(`com.elanous.telegram` · `elanous-telegram.service`) 판정 — 🆕 2026-09-25. */
|
|
89
110
|
telegramRunner?: RelayUpdateOutcome;
|
|
111
|
+
edgeRail?: EdgeRailPlan;
|
|
112
|
+
missingFixes?: string;
|
|
113
|
+
/** 실패율 측정 원천이 없으면 자동 되돌림이 판단되지 않는다 — 측정 원천은 다음 조각(EDGE-RAIL2 범위 밖). */
|
|
114
|
+
autoRollback?: 'active' | 'inactive-no-failure-rate';
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
export interface RelayUpdateOutcome {
|
|
@@ -628,12 +653,186 @@ function readInstallSource(cliRoot: string, exists: (path: string) => boolean, r
|
|
|
628
653
|
}
|
|
629
654
|
}
|
|
630
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
|
+
|
|
631
829
|
/** Route an installed CLI by where it was installed from: a release URL → release installer, a local checkout → git update. */
|
|
632
830
|
export async function runUpdateForInstallation(
|
|
633
831
|
options: SelfUpdateOptions = {},
|
|
634
832
|
deps: { cliRoot?: string; exists?: (path: string) => boolean; readText?: (path: string) => string; release?: ReleaseUpdateDeps; checkout?: SelfUpdateDeps } = {},
|
|
635
833
|
): Promise<SelfUpdateResult> {
|
|
636
834
|
const cliRoot = resolve(deps.cliRoot ?? resolve(import.meta.dir, '../..'));
|
|
835
|
+
if (options.auto) return runEdgeRailUpdate(options, { cliRoot, ...deps.checkout });
|
|
637
836
|
const exists = deps.exists ?? existsSync;
|
|
638
837
|
const holder = resolve(cliRoot, '..', '..');
|
|
639
838
|
const parent = resolve(holder, '..');
|
|
@@ -726,7 +925,7 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
726
925
|
|
|
727
926
|
if (options.dev) {
|
|
728
927
|
try {
|
|
729
|
-
const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path'], checkout) });
|
|
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) });
|
|
730
929
|
return emit({
|
|
731
930
|
exitCode: 0,
|
|
732
931
|
installedVersion: installed.versionName,
|
package/src/cli/tasks-cli.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type
|
|
3
|
+
import { InvalidArgumentError, Option, type Command } from 'commander';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
4
5
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
5
6
|
import { readNexusRuntime } from '../nexus/runtime.js';
|
|
6
7
|
import type { Task } from '../task-orchestrator/types.js';
|
|
8
|
+
import { handTask, nextMoveFor, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote, TASK_CARD_PREFIX, TASK_SEATS, type TaskCard, type TaskLauncher, type TaskSeat } from '../task-agent/task-hand.js';
|
|
9
|
+
import { advanceMission, handMission, MissionAdvanceError, openMissionIds, recordPieceRef, type AdvanceMissionResult, type MissionDecompose, type PieceEvidenceReader } from '../task-agent/mission.js';
|
|
7
10
|
|
|
8
11
|
type TaskRow = Pick<Task, 'id' | 'title' | 'priority' | 'status' | 'createdAt' | 'approval' | 'generatedBy'>;
|
|
9
12
|
|
|
@@ -12,6 +15,130 @@ export interface TasksCliDeps {
|
|
|
12
15
|
bearerToken?: string;
|
|
13
16
|
fetch?: typeof fetch;
|
|
14
17
|
output?: (line: string) => void;
|
|
18
|
+
/** `task hand --live` 의 발사기(시험 주입). 기본은 운영 config-dir 로 `harness say` 를 떼어 띄운다. */
|
|
19
|
+
taskLauncher?: TaskLauncher;
|
|
20
|
+
/** 과제 카드 상태 파일(시험 주입). 기본 = `effectiveInstanceRoot()/task-agent-actions.json`. */
|
|
21
|
+
taskStatePath?: string;
|
|
22
|
+
/** logs.db 싱크 등록(시험 주입). 기본 = `registerStandaloneLogSink`. `hand`·`show` 가 부른다. */
|
|
23
|
+
registerSink?: (surface: string) => Promise<unknown>;
|
|
24
|
+
/** `task hand --mission` 의 분해기(시험 주입). 기본 = 나열형은 결정론 · 아니면 `decomposeSelfDevGoal`. */
|
|
25
|
+
missionDecompose?: MissionDecompose;
|
|
26
|
+
/** `task advance` 의 착지 근거 읽기(시험 주입). 기본 = `predecessorState`(gh · 런 원장). */
|
|
27
|
+
pieceEvidence?: PieceEvidenceReader;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `<조각id>=<값>` 반복 인자. */
|
|
31
|
+
function collectPair(value: string, previous: Array<[string, string]> = []): Array<[string, string]> {
|
|
32
|
+
const at = value.indexOf('=');
|
|
33
|
+
if (at <= 0 || at === value.length - 1) throw new InvalidArgumentError(`<조각id>=<값> 모양이 아니다: ${value}`);
|
|
34
|
+
return [...previous, [value.slice(0, at), value.slice(at + 1)]];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function advanceLines(result: AdvanceMissionResult): string[] {
|
|
38
|
+
// 제안이 끝난 미션은 다시 조회하지 않는다 — «착지 0»으로 보이지 않게 따로 적는다.
|
|
39
|
+
if (result.green === 'already-proposed' && Object.keys(result.evidence).length === 0) return [`${result.missionId}\tadvance\t${result.mode}\tgreen already-proposed — 다시 조회·넘기지 않는다`];
|
|
40
|
+
return [
|
|
41
|
+
`${result.missionId}\tadvance\t${result.mode}\t착지 ${result.landed.length} · 준비 ${result.ready.length} · 넘김 ${result.handed.length} · green ${result.green}`,
|
|
42
|
+
...Object.entries(result.evidence).map(([id, state]) => ` 근거: ${id}\t${state}${result.landed.includes(id) ? '\t착지' : ''}`),
|
|
43
|
+
...result.handed.map((piece) => ` 넘김: ${piece.pieceId}\t${piece.launched ? 'launched' : piece.error ? 'failed' : 'not launched'}${piece.error ? `\t${piece.error}` : ''}`),
|
|
44
|
+
...(result.green === 'proposed' ? [' green 제안을 미션 카드에 적었다 — 체크리스트는 바꾸지 않는다(OP/주인이 뒤집는다)'] : []),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `tasks hand`·`show` 가 남기는 `task-agent` 관측의 logs.db surface 라벨. */
|
|
49
|
+
export const TASK_AGENT_SINK_SURFACE = 'task-agent';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 독립 CLI 프로세스는 데몬의 logs.db 싱크를 상속하지 않는다 — 등록하지 않으면 `debug.log('task-agent', …)` 가
|
|
53
|
+
* 파일 트레일에만 남고 `elanous logs --category task-agent` 에 0건으로 보인다(10-06 실측: 카드 3장 · 이벤트 0).
|
|
54
|
+
* fail-open — 등록 실패가 과제 넘기기를 막지 않는다.
|
|
55
|
+
*/
|
|
56
|
+
export async function registerTaskAgentSink(register?: (surface: string) => Promise<unknown>): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const fn = register ?? (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink;
|
|
59
|
+
await fn(TASK_AGENT_SINK_SURFACE);
|
|
60
|
+
} catch { /* fail-open — 파일 트레일이 진실원 */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 기본 발사기 — 지금 도는 진입점으로 `--config-dir <운영 config>` 를 붙여 떼어 띄운다.
|
|
65
|
+
* config-dir 은 명시(`--config-dir`/`--test`)가 있으면 그것, 없으면 운영 루트(`prodInstanceRoot()`)다.
|
|
66
|
+
* (작업 트리에서 그냥 띄우면 test 우주로 가서 동결·몫·풀 상한 밖에서 돈다.)
|
|
67
|
+
*/
|
|
68
|
+
export async function defaultTaskLauncher(args: string[]): Promise<void> {
|
|
69
|
+
const { spawn } = await import('node:child_process');
|
|
70
|
+
const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
|
|
71
|
+
const { prodInstanceRoot } = await import('../instance/resolve.js');
|
|
72
|
+
const configDir = getElanousConfigDirOverride() ?? prodInstanceRoot();
|
|
73
|
+
await spawnDetachedConfirmed(spawn as unknown as DetachedSpawn, process.execPath, [process.argv[1]!, '--config-dir', configDir, ...args]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `spawn` 의 필요한 면만(시험 주입). */
|
|
77
|
+
export type DetachedSpawn = (command: string, args: string[], options: { detached: true; stdio: 'ignore' }) => {
|
|
78
|
+
pid?: number;
|
|
79
|
+
once(event: 'spawn', listener: () => void): unknown;
|
|
80
|
+
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
81
|
+
removeListener(event: string, listener: (...args: never[]) => void): unknown;
|
|
82
|
+
unref(): void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** 발사 확인 대기 상한 — 'spawn'·'error' 어느 쪽도 이 안에 안 오면 pid 유무로 판정한다. */
|
|
86
|
+
export const SPAWN_CONFIRM_TIMEOUT_MS = 2_000;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 떼어 띄우고 «떴는지» 확인한다 — 'spawn' 이면 성공, 'error'(ENOENT·EACCES 등)면 던진다.
|
|
90
|
+
* 확인 없이 돌려주면 비동기 error 가 처리되지 않고 카드가 `launched` 로 남는다(사후 리뷰 must-fix).
|
|
91
|
+
*/
|
|
92
|
+
export function spawnDetachedConfirmed(spawnFn: DetachedSpawn, command: string, args: string[], timeoutMs = SPAWN_CONFIRM_TIMEOUT_MS): Promise<void> {
|
|
93
|
+
return new Promise<void>((resolve, reject) => {
|
|
94
|
+
let child: ReturnType<DetachedSpawn>;
|
|
95
|
+
try { child = spawnFn(command, args, { detached: true, stdio: 'ignore' }); } catch (error) { reject(error); return; }
|
|
96
|
+
let settled = false;
|
|
97
|
+
const finish = (error?: Error) => {
|
|
98
|
+
if (settled) return;
|
|
99
|
+
settled = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
child.removeListener('spawn', onSpawn);
|
|
102
|
+
// error 리스너는 남겨 둔다 — 확인 뒤 늦게 오는 error 도 처리되지 않은 예외가 되지 않게.
|
|
103
|
+
if (error) { reject(error); return; }
|
|
104
|
+
child.unref();
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const onSpawn = () => finish();
|
|
108
|
+
const onError = (error: Error) => finish(error);
|
|
109
|
+
child.once('spawn', onSpawn);
|
|
110
|
+
child.once('error', onError);
|
|
111
|
+
const timer = setTimeout(() => finish(child.pid ? undefined : new Error(`발사 확인 시간 초과(${timeoutMs}ms) · pid 없음`)), timeoutMs);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function moveLine(card: TaskCard): string {
|
|
116
|
+
const move = nextMoveFor(card);
|
|
117
|
+
return `${move.kind}${move.command ? ` — elanous ${move.command.map(shellQuote).join(' ')}` : ''} (${move.reason})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function headLine(text: string): string {
|
|
121
|
+
const first = text.split('\n')[0]!.trim();
|
|
122
|
+
return first.length > 100 ? `${first.slice(0, 100)}…` : first;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function taskCardLines(card: TaskCard): string[] {
|
|
126
|
+
return [
|
|
127
|
+
[card.id, card.status, card.seat ?? '-', card.checklistId ?? '-', card.createdAt, card.text].join('\t'),
|
|
128
|
+
...(card.mission ? [`미션: ${card.mission} · after: ${card.after?.length ? card.after.join(', ') : '-'}`] : []),
|
|
129
|
+
`다음 수: ${moveLine(card)}`,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 미션 카드 — 머리 한 줄 ⊕ 조각 한 줄씩(상태·다음 수·선행) ⊕ 간선 한 줄씩. */
|
|
134
|
+
function missionLines(mission: TaskCard, pieces: TaskCard[]): string[] {
|
|
135
|
+
const edges = pieces.flatMap((piece) => (piece.after ?? []).map((before) => `간선: ${before} -> ${piece.id}`));
|
|
136
|
+
return [
|
|
137
|
+
[mission.id, 'mission', mission.seat ?? '-', mission.checklistId ?? '-', mission.createdAt, `조각 ${pieces.length} · 간선 ${edges.length} · 출처 ${mission.splitSource ?? '-'}`, headLine(mission.text)].join('\t'),
|
|
138
|
+
...pieces.map((piece) => [`조각: ${piece.id}`, piece.status, nextMoveFor(piece).kind, `after: ${piece.after?.length ? piece.after.join(',') : '-'}`, headLine(piece.text)].join('\t')),
|
|
139
|
+
...edges,
|
|
140
|
+
...(mission.greenProposal ? [`green 제안: ${mission.greenProposal.at} · 칸 ${mission.greenProposal.checklistId ?? '-'} · 근거 ${Object.entries(mission.greenProposal.evidence).map(([id, ref]) => `${id}=${ref}`).join(', ')}`] : []),
|
|
141
|
+
];
|
|
15
142
|
}
|
|
16
143
|
|
|
17
144
|
const PRIORITY_RANK: Record<Task['priority'], number> = { urgent: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -89,7 +216,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
89
216
|
if (!auth) throw new NexusUnavailable();
|
|
90
217
|
return auth;
|
|
91
218
|
};
|
|
92
|
-
const tasks = program.command('tasks').alias('task').description('TOX 태스크 조회 및 승인');
|
|
219
|
+
const tasks = program.command('tasks').alias('task').description('TOX 태스크 조회 및 승인 · TASK-AGENT 과제 넘기기(hand)');
|
|
93
220
|
|
|
94
221
|
tasks.command('list')
|
|
95
222
|
.description('우선순위와 생성 시각 순으로 태스크 보기')
|
|
@@ -124,10 +251,189 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
124
251
|
} catch (error) { handleError(error, out); }
|
|
125
252
|
});
|
|
126
253
|
|
|
254
|
+
tasks.command('hand [text]')
|
|
255
|
+
.description('TASK-AGENT 에 과제 한 줄을 넘긴다 — 카드를 적고 첫 수(harness say --substrate pod --merge-by-host)를 고른다 · 기본 shadow(발사 0) · `--mission` 이면 조각 카드 ⊕ 선후로 쪼개 선행 없는 조각만 넘긴다')
|
|
256
|
+
.addOption(new Option('--seat <seat>', `과제 자리 (${TASK_SEATS.join('|')})`).choices([...TASK_SEATS]))
|
|
257
|
+
.option('--checklist <cellId>', '릴리스 체크리스트 칸 id')
|
|
258
|
+
.option('--mission <text>', '미션 문면 — 조각(`- …`·`① …`·`1. …` ⊕ `after: 1`)으로 나열돼 있으면 결정론, 아니면 분해기로 쪼갠다')
|
|
259
|
+
.option('--live', '실제로 발사한다(기본은 shadow — 명령만 출력)')
|
|
260
|
+
.option('--json', 'JSON 출력')
|
|
261
|
+
.action(async (text: string | undefined, opts: { seat?: TaskSeat; checklist?: string; mission?: string; live?: boolean; json?: boolean }) => {
|
|
262
|
+
await registerTaskAgentSink(deps.registerSink);
|
|
263
|
+
if ((text === undefined) === (opts.mission === undefined)) {
|
|
264
|
+
out('과제 한 줄 «또는» --mission <문면> 중 하나만 준다');
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (opts.mission !== undefined) {
|
|
269
|
+
try {
|
|
270
|
+
const result = await handMission({
|
|
271
|
+
text: opts.mission,
|
|
272
|
+
...(opts.seat ? { seat: opts.seat } : {}),
|
|
273
|
+
...(opts.checklist ? { checklistId: opts.checklist } : {}),
|
|
274
|
+
live: opts.live === true,
|
|
275
|
+
...(deps.taskStatePath ? { statePath: deps.taskStatePath } : {}),
|
|
276
|
+
launcher: deps.taskLauncher ?? defaultTaskLauncher,
|
|
277
|
+
...(deps.missionDecompose ? { decompose: deps.missionDecompose } : {}),
|
|
278
|
+
});
|
|
279
|
+
if (opts.json) out(JSON.stringify(result));
|
|
280
|
+
else {
|
|
281
|
+
out(`${result.mission.id}\tmission\t${result.mode}\t조각 ${result.pieces.length} · 간선 ${result.edges.length} · 출처 ${result.source}`);
|
|
282
|
+
for (const piece of result.pieces) {
|
|
283
|
+
out([piece.card.id, piece.launched ? 'launched' : piece.error ? 'failed' : 'not launched', `${piece.move.kind} (${piece.move.reason})`, headLine(piece.card.text)].join('\t'));
|
|
284
|
+
if (piece.move.command) out(` elanous ${piece.move.command.map(shellQuote).join(' ')}`);
|
|
285
|
+
if (piece.error) out(` ${piece.error}`);
|
|
286
|
+
}
|
|
287
|
+
for (const [before, after] of result.edges) out(`간선: ${before} -> ${after}`);
|
|
288
|
+
if (result.mode === 'shadow') out('(shadow — 띄우지 않았다 · 띄우려면 --live)');
|
|
289
|
+
}
|
|
290
|
+
if (result.pieces.some((piece) => piece.error)) process.exitCode = 1;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
out(error instanceof Error ? error.message : '미션 넘기기 실패');
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const result = await handTask({
|
|
299
|
+
text: text!,
|
|
300
|
+
...(opts.seat ? { seat: opts.seat } : {}),
|
|
301
|
+
...(opts.checklist ? { checklistId: opts.checklist } : {}),
|
|
302
|
+
live: opts.live === true,
|
|
303
|
+
...(deps.taskStatePath ? { statePath: deps.taskStatePath } : {}),
|
|
304
|
+
launcher: deps.taskLauncher ?? defaultTaskLauncher,
|
|
305
|
+
});
|
|
306
|
+
if (opts.json) out(JSON.stringify(result));
|
|
307
|
+
else {
|
|
308
|
+
out(`${result.card.id}\t${result.mode}\t${result.launched ? 'launched' : 'not launched'}`);
|
|
309
|
+
out(`elanous ${result.move.command!.map(shellQuote).join(' ')}`);
|
|
310
|
+
if (!result.launched) out('(shadow — 띄우지 않았다 · 띄우려면 --live)');
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
out(error instanceof Error ? error.message : '과제 넘기기 실패');
|
|
314
|
+
process.exitCode = 1;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
tasks.command('advance [missionId]')
|
|
319
|
+
.description('TASK-AGENT 미션 ② — 조각의 PR 병합(근거: `--pr`/`--goal` 로 카드에 적은 PR 번호·골 id)으로 착지를 가르고 새로 준비된 조각만 한 번 넘긴다 · 모두 착지면 green «제안»만 적는다 · 기본 shadow(발사 0)')
|
|
320
|
+
.option('--all', 'green 제안이 아직 없는 미션 전부')
|
|
321
|
+
.option('--pr <pieceId=N>', '조각 카드에 그 런의 PR 번호를 적는다(반복 가능)', collectPair)
|
|
322
|
+
.option('--goal <pieceId=goalId>', '조각 카드에 골 id(16자)를 적는다 — 런 원장 pr-opened 로 PR 을 찾는다(반복 가능)', collectPair)
|
|
323
|
+
.option('--live', '준비된 조각을 실제로 발사한다(기본은 shadow)')
|
|
324
|
+
.option('--json', 'JSON 출력')
|
|
325
|
+
.action(async (missionId: string | undefined, opts: { all?: boolean; pr?: Array<[string, string]>; goal?: Array<[string, string]>; live?: boolean; json?: boolean }) => {
|
|
326
|
+
await registerTaskAgentSink(deps.registerSink);
|
|
327
|
+
const started = Date.now();
|
|
328
|
+
if ((missionId === undefined) === (opts.all !== true)) {
|
|
329
|
+
const reason = '미션 id «또는» --all 중 하나만 준다';
|
|
330
|
+
try { debug.log('task-agent', 'mission-advance', { missionId: missionId ?? null, all: opts.all === true, mode: opts.live ? 'live' : 'shadow', stage: 'cli-args', error: reason, ms: Date.now() - started }); } catch { /* fail-soft */ }
|
|
331
|
+
out(reason);
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const statePath = deps.taskStatePath;
|
|
336
|
+
try {
|
|
337
|
+
// 근거 인자는 운영자 입력이다 — 모양을 «먼저» 전부 검사해 하나라도 틀리면 아무것도 적지 않고 진행도 안 한다.
|
|
338
|
+
const refs: Array<[string, { pr: number } | { goalId: string }]> = [
|
|
339
|
+
...(opts.pr ?? []).map(([pieceId, value]): [string, { pr: number }] => {
|
|
340
|
+
if (!/^#?[1-9]\d*$/.test(value)) throw new Error(`PR 번호가 아니다: ${value}`);
|
|
341
|
+
return [pieceId, { pr: Number(value.replace(/^#/, '')) }];
|
|
342
|
+
}),
|
|
343
|
+
...(opts.goal ?? []).map(([pieceId, value]): [string, { goalId: string }] => {
|
|
344
|
+
if (!/^[a-f0-9]{16}$/.test(value)) throw new Error(`골 id 는 16자 hex 다: ${value}`);
|
|
345
|
+
return [pieceId, { goalId: value }];
|
|
346
|
+
}),
|
|
347
|
+
];
|
|
348
|
+
const twice = refs.map(([pieceId]) => pieceId).filter((pieceId, at, all) => all.indexOf(pieceId) !== at);
|
|
349
|
+
if (twice.length) throw new Error(`한 조각에 근거를 두 번 줬다(--pr·--goal 은 조각마다 하나): ${[...new Set(twice)].join(', ')}`);
|
|
350
|
+
// 기록 실패(없는 조각 · 제안된 미션)는 그 근거만 실패로 적고 계속 — `--all` 의 나머지 미션을 막지 않는다.
|
|
351
|
+
const refFailures: string[] = [];
|
|
352
|
+
for (const [pieceId, ref] of refs) {
|
|
353
|
+
try { recordPieceRef(pieceId, ref, statePath, missionId); } catch (error) {
|
|
354
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
355
|
+
if (missionId !== undefined) throw error;
|
|
356
|
+
refFailures.push(reason);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const ids = missionId !== undefined ? [missionId] : openMissionIds(statePath);
|
|
360
|
+
const results: AdvanceMissionResult[] = [];
|
|
361
|
+
const failures: Array<{ missionId: string; error: string }> = [];
|
|
362
|
+
for (const id of ids) {
|
|
363
|
+
try {
|
|
364
|
+
results.push(await advanceMission(id, {
|
|
365
|
+
live: opts.live === true,
|
|
366
|
+
...(statePath ? { statePath } : {}),
|
|
367
|
+
launcher: deps.taskLauncher ?? defaultTaskLauncher,
|
|
368
|
+
...(deps.pieceEvidence ? { readEvidence: deps.pieceEvidence } : {}),
|
|
369
|
+
}));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
// 미션 id 하나면 그대로 실패 · --all 이면 그 미션만 실패로 적고 다음 미션으로 간다(크론이 한 미션에 막히지 않게).
|
|
372
|
+
if (missionId !== undefined) throw error;
|
|
373
|
+
failures.push({ missionId: id, error: error instanceof Error ? error.message : String(error) });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (missionId === undefined) {
|
|
377
|
+
// --all 은 호출마다 요약 한 줄 — 대상 0·근거 기록 실패뿐인 호출도 관측에 남는다(미션별 줄은 advanceMission 이 남긴다).
|
|
378
|
+
try { debug.log('task-agent', 'mission-advance', { missionId: null, all: true, mode: opts.live ? 'live' : 'shadow', stage: 'all-summary', missions: ids.length, advanced: results.length, failures: failures.map((failure) => failure.missionId), refFailures: refFailures.length, ms: Date.now() - started }); } catch { /* fail-soft */ }
|
|
379
|
+
}
|
|
380
|
+
if (opts.json) out(JSON.stringify(missionId !== undefined ? results[0] : failures.length || refFailures.length ? { results, failures, refFailures } : results));
|
|
381
|
+
else {
|
|
382
|
+
if (results.length === 0 && failures.length === 0) out('넘길 미션이 없다 (green 제안 전 미션 0)');
|
|
383
|
+
for (const failure of failures) out(`${failure.missionId}\tadvance\tfailed\t${failure.error}`);
|
|
384
|
+
for (const reason of refFailures) out(`근거 기록 실패: ${reason}`);
|
|
385
|
+
for (const result of results) for (const line of advanceLines(result)) out(line);
|
|
386
|
+
if (!opts.live && results.some((result) => result.handed.length > 0)) out('(shadow — 띄우지 않았다 · 띄우려면 --live)');
|
|
387
|
+
}
|
|
388
|
+
if (failures.length || refFailures.length || results.some((result) => result.handed.some((piece) => piece.error))) process.exitCode = 1;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const reason = error instanceof Error ? error.message : '미션 진행 실패';
|
|
391
|
+
// advanceMission 에 닿기 전(근거 인자 검사·기록)에 멈춘 호출도 한 줄 — 관측이 «호출마다» 남는다.
|
|
392
|
+
// advanceMission 안에서 던진 것은 거기서 이미 남겼다(같은 오류를 두 번 적지 않는다).
|
|
393
|
+
if (!(error instanceof MissionAdvanceError)) {
|
|
394
|
+
try { debug.log('task-agent', 'mission-advance', { missionId: missionId ?? null, all: opts.all === true, mode: opts.live ? 'live' : 'shadow', stage: 'cli-args', error: reason, ms: Date.now() - started }); } catch { /* fail-soft */ }
|
|
395
|
+
}
|
|
396
|
+
out(reason);
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
127
401
|
tasks.command('show <id>')
|
|
128
|
-
.description(
|
|
402
|
+
.description(`태스크 상세 보기 (id 가 \`${TASK_CARD_PREFIX}\` 로 시작하면 TASK-AGENT 과제 카드 ⊕ 다음 수)`)
|
|
129
403
|
.option('--json', 'JSON 출력')
|
|
130
404
|
.action(async (id: string, opts: { json?: boolean }) => {
|
|
405
|
+
if (id.startsWith(TASK_CARD_PREFIX)) {
|
|
406
|
+
await registerTaskAgentSink(deps.registerSink);
|
|
407
|
+
let card: TaskCard | undefined;
|
|
408
|
+
try { card = readTaskCard(id, deps.taskStatePath); } catch (error) {
|
|
409
|
+
out(error instanceof Error ? error.message : '과제 카드 읽기 실패');
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!card) { out(`과제 카드 없음: ${id}`); process.exitCode = 1; return; }
|
|
414
|
+
if (card.pieces) {
|
|
415
|
+
let all: Record<string, TaskCard>;
|
|
416
|
+
try { all = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(deps.taskStatePath ?? taskStatePathDefault()).tasks ?? {}; } catch (error) {
|
|
417
|
+
out(error instanceof Error ? error.message : '과제 카드 읽기 실패');
|
|
418
|
+
process.exitCode = 1;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const pieces = card.pieces.map((pieceId) => all[pieceId]).filter((piece): piece is TaskCard => piece !== undefined);
|
|
422
|
+
const missingPieces = card.pieces.filter((pieceId) => !all[pieceId]);
|
|
423
|
+
if (opts.json) {
|
|
424
|
+
out(JSON.stringify({ card, nextMove: nextMoveFor(card), pieces: pieces.map((piece) => ({ card: piece, nextMove: nextMoveFor(piece) })), missingPieces }));
|
|
425
|
+
if (missingPieces.length) process.exitCode = 1;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
for (const line of missionLines(card, pieces)) out(line);
|
|
429
|
+
for (const missing of missingPieces) { out(`조각 카드 없음: ${missing} — 선후 기록이 불완전하다`); process.exitCode = 1; }
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (opts.json) out(JSON.stringify({ card, nextMove: nextMoveFor(card) }));
|
|
434
|
+
else for (const line of taskCardLines(card)) out(line);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
131
437
|
try {
|
|
132
438
|
activeToken = undefined;
|
|
133
439
|
const auth = authenticate();
|