elanous 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.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 +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +25 -24
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +24 -23
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +28 -27
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/package.json +1 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/lib/pch-regress-map.ts +23 -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 +338 -46
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- package/scripts/release-loop/resume-release.ts +64 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +2 -0
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -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/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +32 -7
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +104 -11
- 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 +63 -36
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
- 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/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/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +2 -0
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/index.ts +52 -3
- 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/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +290 -17
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/index.ts +7 -0
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +54 -7
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/rubric.ts +57 -1
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/gate-cli.ts +29 -8
- 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 +2 -1
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +203 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +18 -0
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +32 -6
- package/src/task-agent/shadow.ts +183 -15
- package/src/task-agent/task-hand.ts +117 -15
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
- package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +2 -2
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +134 -2
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-30dff22c9e1f3073.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.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/4565-3d7e3fdeb43686a8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.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/6158.af2e93b7adff709b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-4733c967b56a4277.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-02b073a2ac793b83.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/chat/page-8a9b3594bc23289b.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/control/page-308111abadf35670.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/design-check/page-5d97b2901e0d1ea6.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/exec/page-8c3d8e63ae2e1c98.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-9b72a07d424701c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.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/intake/page-a572f58cd3ca5864.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.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/market/page-fa244bd146a00ae8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-83a700d4c754d802.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7b3170f374f77b9d.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/reflection/page-1467047104141297.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/sessions/page-102310930791197a.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/settings/page-6ed0b73bae8351d8.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/setup/layout-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-3cc47dca0922ab9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-b17cd46cf0e9b8cf.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/trace/page-7bf54c9bb9274af8.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/workspace/page-134da468538baf48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-c2782b330447ffb6.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GATE-PARTIAL (0.2.19 · 10-07): a re-gate after `release cut-branch --append` runs only the test files that can have
|
|
2
|
+
// changed — not the whole 24-shard sweep again (0.2.18: two timeout fixes on top of an 84-minute gate meant another full
|
|
3
|
+
// gate). The plan is opt-in (`release resume --from gate --partial`) and fail-closed: any non-test source change, a prior
|
|
4
|
+
// gate record for another commit, or a missing record means «full gate», never a guess.
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface PartialPlan {
|
|
10
|
+
version: string;
|
|
11
|
+
/** Commit the previous (full) gate measured — its recorded failures are carried for every file not re-run. */
|
|
12
|
+
priorCommit: string;
|
|
13
|
+
/** The commit this plan is for — a gate on any other commit ignores it. */
|
|
14
|
+
forCommit: string;
|
|
15
|
+
/** Test files re-run: changed test files ⊕ files of the prior failures. */
|
|
16
|
+
files: string[];
|
|
17
|
+
priorFailures: string[];
|
|
18
|
+
priorErrors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const partialPlanPath = (root: string, version: string) => join(root, 'release', version, 'gate-partial.json');
|
|
22
|
+
/** The same test-file set the gate sweep runs (`git ls-files` filtered by this suffix). */
|
|
23
|
+
const TEST_FILE = /^(?:[\w.-]+\/)*[\w.-]+\.test\.(?:tsx?|jsx?|mts|cts)$/;
|
|
24
|
+
/** Changes that cannot alter a test result on their own: Markdown that is not test input. A fixture (any file under a
|
|
25
|
+
* test/, fixtures, __fixtures__ or __snapshots__ path), JSON or script is not inert — the gate runs in full. */
|
|
26
|
+
const INERT_MD = /\.md$/;
|
|
27
|
+
const TEST_INPUT = /(?:^|\/)(?:test|tests|fixtures|__fixtures__|__snapshots__|__mocks__)\//;
|
|
28
|
+
const isInert = (file: string) => INERT_MD.test(file) && !TEST_INPUT.test(file);
|
|
29
|
+
|
|
30
|
+
/** The test file of a failure id (`<file> > <case>`). One rule for the gate and the partial plan: ids come only from the
|
|
31
|
+
* gate's output parsers, which accept `dir/….test.ts(x)` — anything else (a root file, `..`, no case) is unsafe. */
|
|
32
|
+
export function testFileOfId(id: string): string {
|
|
33
|
+
const file = id.split(' > ', 1)[0]!;
|
|
34
|
+
if (!id.includes(' > ') || !/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Decide whether a partial re-gate is safe for `changedFiles` (prior → for). Never guesses: any doubt is a full gate. */
|
|
39
|
+
export function planPartialRegate(input: { version: string; priorCommit: string; forCommit: string; changedFiles: readonly string[];
|
|
40
|
+
priorFailures: readonly string[]; priorErrors: readonly string[] }):
|
|
41
|
+
{ ok: true; plan: PartialPlan } | { ok: false; reason: string } {
|
|
42
|
+
if (!/^[0-9a-f]{40}$/i.test(input.priorCommit) || !/^[0-9a-f]{40}$/i.test(input.forCommit)) return { ok: false, reason: 'commit is not a full sha' };
|
|
43
|
+
if (input.priorCommit === input.forCommit) return { ok: false, reason: 'no new commit since the prior gate' };
|
|
44
|
+
const source = input.changedFiles.filter((file) => !TEST_FILE.test(file) && !isInert(file));
|
|
45
|
+
if (source.length) return { ok: false, reason: `non-test change → full gate: ${source.slice(0, 5).join(', ')}${source.length > 5 ? ` (+${source.length - 5})` : ''}` };
|
|
46
|
+
let failureFiles: string[];
|
|
47
|
+
try { failureFiles = [...input.priorFailures, ...input.priorErrors].map(testFileOfId); }
|
|
48
|
+
catch (error) { return { ok: false, reason: String(error instanceof Error ? error.message : error) }; }
|
|
49
|
+
const files = [...new Set([...input.changedFiles.filter((file) => TEST_FILE.test(file)), ...failureFiles])].sort();
|
|
50
|
+
if (!files.length) return { ok: false, reason: 'nothing to re-run' };
|
|
51
|
+
return { ok: true, plan: { version: input.version, priorCommit: input.priorCommit, forCommit: input.forCommit, files,
|
|
52
|
+
priorFailures: [...input.priorFailures], priorErrors: [...input.priorErrors] } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writePartialPlan(root: string, plan: PartialPlan): string {
|
|
56
|
+
const path = partialPlanPath(root, plan.version);
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
59
|
+
writeFileSync(temp, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
renameSync(temp, path);
|
|
61
|
+
return path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearPartialPlan(root: string, version: string): void {
|
|
65
|
+
const path = partialPlanPath(root, version);
|
|
66
|
+
if (existsSync(path)) rmSync(path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The plan for exactly this commit, or null. A malformed plan is an error (never silently a full or partial gate). */
|
|
70
|
+
export function readPartialPlan(roots: readonly string[], version: string, commit: string): PartialPlan | null {
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
const path = partialPlanPath(root, version);
|
|
73
|
+
if (!existsSync(path)) continue;
|
|
74
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as Partial<PartialPlan>;
|
|
75
|
+
if (raw.version !== version || typeof raw.priorCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.priorCommit)
|
|
76
|
+
|| typeof raw.forCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.forCommit) || raw.priorCommit === raw.forCommit
|
|
77
|
+
|| !Array.isArray(raw.files) || raw.files.length === 0 || !raw.files.every((file) => typeof file === 'string' && TEST_FILE.test(file) && !file.split('/').includes('..'))
|
|
78
|
+
|| !Array.isArray(raw.priorFailures) || !Array.isArray(raw.priorErrors)) throw new Error(`invalid gate partial plan: ${path}`);
|
|
79
|
+
// Every prior failure's file is re-run (planPartialRegate guarantees it) — a plan that would carry a failure it did
|
|
80
|
+
// not re-run is malformed, not «partial».
|
|
81
|
+
const rerun = new Set(raw.files);
|
|
82
|
+
let failureFiles: string[];
|
|
83
|
+
try { failureFiles = [...raw.priorFailures, ...raw.priorErrors].map((id) => testFileOfId(String(id))); }
|
|
84
|
+
catch { throw new Error(`invalid gate partial plan: ${path}`); }
|
|
85
|
+
if (failureFiles.some((file) => !rerun.has(file))) throw new Error(`invalid gate partial plan (prior failure not re-run): ${path}`);
|
|
86
|
+
if (raw.forCommit !== commit) continue;
|
|
87
|
+
return raw as PartialPlan;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Cut failures after a partial re-run: prior results for files not re-run ⊕ this run's results for the re-run files. */
|
|
93
|
+
export function mergePartialFailures(plan: PartialPlan, measured: { failures: string[]; errors: string[] }): { failures: string[]; errors: string[] } {
|
|
94
|
+
const rerun = new Set(plan.files);
|
|
95
|
+
const keep = (ids: readonly string[]) => ids.filter((id) => !rerun.has(testFileOfId(id)));
|
|
96
|
+
return { failures: [...keep(plan.priorFailures), ...measured.failures], errors: [...keep(plan.priorErrors), ...measured.errors] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Files changed between two commits — both sides of a rename (`--no-renames`), so moving a source file into a test or
|
|
100
|
+
* Markdown name still shows the source path that disappeared. */
|
|
101
|
+
export function changedFilesBetween(repo: string, from: string, to: string): string[] {
|
|
102
|
+
// git-spawn-allow: read-only diff of two release commits.
|
|
103
|
+
const result = spawnSync('git', ['diff', '--name-only', '--no-renames', `${from}..${to}`], { cwd: repo, encoding: 'utf8' });
|
|
104
|
+
if (result.status !== 0 || result.error) throw new Error(`git diff failed: ${(result.stderr || result.error || 'no output').toString().trim()}`);
|
|
105
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
106
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// F5 GATE-TIMING-REPORT — 게이트 한 판의 «시간 비용»을 보이게 한다(GATE-SPEED 의 자).
|
|
2
|
+
// 입력 = 게이트 로그 디렉토리(`<ledger>/release/<version>/gate-logs/cut`)의 `pod-*.json`.
|
|
3
|
+
// 첫 판 샤드 = `pod-<n>.json` · 재시도 = 그 밖의 모든 `pod-*.json`(`-c<k>`·`-c<k>-file-<m>`·`-retry` …).
|
|
4
|
+
// ⛔ 측정이 없으면 `measured:false` 로 표시한다 — 0 을 «잰 값»처럼 내지 않는다.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
|
|
9
|
+
export interface ShardTiming { name: string; min: number; files: number; plannedMin: number | null }
|
|
10
|
+
|
|
11
|
+
export interface GateTimingReport {
|
|
12
|
+
dir: string;
|
|
13
|
+
/** 읽은 pod-*.json 중 durationMs 를 가진 것이 하나라도 있었나. false 면 아래 수는 «측정 없음». */
|
|
14
|
+
measured: boolean;
|
|
15
|
+
/** JSON 파싱 실패·durationMs 부재로 건너뛴 파일 이름. */
|
|
16
|
+
unreadable: string[];
|
|
17
|
+
firstPass: { shards: number; sumMin: number; medianMin: number; maxMin: number; failedShards: string[] };
|
|
18
|
+
retries: { runs: number; sumMin: number; medianMin: number };
|
|
19
|
+
/** 재시도 합 / (첫 판 합 + 재시도 합). 측정이 없으면 null. */
|
|
20
|
+
retryShare: number | null;
|
|
21
|
+
slowestShards: ShardTiming[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIRST_PASS = /^pod-\d+\.json$/;
|
|
25
|
+
const POD_JSON = /^pod-.+\.json$/;
|
|
26
|
+
|
|
27
|
+
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
28
|
+
|
|
29
|
+
function median(xs: number[]): number {
|
|
30
|
+
if (xs.length === 0) return 0;
|
|
31
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
32
|
+
const m = s.length >> 1;
|
|
33
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PodRecord { durationMs?: unknown; rc?: unknown; files?: unknown; plannedSeconds?: unknown; jobFailed?: unknown }
|
|
37
|
+
|
|
38
|
+
export function gateTiming(dir: string): GateTimingReport {
|
|
39
|
+
const names = existsSync(dir) && statSync(dir).isDirectory()
|
|
40
|
+
? readdirSync(dir).filter((n) => POD_JSON.test(n)).sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
|
|
41
|
+
: [];
|
|
42
|
+
const first: Array<ShardTiming & { failed: boolean }> = [];
|
|
43
|
+
const retryMin: number[] = [];
|
|
44
|
+
const unreadable: string[] = [];
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
let rec: PodRecord;
|
|
47
|
+
try { rec = JSON.parse(readFileSync(join(dir, name), 'utf8')) as PodRecord; } catch { unreadable.push(name); continue; }
|
|
48
|
+
if (!rec || typeof rec !== 'object' || typeof rec.durationMs !== 'number' || !Number.isFinite(rec.durationMs)) { unreadable.push(name); continue; }
|
|
49
|
+
const min = rec.durationMs / 60_000;
|
|
50
|
+
if (FIRST_PASS.test(name)) {
|
|
51
|
+
first.push({
|
|
52
|
+
name: name.replace(/\.json$/, ''),
|
|
53
|
+
min,
|
|
54
|
+
files: Array.isArray(rec.files) ? rec.files.length : 0,
|
|
55
|
+
plannedMin: typeof rec.plannedSeconds === 'number' ? rec.plannedSeconds / 60 : null,
|
|
56
|
+
failed: rec.jobFailed === true || (typeof rec.rc === 'number' && rec.rc !== 0),
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
retryMin.push(min);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const firstMins = first.map((s) => s.min);
|
|
63
|
+
const firstSum = firstMins.reduce((a, b) => a + b, 0);
|
|
64
|
+
const retrySum = retryMin.reduce((a, b) => a + b, 0);
|
|
65
|
+
const total = firstSum + retrySum;
|
|
66
|
+
const measured = first.length + retryMin.length > 0;
|
|
67
|
+
return {
|
|
68
|
+
dir,
|
|
69
|
+
measured,
|
|
70
|
+
unreadable,
|
|
71
|
+
firstPass: {
|
|
72
|
+
shards: first.length,
|
|
73
|
+
sumMin: round1(firstSum),
|
|
74
|
+
medianMin: round1(median(firstMins)),
|
|
75
|
+
maxMin: round1(firstMins.length ? Math.max(...firstMins) : 0),
|
|
76
|
+
failedShards: first.filter((s) => s.failed).map((s) => s.name),
|
|
77
|
+
},
|
|
78
|
+
retries: { runs: retryMin.length, sumMin: round1(retrySum), medianMin: round1(median(retryMin)) },
|
|
79
|
+
retryShare: measured && total > 0 ? Math.round((retrySum / total) * 1000) / 1000 : null,
|
|
80
|
+
slowestShards: [...first].sort((a, b) => b.min - a.min).slice(0, 5)
|
|
81
|
+
.map(({ name, min, files, plannedMin }) => ({ name, min: round1(min), files, plannedMin: plannedMin === null ? null : round1(plannedMin) })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `<version>` 이면 원장 루트(`releaseLedgerRoot()`)의 `release/<version>/gate-logs/cut`, 경로처럼 보이면 그대로. */
|
|
86
|
+
export function resolveGateLogDir(arg: string, ledgerRoot: string = releaseLedgerRoot()): string {
|
|
87
|
+
if (arg.includes('/') || arg.startsWith('.') || existsSync(arg)) return arg;
|
|
88
|
+
return join(ledgerRoot, 'release', arg, 'gate-logs', 'cut');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatGateTiming(r: GateTimingReport): string {
|
|
92
|
+
if (!r.measured) {
|
|
93
|
+
return [`게이트 시간 — 측정 없음 (${r.dir})`, ' pod-*.json 에 durationMs 가 하나도 없다 · 0 은 «잰 값»이 아니다',
|
|
94
|
+
...(r.unreadable.length ? [` 읽지 못한 파일 ${r.unreadable.length}개`] : [])].join('\n');
|
|
95
|
+
}
|
|
96
|
+
const f = r.firstPass;
|
|
97
|
+
const lines = [
|
|
98
|
+
`게이트 시간 (${r.dir})`,
|
|
99
|
+
` 첫 판 샤드 ${f.shards}개 · 합 ${f.sumMin}분 · 중앙 ${f.medianMin}분 · 최대 ${f.maxMin}분 · 실패 샤드 ${f.failedShards.length}개${f.failedShards.length ? ` (${f.failedShards.join(', ')})` : ''}`,
|
|
100
|
+
` 재시도 ${r.retries.runs}회 · 합 ${r.retries.sumMin}분 · 중앙 ${r.retries.medianMin}분`,
|
|
101
|
+
` 재시도 비중 ${r.retryShare === null ? '측정 없음' : `${round1(r.retryShare * 100)}%`} (재시도 합 / 전체 합)`,
|
|
102
|
+
' 느린 샤드 상위 5:',
|
|
103
|
+
...r.slowestShards.map((s) => ` ${s.name} ${s.min}분 · 파일 ${s.files}개 · 계획 ${s.plannedMin === null ? '—' : `${s.plannedMin}분`}`),
|
|
104
|
+
];
|
|
105
|
+
if (r.unreadable.length) lines.push(` ⚠ 읽지 못한 파일 ${r.unreadable.length}개: ${r.unreadable.slice(0, 5).join(', ')}${r.unreadable.length > 5 ? ' …' : ''}`);
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (import.meta.main) {
|
|
110
|
+
const args = process.argv.slice(2);
|
|
111
|
+
const json = args.includes('--json');
|
|
112
|
+
const target = args.find((a) => !a.startsWith('--'));
|
|
113
|
+
if (!target) {
|
|
114
|
+
console.error('사용법: bun scripts/release-loop/gate-timing.ts <version|dir> [--json]');
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
const report = gateTiming(resolveGateLogDir(target));
|
|
118
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatGateTiming(report));
|
|
119
|
+
if (!report.measured) process.exit(1);
|
|
120
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
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
10
|
import { isReleaseVersion } from './release-version.js';
|
|
11
|
+
import { changedFilesBetween, clearPartialPlan, planPartialRegate, writePartialPlan } from './gate-partial.js';
|
|
11
12
|
|
|
12
13
|
const GRAPH = join(import.meta.dir, '../../graphs/release/release-loop.yaml');
|
|
13
14
|
|
|
@@ -24,6 +25,8 @@ export interface ResumeReleaseDeps {
|
|
|
24
25
|
/** Node command runner for the resumed graph (tests record each node's context instead of running it). */
|
|
25
26
|
runBash?: NonNullable<NonNullable<Parameters<typeof runGraph>[1]>['deps']>['runBash'];
|
|
26
27
|
now?: () => Date;
|
|
28
|
+
/** Machine release ledger (gate-failures.json of the prior gate) — default releaseLedgerRoot(). */
|
|
29
|
+
ledgerRoot?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface TipRefresh { version: string; branch: string; from: string; to: string; changed: boolean; statePath: string; backup?: string }
|
|
@@ -106,10 +109,66 @@ export function refreshReleaseBranchTip(runId: string, deps: ResumeReleaseDeps =
|
|
|
106
109
|
return { version, branch, from: recorded, to, changed: true, statePath, backup };
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/** GATE-PARTIAL outcome of a resume — applied (plan written) or the reason it fell back to a full gate. */
|
|
113
|
+
export type PartialOutcome = { requested: false } | { requested: true; applied: true; files: number; priorCommit: string } | { requested: true; applied: false; reason: string };
|
|
114
|
+
|
|
115
|
+
/** Build (or clear) the partial re-gate plan for the new tip. Fail-closed: any doubt leaves a full gate. */
|
|
116
|
+
function preparePartial(tip: TipRefresh, opts: { from: string; partial?: boolean }, deps: ResumeReleaseDeps): PartialOutcome {
|
|
117
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
118
|
+
// Any resume starts from no plan: one without --partial (from whatever node) never inherits an older plan.
|
|
119
|
+
clearPartialPlan(root, tip.version);
|
|
120
|
+
if (!opts.partial) return { requested: false };
|
|
121
|
+
const fallback = (reason: string): PartialOutcome => {
|
|
122
|
+
debug.log('release-loop.resume', 'partial-fallback', { version: tip.version, reason });
|
|
123
|
+
return { requested: true, applied: false, reason };
|
|
124
|
+
};
|
|
125
|
+
if (opts.from !== 'gate') return fallback('--partial only applies to --from gate');
|
|
126
|
+
if (!tip.changed) return fallback('release branch tip unchanged — nothing new to re-check');
|
|
127
|
+
let prior: { commit?: unknown; failures?: unknown; errors?: unknown } | undefined;
|
|
128
|
+
for (const location of [...new Set([deps.ledgerRoot ?? releaseLedgerRoot(), root])]) {
|
|
129
|
+
const path = join(location, 'release', tip.version, 'gate-failures.json');
|
|
130
|
+
if (!existsSync(path)) continue;
|
|
131
|
+
let record: typeof prior;
|
|
132
|
+
try { record = JSON.parse(readFileSync(path, 'utf8')) as typeof prior; }
|
|
133
|
+
catch { return fallback(`unreadable gate record: ${path}`); }
|
|
134
|
+
if (record?.commit === tip.from) { prior = record; break; }
|
|
135
|
+
}
|
|
136
|
+
if (!prior || !Array.isArray(prior.failures) || !(prior.errors === undefined || Array.isArray(prior.errors))) {
|
|
137
|
+
return fallback(`no complete gate record for ${tip.from.slice(0, 12)} (a stalled or errored gate keeps none)`);
|
|
138
|
+
}
|
|
139
|
+
// Both sides of a rename count (`--no-renames`): a source moved to a test/Markdown name is still a source change.
|
|
140
|
+
let changed: string[];
|
|
141
|
+
try {
|
|
142
|
+
changed = deps.git
|
|
143
|
+
? deps.git(['diff', '--name-only', '--no-renames', `${tip.from}..${tip.to}`]).split(/\r?\n/).filter(Boolean)
|
|
144
|
+
: changedFilesBetween(deps.repo ?? process.cwd(), tip.from, tip.to);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// The tip is already refreshed — a failed diff must not strand the resume: run the full gate and say why.
|
|
147
|
+
return fallback(`changed files unreadable: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`);
|
|
148
|
+
}
|
|
149
|
+
const planned = planPartialRegate({ version: tip.version, priorCommit: tip.from, forCommit: tip.to, changedFiles: changed,
|
|
150
|
+
priorFailures: prior.failures as string[], priorErrors: (prior.errors ?? []) as string[] });
|
|
151
|
+
if (!planned.ok) return fallback(planned.reason);
|
|
152
|
+
const path = writePartialPlan(root, planned.plan);
|
|
153
|
+
debug.log('release-loop.resume', 'partial-planned', { version: tip.version, priorCommit: tip.from, commit: tip.to, files: planned.plan.files.length, path });
|
|
154
|
+
return { requested: true, applied: true, files: planned.plan.files.length, priorCommit: tip.from };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The graph file the run started from — a snapshot run refuses any other path (the installed CLI's own copy differs). */
|
|
158
|
+
function savedGraphPath(statePath: string): string | undefined {
|
|
159
|
+
try {
|
|
160
|
+
const saved = JSON.parse(readFileSync(statePath, 'utf8')) as { graphPath?: unknown };
|
|
161
|
+
return typeof saved.graphPath === 'string' && saved.graphPath ? saved.graphPath : undefined;
|
|
162
|
+
} catch { return undefined; }
|
|
163
|
+
}
|
|
164
|
+
|
|
109
165
|
/** 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 }> {
|
|
166
|
+
export async function resumeReleaseRun(opts: { runId: string; from: string; partial?: boolean }, deps: ResumeReleaseDeps = {}): Promise<{ tip: TipRefresh; partial: PartialOutcome; state: GraphRunState }> {
|
|
111
167
|
if (opts.from === 'version-release') throw new Error('--from version-release would cut again — resume at a later node');
|
|
112
168
|
const tip = refreshReleaseBranchTip(opts.runId, deps, opts.from);
|
|
113
|
-
const
|
|
114
|
-
|
|
169
|
+
const partial = preparePartial(tip, opts, deps);
|
|
170
|
+
// Resume the graph at the path it was started from (its snapshot is used); only tests inject another path.
|
|
171
|
+
const graphPath = deps.graphPath ?? savedGraphPath(tip.statePath) ?? GRAPH;
|
|
172
|
+
const state = await (deps.graph ?? runGraph)(graphPath, { resumeRunId: opts.runId, fromNodeId: opts.from, ...(deps.root || deps.runBash ? { deps: { ...(deps.root ? { root: deps.root } : {}), ...(deps.runBash ? { runBash: deps.runBash } : {}) } } : {}) });
|
|
173
|
+
return { tip, partial, state };
|
|
115
174
|
}
|
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,8 @@ export interface ReleaseLoopConfig {
|
|
|
19
19
|
gatePodBunCache?: string;
|
|
20
20
|
gatePodShards?: number;
|
|
21
21
|
gatePodShardTimeoutSeconds?: number;
|
|
22
|
+
/** GATE-SPEED A3①: gate shard Pod CPU — `2` (request = limit) or `{ request, limit }`. Omitted = request 1 / limit 4. */
|
|
23
|
+
gatePodCpu?: number | string | { request?: number | string; limit?: number | string };
|
|
22
24
|
gateRemote?: string;
|
|
23
25
|
gateRemoteMirror?: string;
|
|
24
26
|
opsHosts?: string[];
|
|
@@ -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 });
|