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
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// The saved version-release output is re-pointed at the new branch tip (fast-forward only, state backed up first), so
|
|
3
3
|
// every later node reads the repaired commit without anyone hand-editing the run state.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { constants, copyFileSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { constants, copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
|
-
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { lastJsonObject, runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
|
|
10
|
+
import { releaseRunUniverse } from './release-universe.js';
|
|
10
11
|
import { isReleaseVersion } from './release-version.js';
|
|
12
|
+
import { changedFilesBetween, clearPartialPlan, planPartialRegate, writePartialPlan } from './gate-partial.js';
|
|
11
13
|
|
|
12
14
|
const GRAPH = join(import.meta.dir, '../../graphs/release/release-loop.yaml');
|
|
13
15
|
|
|
@@ -24,6 +26,8 @@ export interface ResumeReleaseDeps {
|
|
|
24
26
|
/** Node command runner for the resumed graph (tests record each node's context instead of running it). */
|
|
25
27
|
runBash?: NonNullable<NonNullable<Parameters<typeof runGraph>[1]>['deps']>['runBash'];
|
|
26
28
|
now?: () => Date;
|
|
29
|
+
/** Machine release ledger (gate-failures.json of the prior gate) — default releaseLedgerRoot(). */
|
|
30
|
+
ledgerRoot?: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface TipRefresh { version: string; branch: string; from: string; to: string; changed: boolean; statePath: string; backup?: string }
|
|
@@ -106,10 +110,68 @@ export function refreshReleaseBranchTip(runId: string, deps: ResumeReleaseDeps =
|
|
|
106
110
|
return { version, branch, from: recorded, to, changed: true, statePath, backup };
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
/** GATE-PARTIAL outcome of a resume — applied (plan written) or the reason it fell back to a full gate. */
|
|
114
|
+
export type PartialOutcome = { requested: false } | { requested: true; applied: true; files: number; priorCommit: string } | { requested: true; applied: false; reason: string };
|
|
115
|
+
|
|
116
|
+
/** Build (or clear) the partial re-gate plan for the new tip. Fail-closed: any doubt leaves a full gate. */
|
|
117
|
+
function preparePartial(tip: TipRefresh, opts: { from: string; partial?: boolean }, deps: ResumeReleaseDeps): PartialOutcome {
|
|
118
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
119
|
+
// Any resume starts from no plan: one without --partial (from whatever node) never inherits an older plan.
|
|
120
|
+
clearPartialPlan(root, tip.version);
|
|
121
|
+
if (!opts.partial) return { requested: false };
|
|
122
|
+
const fallback = (reason: string): PartialOutcome => {
|
|
123
|
+
debug.log('release-loop.resume', 'partial-fallback', { version: tip.version, reason });
|
|
124
|
+
return { requested: true, applied: false, reason };
|
|
125
|
+
};
|
|
126
|
+
if (opts.from !== 'gate') return fallback('--partial only applies to --from gate');
|
|
127
|
+
if (!tip.changed) return fallback('release branch tip unchanged — nothing new to re-check');
|
|
128
|
+
let prior: { commit?: unknown; failures?: unknown; errors?: unknown } | undefined;
|
|
129
|
+
for (const location of [...new Set([deps.ledgerRoot ?? releaseLedgerRoot(), root])]) {
|
|
130
|
+
const path = join(location, 'release', tip.version, 'gate-failures.json');
|
|
131
|
+
if (!existsSync(path)) continue;
|
|
132
|
+
let record: typeof prior;
|
|
133
|
+
try { record = JSON.parse(readFileSync(path, 'utf8')) as typeof prior; }
|
|
134
|
+
catch { return fallback(`unreadable gate record: ${path}`); }
|
|
135
|
+
if (record?.commit === tip.from) { prior = record; break; }
|
|
136
|
+
}
|
|
137
|
+
if (!prior || !Array.isArray(prior.failures) || !(prior.errors === undefined || Array.isArray(prior.errors))) {
|
|
138
|
+
return fallback(`no complete gate record for ${tip.from.slice(0, 12)} (a stalled or errored gate keeps none)`);
|
|
139
|
+
}
|
|
140
|
+
// Both sides of a rename count (`--no-renames`): a source moved to a test/Markdown name is still a source change.
|
|
141
|
+
let changed: string[];
|
|
142
|
+
try {
|
|
143
|
+
changed = deps.git
|
|
144
|
+
? deps.git(['diff', '--name-only', '--no-renames', `${tip.from}..${tip.to}`]).split(/\r?\n/).filter(Boolean)
|
|
145
|
+
: changedFilesBetween(deps.repo ?? process.cwd(), tip.from, tip.to);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// The tip is already refreshed — a failed diff must not strand the resume: run the full gate and say why.
|
|
148
|
+
return fallback(`changed files unreadable: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`);
|
|
149
|
+
}
|
|
150
|
+
const planned = planPartialRegate({ version: tip.version, priorCommit: tip.from, forCommit: tip.to, changedFiles: changed,
|
|
151
|
+
priorFailures: prior.failures as string[], priorErrors: (prior.errors ?? []) as string[] });
|
|
152
|
+
if (!planned.ok) return fallback(planned.reason);
|
|
153
|
+
const path = writePartialPlan(root, planned.plan);
|
|
154
|
+
debug.log('release-loop.resume', 'partial-planned', { version: tip.version, priorCommit: tip.from, commit: tip.to, files: planned.plan.files.length, path });
|
|
155
|
+
return { requested: true, applied: true, files: planned.plan.files.length, priorCommit: tip.from };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The graph file the run started from — a snapshot run refuses any other path (the installed CLI's own copy differs). */
|
|
159
|
+
function savedGraphPath(statePath: string): string | undefined {
|
|
160
|
+
try {
|
|
161
|
+
const saved = JSON.parse(readFileSync(statePath, 'utf8')) as { graphPath?: unknown };
|
|
162
|
+
return typeof saved.graphPath === 'string' && saved.graphPath ? saved.graphPath : undefined;
|
|
163
|
+
} catch { return undefined; }
|
|
164
|
+
}
|
|
165
|
+
|
|
109
166
|
/** Refresh the branch tip, then restart the failed run at `from` with the saved graph snapshot. */
|
|
110
|
-
export async function resumeReleaseRun(opts: { runId: string; from: string }, deps: ResumeReleaseDeps = {}): Promise<{ tip: TipRefresh; state: GraphRunState }> {
|
|
167
|
+
export async function resumeReleaseRun(opts: { runId: string; from: string; partial?: boolean }, deps: ResumeReleaseDeps = {}): Promise<{ tip: TipRefresh; partial: PartialOutcome; state: GraphRunState }> {
|
|
111
168
|
if (opts.from === 'version-release') throw new Error('--from version-release would cut again — resume at a later node');
|
|
169
|
+
// RELEASE-LEDGER-UNIVERSE: decide the universe once; the run lookup and every resumed node use it.
|
|
170
|
+
deps = { ...deps, root: releaseRunUniverse({ root: deps.root }) };
|
|
112
171
|
const tip = refreshReleaseBranchTip(opts.runId, deps, opts.from);
|
|
113
|
-
const
|
|
114
|
-
|
|
172
|
+
const partial = preparePartial(tip, opts, deps);
|
|
173
|
+
// Resume the graph at the path it was started from (its snapshot is used); only tests inject another path.
|
|
174
|
+
const graphPath = deps.graphPath ?? savedGraphPath(tip.statePath) ?? GRAPH;
|
|
175
|
+
const state = await (deps.graph ?? runGraph)(graphPath, { resumeRunId: opts.runId, fromNodeId: opts.from, pinChildUniverse: true, deps: { root: deps.root, ...(deps.runBash ? { runBash: deps.runBash } : {}) } });
|
|
176
|
+
return { tip, partial, state };
|
|
115
177
|
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { debug } from '../../src/debug/log.js';
|
|
4
|
+
|
|
5
|
+
/** Plan weights for a Pod sweep: wall-scaled when the previous cut recorded shard wall times, junit seconds otherwise. */
|
|
6
|
+
export function planDurations(durationSource: string | undefined): { durations: Map<string, number>; wall?: { shards: number; scaledFiles: number } } {
|
|
7
|
+
if (!durationSource) return { durations: new Map() };
|
|
8
|
+
const junit = readFileDurations(durationSource);
|
|
9
|
+
const wall = readFileWallWeights(durationSource, junit);
|
|
10
|
+
return wall ? { durations: wall.weights, wall: { shards: wall.shards, scaledFiles: wall.scaledFiles } } : { durations: junit };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Observation for GATE-PLAN-WALLTIME, emitted once the shards are planned so `maxPlannedMin` is the heaviest shard. */
|
|
14
|
+
export function logPlanWalltime(wall: { shards: number; scaledFiles: number } | undefined, shards: ReadonlyArray<{ plannedSeconds: number }>): void {
|
|
15
|
+
if (!wall) return;
|
|
16
|
+
const maxPlannedSeconds = Math.max(0, ...shards.map((shard) => shard.plannedSeconds));
|
|
17
|
+
debug.log('release-loop.gate', 'plan-walltime', { shards: wall.shards, scaledFiles: wall.scaledFiles, maxPlannedMin: Math.round(maxPlannedSeconds / 6) / 10 });
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
/** Read per-file seconds from previous cut Pod reports; missing or damaged reports are not estimates. */
|
|
5
21
|
export function readFileDurations(dir: string): Map<string, number> {
|
|
@@ -62,3 +78,52 @@ export function planShards(files: string[], durations: Map<string, number>, coun
|
|
|
62
78
|
}
|
|
63
79
|
return shards.filter((shard) => shard.files.length > 0);
|
|
64
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* GATE-PLAN-WALLTIME: junit testsuite times run far below a shard's real wall time (0.2.18 pod-23: planned 2.7 min,
|
|
84
|
+
* measured 33.3 min), so LPT balanced the wrong quantity. When the previous cut left `pod-*.json` records
|
|
85
|
+
* (`durationMs` ⊕ `files`), each file's junit seconds are scaled so its shard's planned total equals the measured wall
|
|
86
|
+
* time; files without junit take that shard's average per-file wall time. A file measured in several records takes
|
|
87
|
+
* its first-pass shard (`pod-<n>.json` — the shape the next cut plans) over descent/retry children, whose per-Pod
|
|
88
|
+
* install overhead lands on one or two files; among equals, the record with the fewest files.
|
|
89
|
+
* Returns undefined when no usable record exists, so callers keep the junit-only behaviour.
|
|
90
|
+
*/
|
|
91
|
+
export function readFileWallWeights(dir: string, junit: Map<string, number>): { weights: Map<string, number>; shards: number; scaledFiles: number } | undefined {
|
|
92
|
+
let names: string[];
|
|
93
|
+
try { names = readdirSync(dir).filter((name) => /^pod-.*\.json$/.test(name)).sort(); }
|
|
94
|
+
catch { return undefined; }
|
|
95
|
+
const records: Array<{ wallSeconds: number; files: string[]; firstPass: boolean }> = [];
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
try {
|
|
98
|
+
const meta = JSON.parse(readFileSync(join(dir, name), 'utf8')) as { durationMs?: unknown; files?: unknown };
|
|
99
|
+
if (typeof meta.durationMs !== 'number' || !Number.isFinite(meta.durationMs) || meta.durationMs <= 0) continue;
|
|
100
|
+
if (!Array.isArray(meta.files) || !meta.files.length || !meta.files.every((file) => typeof file === 'string')) continue;
|
|
101
|
+
records.push({ wallSeconds: meta.durationMs / 1000, files: [...new Set(meta.files as string[])], firstPass: /^pod-\d+\.json$/.test(name) });
|
|
102
|
+
} catch { /* One damaged record must not discard the others. */ }
|
|
103
|
+
}
|
|
104
|
+
if (!records.length) return undefined;
|
|
105
|
+
const valid = (value: number | undefined): value is number => value !== undefined && Number.isFinite(value) && value >= 0;
|
|
106
|
+
const best = new Map<string, { rank: number; seconds: number }>();
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
const unknownCount = record.files.filter((file) => !valid(junit.get(file))).length;
|
|
109
|
+
const junitSum = record.files.reduce((sum, file) => { const value = junit.get(file); return sum + (valid(value) ? value : 0); }, 0);
|
|
110
|
+
const average = record.wallSeconds / record.files.length;
|
|
111
|
+
const remaining = record.wallSeconds - unknownCount * average;
|
|
112
|
+
for (const file of record.files) {
|
|
113
|
+
const seconds = junit.get(file);
|
|
114
|
+
const weight = valid(seconds) && junitSum > 0 && remaining > 0 ? seconds * (remaining / junitSum) : average;
|
|
115
|
+
const prior = best.get(file);
|
|
116
|
+
const rank = (record.firstPass ? 0 : 1_000_000) + record.files.length;
|
|
117
|
+
if (!prior || rank < prior.rank) best.set(file, { rank, seconds: weight });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Files timed by junit but absent from every record (new or renamed since the cut) take the overall scale, so the
|
|
121
|
+
// plan never mixes wall seconds with raw junit seconds.
|
|
122
|
+
let scaledSum = 0;
|
|
123
|
+
let junitSum = 0;
|
|
124
|
+
for (const [file, { seconds }] of best) { const value = junit.get(file); if (valid(value) && value > 0) { scaledSum += seconds; junitSum += value; } }
|
|
125
|
+
const overall = junitSum > 0 ? scaledSum / junitSum : 1;
|
|
126
|
+
const weights = new Map([...junit].map(([file, seconds]) => [file, seconds * overall] as [string, number]));
|
|
127
|
+
for (const [file, { seconds }] of best) weights.set(file, seconds);
|
|
128
|
+
return { weights, shards: records.length, scaledFiles: best.size };
|
|
129
|
+
}
|
|
@@ -9,6 +9,7 @@ import { getSchedule } from '../../src/release-loop/release-schedule.js';
|
|
|
9
9
|
import { isLandingFreezeRefusal, readLandingFreeze, LandingFrozenError } from '../../src/release-loop/landing-freeze.js';
|
|
10
10
|
import { debug } from '../../src/debug/log.js';
|
|
11
11
|
import { runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
|
|
12
|
+
import { releaseRunUniverse } from './release-universe.js';
|
|
12
13
|
import { baseVersion, isStableVersion, nextPrereleaseVersion, type PrereleaseKind } from './release-version.js';
|
|
13
14
|
|
|
14
15
|
const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
@@ -18,7 +19,14 @@ export interface ReleaseLoopConfig {
|
|
|
18
19
|
gatePodPool?: string;
|
|
19
20
|
gatePodBunCache?: string;
|
|
20
21
|
gatePodShards?: number;
|
|
22
|
+
/** GATE-TIMEOUT-HEAL — the per-shard deadline floor; each shard gets planned × 1.5 above it (capped). */
|
|
21
23
|
gatePodShardTimeoutSeconds?: number;
|
|
24
|
+
/** GATE-INSTALL-CACHE — concurrent `bun install`s per gate node (default 4 · 0 = off). */
|
|
25
|
+
gatePodInstallSlots?: number;
|
|
26
|
+
/** GATE-MEM-ADMIT — seconds a gate shard waits for pool admission (memory headroom) before launching anyway with `admission-timeout` (default 300 · 0 = off). */
|
|
27
|
+
gatePodAdmissionWaitSeconds?: number;
|
|
28
|
+
/** GATE-SPEED A3①: gate shard Pod CPU — `2` (request = limit) or `{ request, limit }`. Omitted = request 1 / limit 4. */
|
|
29
|
+
gatePodCpu?: number | string | { request?: number | string; limit?: number | string };
|
|
22
30
|
gateRemote?: string;
|
|
23
31
|
gateRemoteMirror?: string;
|
|
24
32
|
opsHosts?: string[];
|
|
@@ -40,7 +48,7 @@ export interface UnattendedReleaseDeps {
|
|
|
40
48
|
ledgerRoot?: string;
|
|
41
49
|
config?: ReleaseLoopConfig;
|
|
42
50
|
configPath?: string;
|
|
43
|
-
graph?: (path: string, options: { input: ReleaseRunInput }) => Promise<GraphRunState>;
|
|
51
|
+
graph?: (path: string, options: { input: ReleaseRunInput; pinChildUniverse?: boolean; deps?: { root?: string } }) => Promise<GraphRunState>;
|
|
44
52
|
checklist?: (version: string) => Pick<ReturnType<typeof cutChecklistGate>, 'ok' | 'red' | 'undecided' | 'blocked'>;
|
|
45
53
|
freezeRoot?: string;
|
|
46
54
|
/** Names already taken for prerelease numbering (remote release branches and tags). */
|
|
@@ -140,7 +148,9 @@ export async function runUnattendedRelease(
|
|
|
140
148
|
}
|
|
141
149
|
const beforeGraph = readLandingFreeze(deps.freezeRoot);
|
|
142
150
|
if (beforeGraph && !opts.forceFreeze) throw new LandingFrozenError(beforeGraph);
|
|
143
|
-
|
|
151
|
+
// RELEASE-LEDGER-UNIVERSE: one universe for the run ledger, the release ledger and every node subprocess.
|
|
152
|
+
const root = releaseRunUniverse({ root: deps.ledgerRoot });
|
|
153
|
+
const state = await (deps.graph ?? runGraph)(GRAPH, { input, pinChildUniverse: true, deps: { root } });
|
|
144
154
|
// A freeze switched on while the graph ran stops gate or publish inside it. Only a failed gate/publish node whose own
|
|
145
155
|
// record is the freeze refusal is reported as the freeze; any other failure keeps its own reason.
|
|
146
156
|
if (state.status === 'failed' && !opts.forceFreeze) {
|
|
@@ -55,7 +55,7 @@ function userFacingLines(markdown: string): { lines: string[]; internalDropped:
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function firstPhrase(title: string): string {
|
|
58
|
-
let quote: '»' | '"' | '`' | undefined;
|
|
58
|
+
let quote: '»' | '"' | '`' | '”' | '’' | '」' | undefined;
|
|
59
59
|
let angleStart = -1;
|
|
60
60
|
for (let i = 0; i < title.length; i++) {
|
|
61
61
|
const char = title[i]!;
|
|
@@ -64,6 +64,10 @@ function firstPhrase(title: string): string {
|
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
if (char === '«') { quote = '»'; angleStart = i; continue; }
|
|
67
|
+
if (char === '“') { quote = '”'; continue; }
|
|
68
|
+
// 여는 ‘ 는 생략 부호로 안 쓰이니 짝을 기다린다 — ASCII ' 는 한국어 생략 부호라 짝으로 보지 않는다.
|
|
69
|
+
if (char === '‘') { quote = '’'; continue; }
|
|
70
|
+
if (char === '「') { quote = '」'; continue; }
|
|
67
71
|
if (char === '"' || char === '`') { quote = char; continue; }
|
|
68
72
|
if (/[::]/.test(char) || (/[—·–]/.test(char) && /\s/.test(title[i - 1] ?? '') && /\s/.test(title[i + 1] ?? ''))) {
|
|
69
73
|
return title.slice(0, i).trim();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* RETRO-DAILY 그림자 대조 — 매일 회고 그래프를 «정답지»(RFC §0 다섯 반복 문제) 위에서 돌려 맞혔는지 본다.
|
|
4
|
+
*
|
|
5
|
+
* bun scripts/retro/shadow-compare.ts [--facts <facts.json>] [--key <answer-key.json>] [--json]
|
|
6
|
+
*
|
|
7
|
+
* 기본 입력 = test/fixtures/retro/ 의 10-07 사실 ⊕ 정답지. 그래프는 임시 우주(mkdtemp)에서 shadow 로만 돈다 —
|
|
8
|
+
* 운영 회고 파일·체크리스트·메시지 어느 것도 건드리지 않는다. 놓친 주제가 있으면 exit 1.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { runGraph } from '../../src/graph-runner/runner.js';
|
|
14
|
+
import { retroDayFile } from '../../src/retro/daily.js';
|
|
15
|
+
|
|
16
|
+
const repo = resolve(import.meta.dir, '..', '..');
|
|
17
|
+
export const RETRO_DAILY_GRAPH = join(repo, 'graphs', 'retro', 'daily.yaml');
|
|
18
|
+
export const DEFAULT_FACTS = join(repo, 'test', 'fixtures', 'retro', 'rfc0-2026-10-07-facts.json');
|
|
19
|
+
export const DEFAULT_KEY = join(repo, 'test', 'fixtures', 'retro', 'rfc0-answer-key.json');
|
|
20
|
+
|
|
21
|
+
type Theme = { theme: string; title: string; recurring: boolean; reBrokeAfterGreen: boolean; rank: number };
|
|
22
|
+
type Key = { day: string; greenCells?: unknown[]; expected: { theme: string; title: string; reBrokeAfterGreen?: boolean }[]; notRecurring?: string[] };
|
|
23
|
+
export type ShadowComparison = {
|
|
24
|
+
ok: boolean; status: string; day: string; detected: string[]; missed: string[]; falsePositives: string[];
|
|
25
|
+
reBrokeMismatch: string[]; firstRank: string | null; actions: number; summary: string[]; wroteChecklist: boolean; mode: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function compareWithKey(dayFile: { themes: Theme[]; actions: unknown[]; summary: string[]; mode: string }, key: Key, status = 'done'): Omit<ShadowComparison, 'wroteChecklist'> {
|
|
29
|
+
const recurring = dayFile.themes.filter(t => t.recurring);
|
|
30
|
+
const detected = recurring.map(t => t.theme);
|
|
31
|
+
const missed = key.expected.filter(e => !detected.includes(e.theme)).map(e => e.theme);
|
|
32
|
+
// 정답지 밖의 반복 주제는 전부 오탐이다(목록에 적은 것만 보지 않는다).
|
|
33
|
+
const expected = new Set(key.expected.map(e => e.theme));
|
|
34
|
+
const falsePositives = [...new Set([...detected.filter(t => !expected.has(t)), ...(key.notRecurring ?? []).filter(t => detected.includes(t))])];
|
|
35
|
+
const reBrokeMismatch = key.expected.filter(e => e.reBrokeAfterGreen !== undefined
|
|
36
|
+
&& recurring.find(t => t.theme === e.theme)?.reBrokeAfterGreen !== e.reBrokeAfterGreen).map(e => e.theme);
|
|
37
|
+
const firstRank = recurring.sort((a, b) => a.rank - b.rank)[0]?.theme ?? null;
|
|
38
|
+
// «초록 뒤 재발»이 정답지에 있으면 그것이 1순위여야 한다(RFC §1 recur).
|
|
39
|
+
const mustLead = key.expected.find(e => e.reBrokeAfterGreen)?.theme;
|
|
40
|
+
const ok = status === 'done' && !missed.length && !falsePositives.length && !reBrokeMismatch.length
|
|
41
|
+
&& (!mustLead || firstRank === mustLead) && dayFile.actions.length <= 3 && dayFile.summary.length === 3;
|
|
42
|
+
return { ok, status, day: key.day, detected, missed, falsePositives, reBrokeMismatch, firstRank, actions: dayFile.actions.length, summary: dayFile.summary, mode: dayFile.mode };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runShadowComparison(factsFile = DEFAULT_FACTS, keyFile = DEFAULT_KEY): Promise<ShadowComparison> {
|
|
46
|
+
const key = JSON.parse(readFileSync(keyFile, 'utf8')) as Key;
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), 'retro-shadow-'));
|
|
48
|
+
try {
|
|
49
|
+
const run = await runGraph(RETRO_DAILY_GRAPH, { input: { factsFile: resolve(factsFile), day: key.day, greenCells: key.greenCells ?? [], mode: 'shadow' }, deps: { root } });
|
|
50
|
+
if (run.status !== 'done') return { ok: false, status: run.status, day: key.day, detected: [], missed: key.expected.map(e => e.theme), falsePositives: [], reBrokeMismatch: [], firstRank: null, actions: 0, summary: [], wroteChecklist: false, mode: 'shadow' };
|
|
51
|
+
const dayFile = JSON.parse(readFileSync(retroDayFile(root, key.day), 'utf8'));
|
|
52
|
+
const act = run.nodes.find(n => n.nodeId === 'act');
|
|
53
|
+
const wroteChecklist = typeof act?.output === 'string' && /"wroteChecklist":true/.test(act.output);
|
|
54
|
+
const result = compareWithKey(dayFile, key, run.status);
|
|
55
|
+
return { ...result, ok: result.ok && !wroteChecklist && dayFile.mode === 'shadow', wroteChecklist };
|
|
56
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (import.meta.main) {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const flag = (name: string) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
62
|
+
const result = await runShadowComparison(flag('--facts'), flag('--key'));
|
|
63
|
+
console.log(args.includes('--json') ? JSON.stringify(result) : [
|
|
64
|
+
`retro-daily 그림자 대조 ${result.ok ? 'OK' : 'MISS'} · ${result.day} · status=${result.status} · mode=${result.mode}`,
|
|
65
|
+
`잡음: ${result.detected.join(', ') || '없음'}`,
|
|
66
|
+
`놓침: ${result.missed.join(', ') || '없음'} · 오탐: ${result.falsePositives.join(', ') || '없음'} · 초록뒤재발 불일치: ${result.reBrokeMismatch.join(', ') || '없음'}`,
|
|
67
|
+
`1순위: ${result.firstRank ?? '없음'} · 조치 초안 ${result.actions} · 체크리스트 쓰기 ${result.wroteChecklist}`,
|
|
68
|
+
...result.summary.map(line => ` ${line}`),
|
|
69
|
+
].join('\n'));
|
|
70
|
+
if (!result.ok) process.exitCode = 1;
|
|
71
|
+
}
|
package/scripts/rhythm/daily.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { kindRouteTarget, mainHomeTarget } from '../../src/domains/telegram-kind
|
|
|
16
16
|
import { recordOutbound } from '../../src/domains/outbound-alert.js';
|
|
17
17
|
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../src/self-dev/run-store.js';
|
|
18
18
|
import { runDraftSweep } from '../../src/self-dev/draft-sweep.js';
|
|
19
|
+
import { retroBriefingLines } from '../../src/retro/daily.js';
|
|
19
20
|
|
|
20
21
|
export type Section<T> = { status: 'ok'; value: T } | { status: 'unreadable'; reason: string };
|
|
21
22
|
export type Landing = { title: string; seat: string; mergedAt: string; prNumber?: number };
|
|
@@ -529,6 +530,10 @@ export async function runDaily(options: { dryRun?: boolean; noNews?: boolean; st
|
|
|
529
530
|
.map(suggestion => `- ${suggestion.title} — ${suggestion.reason} (${suggestion.source}) [proact:${suggestion.key}]`).join('\n');
|
|
530
531
|
const recorded = [retained, newSuggestions].filter(Boolean).join('\n');
|
|
531
532
|
if (recorded) review.markdown += `\n## PROACT1-LITE 제안\n${recorded}\n`;
|
|
533
|
+
// RETRO-DAILY — 어젯밤 22:40 회고가 live 로 쓴 세 줄만 싣는다(shadow 회고는 브리핑에 안 나간다).
|
|
534
|
+
const retroLines = retroBriefingLines(deps.root ?? effectiveInstanceRoot(), kstDay(new Date(now.getTime() - 86_400_000)));
|
|
535
|
+
const retroBlock = retroLines.length ? `\n\n## 회고 세 줄\n${retroLines.map(line => `- ${line}`).join('\n')}` : '';
|
|
536
|
+
if (retroBlock) review.markdown += `${retroBlock.slice(1)}\n`;
|
|
532
537
|
const file = join(deps.root ?? effectiveInstanceRoot(), 'rhythm', 'daily', `${kstDay(now)}.md`);
|
|
533
538
|
if (!options.dryRun) {
|
|
534
539
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -548,7 +553,7 @@ export async function runDaily(options: { dryRun?: boolean; noNews?: boolean; st
|
|
|
548
553
|
let chars = 0;
|
|
549
554
|
let target = 'unknown';
|
|
550
555
|
if (!options.dryRun && options.stage !== 'collect' && enabled) {
|
|
551
|
-
const text = `${review.header}\n\n## 위험 톱 5\n${review.risks.map((r, i) => `${i + 1}. ${r.name} — ${r.score}점 · ${r.reason}`).join('\n') || '확인된 위험 없음'}${recorded ? `\n\n## PROACT1-LITE 제안\n${recorded}` : ''}\n\n${file}`;
|
|
556
|
+
const text = `${review.header}\n\n## 위험 톱 5\n${review.risks.map((r, i) => `${i + 1}. ${r.name} — ${r.score}점 · ${r.reason}`).join('\n') || '확인된 위험 없음'}${recorded ? `\n\n## PROACT1-LITE 제안\n${recorded}` : ''}${retroBlock}\n\n${file}`;
|
|
552
557
|
chars = `${text}\n${receiptKey}`.length;
|
|
553
558
|
let resolved: ReturnType<typeof recipient> = null;
|
|
554
559
|
try { resolved = recipient(deps); }
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// ── 장면표(scenes.json) 한 벌에서 SRT·VO 대본을 뽑고, 공개 전 검사를 한다 ──────
|
|
3
|
+
//
|
|
4
|
+
// 계기(DEMO-VIDEO-1008 · 2026-10-07): 스토리보드 SRT 초안이 «한 줄 22자 · 두 줄»을
|
|
5
|
+
// 스스로 어겼다(한 블록 세 줄 · 24자 줄). 자막·VO·숫자 카드가 표 «셋»에 따로 있으면
|
|
6
|
+
// 한쪽만 고쳐지고 갈린다 ⇒ 장면표가 SSOT 이고 SRT·VO 는 «뽑아낸 것»이다.
|
|
7
|
+
//
|
|
8
|
+
// 사용:
|
|
9
|
+
// bun scripts/video/scene-table.ts check <scenes.json> 검사(exit 0 통과 · 1 걸림 · 3 입력 불량)
|
|
10
|
+
// bun scripts/video/scene-table.ts srt <scenes.json> 한국어 SRT 를 stdout 으로(검사 통과 때만)
|
|
11
|
+
// bun scripts/video/scene-table.ts vo <scenes.json> 영어 VO 대본(장면 번호 · 시각)을 stdout 으로
|
|
12
|
+
//
|
|
13
|
+
// ⛔ 숫자 자리표시 `{{KEY}}` 는 `measured.KEY.value` 로만 채운다 — 값이 없으면 «걸림»이다(지어낸 수치 금지).
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
export type Cue = { start: number; end: number; lines: string[] };
|
|
18
|
+
export type Scene = {
|
|
19
|
+
n: number; start: number; end: number; source: string[];
|
|
20
|
+
vo?: string | null; ko?: Cue[]; numberCard?: string; requestText?: string;
|
|
21
|
+
};
|
|
22
|
+
export type SceneTable = {
|
|
23
|
+
durationSec: number;
|
|
24
|
+
vo?: { maxWpm?: number };
|
|
25
|
+
subtitles?: { maxCharsPerLine?: number; maxLines?: number };
|
|
26
|
+
measured?: Record<string, { value?: number | string | null }>;
|
|
27
|
+
scenes: Scene[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 공개 수위: 특허 보류 기전 · 경쟁사 이름 · 미래 약속 · 내부 표식 · 기기 경로.
|
|
31
|
+
// 단어 경계가 없는 한국어는 부분 일치로 잡는다.
|
|
32
|
+
export const BANNED: Array<{ re: RegExp; why: string }> = [
|
|
33
|
+
{ re: /시냅스|synap/i, why: '특허 보류 기전' },
|
|
34
|
+
{ re: /수면|\bsleep/i, why: '특허 보류 기전' },
|
|
35
|
+
{ re: /그라운딩\s*방아쇠|grounding\s+trigger/i, why: '특허 보류 기전' },
|
|
36
|
+
{ re: /선제\s*PTY|pre-?emptive\s+PTY/i, why: '특허 보류 기전' },
|
|
37
|
+
{ re: /면역|immun|암\s*억제|\bcancer\b/i, why: '특허 보류 기전' },
|
|
38
|
+
{ re: /openclaw|hermes|codex|cursor|devin|copilot|claude\s*code/i, why: '경쟁사 이름' },
|
|
39
|
+
{ re: /곧\s|\bsoon\b|\bcoming\b|available now|out now/i, why: '미래 약속' },
|
|
40
|
+
// 표식 문자는 코드 포인트로 적는다 — 이 파일 자체가 공개본 유출 검사에 걸리지 않게.
|
|
41
|
+
{ re: /[\u{1F451}\u{1F162}\u{1F163}\u{1F155}\u{1F15E}]|RFC-|HAND[O]FF-/u, why: '내부 표식' },
|
|
42
|
+
{ re: /\/Users\/|\.ts\.net|tail[0-9a-f]{6}/, why: '기기 경로·이름' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const PLACEHOLDER = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
46
|
+
|
|
47
|
+
export function fill(text: string, t: SceneTable, problems: string[], where: string): string {
|
|
48
|
+
return text.replace(PLACEHOLDER, (_m, key: string) => {
|
|
49
|
+
const v = t.measured?.[key]?.value;
|
|
50
|
+
if (v === undefined || v === null || v === '') {
|
|
51
|
+
problems.push(`${where}: {{${key}}} 의 실측값이 없다(measured.${key}.value)`);
|
|
52
|
+
return `{{${key}}}`;
|
|
53
|
+
}
|
|
54
|
+
return String(v);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const words = (s: string) => s.trim().split(/\s+/).filter(Boolean).length;
|
|
59
|
+
|
|
60
|
+
export function checkTable(t: SceneTable): string[] {
|
|
61
|
+
const p: string[] = [];
|
|
62
|
+
const maxChars = t.subtitles?.maxCharsPerLine ?? 22;
|
|
63
|
+
const maxLines = t.subtitles?.maxLines ?? 2;
|
|
64
|
+
const maxWpm = t.vo?.maxWpm ?? 150;
|
|
65
|
+
const scenes = [...t.scenes].sort((a, b) => a.start - b.start);
|
|
66
|
+
|
|
67
|
+
let cursor = 0;
|
|
68
|
+
let lastCueEnd = -1;
|
|
69
|
+
for (const s of scenes) {
|
|
70
|
+
const at = `장면 ${s.n}`;
|
|
71
|
+
if (s.start !== cursor) p.push(`${at}: 시작 ${s.start}s — 앞 장면 끝 ${cursor}s 와 이어지지 않는다`);
|
|
72
|
+
if (!(s.end > s.start)) p.push(`${at}: 끝(${s.end}s)이 시작(${s.start}s) 뒤가 아니다`);
|
|
73
|
+
cursor = s.end;
|
|
74
|
+
|
|
75
|
+
const pub: Array<[string, string]> = [];
|
|
76
|
+
if (s.vo) {
|
|
77
|
+
const wpm = (words(s.vo) / (s.end - s.start)) * 60;
|
|
78
|
+
if (wpm > maxWpm) p.push(`${at}: VO ${wpm.toFixed(0)} wpm > ${maxWpm}`);
|
|
79
|
+
pub.push([`${at} VO`, s.vo]);
|
|
80
|
+
}
|
|
81
|
+
if (s.numberCard) pub.push([`${at} 숫자 카드`, fill(s.numberCard, t, p, `${at} 숫자 카드`)]);
|
|
82
|
+
if (s.requestText) pub.push([`${at} 요청 문구`, s.requestText]);
|
|
83
|
+
|
|
84
|
+
for (const [i, c] of (s.ko ?? []).entries()) {
|
|
85
|
+
const cat = `${at} 자막 ${i + 1}`;
|
|
86
|
+
if (!(c.end > c.start)) p.push(`${cat}: 끝이 시작 뒤가 아니다`);
|
|
87
|
+
if (c.start < s.start || c.end > s.end) p.push(`${cat}: ${c.start}–${c.end}s 가 장면 ${s.start}–${s.end}s 밖으로 나간다`);
|
|
88
|
+
if (c.start < lastCueEnd) p.push(`${cat}: 앞 자막(끝 ${lastCueEnd}s)과 겹친다`);
|
|
89
|
+
lastCueEnd = c.end;
|
|
90
|
+
if (c.lines.length === 0 || c.lines.length > maxLines) p.push(`${cat}: ${c.lines.length}줄(1~${maxLines})`);
|
|
91
|
+
for (const raw of c.lines) {
|
|
92
|
+
const line = fill(raw, t, p, cat);
|
|
93
|
+
const len = [...line].length;
|
|
94
|
+
if (len > maxChars) p.push(`${cat}: «${line}» ${len}자 > ${maxChars}`);
|
|
95
|
+
pub.push([cat, line]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const [where, text] of pub) {
|
|
99
|
+
for (const b of BANNED) if (b.re.test(text)) p.push(`${where}: ${b.why} — «${text}»`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (cursor !== t.durationSec) p.push(`마지막 장면 끝 ${cursor}s ≠ durationSec ${t.durationSec}s`);
|
|
103
|
+
return p;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ts = (sec: number) => {
|
|
107
|
+
const ms = Math.round(sec * 1000);
|
|
108
|
+
const h = Math.floor(ms / 3_600_000), m = Math.floor((ms % 3_600_000) / 60_000);
|
|
109
|
+
const s = Math.floor((ms % 60_000) / 1000), r = ms % 1000;
|
|
110
|
+
const z = (n: number, w = 2) => String(n).padStart(w, '0');
|
|
111
|
+
return `${z(h)}:${z(m)}:${z(s)},${z(r, 3)}`;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function toSrt(t: SceneTable): string {
|
|
115
|
+
const scratch: string[] = [];
|
|
116
|
+
const cues = t.scenes.flatMap((s) => s.ko ?? []).sort((a, b) => a.start - b.start);
|
|
117
|
+
return cues
|
|
118
|
+
.map((c, i) => `${i + 1}\n${ts(c.start)} --> ${ts(c.end)}\n${c.lines.map((l) => fill(l, t, scratch, '')).join('\n')}\n`)
|
|
119
|
+
.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function toVoScript(t: SceneTable): string {
|
|
123
|
+
return t.scenes
|
|
124
|
+
.filter((s) => s.vo)
|
|
125
|
+
.map((s) => `[${s.n}] ${ts(s.start).slice(3, 8)}–${ts(s.end).slice(3, 8)} ${s.vo}`)
|
|
126
|
+
.join('\n') + '\n';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readTable(file: string): SceneTable {
|
|
130
|
+
try {
|
|
131
|
+
const t = JSON.parse(readFileSync(file, 'utf8')) as SceneTable;
|
|
132
|
+
if (!Array.isArray(t.scenes) || typeof t.durationSec !== 'number') throw new Error('scenes[] · durationSec 가 필요하다');
|
|
133
|
+
return t;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`⛔ 장면표를 못 읽었다: ${(e as Error).message}`);
|
|
136
|
+
process.exit(3);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (import.meta.main) {
|
|
141
|
+
const [cmd, file] = process.argv.slice(2);
|
|
142
|
+
if (!cmd || !file || !['check', 'srt', 'vo'].includes(cmd)) {
|
|
143
|
+
console.error('사용: bun scripts/video/scene-table.ts <check|srt|vo> <scenes.json>');
|
|
144
|
+
process.exit(3);
|
|
145
|
+
}
|
|
146
|
+
const t = readTable(file);
|
|
147
|
+
const problems = checkTable(t);
|
|
148
|
+
if (cmd === 'check') {
|
|
149
|
+
for (const x of problems) console.log(`⛔ ${x}`);
|
|
150
|
+
console.log(problems.length ? `걸림 ${problems.length}` : `✅ 통과 · 장면 ${t.scenes.length} · 자막 ${t.scenes.flatMap((s) => s.ko ?? []).length}`);
|
|
151
|
+
process.exit(problems.length ? 1 : 0);
|
|
152
|
+
}
|
|
153
|
+
if (problems.length) {
|
|
154
|
+
for (const x of problems) console.error(`⛔ ${x}`);
|
|
155
|
+
console.error(`걸림 ${problems.length} — 검사를 통과한 장면표에서만 ${cmd} 를 뽑는다`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(cmd === 'srt' ? toSrt(t) : toVoScript(t));
|
|
159
|
+
}
|
|
@@ -49,9 +49,24 @@ import type { AcpServerOptions, AcpTurnContext } from './server.js';
|
|
|
49
49
|
import { acpToolDenialResult } from './tool-approval.js';
|
|
50
50
|
import { readOriginSessionMeta } from './origin-session-meta.js';
|
|
51
51
|
import { isFactQuestion } from './fact-question.js';
|
|
52
|
+
import { classifyTuiChatIntent, type TuiChatRoute } from '../core-turn/short-question.js';
|
|
53
|
+
import { HARNESS_ROLE_ENV } from '../harness/harness-space.js';
|
|
52
54
|
|
|
53
55
|
const FACT_QUESTION_INSTRUCTION = '사실·최신 정보 질문이다. WebSearch 를 1~2회 쓰고 검색 결과(제목·스니펫·URL)로 답하라. 출처 링크를 붙여라. 셸·브라우저·파일 도구는 검색 결과로 부족할 때만. 도구 예산에 닿으면 지금까지 근거로 답하고 끝에 «더 찾아볼까요?»를 붙여라.';
|
|
54
56
|
|
|
57
|
+
// An allowlist, not a denylist: a new repository writer cannot slip into a quick turn.
|
|
58
|
+
const QUICK_TOOLS: Record<Exclude<TuiChatRoute, '코딩·구현'>, readonly string[]> = {
|
|
59
|
+
'조회': ['decisions_pending', 'ops_seats', 'ops_status', 'release_status', 'logs_query', 'context_now', 'memory_recall', 'Read'],
|
|
60
|
+
'설정 변경': ['DashboardConfigGet', 'DashboardConfigSet', 'LlmListAvailableModels', 'prompt_getRuntimeConfig', 'prompt_setRuntimeConfig', 'view_getConfig', 'view_applyRuntimeConfig', 'view_saveConfig'],
|
|
61
|
+
'조사': ['WebSearch', 'OmniSearch', 'Grep', 'Glob', 'Read', 'fact_check'],
|
|
62
|
+
'상시 일': ['schedule_manage', 'schedule_list', 'schedule_create', 'schedule_update', 'session_manage', 'ops_status'],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function toolsForTuiRoute(tools: readonly LLMToolSpec[], route: Exclude<TuiChatRoute, '코딩·구현'>): LLMToolSpec[] {
|
|
66
|
+
const preferred = QUICK_TOOLS[route];
|
|
67
|
+
return preferred.flatMap((name) => tools.filter((tool) => tool.name === name));
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
export interface CoreTurnBridgeDeps {
|
|
56
71
|
/** Build the seed message list for this ACP prompt. Implementations
|
|
57
72
|
* typically concatenate per-session history with
|
|
@@ -110,6 +125,8 @@ export interface CoreTurnBridgeDeps {
|
|
|
110
125
|
* to 1ms so they don't spend real wall clock waiting for the
|
|
111
126
|
* flag to propagate. */
|
|
112
127
|
abortPollMs?: number;
|
|
128
|
+
/** Only the in-process dashboard session opts into TUI routing; daemon ACP stays unchanged. */
|
|
129
|
+
tuiRouting?: boolean;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
/** Default abort-poll cadence. 50ms = 20 Hz: responsive without
|
|
@@ -192,12 +209,32 @@ export function bridgeCoreTurnToAcp(
|
|
|
192
209
|
promptMeta: turnCtx.promptMeta,
|
|
193
210
|
}),
|
|
194
211
|
);
|
|
195
|
-
const
|
|
212
|
+
const availableTools = await Promise.resolve(
|
|
196
213
|
deps.getTools({
|
|
197
214
|
sessionId: turnCtx.sessionId,
|
|
198
215
|
userText: turnCtx.userText,
|
|
199
216
|
}),
|
|
200
217
|
);
|
|
218
|
+
// Only a goal-driven harness child (explicit executor role — dev-harness · self-implement surfaces) keeps every
|
|
219
|
+
// turn on the goal loop. A human TUI inside a harness space (one-word `dev --elanous`, `--hold`) is routed (UX 10-07).
|
|
220
|
+
const harness = process.env[HARNESS_ROLE_ENV]?.trim() === 'executor';
|
|
221
|
+
const route = deps.tuiRouting && !harness
|
|
222
|
+
? classifyTuiChatIntent(turnCtx.userText, {
|
|
223
|
+
hasAttachments: turnCtx.promptBlocks?.some((block) => block.type !== 'text') ?? false,
|
|
224
|
+
})
|
|
225
|
+
: { route: '코딩·구현' as const, reason: harness ? 'harness' : 'legacy', goalLoop: true };
|
|
226
|
+
const tools = !route.goalLoop && route.route !== '코딩·구현'
|
|
227
|
+
? toolsForTuiRoute(availableTools, route.route)
|
|
228
|
+
: availableTools;
|
|
229
|
+
if (deps.tuiRouting) {
|
|
230
|
+
debug.log('chat.route', 'decided', { route: route.route, reason: route.reason, goalLoop: route.goalLoop });
|
|
231
|
+
// Only a quick route is announced: a coding turn's transcript stays exactly as before (preservation).
|
|
232
|
+
// Awaited so the route line precedes the answer; a failed write is observed, not fatal to the turn.
|
|
233
|
+
if (!route.goalLoop) {
|
|
234
|
+
try { await turnCtx.push(`라우팅: ${route.route}\n`); }
|
|
235
|
+
catch (error) { debug.log('chat.route', 'push-failed', { route: route.route, error: String(error) }); }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
201
238
|
const modelOverride = deps.resolveModel?.({ sessionId: turnCtx.sessionId, userText: turnCtx.userText });
|
|
202
239
|
const factQuestion = isFactQuestion(turnCtx.userText);
|
|
203
240
|
const explicitMaxToolTurns = deps.resolveMaxToolTurns?.({ sessionId: turnCtx.sessionId });
|
|
@@ -233,7 +270,7 @@ export function bridgeCoreTurnToAcp(
|
|
|
233
270
|
const goalLoopCfg = getUserConfig().llm.goalLoop;
|
|
234
271
|
const turnModel = modelOverride ?? getUserConfig().llm.model;
|
|
235
272
|
const turnFamily = getModelFamily(turnModel);
|
|
236
|
-
const armGoalLoop = goalLoopCfg?.enabled === true;
|
|
273
|
+
const armGoalLoop = goalLoopCfg?.enabled === true && route.goalLoop;
|
|
237
274
|
if (armGoalLoop) {
|
|
238
275
|
// 관측 — 어느 family/모델에서 across-turn goal-loop 가 아밍됐나(전-family 개방 검증).
|
|
239
276
|
debug.log('goal.loop', 'arm', { family: turnFamily, model: turnModel });
|