elanous 0.2.9 → 0.2.10
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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f972fc1c00a62e88.js → page-54b32bd27726588d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.js → page-6cb0e1bbefe5fb3d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0df30e0c7e50148a.js → page-e168fa1030af0548.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.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/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/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/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/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 -0
- package/apps/pwa/out/ops/seats/index.txt +30 -0
- 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 -0
- package/apps/pwa/out/today/index.txt +33 -0
- 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/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/package.json +1 -1
- package/plugins/card-followup/graphs/run-step.ts +48 -13
- package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
- package/plugins/doc-draft/graphs/run-step.ts +13 -6
- package/scripts/ci-pwa-build-gate.ts +160 -0
- package/scripts/coord-channel-watch.sh +42 -8
- package/scripts/coord-digest.ts +199 -0
- package/scripts/coord-post.sh +60 -3
- package/scripts/install.ps1 +68 -9
- package/scripts/install.sh +39 -12
- package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
- package/scripts/lib/tui-regress-checks.ts +146 -0
- package/scripts/memory-lifecycle-cycle.ts +2 -2
- package/scripts/ops-now-probe.ts +327 -0
- package/scripts/publish-internal-dist.ts +11 -1
- package/scripts/release-loop/cutoff.ts +19 -8
- package/scripts/release-loop/ops-upgrade-node.ts +72 -16
- package/scripts/release-loop/release-readiness.ts +9 -0
- package/scripts/release-loop/version-node.ts +58 -10
- package/scripts/repo-watch-cycle.ts +1 -1
- package/scripts/retro-cycle.ts +2 -2
- package/scripts/schedule-health-report.ts +1 -1
- package/scripts/storyboard/storyboard.ts +5 -161
- package/scripts/test-diet/lib.ts +30 -1
- package/scripts/test-diet/node.ts +82 -7
- package/scripts/test-timeout-audit.ts +114 -0
- package/scripts/tree-sync-alert.ts +1 -1
- package/scripts/tui-regress.ts +71 -0
- package/scripts/waitlist-daily-digest.ts +1 -1
- package/skills/explainer-video/SKILL.md +4 -0
- package/skills/explainer-video/engine/reel.mjs +18 -13
- package/skills/explainer-video/engine/reel.sh +24 -5
- package/skills/explainer-video/engine/stitch.mjs +145 -0
- package/skills/pitch-deck/SKILL.md +36 -0
- package/skills/pitch-deck/template.html +80 -0
- package/src/acp/codex-approval-adapter.ts +9 -3
- package/src/acp/core-turn-bridge.ts +117 -18
- package/src/acp/fact-question.ts +11 -0
- package/src/acp/server.ts +48 -7
- package/src/acp/tool-approval.ts +126 -0
- package/src/acp/turn-runner.ts +36 -0
- package/src/agent/monad-agent-turn.ts +8 -0
- package/src/ask-user-question/pending-questions.ts +25 -10
- package/src/ask-user-question/types.ts +9 -0
- package/src/auth/setup-link-tokens.ts +16 -5
- package/src/autopilot/build/integrity-gate.ts +12 -0
- package/src/boot/daemon-prompt-turn.ts +3 -0
- package/src/boot/daemon-tools/index.ts +8 -0
- package/src/boot/daemon-tools/types.ts +2 -0
- package/src/boot/daemon-tools/web-search.ts +1 -0
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +52 -0
- package/src/chat/pickers/state.ts +6 -2
- package/src/cli/agent-cli.ts +7 -0
- package/src/cli/decisions-cli.ts +25 -3
- package/src/cli/doctor-bundle-upload.ts +68 -0
- package/src/cli/doctor-cli.ts +38 -4
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +31 -2
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +63 -20
- package/src/cli/self-update.ts +14 -0
- package/src/cli/storyboard-cli.ts +37 -0
- package/src/cli/test-state-dir-flag.ts +11 -4
- package/src/connectors/linear.ts +146 -0
- package/src/context-bus/coord-events.ts +115 -0
- package/src/dashboard/dashboard-boot-fixture.ts +23 -0
- package/src/dashboard/index.ts +58 -21
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/decisions/decision-cards.ts +49 -1
- package/src/decisions/decision-ledger.ts +75 -4
- package/src/decisions/decision-linear-projection.ts +157 -0
- package/src/decisions/web-push-decision-cards.ts +50 -0
- package/src/directive/interpretation-card.ts +36 -0
- package/src/discord.ts +2 -1
- package/src/domains/coo-admin-tool.ts +62 -0
- package/src/domains/core-tools.ts +11 -3
- package/src/domains/elanous-card.ts +29 -0
- package/src/domains/ops-facts-tool.ts +76 -0
- package/src/domains/outbound-alert.ts +41 -13
- package/src/domains/release-tool.ts +168 -0
- package/src/domains/telegram-kind-route.ts +19 -1
- package/src/exec-requests/planner.ts +98 -28
- package/src/exec-requests/runner.ts +107 -10
- package/src/exec-requests/store.ts +8 -2
- package/src/field/field-media.ts +10 -7
- package/src/field/field-reel.ts +14 -4
- package/src/git-fs/worktree.ts +45 -6
- package/src/graph-runner/graph-approval-card.ts +113 -0
- package/src/graph-runner/graph-cli.ts +129 -4
- package/src/graph-runner/graph-notify.ts +25 -3
- package/src/graph-runner/runner.ts +142 -16
- package/src/harness/browser-verify.ts +25 -2
- package/src/harness/harness-cli-command.ts +231 -35
- package/src/harness/harness-incidents-writer.fixture.ts +24 -0
- package/src/harness/harness-incidents.ts +129 -0
- package/src/harness/harness-stop.ts +53 -3
- package/src/hitl/clarification-policy.ts +3 -1
- package/src/index.ts +156 -37
- package/src/intake-plane/author-goals.ts +37 -4
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/discord-seat-work.ts +47 -2
- package/src/intake-plane/seat-doc-route.ts +170 -6
- package/src/intake-plane/telegram-seat-work.ts +44 -2
- package/src/intake-plane/telegram-work.ts +1 -0
- package/src/maturity/cli-maturity.ts +34 -0
- package/src/maturity/feature-maturity.ts +98 -1
- package/src/maturity/tui-slash-maturity.ts +31 -0
- package/src/memory.ts +32 -4
- package/src/nexus/api/exec-requests.ts +33 -3
- package/src/nexus/api/http-server.ts +53 -1
- package/src/nexus/api/meta-api.ts +11 -0
- package/src/nexus/api/ops-api.ts +10 -1
- package/src/nexus/api/ops-seats-sources.ts +37 -7
- package/src/nexus/api/ops-seats.ts +16 -6
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/index.ts +45 -4
- package/src/onboarding/first-run.ts +89 -0
- package/src/onboarding/lan-address.ts +45 -0
- package/src/onboarding/web-first.ts +119 -24
- package/src/onboarding.ts +4 -0
- package/src/ops-now/ops-now-refresh.ts +100 -0
- package/src/project/project-store.ts +115 -0
- package/src/release-loop/checklist.ts +78 -4
- package/src/release-loop/feature-store.ts +154 -21
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +41 -8
- package/src/seat-dispatch/ceo-intent.ts +38 -0
- package/src/seat-loop/seat-loop.ts +231 -0
- package/src/seat-loop/seat-report.ts +64 -0
- package/src/self-dev/decompose.ts +66 -11
- package/src/self-dev/run-store.ts +10 -1
- package/src/self-implement/execution-clarification.ts +115 -0
- package/src/self-implement/gate-cli.ts +6 -3
- package/src/self-implement/orchestrator.ts +37 -3
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/run-status-mapping.ts +5 -3
- package/src/session/chat.ts +2 -0
- package/src/session/index.ts +9 -0
- package/src/shell-env-bootstrap.ts +17 -0
- package/src/steward/directive.ts +51 -2
- package/src/steward/launch.ts +250 -0
- package/src/steward/outcome-notify.ts +115 -0
- package/src/steward/steward-cards.ts +8 -2
- package/src/steward/triage-plan.ts +32 -4
- package/src/steward/triage.ts +93 -9
- package/src/steward/working-backwards.ts +64 -0
- package/src/storyboard/storyboard.ts +141 -0
- package/src/task-cards/card-store.ts +2 -2
- package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
- package/src/telegram-card-followup.ts +233 -0
- package/src/telegram-report.ts +15 -9
- package/src/telegram-run.ts +7 -0
- package/src/telegram.ts +29 -8
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
package/scripts/test-diet/lib.ts
CHANGED
|
@@ -9,7 +9,9 @@ export const HEAVY_MB = 2048;
|
|
|
9
9
|
|
|
10
10
|
export interface Measurement { file: string; secs: number; rssMb: number | null; rc: number | null; pass: number | null; fail: number | null; reason?: string }
|
|
11
11
|
export type Verdict = 'keep' | 'failing' | 'review';
|
|
12
|
-
export
|
|
12
|
+
export type Proposal = 'delete' | 'shrink' | 'rewrite-cheap' | 'move-out-of-gate' | 'investigate';
|
|
13
|
+
export interface Td1Disposition { disposition: string; alternative: string; guards: string; why_slow: string }
|
|
14
|
+
export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict; proposal?: Proposal | null; basis?: string | null }
|
|
13
15
|
export interface LedgerLine { at: string; range: string; start: number; end: number; next: number; total: number; commit: string; budgetSecs: number; results: Judged[] }
|
|
14
16
|
|
|
15
17
|
/** `file\tsecs\t…` (TD1 whole-gate TSV) → seconds per file. */
|
|
@@ -23,6 +25,33 @@ export function costTable(tsv: string): Map<string, number> {
|
|
|
23
25
|
return costs;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/** TD1's human-reviewed dispositions, resolved by column name rather than TSV position. */
|
|
29
|
+
export function td1Dispositions(tsv: string): Map<string, Td1Disposition> {
|
|
30
|
+
const [header, ...rows] = tsv.trimEnd().split('\n');
|
|
31
|
+
const columns = header?.replace(/\r$/, '').split('\t') ?? [];
|
|
32
|
+
const index = (name: string) => columns.indexOf(name);
|
|
33
|
+
const result = new Map<string, Td1Disposition>();
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const cells = row.replace(/\r$/, '').split('\t');
|
|
36
|
+
const get = (name: string) => cells[index(name)] ?? '';
|
|
37
|
+
if (get('file') && get('disposition')) result.set(get('file'), {
|
|
38
|
+
disposition: get('disposition'), alternative: get('alternative'), guards: get('guards'), why_slow: get('why_slow'),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function propose(judged: Judged, td1: ReadonlyMap<string, Td1Disposition>): Judged {
|
|
45
|
+
const reviewed = td1.get(judged.file);
|
|
46
|
+
if (reviewed) {
|
|
47
|
+
const proposal = reviewed.disposition === 'keep' ? null : reviewed.disposition as Proposal;
|
|
48
|
+
return { ...judged, proposal, basis: proposal ? [reviewed.alternative, reviewed.why_slow, reviewed.guards && `guards: ${reviewed.guards}`].filter(Boolean).join(' · ') : null };
|
|
49
|
+
}
|
|
50
|
+
if (judged.verdict === 'failing') return { ...judged, proposal: 'investigate', basis: `failing (rc=${judged.rc}) · caught90=${judged.caught90}` };
|
|
51
|
+
if (judged.verdict === 'review') return { ...judged, proposal: 'shrink', basis: `${judged.flags.join(', ')} · caught90=${judged.caught90}` };
|
|
52
|
+
return { ...judged, proposal: null, basis: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
/** Next slice from the cursor until the known cost reaches the budget (at least one file; wraps at the end). */
|
|
27
56
|
export function pickRange(files: readonly string[], start: number, costs: ReadonlyMap<string, number>, budgetSecs: number): { start: number; end: number; next: number; files: string[]; estimatedSecs: number } {
|
|
28
57
|
if (files.length === 0) return { start: 0, end: -1, next: 0, files: [], estimatedSecs: 0 };
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// TD2 graph nodes: pick → measure → record. Shadow only — measures alone on a remote host and drafts a card.
|
|
3
3
|
// bun scripts/test-diet/node.ts pick|measure|audit|record (test-diet or nightly-audit graph)
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { debug } from '../../src/debug/log.js';
|
|
8
9
|
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
10
|
+
import { CardStore } from '../../src/task-cards/card-store.js';
|
|
9
11
|
import { GATE_NIGHTLY_AUDITS } from '../release-loop/gate-node.js';
|
|
10
|
-
import { appendLedger, costTable, judge, lastLedgerLine, nightlyAuditLedgerPath, pickRange, writeCardDraft, type Measurement } from './lib.js';
|
|
12
|
+
import { appendLedger, costTable, judge, lastLedgerLine, ledgerPath, nightlyAuditLedgerPath, pickRange, propose, td1Dispositions, writeCardDraft, type LedgerLine, type Measurement, type Td1Disposition } from './lib.js';
|
|
11
13
|
|
|
12
14
|
type Context = { graphId?: string; input: Record<string, unknown>; outputs: Record<string, Record<string, unknown> | null> };
|
|
13
15
|
function context(): Context {
|
|
@@ -114,12 +116,13 @@ function recordNightlyAudit(ctx: Context): number {
|
|
|
114
116
|
return emit({ outcome: failing ? 'fail' : 'ok', summary: `nightly audit ${results.length} · failing ${failing}${card ? ' · card draft' : ''}`, failing, ledger: path, ...(card ? { card } : {}) });
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
function record(ctx: Context): number {
|
|
119
|
+
export function record(ctx: Context, deps: { createStore?: (root: string) => Pick<CardStore, 'createCard' | 'appendSection' | 'close'>; td1?: ReadonlyMap<string, Td1Disposition> } = {}): number {
|
|
118
120
|
const p = ctx.outputs.pick;
|
|
119
121
|
const m = ctx.outputs.measure;
|
|
120
122
|
const measurements = (m?.measurements ?? []) as Measurement[];
|
|
121
123
|
if (!p || measurements.length === 0) return emit({ outcome: 'fail', summary: 'nothing measured' });
|
|
122
|
-
const
|
|
124
|
+
const td1 = deps.td1 ?? td1Dispositions(readFileSync(join(process.cwd(), 'docs/measurements/td1-top200-content-review-2026-10-01.tsv'), 'utf8'));
|
|
125
|
+
const results = measurements.map((x) => propose(judge(x, git(['log', '--since=90.days', '--format=%s', '--', x.file]).split('\n').filter((s) => /^fix\b/i.test(s)).length), td1));
|
|
123
126
|
const line = {
|
|
124
127
|
at: new Date().toISOString(), range: `#${p.start}~#${p.end}`, start: Number(p.start), end: Number(p.end), next: Number(p.next), total: Number(p.total),
|
|
125
128
|
commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results,
|
|
@@ -127,17 +130,89 @@ function record(ctx: Context): number {
|
|
|
127
130
|
const root = effectiveInstanceRoot();
|
|
128
131
|
appendLedger(root, line);
|
|
129
132
|
const card = writeCardDraft(root, line);
|
|
133
|
+
const proposals: Record<string, number> = {};
|
|
134
|
+
for (const result of results) {
|
|
135
|
+
if (!result.proposal) continue;
|
|
136
|
+
proposals[result.proposal] = (proposals[result.proposal] ?? 0) + 1;
|
|
137
|
+
let store: Pick<CardStore, 'createCard' | 'appendSection' | 'close'> | undefined;
|
|
138
|
+
try {
|
|
139
|
+
store = deps.createStore?.(root) ?? new CardStore(root);
|
|
140
|
+
const task = store.createCard({ goalId: `test-diet:${result.file}:${result.proposal}`, title: `시험 다이어트 제안 — ${result.file} · ${result.proposal}` });
|
|
141
|
+
const observation = { proposal: result.proposal, basis: result.basis, guards: td1.get(result.file)?.guards ?? '',
|
|
142
|
+
secs: result.secs, rssMb: result.rssMb, caught90: result.caught90, range: line.range };
|
|
143
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(observation)).digest('hex');
|
|
144
|
+
const key = `test-diet:${line.commit}:${fingerprint}`;
|
|
145
|
+
if (!task.sections.some((section) => section.key === key)) store.appendSection(task.id, {
|
|
146
|
+
key, owner: 'test-diet', content: JSON.stringify({ ...observation, at: line.at }),
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn(`test-diet card failed: ${result.file}: ${String(error)}`);
|
|
150
|
+
debug.log('test-diet', 'card-failed', { file: result.file });
|
|
151
|
+
} finally {
|
|
152
|
+
try { store?.close(); } catch (error) {
|
|
153
|
+
console.warn(`test-diet card close failed: ${result.file}: ${String(error)}`);
|
|
154
|
+
debug.log('test-diet', 'card-failed', { file: result.file });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
debug.log('test-diet', 'proposed', { range: line.range, proposals });
|
|
130
159
|
const counts = { keep: results.filter((r) => r.verdict === 'keep').length, review: results.filter((r) => r.verdict === 'review').length, failing: results.filter((r) => r.verdict === 'failing').length };
|
|
131
160
|
debug.log('test-diet', 'recorded', { range: line.range, ...counts, card: card !== null });
|
|
132
161
|
return emit({ outcome: 'ok', summary: `test-diet ${line.range} · keep ${counts.keep} · review ${counts.review} · failing ${counts.failing}${card ? ' · card draft' : ''}`, range: line.range, ...counts, ...(card ? { card } : {}) });
|
|
133
162
|
}
|
|
134
163
|
|
|
164
|
+
function status(root: string, options: { json?: boolean; days?: number } = {}): number {
|
|
165
|
+
const path = ledgerPath(root);
|
|
166
|
+
const ledger = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
167
|
+
if (!ledger.trim()) {
|
|
168
|
+
if (options.json) console.log(JSON.stringify({ error: '원장 없음 — 한 번도 안 돌았다' }));
|
|
169
|
+
else console.log('원장 없음 — 한 번도 안 돌았다');
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const latest = new Map<string, LedgerLine>();
|
|
173
|
+
for (const raw of ledger.split('\n').filter(Boolean)) {
|
|
174
|
+
const line = JSON.parse(raw) as LedgerLine;
|
|
175
|
+
const date = new Date(new Date(line.at).getTime() + 9 * 3600_000).toISOString().slice(0, 10);
|
|
176
|
+
if (!latest.has(date) || latest.get(date)!.at <= line.at) latest.set(date, line);
|
|
177
|
+
}
|
|
178
|
+
const days = options.days ?? 3;
|
|
179
|
+
const dates = [...latest.keys()].sort().reverse().slice(0, days);
|
|
180
|
+
let consecutive = 0;
|
|
181
|
+
for (const date of [...latest.keys()].sort().reverse()) {
|
|
182
|
+
const expected = new Date(Date.parse(`${dates[0]}T00:00:00Z`) - consecutive * 86_400_000).toISOString().slice(0, 10);
|
|
183
|
+
if (date !== expected) break;
|
|
184
|
+
consecutive++;
|
|
185
|
+
}
|
|
186
|
+
const rows = dates.map((date) => {
|
|
187
|
+
const line = latest.get(date)!;
|
|
188
|
+
return { date, range: line.range, measured: line.results.length, total: line.total,
|
|
189
|
+
keep: line.results.filter((r) => r.verdict === 'keep').length,
|
|
190
|
+
review: line.results.filter((r) => r.verdict === 'review').length,
|
|
191
|
+
failing: line.results.filter((r) => r.verdict === 'failing').length,
|
|
192
|
+
proposals: line.results.filter((r) => r.proposal != null).length };
|
|
193
|
+
});
|
|
194
|
+
if (options.json) console.log(JSON.stringify({ rows, consecutive, days }));
|
|
195
|
+
else {
|
|
196
|
+
for (const row of rows) console.log(`${row.date} · ${row.range} · ${row.measured}/${row.total} · keep ${row.keep}/review ${row.review}/failing ${row.failing} · 제안 ${row.proposals}`);
|
|
197
|
+
console.log(`연속 산출: ${consecutive}일 (기준 ${days})`);
|
|
198
|
+
}
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
135
202
|
if (import.meta.main) {
|
|
136
203
|
const step = process.argv[2];
|
|
137
204
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
if (step === 'status') {
|
|
206
|
+
const args = process.argv.slice(3);
|
|
207
|
+
const daysAt = args.indexOf('--days');
|
|
208
|
+
const days = daysAt < 0 ? 3 : Number(args[daysAt + 1]);
|
|
209
|
+
if (!Number.isSafeInteger(days) || days < 1) throw new Error('--days needs a positive integer');
|
|
210
|
+
process.exitCode = status(effectiveInstanceRoot(), { json: args.includes('--json'), days });
|
|
211
|
+
} else {
|
|
212
|
+
const ctx = context();
|
|
213
|
+
process.exitCode = step === 'pick' ? pick(ctx) : step === 'measure' ? measure(ctx) : step === 'audit' ? measure(ctx, true)
|
|
214
|
+
: step === 'record' ? (ctx.graphId === 'nightly-audit' || Object.hasOwn(ctx.outputs, 'audit') ? recordNightlyAudit(ctx) : record(ctx)) : emit({ outcome: 'error', summary: `unknown step: ${step}` });
|
|
215
|
+
}
|
|
141
216
|
} catch (error) {
|
|
142
217
|
process.exitCode = emit({ outcome: 'error', summary: error instanceof Error ? error.message : String(error) });
|
|
143
218
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
export type TimeoutStatus = '파일 기본' | '시험별 일부' | '없음';
|
|
7
|
+
export type TimeoutFinding = { file: string; spawnLines: number; timeout: TimeoutStatus };
|
|
8
|
+
|
|
9
|
+
const target = /\bbin\/elanous\.mjs\b|\bprocess\.execPath\b|["']bun["']|\bscripts\/[^\s"']+\.(?:ts|tsx|sh)\b/;
|
|
10
|
+
const processCalls = new Set(['spawnSync', 'Bun.spawnSync', 'Bun.spawn', 'execFileSync', 'execSync']);
|
|
11
|
+
|
|
12
|
+
function callName(node: ts.CallExpression): string {
|
|
13
|
+
return node.expression.getText();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inspect the command and argv, not unrelated options, resolving local const aliases. */
|
|
17
|
+
function commandText(node: ts.Expression, constants: ReadonlyMap<string, ts.Expression>, seen = new Set<string>()): string {
|
|
18
|
+
if (ts.isIdentifier(node) && constants.has(node.text)) {
|
|
19
|
+
if (seen.has(node.text)) return '';
|
|
20
|
+
seen.add(node.text);
|
|
21
|
+
return commandText(constants.get(node.text)!, constants, seen);
|
|
22
|
+
}
|
|
23
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.map((element) => commandText(element as ts.Expression, constants, new Set(seen))).join(' ');
|
|
24
|
+
if (ts.isObjectLiteralExpression(node)) return node.properties.map((property) =>
|
|
25
|
+
ts.isPropertyAssignment(property) ? commandText(property.initializer, constants, new Set(seen)) : '').join(' ');
|
|
26
|
+
if (ts.isStringLiteralLike(node)) return JSON.stringify(node.text);
|
|
27
|
+
if (ts.isPropertyAccessExpression(node) && node.getText() === 'process.execPath') return node.getText();
|
|
28
|
+
if (ts.isTemplateExpression(node)) return JSON.stringify(node.head.text) + node.templateSpans.map((span) => commandText(span.expression, constants, new Set(seen)) + JSON.stringify(span.literal.text)).join('');
|
|
29
|
+
if (ts.isParenthesizedExpression(node)) return commandText(node.expression, constants, seen);
|
|
30
|
+
if (ts.isCallExpression(node)) return node.arguments.map((arg) => commandText(arg, constants, new Set(seen))).join(' ');
|
|
31
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) return commandText(node.left, constants, new Set(seen)) + commandText(node.right, constants, new Set(seen));
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasNumericTimeout(node: ts.Expression | undefined): boolean {
|
|
36
|
+
return !!node && (ts.isNumericLiteral(node) || (ts.isObjectLiteralExpression(node) && node.properties.some((property) =>
|
|
37
|
+
ts.isPropertyAssignment(property) && property.name.getText() === 'timeout' && ts.isNumericLiteral(property.initializer))));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function testTimeout(call: ts.CallExpression): boolean {
|
|
41
|
+
const name = callName(call);
|
|
42
|
+
if (name === 'test' || name === 'it') return hasNumericTimeout(call.arguments[2]);
|
|
43
|
+
if (name === 'describe') return call.arguments.some((arg) => hasNumericTimeout(arg));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function withinTimedTest(node: ts.Node): boolean {
|
|
48
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
49
|
+
if (ts.isCallExpression(parent) && ['test', 'it', 'describe'].includes(callName(parent)) && testTimeout(parent)) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Classify files that actually invoke a Bun/elanous/script subprocess. */
|
|
55
|
+
export function classifyTimeouts(file: string, source: string): TimeoutFinding | null {
|
|
56
|
+
const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
57
|
+
const constants = new Map<string, ts.Expression>();
|
|
58
|
+
const collect = (node: ts.Node): void => {
|
|
59
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isVariableDeclarationList(node.parent)
|
|
60
|
+
&& (node.parent.flags & ts.NodeFlags.Const)) constants.set(node.name.text, node.initializer);
|
|
61
|
+
ts.forEachChild(node, collect);
|
|
62
|
+
};
|
|
63
|
+
collect(tree);
|
|
64
|
+
const lines = new Set<number>();
|
|
65
|
+
let fileDefault = false;
|
|
66
|
+
let uncovered = false;
|
|
67
|
+
const scan = (node: ts.Node): void => {
|
|
68
|
+
if (ts.isCallExpression(node)) {
|
|
69
|
+
const name = callName(node);
|
|
70
|
+
if (name === 'setDefaultTimeout' && node.arguments.length > 0) fileDefault = true;
|
|
71
|
+
if (processCalls.has(name)) {
|
|
72
|
+
const command = node.arguments.slice(0, name === 'execSync' || name.startsWith('Bun.') ? 1 : 2)
|
|
73
|
+
.map((arg) => commandText(arg, constants)).join(' ');
|
|
74
|
+
const executable = node.arguments[0] ? commandText(node.arguments[0], constants) : '';
|
|
75
|
+
if (target.test(command) && !/^\s*"git"/.test(executable)) {
|
|
76
|
+
lines.add(tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1);
|
|
77
|
+
if (!withinTimedTest(node)) uncovered = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ts.forEachChild(node, scan);
|
|
82
|
+
};
|
|
83
|
+
scan(tree);
|
|
84
|
+
if (lines.size === 0) return null;
|
|
85
|
+
return { file, spawnLines: lines.size, timeout: fileDefault ? '파일 기본' : uncovered ? '없음' : '시험별 일부' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function auditTimeouts(root: string, paths: readonly string[]): TimeoutFinding[] {
|
|
89
|
+
return paths.map((file) => classifyTimeouts(file, readFileSync(join(root, file), 'utf8')))
|
|
90
|
+
.filter((finding): finding is TimeoutFinding => finding !== null)
|
|
91
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderTimeouts(findings: readonly TimeoutFinding[], json = false): string {
|
|
95
|
+
if (json) return JSON.stringify(findings, null, 2);
|
|
96
|
+
return ['파일 · 띄우는 줄 수 · 시한 상태', ...findings.map((f) => `${f.file} · ${f.spawnLines} · ${f.timeout}`)].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function main(root = process.cwd(), args = process.argv.slice(2)): number {
|
|
100
|
+
if (args.some((arg) => arg !== '--json')) {
|
|
101
|
+
console.error('Usage: bun scripts/test-timeout-audit.ts [--json]');
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
const git = spawnSync('git', ['ls-files', '-z', '*.test.ts', '*.test.tsx'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
105
|
+
if (git.status !== 0 || git.error) {
|
|
106
|
+
console.error(git.error?.message ?? git.stderr ?? 'git ls-files failed');
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const findings = auditTimeouts(root, git.stdout.split('\0').filter(Boolean));
|
|
110
|
+
console.log(renderTimeouts(findings, args.includes('--json')));
|
|
111
|
+
return findings.some((f) => f.timeout === '없음') ? 1 : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (import.meta.main) process.exitCode = main();
|
|
@@ -20,6 +20,6 @@ export function renderTreeSyncAlert(tree: string, streak: number, decision: stri
|
|
|
20
20
|
|
|
21
21
|
if (import.meta.main) {
|
|
22
22
|
const [tree = '', streak = '0', decision = ''] = process.argv.slice(2);
|
|
23
|
-
const ok = sendOutbound(renderTreeSyncAlert(tree, Number(streak) || 0, decision), 'alert');
|
|
23
|
+
const ok = sendOutbound(renderTreeSyncAlert(tree, Number(streak) || 0, decision), 'ops-alert');
|
|
24
24
|
process.exit(ok ? 0 : 1);
|
|
25
25
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// V1 — rerun the hand-measured TUI checks (scripts/lib/tui-regress-checks.ts) on a real PTY, every release.
|
|
2
|
+
//
|
|
3
|
+
// bun scripts/tui-regress.ts [--only R2,R5] [--json]
|
|
4
|
+
//
|
|
5
|
+
// ⛔ Not under `bun test`: a real PTY of the real TUI is outside the test gate (NODE_ENV=test turns the PTY
|
|
6
|
+
// manifest off, and the run takes ~1 minute). Each check gets a fresh temp config/state dir and its own PTY;
|
|
7
|
+
// both are removed when the check ends, so the run leaves no process or folder behind.
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { startPty } from '../src/pty-shell/registry.js';
|
|
13
|
+
import { ERASE, TUI_REGRESS_CHECKS, type TuiCheck, type Verdict } from './lib/tui-regress-checks.js';
|
|
14
|
+
|
|
15
|
+
const COLS = 160;
|
|
16
|
+
const ROWS = 50;
|
|
17
|
+
const BOOT_MS = 9_000;
|
|
18
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
20
|
+
async function runCheck(check: TuiCheck): Promise<Verdict> {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), `tui-regress-${check.id}-`));
|
|
22
|
+
const cli = join(import.meta.dir, '..', 'bin', 'elanous.mjs');
|
|
23
|
+
if (check.role && check.role !== 'owner') {
|
|
24
|
+
// A config written from scratch has onboarding unfinished → bare entry would take the first-run (web-first) path,
|
|
25
|
+
// not the TUI. Mark onboarding done so the check sees the same TUI as an existing user.
|
|
26
|
+
spawnSync(process.execPath, [cli, '--config-dir', dir, 'config', 'set', 'tui.role', check.role], { stdio: 'ignore' });
|
|
27
|
+
spawnSync(process.execPath, [cli, '--config-dir', dir, 'config', 'set', 'onboarding.completed', 'true'], { stdio: 'ignore' });
|
|
28
|
+
}
|
|
29
|
+
const handle = startPty({
|
|
30
|
+
cmd: process.execPath, args: [cli, '--config-dir', dir, '--test-state-dir', dir],
|
|
31
|
+
cols: COLS, rows: ROWS, workdir: join(import.meta.dir, '..'),
|
|
32
|
+
env: { ...process.env, ELANOUS_DRIVE_TUI: '1', ELANOUS_STATE_DIR: dir },
|
|
33
|
+
});
|
|
34
|
+
const snaps: Record<string, string> = {};
|
|
35
|
+
try {
|
|
36
|
+
await sleep(BOOT_MS);
|
|
37
|
+
for (const step of check.steps) {
|
|
38
|
+
if (step.kind === 'type') handle.write(step.text);
|
|
39
|
+
else if (step.kind === 'key') handle.write(step.data);
|
|
40
|
+
else if (step.kind === 'erase') handle.write(ERASE.repeat(step.count));
|
|
41
|
+
else if (step.kind === 'wait') await sleep(step.ms);
|
|
42
|
+
else snaps[step.as] = await handle.renderScreen();
|
|
43
|
+
}
|
|
44
|
+
// TUI_REGRESS_DUMP=<id> prints that check's screens (to see why a verdict failed).
|
|
45
|
+
if (process.env.TUI_REGRESS_DUMP === check.id) for (const [k, v] of Object.entries(snaps)) console.log(`--- ${check.id}:${k}\n${v.split('\n').filter((l) => l.trim()).join('\n')}`);
|
|
46
|
+
return check.judge(snaps);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { ok: false, reason: `실행 오류: ${String(error).slice(0, 120)}` };
|
|
49
|
+
} finally {
|
|
50
|
+
try { handle.kill('SIGKILL'); } catch { /* already gone */ }
|
|
51
|
+
rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
const onlyArg = args.indexOf('--only');
|
|
57
|
+
const only = onlyArg >= 0 ? new Set((args[onlyArg + 1] ?? '').split(',').map((s) => s.trim()).filter(Boolean)) : null;
|
|
58
|
+
const json = args.includes('--json');
|
|
59
|
+
const checks = TUI_REGRESS_CHECKS.filter((check) => !only || only.has(check.id));
|
|
60
|
+
if (checks.length === 0) { console.error('tui-regress: 고른 칸이 없다 (--only 확인)'); process.exit(2); }
|
|
61
|
+
|
|
62
|
+
const results: { id: string; title: string; ok: boolean; reason: string }[] = [];
|
|
63
|
+
for (const check of checks) {
|
|
64
|
+
const verdict = await runCheck(check);
|
|
65
|
+
results.push({ id: check.id, title: check.title, ...verdict });
|
|
66
|
+
if (!json) console.log(`${check.id} ${verdict.ok ? '✅' : '❌'} ${check.title} — ${verdict.reason}`);
|
|
67
|
+
}
|
|
68
|
+
const fail = results.filter((r) => !r.ok).length;
|
|
69
|
+
if (json) console.log(JSON.stringify({ results, pass: results.length - fail, fail }));
|
|
70
|
+
console.log(`tui-regress: pass ${results.length - fail} · fail ${fail}`);
|
|
71
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -99,7 +99,7 @@ export async function main(): Promise<void> {
|
|
|
99
99
|
try { return JSON.parse(readFileSync(statePath, 'utf8')); } catch { return null; }
|
|
100
100
|
},
|
|
101
101
|
writeState: (s) => { mkdirSync(dirname(statePath), { recursive: true }); writeFileSync(statePath, JSON.stringify(s)); },
|
|
102
|
-
send: (text) => sendOutbound(text, 'report'),
|
|
102
|
+
send: (text) => sendOutbound(text, 'ops-report'),
|
|
103
103
|
});
|
|
104
104
|
debug.log('waitlist.digest', 'reported', result);
|
|
105
105
|
console.log(`[waitlist-daily-digest] sent=${result.sent} total=${result.total} fresh=${result.fresh}`);
|
|
@@ -91,8 +91,12 @@ The reference is used only for its structure (header, chapter cards, a diagram t
|
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
93
|
zsh $SKILL/engine/reel.sh <folder> --title "마케터의 밤" --sub "2026.10.02 · 서울"
|
|
94
|
+
zsh $SKILL/engine/reel.sh --warm-cards --title "마케터의 밤" --sub "2026.10.02 · 서울" # 행사 전 카드 미리 굽기
|
|
95
|
+
zsh $SKILL/engine/reel.sh <folder> --title "마케터의 밤" --sub "2026.10.02 · 서울" --instant # 사진 구간만 렌더
|
|
94
96
|
```
|
|
95
97
|
|
|
98
|
+
즉석판 카드 캐시: `~/.cache/elanous-explainer/cards/<sha256(JSON 배열 [title, sub, 1080, 1920, 카드판번호]) 앞 16자>/{intro,end}.mp4` (`EXPLAINER_CACHE`로 루트 변경 가능).
|
|
99
|
+
|
|
96
100
|
- **Input**: a folder of phone photos (jpg · png · heic) and short clips (mp4 · mov). The default upload folder is `~/.elanous/field/<event>/`.
|
|
97
101
|
- **Order**: by capture time (`mdls` creation date), then by file name.
|
|
98
102
|
- **Captions**: optional `<folder>/captions.txt`, one line per item as `파일명 | 자막`. A photo without a line gets one line from a vision model (`codex exec -i`, all photos in parallel, 30 s cap each); if that fails it shows «현장 N». `FIELD_REEL_VISION=0` turns the vision step off.
|
|
@@ -12,15 +12,18 @@ import { join, dirname, resolve, extname, basename } from "node:path";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { normalizeCardTitle } from "./stitch.mjs";
|
|
15
16
|
|
|
16
17
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const argv = process.argv.slice(2);
|
|
18
19
|
const opt = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; };
|
|
19
20
|
const folder = resolve(argv.find((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--"))) ?? ".");
|
|
20
21
|
const PART = opt("part", "full");
|
|
22
|
+
if (!["full", "body", "intro", "end"].includes(PART)) throw new Error(`unknown reel part: ${PART}`);
|
|
23
|
+
const CARD = PART === "intro" || PART === "end";
|
|
21
24
|
const MAX = Number(opt("max", 60)), MIN = Number(opt("min", 24));
|
|
22
25
|
const W = 1080, H = 1920, PHOTO = 4.2, CLIP_MAX = 5, XF = 0.5;
|
|
23
|
-
const INTRO = PART === "body" ? 0 : 3.0, END = PART === "body" ? 0 : 3.4;
|
|
26
|
+
const INTRO = PART === "body" || PART === "end" ? 0 : 3.0, END = PART === "body" || PART === "intro" ? 0 : 3.4;
|
|
24
27
|
const out = join(folder, "reel"), hf = join(out, "hf"), media = join(hf, "assets", "media");
|
|
25
28
|
mkdirSync(media, { recursive: true }); mkdirSync(join(hf, "assets", "fonts"), { recursive: true });
|
|
26
29
|
const T0 = Date.now(), lap = (s) => console.log(`reel: ${s} · ${((Date.now() - T0) / 1000).toFixed(1)}s`);
|
|
@@ -37,18 +40,19 @@ const created = (p) => {
|
|
|
37
40
|
return Number.isFinite(t) ? t : statSync(p).mtimeMs;
|
|
38
41
|
};
|
|
39
42
|
const probeDur = (p) => Number(run("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", p]).stdout.trim()) || 0;
|
|
40
|
-
|
|
43
|
+
let TITLE = opt("title", ""), SUB = opt("sub", "");
|
|
44
|
+
const caps = Object.fromEntries((!CARD && existsSync(join(folder, "captions.txt")) ? readFileSync(join(folder, "captions.txt"), "utf8") : "")
|
|
41
45
|
.split("\n").filter((l) => l.includes("|")).map((l) => l.split("|").map((s) => s.trim())));
|
|
42
|
-
let items = readdirSync(folder).filter((f) => [...IMG, ...VID].includes(extname(f).toLowerCase()))
|
|
46
|
+
let items = CARD ? [] : readdirSync(folder).filter((f) => [...IMG, ...VID].includes(extname(f).toLowerCase()))
|
|
43
47
|
.map((f) => ({ f, p: join(folder, f), video: VID.includes(extname(f).toLowerCase()), t: created(join(folder, f)) }))
|
|
44
48
|
.sort((a, b) => a.t - b.t || a.f.localeCompare(b.f));
|
|
45
|
-
if (!items.length) throw new Error(`no photos or clips in ${folder}`);
|
|
49
|
+
if (!CARD && !items.length) throw new Error(`no photos or clips in ${folder}`);
|
|
46
50
|
|
|
47
51
|
// ── durations: fit into [MIN, MAX] ─────────────────────────────────────────────
|
|
48
52
|
for (const it of items) it.dur = it.video ? Math.min(CLIP_MAX, Math.max(1.5, probeDur(it.p))) : PHOTO;
|
|
49
53
|
const body = () => items.reduce((a, it) => a + it.dur, 0);
|
|
50
54
|
while (INTRO + body() + END > MAX && items.length > 1) items.splice(Math.floor(items.length / 2), 1); // drop from the middle, keep first & last
|
|
51
|
-
if (PART
|
|
55
|
+
if (PART === "full" && INTRO + body() + END < MIN) { const photos = items.filter((i) => !i.video); const need = MIN - (INTRO + body() + END); for (const p of photos) p.dur += need / Math.max(1, photos.length); }
|
|
52
56
|
|
|
53
57
|
// ── media prep: HEIC → jpg · big photos down to 2160 · a small copy for the vision model · clips re-encoded short & muted ──
|
|
54
58
|
let t = INTRO;
|
|
@@ -92,18 +96,17 @@ const oneLine = (s, max) => {
|
|
|
92
96
|
const CAP_PROMPT = "행사 현장 사진이다. 세로 영상 자막으로 쓸 한국어 한 줄을 출력하라 — 18자 이내 · 마침표 없이 · 보이는 장면만 · 사람 이름·외모 묘사·추측 금지 · 화면 속 글자(행사명·회사명)는 읽히면 써도 된다. 그 한 줄만.";
|
|
93
97
|
const HEAD_PROMPT = "같은 행사에서 찍은 사진들이다. 영상 첫 화면 제목으로 쓸 한국어 행사 이름 한 줄을 출력하라 — 16자 이내 · 화면 속 글자에서 읽히는 행사·회사 이름을 우선 · 읽히지 않으면 장면을 짧게(예: «상장 기념식 현장») · 추측 금지. 그 한 줄만.";
|
|
94
98
|
const slugTitle = (s) => !s || /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(s);
|
|
95
|
-
let TITLE = opt("title", ""), SUB = opt("sub", "");
|
|
96
99
|
const photos = items.filter((it) => !it.video);
|
|
97
100
|
const VT0 = Date.now();
|
|
98
|
-
const jobs = VISION ? [
|
|
101
|
+
const jobs = VISION && !CARD ? [
|
|
99
102
|
...photos.filter((it) => !caps[it.f]).map(async (it) => { it.vcap = oneLine(await ask(CAP_PROMPT, [join(out, `v${items.indexOf(it)}.jpg`)]), 22); }),
|
|
100
103
|
...(PART !== "body" && slugTitle(TITLE) && photos.length ? [(async () => { TITLE = oneLine(await ask(HEAD_PROMPT, photos.slice(0, 3).map((it) => join(out, `v${items.indexOf(it)}.jpg`))), 20) || TITLE; })()] : []),
|
|
101
104
|
] : [];
|
|
102
105
|
await Promise.all(jobs);
|
|
103
106
|
const visionMs = Date.now() - VT0;
|
|
104
107
|
items.forEach((it, i) => { it.cap = caps[it.f] || it.vcap || (it.video ? "" : `현장 ${i + 1}`); });
|
|
105
|
-
|
|
106
|
-
if (!SUB) { const d = new Date(items[0].t + 9 * 3600e3); SUB = `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, "0")}.${String(d.getUTCDate()).padStart(2, "0")}`; }
|
|
108
|
+
TITLE = normalizeCardTitle(TITLE);
|
|
109
|
+
if (!CARD && !SUB) { const d = new Date(items[0].t + 9 * 3600e3); SUB = `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, "0")}.${String(d.getUTCDate()).padStart(2, "0")}`; }
|
|
107
110
|
lap(`vision ${jobs.length ? `${items.filter((i) => i.vcap).length}/${photos.filter((p) => !caps[p.f]).length} captions` : "off"} · title «${TITLE}»`);
|
|
108
111
|
|
|
109
112
|
// ── assets (same cache as build.mjs) ───────────────────────────────────────────
|
|
@@ -119,7 +122,7 @@ for (const [rel, url] of Object.entries(ASSETS)) {
|
|
|
119
122
|
// ── music: cut to length · fade in/out · level to about -16 LUFS (the whole mix is music) ──
|
|
120
123
|
const BGM = process.env.FIELD_REEL_BGM || join(CACHE, "bgm", "field.wav");
|
|
121
124
|
let music = "";
|
|
122
|
-
if (PART
|
|
125
|
+
if (PART === "full" && existsSync(BGM)) {
|
|
123
126
|
const r = run("ffmpeg", ["-v", "error", "-y", "-i", BGM, "-t", String(TOTAL), "-af", `afade=t=in:d=0.4,afade=t=out:st=${f(TOTAL - 1.6)}:d=1.6,loudnorm=I=-16:TP=-1.5:LRA=9`, "-ar", "48000", "-ac", "2", join(media, "bgm.wav")]);
|
|
124
127
|
if (r.status === 0) music = "bgm.wav"; else console.error(`reel: music skipped — ${r.stderr.slice(0, 160)}`);
|
|
125
128
|
}
|
|
@@ -165,16 +168,18 @@ const clips = items.map((it, i) => {
|
|
|
165
168
|
return vis + cap;
|
|
166
169
|
}).join("\n");
|
|
167
170
|
|
|
168
|
-
const intro = PART === "body" ? "" : `<section class="clip card" data-start="0" data-duration="${INTRO}" data-track-index="6">`
|
|
171
|
+
const intro = PART === "body" || PART === "end" ? "" : `<section class="clip card" data-start="0" data-duration="${INTRO}" data-track-index="6">`
|
|
169
172
|
+ `<div class="kick ik">현장 스케치</div>${mark(200, "im")}<h1>${words(TITLE, "it")}</h1><div class="line il"></div><p class="is">${esc(SUB)}</p></section>`;
|
|
170
|
-
const end = PART === "body" ? "" : `<section class="clip card" data-start="${f(t)}" data-duration="${END}" data-track-index="6">`
|
|
173
|
+
const end = PART === "body" || PART === "intro" ? "" : `<section class="clip card" data-start="${f(t)}" data-duration="${END}" data-track-index="6">`
|
|
171
174
|
+ `${mark(230, "em")}<h1 class="et">Elanous</h1><p class="et">사진을 올리면, 영상이 됩니다</p><p class="et red">elanous.ai</p></section>`;
|
|
172
|
-
if (PART
|
|
175
|
+
if (PART === "full" || PART === "intro") {
|
|
173
176
|
tw.push(`tl.fromTo(".ik", {opacity:0, letterSpacing:"0.6em"}, {opacity:1, letterSpacing:"0.28em", duration:0.8, ease:"power3.out"}, 0.1);`);
|
|
174
177
|
tw.push(`tl.fromTo(".im", {rotation:-160, scale:0.6, opacity:0}, {rotation:0, scale:1, opacity:1, duration:1.0, ease:"power3.out"}, 0.0);`);
|
|
175
178
|
tw.push(`tl.fromTo(".it", {opacity:0, y:50}, {opacity:1, y:0, duration:0.55, stagger:0.1, ease:"power3.out"}, 0.45);`);
|
|
176
179
|
tw.push(`tl.fromTo(".il", {scaleX:0}, {scaleX:1, duration:0.6, ease:"power3.inOut"}, 0.9);`);
|
|
177
180
|
tw.push(`tl.fromTo(".is", {opacity:0, y:16}, {opacity:1, y:0, duration:0.45, ease:"power3.out"}, 1.15);`);
|
|
181
|
+
}
|
|
182
|
+
if (PART === "full" || PART === "end") {
|
|
178
183
|
tw.push(`tl.fromTo(".em", {rotation:-200, scale:0.5}, {rotation:0, scale:1, duration:1.2, ease:"power3.out"}, ${f(t)});`);
|
|
179
184
|
tw.push(`tl.fromTo(".et", {opacity:0, y:20}, {opacity:1, y:0, duration:0.5, stagger:0.14, ease:"power3.out"}, ${f(t + 0.5)});`);
|
|
180
185
|
}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
#!/bin/zsh
|
|
2
|
-
# Field reel
|
|
2
|
+
# Field reel: standard full composition or instant body with cached cards.
|
|
3
3
|
# The layout/contrast check is advisory: phone photos behind captions often miss 3:1, and a failed check must not stop the reel.
|
|
4
|
-
# Usage: zsh reel.sh <folder> [--title "…"] [--sub "…"] (captions: <folder>/captions.txt — `파일 | 자막`)
|
|
5
|
-
E=${0:A:h}
|
|
4
|
+
# Usage: zsh reel.sh <folder> [--title "…"] [--sub "…"] [--instant] (captions: <folder>/captions.txt — `파일 | 자막`)
|
|
5
|
+
E=${0:A:h}
|
|
6
|
+
if [[ $1 == --warm-cards ]]; then
|
|
7
|
+
shift
|
|
8
|
+
node $E/stitch.mjs --warm-cards "$@"
|
|
9
|
+
exit $?
|
|
10
|
+
fi
|
|
11
|
+
F=${1:A}; shift
|
|
6
12
|
[[ -d $F ]] || { print -u2 "reel: no folder $F"; exit 2 }
|
|
7
13
|
T0=$(date +%s)
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
instant=0
|
|
15
|
+
args=()
|
|
16
|
+
for arg in "$@"; do
|
|
17
|
+
if [[ $arg == --instant ]]; then instant=1; else args+=("$arg"); fi
|
|
18
|
+
done
|
|
19
|
+
rm -rf $F/reel
|
|
20
|
+
if (( instant )); then
|
|
21
|
+
node $E/reel.mjs $F "${args[@]}" --part body &&
|
|
22
|
+
(cd $F/reel/hf && { npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} check >../render.log 2>&1 || print "reel: check reported issues (continuing — user photos vary; see above)" >>../render.log }; npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} render --fps 30 --workers 4 --output ../body.mp4 >>../render.log 2>&1) &&
|
|
23
|
+
node $E/stitch.mjs --instant $F >>$F/reel/render.log 2>&1 || { print -u2 "reel: FAILED — see $F/reel/render.log"; exit 1 }
|
|
24
|
+
print "reel: $F/reel/reel-9x16.mp4 · $(( $(date +%s) - T0 ))s (instant)"
|
|
25
|
+
else
|
|
26
|
+
node $E/reel.mjs $F "${args[@]}" && (cd $F/reel/hf && { npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} check >../render.log 2>&1 || print "reel: check reported issues (continuing — user photos vary; see above)" >>../render.log }; npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} render --fps 30 --workers 4 --output ../reel-9x16.mp4 >>../render.log 2>&1) || { print -u2 "reel: FAILED — see $F/reel/render.log"; exit 1 }
|
|
27
|
+
print "reel: $F/reel/reel-9x16.mp4 · $(( $(date +%s) - T0 ))s"
|
|
28
|
+
fi
|