elanous 0.2.18 → 0.2.20
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-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.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 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- 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 +16 -15
- 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 +16 -15
- 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 +24 -23
- 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/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIBLING-RESYNC-ON-MERGE (0.2.21 P0) — when one harness PR merges, the open sibling PRs that touch the
|
|
3
|
+
* same paths learn it «now», not when their gate fails or main-sync breaks.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-10-07: 76% of launches overlap an open PR or a running run on the same paths.
|
|
6
|
+
*
|
|
7
|
+
* Delivery reuses existing channels only:
|
|
8
|
+
* · live owning run → one supervisor memo in its control inbox (`elanous self send --memo` path)
|
|
9
|
+
* · no live run → the registered `elanous:needs-rebase` label (QUIET-PR-COMMENTS: never a comment)
|
|
10
|
+
*
|
|
11
|
+
* Cost bound (#24893 review should-fix ①, measured 186 open PRs → 186 `…/files` calls per merge):
|
|
12
|
+
* · prefilter — a PR already carrying the marker is skipped before any file fetch
|
|
13
|
+
* · priority — PRs whose branch shares path tokens with the merged files are fetched first
|
|
14
|
+
* · fetch cap — at most `maxFetch` file inventories per merge (config `harness.siblingResync.maxFetch`, default 40)
|
|
15
|
+
* · concurrency — at most `fetchConcurrency` (default 6) fetches in flight
|
|
16
|
+
*
|
|
17
|
+
* Pure core: every side effect is an injected adapter so tests never touch GitHub or the inbox.
|
|
18
|
+
*/
|
|
19
|
+
import { debug } from '../debug/log.js';
|
|
20
|
+
import type { ControlMemoPayload } from '../harness/control-inbox.js';
|
|
21
|
+
|
|
22
|
+
export const SIBLING_RESYNC_CATEGORY = 'self-implement.sibling-resync';
|
|
23
|
+
export const SIBLING_RESYNC_DEFAULT_CAP = 10;
|
|
24
|
+
export const SIBLING_RESYNC_DEFAULT_MAX_FETCH = 40;
|
|
25
|
+
export const SIBLING_RESYNC_DEFAULT_FETCH_CONCURRENCY = 6;
|
|
26
|
+
export const SIBLING_RESYNC_MARKER_LABEL = 'elanous:needs-rebase';
|
|
27
|
+
const HARNESS_BRANCH_PREFIX = 'self-impl/';
|
|
28
|
+
|
|
29
|
+
/** An open PR before its file inventory is fetched. */
|
|
30
|
+
export interface SiblingResyncCandidate {
|
|
31
|
+
readonly number: number;
|
|
32
|
+
readonly branch: string;
|
|
33
|
+
readonly labels: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SiblingResyncPr extends SiblingResyncCandidate {
|
|
37
|
+
/** `undefined` means the file list could not be proven complete — such a PR never qualifies. */
|
|
38
|
+
readonly files: readonly string[] | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SiblingRunTarget =
|
|
42
|
+
| { readonly alive: true; readonly runId: string; readonly spaceId: string }
|
|
43
|
+
| { readonly alive: false; readonly runId?: string; readonly reason: string };
|
|
44
|
+
|
|
45
|
+
export interface SiblingResyncAdapters {
|
|
46
|
+
/** Open PRs (draft or ready) against the default branch, without files. A rejection is not an empty list. */
|
|
47
|
+
listOpenPrs(): Promise<readonly SiblingResyncCandidate[]>;
|
|
48
|
+
/** One PR's files; `undefined` (or a rejection) means unknown and that PR never qualifies. */
|
|
49
|
+
prFiles(number: number): Promise<readonly string[] | undefined>;
|
|
50
|
+
/** The merged PR's files; `undefined` means unknown and nothing is sent. */
|
|
51
|
+
mergedFiles(): Promise<readonly string[] | undefined>;
|
|
52
|
+
resolveRun(pr: SiblingResyncPr): Promise<SiblingRunTarget>;
|
|
53
|
+
sendResync(target: { runId: string; spaceId: string }, memo: ControlMemoPayload): Promise<void> | void;
|
|
54
|
+
addMarker(prNumber: number, label: string): Promise<void> | void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SiblingResyncResult {
|
|
58
|
+
readonly requested: readonly number[];
|
|
59
|
+
readonly marked: readonly number[];
|
|
60
|
+
readonly failed: readonly number[];
|
|
61
|
+
readonly skippedOverCap: readonly number[];
|
|
62
|
+
/** Already marked before any fetch — not fetched. */
|
|
63
|
+
readonly skippedPrefilter: readonly number[];
|
|
64
|
+
/** Over the per-merge fetch cap — not fetched. */
|
|
65
|
+
readonly skippedFetchCap: readonly number[];
|
|
66
|
+
readonly filesFetched: number;
|
|
67
|
+
readonly durationMs: number;
|
|
68
|
+
readonly reason?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function siblingResyncMemo(merged: number, overlap: readonly string[]): ControlMemoPayload {
|
|
72
|
+
const shown = overlap.slice(0, 5).join(', ');
|
|
73
|
+
const more = overlap.length > 5 ? ` (+${overlap.length - 5} more)` : '';
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
kind: 'sibling-resync',
|
|
77
|
+
urgency: 'normal',
|
|
78
|
+
body: `[sibling-resync] Sibling PR #${merged} just merged into main touching the same paths: ${shown}${more}. `
|
|
79
|
+
+ 'Before continuing: git fetch origin main, rebase this branch onto origin/main (resolve conflicts keeping both intents), then re-run the gate on your changed files.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Exact path intersection; a PR without a provable file list is never a sibling. */
|
|
84
|
+
export function overlappingFiles(merged: readonly string[], sibling: readonly string[] | undefined): string[] {
|
|
85
|
+
if (!sibling || sibling.length === 0) return [];
|
|
86
|
+
const mergedSet = new Set(merged);
|
|
87
|
+
return [...new Set(sibling.filter((file) => mergedSet.has(file)))].sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const GENERIC_TOKENS = new Set(['src', 'test', 'tests', 'index', 'scripts', 'docs', 'goals', 'goal', 'self', 'impl', 'apps', 'main', 'json', 'html', 'spec', 'types', 'lib']);
|
|
91
|
+
|
|
92
|
+
/** Lower-case path tokens (dir segments and file stems split on `-_.`), generic and short ones removed. */
|
|
93
|
+
export function pathTokens(text: string): Set<string> {
|
|
94
|
+
return new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 3 && !GENERIC_TOKENS.has(token) && !/^\d+$/.test(token)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Shared path tokens between the merged files and a branch name — a cheap ordering hint, never a filter. */
|
|
98
|
+
export function branchAffinity(mergedTokens: ReadonlySet<string>, branch: string): number {
|
|
99
|
+
let score = 0;
|
|
100
|
+
for (const token of pathTokens(branch)) if (mergedTokens.has(token)) score += 1;
|
|
101
|
+
return score;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function mapBounded<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]> {
|
|
105
|
+
const results = new Array<R>(items.length);
|
|
106
|
+
let next = 0;
|
|
107
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
108
|
+
while (next < items.length) {
|
|
109
|
+
const index = next++;
|
|
110
|
+
results[index] = await work(items[index]!);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await Promise.all(workers);
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nonNegativeInt = (value: number | undefined, fallback: number): number =>
|
|
118
|
+
value !== undefined && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
119
|
+
|
|
120
|
+
export async function requestSiblingResync(input: {
|
|
121
|
+
merged: { number: number };
|
|
122
|
+
adapters: SiblingResyncAdapters;
|
|
123
|
+
cap?: number;
|
|
124
|
+
maxFetch?: number;
|
|
125
|
+
fetchConcurrency?: number;
|
|
126
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
127
|
+
now?: () => number;
|
|
128
|
+
}): Promise<SiblingResyncResult> {
|
|
129
|
+
const { merged, adapters } = input;
|
|
130
|
+
const now = input.now ?? Date.now;
|
|
131
|
+
const startedAt = now();
|
|
132
|
+
const cap = nonNegativeInt(input.cap, SIBLING_RESYNC_DEFAULT_CAP);
|
|
133
|
+
const maxFetch = nonNegativeInt(input.maxFetch, SIBLING_RESYNC_DEFAULT_MAX_FETCH);
|
|
134
|
+
const concurrency = Math.max(1, nonNegativeInt(input.fetchConcurrency, SIBLING_RESYNC_DEFAULT_FETCH_CONCURRENCY));
|
|
135
|
+
const log = input.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
136
|
+
const emit = (event: string, data: Record<string, unknown>) => {
|
|
137
|
+
try { log(SIBLING_RESYNC_CATEGORY, event, { merged: merged.number, ...data }); } catch { /* fail-soft */ }
|
|
138
|
+
};
|
|
139
|
+
const requested: number[] = [];
|
|
140
|
+
const marked: number[] = [];
|
|
141
|
+
const failed: number[] = [];
|
|
142
|
+
const skippedOverCap: number[] = [];
|
|
143
|
+
const skippedPrefilter: number[] = [];
|
|
144
|
+
const skippedFetchCap: number[] = [];
|
|
145
|
+
let filesFetched = 0;
|
|
146
|
+
const result = (reason?: string): SiblingResyncResult => ({
|
|
147
|
+
requested, marked, failed, skippedOverCap, skippedPrefilter, skippedFetchCap, filesFetched,
|
|
148
|
+
durationMs: Math.max(0, now() - startedAt), ...(reason ? { reason } : {}),
|
|
149
|
+
});
|
|
150
|
+
let mergedFiles: readonly string[] | undefined;
|
|
151
|
+
let open: readonly SiblingResyncCandidate[];
|
|
152
|
+
try {
|
|
153
|
+
mergedFiles = await adapters.mergedFiles();
|
|
154
|
+
if (!mergedFiles || mergedFiles.length === 0) {
|
|
155
|
+
const out = result('merged-files-unknown');
|
|
156
|
+
emit('skipped', { reason: out.reason, durationMs: out.durationMs });
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
open = await adapters.listOpenPrs();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const out = result('inventory-failed');
|
|
162
|
+
emit('skipped', { reason: out.reason, error: String(error), durationMs: out.durationMs });
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
const mergedTokens = new Set<string>();
|
|
166
|
+
for (const file of mergedFiles) for (const token of pathTokens(file)) mergedTokens.add(token);
|
|
167
|
+
const candidates: { pr: SiblingResyncCandidate; affinity: number }[] = [];
|
|
168
|
+
for (const pr of open) {
|
|
169
|
+
if (pr.number === merged.number || !pr.branch.startsWith(HARNESS_BRANCH_PREFIX)) continue;
|
|
170
|
+
// Already marked: the quiet signal is in place, and a re-fetch would only re-confirm it.
|
|
171
|
+
if (pr.labels.includes(SIBLING_RESYNC_MARKER_LABEL)) { skippedPrefilter.push(pr.number); continue; }
|
|
172
|
+
candidates.push({ pr, affinity: branchAffinity(mergedTokens, pr.branch) });
|
|
173
|
+
}
|
|
174
|
+
// Most likely siblings first; ties go to the newest PR (most likely to have a live run).
|
|
175
|
+
candidates.sort((a, b) => b.affinity - a.affinity || b.pr.number - a.pr.number);
|
|
176
|
+
const toFetch = candidates.slice(0, maxFetch);
|
|
177
|
+
skippedFetchCap.push(...candidates.slice(maxFetch).map(({ pr }) => pr.number));
|
|
178
|
+
const fetched = await mapBounded(toFetch, concurrency, async ({ pr }): Promise<SiblingResyncPr> => {
|
|
179
|
+
let files: readonly string[] | undefined;
|
|
180
|
+
try { files = await adapters.prFiles(pr.number); } catch { files = undefined; }
|
|
181
|
+
filesFetched += 1;
|
|
182
|
+
return { ...pr, files };
|
|
183
|
+
});
|
|
184
|
+
const siblings = fetched
|
|
185
|
+
.map((pr) => ({ pr, overlap: overlappingFiles(mergedFiles!, pr.files) }))
|
|
186
|
+
.filter(({ overlap }) => overlap.length > 0)
|
|
187
|
+
.sort((a, b) => a.pr.number - b.pr.number);
|
|
188
|
+
for (const [index, { pr, overlap }] of siblings.entries()) {
|
|
189
|
+
if (index >= cap) { skippedOverCap.push(pr.number); continue; }
|
|
190
|
+
let target: SiblingRunTarget;
|
|
191
|
+
try {
|
|
192
|
+
target = await adapters.resolveRun(pr);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
target = { alive: false, reason: `run-resolution-failed: ${String(error)}` };
|
|
195
|
+
}
|
|
196
|
+
if (target.alive) {
|
|
197
|
+
try {
|
|
198
|
+
await adapters.sendResync({ runId: target.runId, spaceId: target.spaceId }, siblingResyncMemo(merged.number, overlap));
|
|
199
|
+
requested.push(pr.number);
|
|
200
|
+
emit('requested', { sibling: pr.number, runId: target.runId, spaceId: target.spaceId, overlap });
|
|
201
|
+
continue;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// A live run we could not reach still needs to be visible — fall through to the quiet marker.
|
|
204
|
+
emit('send-failed', { sibling: pr.number, runId: target.runId, error: String(error) });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await adapters.addMarker(pr.number, SIBLING_RESYNC_MARKER_LABEL);
|
|
209
|
+
marked.push(pr.number);
|
|
210
|
+
emit('marked', { sibling: pr.number, label: SIBLING_RESYNC_MARKER_LABEL, reason: target.alive ? 'send-failed' : target.reason, ...(target.runId ? { runId: target.runId } : {}), overlap });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
failed.push(pr.number);
|
|
213
|
+
emit('mark-failed', { sibling: pr.number, error: String(error) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const out = result();
|
|
217
|
+
emit('summary', {
|
|
218
|
+
siblings: siblings.length, requested, marked, failed, skippedOverCap, cap,
|
|
219
|
+
filesFetched, skippedPrefilter: skippedPrefilter.length, skippedFetchCap: skippedFetchCap.length, maxFetch, concurrency,
|
|
220
|
+
durationMs: out.durationMs,
|
|
221
|
+
});
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STOP-AUTOHEAL (0.2.20 P0 · 첫 조각) — 멈춘 런의 stop 레코드(STOP-RECORD #24907)를 읽고
|
|
3
|
+
* 갈래마다 «한 번»만 스스로 고쳐 본다. 안 되면 `needs-owner` 로 사람에게 넘긴다.
|
|
4
|
+
*
|
|
5
|
+
* S: 런이 멈추면 원장에 `event='stop'` 한 줄(class·cause·evidenceRef·nextMove)이 남는다.
|
|
6
|
+
* C: 그 줄을 읽는 쪽이 없어서 환경 결손·main 동기화·Pod 사망·증거 누락처럼 «기계가 고칠 수 있는» 멈춤도 사람을 기다렸다.
|
|
7
|
+
* Q: 갈래별로 한 수를 정해 두고 «한 번만» 시도할 수 있나?
|
|
8
|
+
* A: `autohealFromStop` 하나가 class → action 을 고르고, 원장 `event='autoheal'` 줄로 시도를 남긴다.
|
|
9
|
+
* 같은 런·같은 갈래·같은 모드에 이미 줄이 있으면 다시 하지 않는다(원장 가드).
|
|
10
|
+
*
|
|
11
|
+
* 사다리(class → action):
|
|
12
|
+
* env-unrelated → clean-workspace-regate 깨끗한 자리에서 gate 만 다시 1회(GATE-ENV-RETRY 재사용 자리)
|
|
13
|
+
* main-sync → rebase-regate 리베이스(MERGE-INTENT-RESOLVE 의도 해석) 뒤 재게이트
|
|
14
|
+
* pod-died → resume-from-salvage `salvage/run-<6hex>/<leaf>` 가지에서 resume(DRAFT-NOT-ARCHIVE)
|
|
15
|
+
* evidence-uncovered → derive-evidence-rejudge 런 출력·시험·배선에서 증거를 끌어내 1회 재판정
|
|
16
|
+
* review-repeat · review-out-of-scope · fabric · launch-failed · unclassified → needs-owner(새 라운드 없음)
|
|
17
|
+
*
|
|
18
|
+
* 모드 `tools.selfImplement.autoheal.mode` = `shadow`(기본: 할 일을 원장에만 적고 손대지 않는다) | `live`.
|
|
19
|
+
* ⛔ live 에서도 행동은 주입 seam 으로만 한다 — seam 이 없는 행동은 `needs-owner`(action-unwired)로 떨어진다.
|
|
20
|
+
* «안 이어진 손»을 「고쳤다」로 읽지 않게.
|
|
21
|
+
* 관측 = `debug.log('self-implement.autoheal', …)` ⊕ 원장 `event='autoheal'`.
|
|
22
|
+
*/
|
|
23
|
+
import { debug } from '../debug/log.js';
|
|
24
|
+
import { appendRunLedgerEntry, loadRunLedger, type RunLedgerEntry, type RunLedgerWriter } from './run-ledger.js';
|
|
25
|
+
import type { RunStopClass, RunStopRecord } from './run-stop.js';
|
|
26
|
+
|
|
27
|
+
export const AUTOHEAL_EVENT = 'autoheal';
|
|
28
|
+
export type AutohealMode = 'shadow' | 'live';
|
|
29
|
+
export const DEFAULT_AUTOHEAL_MODE: AutohealMode = 'shadow';
|
|
30
|
+
|
|
31
|
+
export type AutohealAction =
|
|
32
|
+
| 'clean-workspace-regate'
|
|
33
|
+
| 'rebase-regate'
|
|
34
|
+
| 'resume-from-salvage'
|
|
35
|
+
| 'derive-evidence-rejudge'
|
|
36
|
+
| 'needs-owner';
|
|
37
|
+
|
|
38
|
+
/** 닫힌 목록 전체를 덮는다 — 새 class 가 생기면 여기서 타입 오류가 난다. */
|
|
39
|
+
export const AUTOHEAL_ACTION_BY_CLASS: Readonly<Record<RunStopClass, AutohealAction>> = {
|
|
40
|
+
'env-unrelated': 'clean-workspace-regate',
|
|
41
|
+
'main-sync': 'rebase-regate',
|
|
42
|
+
'pod-died': 'resume-from-salvage',
|
|
43
|
+
'evidence-uncovered': 'derive-evidence-rejudge',
|
|
44
|
+
'review-repeat': 'needs-owner',
|
|
45
|
+
'review-out-of-scope': 'needs-owner',
|
|
46
|
+
fabric: 'needs-owner',
|
|
47
|
+
'launch-failed': 'needs-owner',
|
|
48
|
+
unclassified: 'needs-owner',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** 설정 원값 → 모드. 모르는 값은 «좁은 쪽»(shadow). */
|
|
52
|
+
export function parseAutohealMode(raw: unknown): AutohealMode {
|
|
53
|
+
return raw === 'live' ? 'live' : 'shadow';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AutohealContext {
|
|
57
|
+
runId: string;
|
|
58
|
+
goalId?: string;
|
|
59
|
+
record: RunStopRecord;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AutohealActionResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
/** 한 줄 — 무엇을 했나/왜 못 했나. */
|
|
65
|
+
detail?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type AutohealActionFn = (ctx: AutohealContext) => Promise<AutohealActionResult> | AutohealActionResult;
|
|
69
|
+
|
|
70
|
+
/** 행동 seam — needs-owner 를 뺀 넷. 없는 seam 은 live 에서도 실행하지 않는다. */
|
|
71
|
+
export type AutohealActions = Partial<Record<Exclude<AutohealAction, 'needs-owner'>, AutohealActionFn>>;
|
|
72
|
+
|
|
73
|
+
export type AutohealOutcome =
|
|
74
|
+
/** shadow — 할 일을 적기만 했다. */
|
|
75
|
+
| 'would-act'
|
|
76
|
+
/** live — 행동을 했고 성공했다. */
|
|
77
|
+
| 'healed'
|
|
78
|
+
/** 사람에게 넘긴다(갈래가 needs-owner 이거나 행동이 실패·미배선). */
|
|
79
|
+
| 'needs-owner'
|
|
80
|
+
/** 같은 런·갈래·모드에 이미 시도가 있다 — 아무것도 안 했다(원장에 새 줄도 안 쓴다). */
|
|
81
|
+
| 'already-attempted';
|
|
82
|
+
|
|
83
|
+
export interface AutohealResult {
|
|
84
|
+
runId: string;
|
|
85
|
+
class: RunStopClass;
|
|
86
|
+
action: AutohealAction;
|
|
87
|
+
mode: AutohealMode;
|
|
88
|
+
outcome: AutohealOutcome;
|
|
89
|
+
/** needs-owner 일 때 사람이 볼 이유(멈춘 원인 그대로 또는 행동 실패). */
|
|
90
|
+
reason?: string;
|
|
91
|
+
detail?: string;
|
|
92
|
+
entry?: RunLedgerEntry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface AutohealDeps {
|
|
96
|
+
mode?: AutohealMode;
|
|
97
|
+
actions?: AutohealActions;
|
|
98
|
+
/** 가드용 원장 읽기 — 기본 `loadRunLedger(runId)`. null = 원장 없음(첫 시도). */
|
|
99
|
+
loadLedger?: (runId: string) => readonly RunLedgerEntry[] | null;
|
|
100
|
+
writeLedger?: RunLedgerWriter;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 이 프로세스가 쓴 autoheal 줄 — 원장 읽기가 주입 writer 와 다른 곳을 보거나(시험 seam) 비어 있어도
|
|
105
|
+
* 같은 프로세스 안에서는 «한 번»이 지켜지게 한다(리뷰 must-fix #24912).
|
|
106
|
+
*/
|
|
107
|
+
const writtenThisProcess = new Map<string, RunLedgerEntry[]>();
|
|
108
|
+
const WRITTEN_MAX_RUNS = 4096;
|
|
109
|
+
|
|
110
|
+
/** 시험용 — 프로세스 기억을 비운다. */
|
|
111
|
+
export function resetAutohealProcessMemory(): void {
|
|
112
|
+
writtenThisProcess.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 원장 가드 — 같은 런·같은 갈래·같은 모드의 autoheal 줄이 있으면 참. */
|
|
116
|
+
export function autohealAlreadyAttempted(ledger: readonly RunLedgerEntry[] | null, cls: RunStopClass, mode: AutohealMode): boolean {
|
|
117
|
+
if (!ledger) return false;
|
|
118
|
+
return ledger.some((e) => e.event === AUTOHEAL_EVENT && e.data?.class === cls && e.data?.mode === mode);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function observe(event: string, data: Record<string, unknown>): void {
|
|
122
|
+
try { debug.log('self-implement.autoheal', event, data); } catch { /* observation is fail-soft */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* stop 레코드 하나 → 최대 한 번의 힐 시도. 예외를 던지지 않는다(행동 seam 의 throw 도 needs-owner 로 접는다).
|
|
127
|
+
*/
|
|
128
|
+
export async function autohealFromStop(ctx: AutohealContext, deps: AutohealDeps = {}): Promise<AutohealResult> {
|
|
129
|
+
const mode = deps.mode ?? DEFAULT_AUTOHEAL_MODE;
|
|
130
|
+
const cls = ctx.record.class;
|
|
131
|
+
const action = AUTOHEAL_ACTION_BY_CLASS[cls] ?? 'needs-owner';
|
|
132
|
+
const base = { runId: ctx.runId, class: cls, action, mode };
|
|
133
|
+
|
|
134
|
+
let ledger: readonly RunLedgerEntry[] | null = null;
|
|
135
|
+
try {
|
|
136
|
+
ledger = (deps.loadLedger ?? ((id) => loadRunLedger(id)))(ctx.runId);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// ⛔ 가드를 못 읽으면 «한 번»을 보장할 수 없다 — live 행동을 하지 않고 사람에게 넘긴다.
|
|
139
|
+
const reason = `ledger-unreadable: ${error instanceof Error ? error.message : String(error)}`;
|
|
140
|
+
observe('guard-unreadable', { ...base, reason });
|
|
141
|
+
return { ...base, outcome: 'needs-owner', reason };
|
|
142
|
+
}
|
|
143
|
+
if (autohealAlreadyAttempted(ledger, cls, mode) || autohealAlreadyAttempted(writtenThisProcess.get(ctx.runId) ?? null, cls, mode)) {
|
|
144
|
+
observe('already-attempted', base);
|
|
145
|
+
return { ...base, outcome: 'already-attempted' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let outcome: AutohealOutcome;
|
|
149
|
+
let reason: string | undefined;
|
|
150
|
+
let detail: string | undefined;
|
|
151
|
+
if (action === 'needs-owner') {
|
|
152
|
+
outcome = 'needs-owner';
|
|
153
|
+
reason = `stop ${cls}: ${ctx.record.cause}`;
|
|
154
|
+
} else if (mode === 'shadow') {
|
|
155
|
+
outcome = 'would-act';
|
|
156
|
+
} else {
|
|
157
|
+
const fn = deps.actions?.[action];
|
|
158
|
+
if (!fn) {
|
|
159
|
+
outcome = 'needs-owner';
|
|
160
|
+
reason = `action-unwired: ${action} (stop ${cls}: ${ctx.record.cause})`;
|
|
161
|
+
} else {
|
|
162
|
+
// ⭐ 시도 «전»에 원장 줄을 쓴다 — 행동 중 프로세스가 죽어도 두 번째 시도가 막힌다.
|
|
163
|
+
// ⛔ 그 줄을 못 쓰면 «한 번»을 보장할 수 없으니 행동하지 않는다.
|
|
164
|
+
if (!writeEntry(ctx, deps, { ...base, phase: 'attempt' })) {
|
|
165
|
+
const unguarded = `guard-write-failed: ${action} not attempted (stop ${cls}: ${ctx.record.cause})`;
|
|
166
|
+
observe('needs-owner', { ...base, reason: unguarded });
|
|
167
|
+
return { ...base, outcome: 'needs-owner', reason: unguarded };
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const res = await fn(ctx);
|
|
171
|
+
detail = res.detail;
|
|
172
|
+
outcome = res.ok ? 'healed' : 'needs-owner';
|
|
173
|
+
if (!res.ok) reason = `${action} failed${res.detail ? `: ${res.detail}` : ''}`;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
outcome = 'needs-owner';
|
|
176
|
+
reason = `${action} threw: ${error instanceof Error ? error.message : String(error)}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const entry = writeEntry(ctx, deps, {
|
|
182
|
+
...base, phase: 'result', outcome,
|
|
183
|
+
...(reason ? { reason } : {}), ...(detail ? { detail } : {}),
|
|
184
|
+
evidenceRef: ctx.record.evidenceRef, nextMove: ctx.record.nextMove,
|
|
185
|
+
});
|
|
186
|
+
observe(outcome, { ...base, ...(reason ? { reason } : {}), ...(detail ? { detail } : {}) });
|
|
187
|
+
return { ...base, outcome, ...(reason ? { reason } : {}), ...(detail ? { detail } : {}), ...(entry ? { entry } : {}) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function writeEntry(ctx: AutohealContext, deps: AutohealDeps, data: Record<string, unknown>): RunLedgerEntry | undefined {
|
|
191
|
+
const entry: RunLedgerEntry = {
|
|
192
|
+
timestamp: new Date().toISOString(), runId: ctx.runId, event: AUTOHEAL_EVENT, data,
|
|
193
|
+
...(ctx.goalId ? { goalId: ctx.goalId } : {}),
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
(deps.writeLedger ?? appendRunLedgerEntry)(entry);
|
|
197
|
+
if (!writtenThisProcess.has(ctx.runId) && writtenThisProcess.size >= WRITTEN_MAX_RUNS) writtenThisProcess.clear();
|
|
198
|
+
writtenThisProcess.set(ctx.runId, [...(writtenThisProcess.get(ctx.runId) ?? []), entry]);
|
|
199
|
+
return entry;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
observe('ledger-write-failed', { runId: ctx.runId, error: error instanceof Error ? error.message : String(error) });
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { copyFileSync, lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { judgeNextMove } from './judge.js';
|
|
5
|
+
import { checkCompletionEvidence } from './completion-evidence.js';
|
|
6
|
+
|
|
7
|
+
export type DeliveryKind = 'research-report' | 'artifact' | 'content';
|
|
8
|
+
export interface DeliveryEvidence { ok: boolean; files: string[]; reason: string }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DEFAULT-HANDS-a: 판독은 복사된 최종 산출물에서만 하고, 규칙은 `completion-evidence.ts` 의
|
|
12
|
+
* `checkCompletionEvidence` 하나를 쓴다(종류별 규칙을 여기서 다시 짓지 않는다).
|
|
13
|
+
* 후보 중 하나라도 그 종류의 증거를 다 갖추면 ok — 후보 목록엔 곁 파일(미리보기 등)도 섞인다.
|
|
14
|
+
*/
|
|
15
|
+
export function readDeliveryEvidence(kind: DeliveryKind, outputDir: string, files: readonly string[]): DeliveryEvidence {
|
|
16
|
+
if (!files.length) return { ok: false, files: [], reason: '전달 후보 없음' };
|
|
17
|
+
const missing = new Set<string>();
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const verdict = checkCompletionEvidence(kind, { dir: outputDir, ref: relative(outputDir, file) });
|
|
20
|
+
if (verdict.ok) return { ok: true, files: [...files], reason: `전달 산출물 확인: ${verdict.ref}` };
|
|
21
|
+
for (const item of verdict.missing) missing.add(item);
|
|
22
|
+
}
|
|
23
|
+
return { ok: false, files: [...files], reason: `전달 증거 부족: ${[...missing].join(', ')}` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type NextActionKind = 'review' | 'land' | 'retry' | 'green' | 'decision' | 'deliver';
|
|
2
27
|
/**
|
|
3
28
|
* `retry` 의 갈래(RFC §A9 G1·G2). 없으면 종전 그대로 «must-fix 로 좁힌 재발사»다(must-fix 가 없으면 카드).
|
|
4
29
|
* - narrow: 범위를 좁혀 한 번 재발사(must-fix 없이도 간다 — no-progress)
|
|
@@ -22,6 +47,7 @@ export interface NextAction {
|
|
|
22
47
|
mustFix?: string[];
|
|
23
48
|
cwd?: string;
|
|
24
49
|
risk?: 'money' | 'security' | 'irreversible-publication';
|
|
50
|
+
delivery?: { kind: DeliveryKind; worktreePath: string; projectTarget: string; files: string[] };
|
|
25
51
|
}
|
|
26
52
|
export interface ActionDeps {
|
|
27
53
|
command(args: string[]): Promise<{ status: number; stdout: string; stderr?: string }>;
|
|
@@ -63,7 +89,11 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
|
|
|
63
89
|
deps.observe('task-agent.action', { taskId: action.taskId, kind: action.kind, result, reason });
|
|
64
90
|
return result;
|
|
65
91
|
};
|
|
66
|
-
if (deps.mode !== 'live')
|
|
92
|
+
if (deps.mode !== 'live') {
|
|
93
|
+
const destination = action.delivery && action.kind === 'deliver'
|
|
94
|
+
? ` → ${join(action.delivery.projectTarget, 'elanous-out', action.taskId)}/${action.delivery.files.join(', ')}` : '';
|
|
95
|
+
return record('shadow', `would ${action.kind}${action.variant ? `:${action.variant}` : ''}: ${action.rationale}${destination}`);
|
|
96
|
+
}
|
|
67
97
|
const card = async (reason: string) => {
|
|
68
98
|
const evidence = JSON.stringify({ runId: action.runId, pr: action.pr, mustFix: action.mustFix ?? [], history: action.history ?? [], selected: action.kind, reason });
|
|
69
99
|
const result = await deps.command(['decisions', 'raise', '--title', `${action.taskId}: ${reason}`, '--category', action.risk === 'money' ? 'money' : action.risk === 'security' ? 'security' : action.risk === 'irreversible-publication' ? 'publish' : 'other', '--s', `Task ${action.taskId} needs a decision.`, '--c', reason, '--ref', evidence, '--q', 'Which action should be taken?', '--option', 'h=Keep on hold:no automatic execution', '--option', 'p=Approve action:execute after approval', '--skip-recommend', 'requires human decision', '--no-xcheck', 'automated escalation']);
|
|
@@ -87,6 +117,37 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
|
|
|
87
117
|
if (deps.reserveLanding ? !deps.reserveLanding(day, limit) : (deps.landingsToday ?? 0) >= limit) return card('daily landing limit');
|
|
88
118
|
}
|
|
89
119
|
if (action.kind === 'decision') return card(action.rationale);
|
|
120
|
+
if (action.kind === 'deliver') {
|
|
121
|
+
const delivery = action.delivery;
|
|
122
|
+
if (!delivery?.files.length) return record('waiting', '전달 후보 없음');
|
|
123
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(action.taskId)) return record('failed', 'invalid delivery taskId');
|
|
124
|
+
const output = join(delivery.projectTarget, 'elanous-out', action.taskId);
|
|
125
|
+
const copied: string[] = [];
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(output, { recursive: true });
|
|
128
|
+
// 경로 비교는 «실경로끼리» 한다 — macOS 의 /var→/private/var 처럼 조상 링크가 있으면 resolve() 와 realpath 가 늘 갈린다.
|
|
129
|
+
const outputReal = realpathSync(output);
|
|
130
|
+
if (outputReal !== join(realpathSync(delivery.projectTarget), 'elanous-out', action.taskId)) throw new Error('delivery output points outside target');
|
|
131
|
+
const root = realpathSync(delivery.worktreePath) + sep;
|
|
132
|
+
for (const name of delivery.files) {
|
|
133
|
+
const source = resolve(root, name);
|
|
134
|
+
if (!source.startsWith(root) || !lstatSync(source).isFile() || !realpathSync(source).startsWith(root)) throw new Error(`invalid delivery file: ${name}`);
|
|
135
|
+
const destination = resolve(output, name);
|
|
136
|
+
if (!destination.startsWith(resolve(output) + sep)) throw new Error(`invalid destination: ${name}`);
|
|
137
|
+
mkdirSync(join(destination, '..'), { recursive: true });
|
|
138
|
+
const parent = realpathSync(join(destination, '..'));
|
|
139
|
+
if (parent !== outputReal && !parent.startsWith(outputReal + sep)) throw new Error(`invalid destination parent: ${name}`);
|
|
140
|
+
try { if (!lstatSync(destination).isFile()) throw new Error(`invalid destination: ${name}`); }
|
|
141
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
142
|
+
copyFileSync(source, destination);
|
|
143
|
+
copied.push(destination);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) { return record('failed', `delivery copy failed: ${String(error)}`); }
|
|
146
|
+
const evidence = readDeliveryEvidence(delivery.kind, resolve(output), copied);
|
|
147
|
+
debug.log('task-agent', 'deliver', { taskId: action.taskId, kind: delivery.kind, files: copied, evidenceOk: evidence.ok });
|
|
148
|
+
const judgement = judgeNextMove({ deliveryEvidence: evidence });
|
|
149
|
+
return record(judgement.move === 'propose-green' ? 'done' : 'waiting', `${judgement.move}: ${judgement.reason}`);
|
|
150
|
+
}
|
|
90
151
|
if (action.kind === 'green') return card('green requires a verified landing in this execution');
|
|
91
152
|
// G1 — 기다림은 발사가 아니다: 대기 표지만 남기고 다음 틱이 다시 판단한다(카드로 올리지 않는다).
|
|
92
153
|
if (action.kind === 'retry' && action.variant === 'wait') return record('waiting', `wait marker: ${action.rationale}`);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 보드 일반형 — 프로젝트 › 목표 › 이정표 › 과제 (RFC-loop-agent-map §A4b①).
|
|
3
|
+
*
|
|
4
|
+
* «판(0.2.x)·칸» 체크리스트는 이 보드의 한 사례(내부 팩)다. 어댑터 둘은 «읽기»만 한다:
|
|
5
|
+
* - `cardsBoard(statePath)` — task-agent-actions.json 의 과제 카드(project.id · goal · milestone)를 트리로.
|
|
6
|
+
* - `releasePackBoard(version)` — 릴리스 체크리스트를 project=elanous · milestone=판 · task=칸 으로 사상. 원장은 쓰지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
11
|
+
import { listChecklist, type ChecklistStatus } from '../release-loop/checklist.js';
|
|
12
|
+
import { readTaskAgentState, type CompletionKind, type TaskCard } from './task-hand.js';
|
|
13
|
+
|
|
14
|
+
export type BoardLevel = 'project' | 'goal' | 'milestone' | 'task';
|
|
15
|
+
export type BoardStatus = 'open' | 'done' | 'blocked';
|
|
16
|
+
|
|
17
|
+
export interface BoardNode {
|
|
18
|
+
level: BoardLevel;
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
status: BoardStatus;
|
|
22
|
+
/** 과제 노드만 — 카드의 종결 종류(비면 code-pr 로 읽는다 · 칸은 싣지 않는다). */
|
|
23
|
+
completion?: CompletionKind;
|
|
24
|
+
/** 부모 노드 id(프로젝트는 없다). */
|
|
25
|
+
parent?: string;
|
|
26
|
+
/** 프로젝트 노드만 — 프로젝트 없는 카드의 자리(id 는 빈 문자열이라 실제 프로젝트 id 와 겹치지 않는다). */
|
|
27
|
+
unassigned?: true;
|
|
28
|
+
children?: BoardNode[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 프로젝트가 없는 카드가 모이는 자리의 id — 빈 문자열(실제 프로젝트 id 는 비지 않는다 · `handTask` 가 빈 id 를 거부). */
|
|
32
|
+
export const UNASSIGNED_PROJECT = '';
|
|
33
|
+
/** 내부 팩(릴리스 체크리스트)의 프로젝트 id. */
|
|
34
|
+
export const INTERNAL_PACK_PROJECT = 'elanous';
|
|
35
|
+
|
|
36
|
+
function branch(level: BoardLevel, id: string, title: string, parent?: string): BoardNode {
|
|
37
|
+
// 무소속 프로젝트의 id 는 '' — 빈 문자열도 부모 id 로 보존한다.
|
|
38
|
+
return { level, id, title, status: 'open', ...(parent !== undefined ? { parent } : {}), children: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function child(parent: BoardNode, level: BoardLevel, id: string, title = id): BoardNode {
|
|
42
|
+
let node = parent.children!.find((candidate) => candidate.level === level && candidate.id === id);
|
|
43
|
+
if (!node) {
|
|
44
|
+
node = branch(level, id, title, parent.id);
|
|
45
|
+
parent.children!.push(node);
|
|
46
|
+
}
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 자식이 있는 노드의 상태 = 막힘 하나라도 → blocked · 전부 done → done · 그 밖 open. */
|
|
51
|
+
function aggregate(node: BoardNode): void {
|
|
52
|
+
if (!node.children?.length) return;
|
|
53
|
+
for (const next of node.children) aggregate(next);
|
|
54
|
+
node.status = node.children.some((next) => next.status === 'blocked') ? 'blocked'
|
|
55
|
+
: node.children.every((next) => next.status === 'done') ? 'done' : 'open';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cardStatus(card: TaskCard): BoardStatus {
|
|
59
|
+
if (card.status === 'launch-failed' || card.status === 'failed') return 'blocked';
|
|
60
|
+
return card.greenProposal ? 'done' : 'open';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 과제 카드 → 프로젝트 트리. 프로젝트 없는 카드는 «무소속»(`unassigned: true` · id '') 아래 — 어떤 프로젝트 id 로도 걸러지지 않는다. */
|
|
64
|
+
export function cardsBoard(statePath: string): BoardNode[] {
|
|
65
|
+
const projects: BoardNode[] = [];
|
|
66
|
+
const cards = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {};
|
|
67
|
+
for (const card of Object.values(cards)) {
|
|
68
|
+
const projectId = card.project?.id?.trim() || UNASSIGNED_PROJECT;
|
|
69
|
+
const unassigned = projectId === UNASSIGNED_PROJECT;
|
|
70
|
+
let project = projects.find((node) => node.id === projectId && (node.unassigned === true) === unassigned);
|
|
71
|
+
if (!project) {
|
|
72
|
+
project = unassigned ? { ...branch('project', UNASSIGNED_PROJECT, '무소속'), unassigned: true } : branch('project', projectId, projectId);
|
|
73
|
+
projects.push(project);
|
|
74
|
+
}
|
|
75
|
+
const goal = card.goal ? child(project, 'goal', card.goal) : project;
|
|
76
|
+
const milestone = card.milestone ? child(goal, 'milestone', card.milestone) : goal;
|
|
77
|
+
milestone.children!.push({
|
|
78
|
+
level: 'task', id: card.id, title: card.text, status: cardStatus(card),
|
|
79
|
+
...(card.completion ? { completion: card.completion } : {}),
|
|
80
|
+
parent: milestone.id,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
for (const project of projects) aggregate(project);
|
|
84
|
+
return projects;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function releaseStatus(status: ChecklistStatus): BoardStatus {
|
|
88
|
+
return status === 'green' || status === 'done' ? 'done' : status === 'red' ? 'blocked' : 'open';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 릴리스 체크리스트 → 내부 팩 보드(읽기 사상). 원장이 없으면 빈 보드 — 만들지 않는다.
|
|
93
|
+
* root 를 명시해 넘겨 옛 JSON 가져오기(쓰기)를 건너뛴다: 이 어댑터는 원장을 바꾸지 않는다.
|
|
94
|
+
*/
|
|
95
|
+
export function releasePackBoard(version: string, root = releaseLedgerRoot()): BoardNode[] {
|
|
96
|
+
if (!existsSync(join(root, 'release', 'features.sqlite'))) return [];
|
|
97
|
+
const checklist = listChecklist(version, root);
|
|
98
|
+
const project = branch('project', INTERNAL_PACK_PROJECT, 'elanous (내부 팩)');
|
|
99
|
+
const milestone = child(project, 'milestone', checklist.version, `판 ${checklist.version}`);
|
|
100
|
+
for (const item of checklist.items) {
|
|
101
|
+
milestone.children!.push({ level: 'task', id: item.id, title: item.title, status: releaseStatus(item.status), parent: milestone.id });
|
|
102
|
+
}
|
|
103
|
+
aggregate(project);
|
|
104
|
+
return [project];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cloneNode(node: BoardNode): BoardNode {
|
|
108
|
+
return { ...node, ...(node.children ? { children: node.children.map(cloneNode) } : {}) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 같은 수준·id 의 프로젝트·목표·이정표는 한 노드로 합친다(과제는 그대로 잇는다 — 다른 원천의 과제를 숨기지 않는다). */
|
|
112
|
+
function mergeInto(target: BoardNode[], incoming: readonly BoardNode[]): void {
|
|
113
|
+
for (const node of incoming) {
|
|
114
|
+
const existing = node.level === 'task' ? undefined
|
|
115
|
+
: target.find((candidate) => candidate.level === node.level && candidate.id === node.id && candidate.unassigned === node.unassigned);
|
|
116
|
+
if (existing) mergeInto(existing.children ??= [], node.children ?? []);
|
|
117
|
+
else target.push(cloneNode(node));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 두 어댑터를 합친다 — 같은 수준·id 의 노드는 한 노드로(예: 카드의 이정표 0.2.19 ⊕ 체크리스트의 판 0.2.19) · 상태를 다시 모은다. 입력은 바꾸지 않는다. */
|
|
122
|
+
export function mergeBoards(...boards: readonly BoardNode[][]): BoardNode[] {
|
|
123
|
+
const merged: BoardNode[] = [];
|
|
124
|
+
for (const board of boards) mergeInto(merged, board);
|
|
125
|
+
for (const project of merged) aggregate(project);
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 트리 안의 과제 노드 수. */
|
|
130
|
+
export function countBoardTasks(nodes: readonly BoardNode[]): number {
|
|
131
|
+
return nodes.reduce((count, node) => count + (node.level === 'task' ? 1 : 0) + countBoardTasks(node.children ?? []), 0);
|
|
132
|
+
}
|