elanous 0.2.8 → 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-d4b6df905f185055.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-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
- 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-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
- 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-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.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-7464c1987dd76e6d.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-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
- 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-26102fd7350f171e.js +1 -0
- 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-139ba5c367bbf60a.js +1 -0
- 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 +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- 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 +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- 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 +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +3 -0
- 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 +14 -7
- 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 +31 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +18 -4
- package/scripts/release-loop/node-verdict.ts +8 -1
- 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 +31 -1
- package/scripts/test-diet/node.ts +134 -17
- 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.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +164 -0
- 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/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +61 -9
- 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/op-proxy-sync.ts +83 -0
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +37 -4
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +98 -15
- 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/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +128 -34
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +50 -2
- 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 +172 -41
- 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 +100 -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 +73 -1
- package/src/nexus/api/meta-api.ts +38 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +148 -0
- package/src/nexus/api/ops-seats-sources.ts +95 -0
- package/src/nexus/api/ops-seats.ts +124 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +45 -4
- package/src/onboarding/first-run-doctor.ts +54 -0
- 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 +76 -61
- package/src/release-loop/feature-store.ts +460 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +98 -0
- 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/skills/tools/usage-slash.ts +8 -7
- 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-commands.ts +16 -0
- package/src/telegram-report.ts +15 -9
- package/src/telegram-run.ts +90 -13
- package/src/telegram.ts +29 -8
- package/src/thinking-line.ts +28 -20
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- 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/4836-3084c58c1725a40b.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/8894-7317998f6cb97d8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.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-9aa47b0bc6d570ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, type Dirent } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
6
6
|
import { defaultGraphsDir } from '../self-implement/graph-templates.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
8
|
+
import type { ExecAttachment } from './store.js';
|
|
8
9
|
import { debug } from '../debug/log.js';
|
|
9
10
|
|
|
10
11
|
export interface InstalledGraph {
|
|
@@ -12,7 +13,7 @@ export interface InstalledGraph {
|
|
|
12
13
|
title: string;
|
|
13
14
|
description: string;
|
|
14
15
|
path: string;
|
|
15
|
-
/**
|
|
16
|
+
/** 코어 loop.inputs 또는 플러그인 examples/*.json 표본의 입력 키. */
|
|
16
17
|
inputKeys?: string[];
|
|
17
18
|
}
|
|
18
19
|
export interface ExecPlanItem {
|
|
@@ -83,24 +84,41 @@ export async function installedGraphs(coreDir = defaultGraphsDir(), mineDir = jo
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<InstalledGraph[]> {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
const graphs: InstalledGraph[] = [];
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
const scan = (dir: string, core: boolean): void => {
|
|
90
|
+
let entries: Dirent[];
|
|
91
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
92
|
+
catch { return; }
|
|
93
|
+
if (!core || dir === coreDir || entries.some(entry => entry.isFile() && entry.name === 'recipes.yaml')) {
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isFile() || !/\.ya?ml$/.test(entry.name) || entry.name === 'recipes.yaml') continue;
|
|
96
|
+
const path = join(dir, entry.name);
|
|
97
|
+
try {
|
|
98
|
+
const doc = parseYaml(readFileSync(path, 'utf8')) as {
|
|
99
|
+
graph_id?: unknown;
|
|
100
|
+
loop?: { title?: string; description?: string; exec_request?: unknown; inputs?: unknown };
|
|
101
|
+
} | null;
|
|
102
|
+
const id = doc?.graph_id;
|
|
103
|
+
if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id) || seen.has(id)) continue;
|
|
104
|
+
if (core && doc?.loop?.exec_request !== true) continue;
|
|
105
|
+
const inputs = doc?.loop?.inputs;
|
|
106
|
+
const inputKeys = core && Array.isArray(inputs)
|
|
107
|
+
? inputs.filter((key): key is string => typeof key === 'string')
|
|
108
|
+
: core && inputs && typeof inputs === 'object' ? Object.keys(inputs) : undefined;
|
|
109
|
+
graphs.push({ id, title: doc?.loop?.title ?? id, description: doc?.loop?.description ?? '', path,
|
|
110
|
+
...(inputKeys ? { inputKeys } : {}) });
|
|
111
|
+
seen.add(id);
|
|
112
|
+
} catch { /* A malformed file is not an executable graph. */ }
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
if (core) for (const entry of entries) {
|
|
116
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) scan(join(dir, entry.name), true);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
scan(coreDir, true);
|
|
120
|
+
scan(mineDir, false);
|
|
121
|
+
return graphs;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tracks.json', import.meta.url))): string[] {
|
|
@@ -108,8 +126,32 @@ export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tra
|
|
|
108
126
|
return data.tracks.flatMap(track => track.title ? [track.title] : []);
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
export
|
|
112
|
-
const
|
|
129
|
+
export function latestFieldFolder(configRoot = getElanousConfigDir()): string | null {
|
|
130
|
+
const root = join(configRoot, 'field');
|
|
131
|
+
let latest: { path: string; time: number } | null = null;
|
|
132
|
+
try {
|
|
133
|
+
for (const event of readdirSync(root, { withFileTypes: true })) {
|
|
134
|
+
if (!event.isDirectory() || event.name.startsWith('.')) continue;
|
|
135
|
+
const path = join(root, event.name);
|
|
136
|
+
try {
|
|
137
|
+
const photos = readdirSync(path, { withFileTypes: true }).filter(file => file.isFile() && /\.(?:png|jpe?g|webp|heic)$/i.test(file.name));
|
|
138
|
+
for (const photo of photos) {
|
|
139
|
+
const time = statSync(join(path, photo.name)).mtimeMs;
|
|
140
|
+
if (!latest || time > latest.time || (time === latest.time && path.localeCompare(latest.path) > 0)) latest = { path, time };
|
|
141
|
+
}
|
|
142
|
+
} catch { /* Unreadable event does not supply photos. */ }
|
|
143
|
+
}
|
|
144
|
+
} catch { /* No field directory. */ }
|
|
145
|
+
return latest?.path ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function execPlanPrompt(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): string {
|
|
149
|
+
const supplied = attachments.map(({ name, path }) => `첨부: ${name} (${/\.(?:png|jpe?g|webp|gif|heic)$/i.test(name) ? '이미지' : '파일'}) — ${path}`).join('\n');
|
|
150
|
+
return `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄과 아래 알려진 값에서 얻을 수 있는 값만 넣는다(모르는 값은 넣지 않는다). image/photo/file 입력에는 알맞은 첨부 경로를 쓸 수 있다. folder 입력에는 첨부가 없거나 첨부 사진이 그 현장 폴더 안에 있을 때만 오늘 현장 폴더를 쓸 수 있다. 다른 현장 사진이나 출처를 모르는 업로드 사진이 있으면 폴더를 추측하지 마라. 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 알 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 단순히 묻는 말에는 없는 그래프를 지어내지 말고 graphId=""로 표시한다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n${fieldFolder ? `알려진 값: 오늘 현장 폴더 = ${fieldFolder}\n` : ''}${supplied ? `${supplied}\n` : ''}한 줄: ${JSON.stringify(text)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): Promise<unknown> {
|
|
154
|
+
const prompt = execPlanPrompt(text, graphs, seats, attachments, fieldFolder);
|
|
113
155
|
const { streamLLM } = await import('../llm.js');
|
|
114
156
|
const { tierModel } = await import('../llm/model-defaults.js');
|
|
115
157
|
const raw = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
|
|
@@ -120,13 +162,17 @@ export async function planExecRequest(text: string, deps: {
|
|
|
120
162
|
graphs?: () => Promise<InstalledGraph[]>;
|
|
121
163
|
seats?: () => string[];
|
|
122
164
|
judge?: typeof judgeExecPlan;
|
|
165
|
+
attachments?: readonly ExecAttachment[];
|
|
166
|
+
fieldFolder?: () => string | null;
|
|
123
167
|
} = {}): Promise<ExecPlanItem[]> {
|
|
124
168
|
const graphs = await (deps.graphs ?? installedGraphs)();
|
|
125
169
|
const seats = (deps.seats ?? seatTitles)();
|
|
126
|
-
const
|
|
170
|
+
const attachments = deps.attachments ?? [];
|
|
171
|
+
const fieldFolder = (deps.fieldFolder ?? latestFieldFolder)();
|
|
172
|
+
const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats, attachments, fieldFolder);
|
|
127
173
|
if (!Array.isArray(raw) || raw.length === 0) throw new Error('COO 계획이 비었거나 배열이 아닙니다');
|
|
128
174
|
const known = new Map(graphs.map(graph => [graph.id, graph]));
|
|
129
|
-
|
|
175
|
+
const plans: ExecPlanItem[] = raw.map((item: unknown, index: number) => {
|
|
130
176
|
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('COO 계획 항목 형식 오류');
|
|
131
177
|
const row = item as Record<string, unknown>;
|
|
132
178
|
if (typeof row.seat !== 'string' || !seats.includes(row.seat) || typeof row.title !== 'string' || !row.title.trim()
|
|
@@ -136,11 +182,35 @@ export async function planExecRequest(text: string, deps: {
|
|
|
136
182
|
const after = Array.isArray(row.after)
|
|
137
183
|
? [...new Set(row.after.filter((n): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) < index))]
|
|
138
184
|
: [];
|
|
185
|
+
const graph = known.get(row.graphId);
|
|
186
|
+
const inputs = { ...row.inputs as Record<string, unknown> };
|
|
187
|
+
let folderReason: string | undefined;
|
|
188
|
+
if (graph) {
|
|
189
|
+
for (const key of graph.inputKeys ?? []) {
|
|
190
|
+
if (key === 'folder') {
|
|
191
|
+
// A folder input must contain every attached photo — never guess it from «the latest event» (review r1).
|
|
192
|
+
const photos = attachments.filter(a => /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name)).map(a => resolve(a.path));
|
|
193
|
+
const holds = (folder: string) => photos.every(photo => photo.startsWith(`${resolve(folder)}${sep}`));
|
|
194
|
+
const given = typeof inputs.folder === 'string' ? inputs.folder : undefined;
|
|
195
|
+
if (given !== undefined) {
|
|
196
|
+
if (!holds(given)) { delete inputs.folder; folderReason = `${row.seat}: folder 필요 — 첨부 사진이 그 현장 폴더 안에 없습니다`; }
|
|
197
|
+
} else if (fieldFolder && holds(fieldFolder)) inputs.folder = fieldFolder;
|
|
198
|
+
else if (photos.length) folderReason = `${row.seat}: folder 필요 — 첨부 사진의 현장 폴더를 알 수 없습니다`;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (inputs[key] !== undefined) continue;
|
|
202
|
+
if (['image', 'photo', 'file'].includes(key)) {
|
|
203
|
+
const attachment = attachments.find(a => key === 'file' || /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name));
|
|
204
|
+
if (attachment) inputs[key] = attachment.path;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
139
208
|
return {
|
|
140
|
-
seat: row.seat, title: row.title.trim(), graphId: row.graphId,
|
|
141
|
-
inputs: row.inputs as Record<string, unknown>,
|
|
209
|
+
seat: row.seat, title: row.title.trim(), graphId: row.graphId, inputs,
|
|
142
210
|
...(after.length ? { after } : {}),
|
|
143
|
-
...(!
|
|
211
|
+
...(!graph ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : folderReason ? { reason: folderReason } : {}),
|
|
144
212
|
};
|
|
145
213
|
});
|
|
214
|
+
for (const plan of plans) debug.log('exec.planner', 'select', { seat: plan.seat, chosen: plan.graphId, candidates: graphs.slice(0, 10).map(graph => graph.id) });
|
|
215
|
+
return plans;
|
|
146
216
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
|
+
import { notifyExecRequestTransition } from '../web-push/notify-exec-request.js';
|
|
5
6
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
7
|
import { lastJsonObject, runGraph, type GraphRunState } from '../graph-runner/runner.js';
|
|
7
8
|
import { installedGraphs, planExecRequest, type ExecPlanItem, type InstalledGraph } from './planner.js';
|
|
8
|
-
import { ExecRequestStore, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
|
|
9
|
+
import { ExecRequestStore, type ExecAttachment, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
|
|
10
|
+
import { answerAsSeat } from '../intake-plane/seat-answer.js';
|
|
11
|
+
import { redactSecrets } from '../task-cards/card-store.js';
|
|
9
12
|
|
|
10
13
|
export interface ExecRunnerDeps {
|
|
11
14
|
store?: ExecRequestStore;
|
|
12
15
|
graphs?: () => Promise<InstalledGraph[]>;
|
|
13
|
-
plan?: (text: string) => Promise<ExecPlanItem[]>;
|
|
16
|
+
plan?: (text: string, attachments?: readonly ExecAttachment[]) => Promise<ExecPlanItem[]>;
|
|
17
|
+
answer?: typeof answerAsSeat;
|
|
14
18
|
run?: typeof runGraph;
|
|
15
19
|
root?: string;
|
|
16
20
|
}
|
|
@@ -38,7 +42,11 @@ function safeFile(path: string, root: string, graphId: string, runId: string): s
|
|
|
38
42
|
? realpathSync(artifactsDir) : null;
|
|
39
43
|
const inRunDirectory = realArtifactsDir === join(base, `${runId}.json.artifacts`)
|
|
40
44
|
&& real.startsWith(`${realArtifactsDir}${sep}`);
|
|
41
|
-
|
|
45
|
+
// EXEC2 — plugin graphs (doc-draft · geo-check) write into `<graphId>/<runId>/`; that directory is this run's own too.
|
|
46
|
+
const runDir = join(allowed, runId);
|
|
47
|
+
const realRunDir = existsSync(runDir) && statSync(runDir).isDirectory() ? realpathSync(runDir) : null;
|
|
48
|
+
const inOwnRunDir = realRunDir === join(base, runId) && real.startsWith(`${realRunDir}${sep}`);
|
|
49
|
+
if ((!local && !inRunDirectory && !inOwnRunDir) || !statSync(real).isFile()) return null;
|
|
42
50
|
return real;
|
|
43
51
|
} catch { return null; }
|
|
44
52
|
}
|
|
@@ -56,8 +64,32 @@ function fileName(path: string, runId: string): string {
|
|
|
56
64
|
return `${runId}--${digest}--${basename(path)}`;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
/** EXEC2 — `report` is how geo-check names its file; treat it like `file`/`path`. */
|
|
68
|
+
function fileCandidate(output: Record<string, unknown>): string | null {
|
|
69
|
+
if (typeof output.file === 'string') return output.file;
|
|
70
|
+
if (typeof output.path === 'string') return output.path;
|
|
71
|
+
if (typeof output.report === 'string' && /\.(md|txt|pdf)$/i.test(output.report)) return output.report;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TEXT_RESULT_LIMIT = 200_000;
|
|
76
|
+
const TEXT_RESULT_NAME = 'result.md';
|
|
77
|
+
|
|
78
|
+
/** EXEC2 — a done seat whose graph only answered in text gets that text as a run artifact (redacted), so the result is viewable. */
|
|
79
|
+
function textResultFile(seat: ExecSeat, state: GraphRunState, root: string): string | null {
|
|
80
|
+
const outputs = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)).filter((o): o is Record<string, unknown> => !!o);
|
|
81
|
+
const text = outputs.map(o => o.markdown ?? o.text).find((v): v is string => typeof v === 'string' && v.trim().length > 0);
|
|
82
|
+
if (!text || !SEGMENT.test(seat.graphId) || !SEGMENT.test(seat.runId)) return null;
|
|
83
|
+
const dir = join(root, 'graph-runs', seat.graphId, `${seat.runId}.json.artifacts`);
|
|
84
|
+
const path = join(dir, TEXT_RESULT_NAME);
|
|
85
|
+
try {
|
|
86
|
+
if (!existsSync(path)) { mkdirSync(dir, { recursive: true }); writeFileSync(path, redactSecrets(text.slice(0, TEXT_RESULT_LIMIT)), { mode: 0o600 }); }
|
|
87
|
+
return path;
|
|
88
|
+
} catch { return null; }
|
|
89
|
+
}
|
|
90
|
+
|
|
59
91
|
function resultOf(output: Record<string, unknown>, seat: ExecSeat, id: string, root: string): ExecResult | null {
|
|
60
|
-
const candidate =
|
|
92
|
+
const candidate = fileCandidate(output);
|
|
61
93
|
let url: string;
|
|
62
94
|
let kind: ExecResult['kind'];
|
|
63
95
|
let name: string;
|
|
@@ -95,6 +127,7 @@ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root:
|
|
|
95
127
|
if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision && !existsSync(claim)) {
|
|
96
128
|
if (!item.approvals.some(a => a.graphId === seat.graphId && a.runId === seat.runId)) {
|
|
97
129
|
item.approvals.push({ graphId: seat.graphId, runId: seat.runId, message: state.pending.message });
|
|
130
|
+
notifyTransition(item, item.status, item.approvals.length);
|
|
98
131
|
}
|
|
99
132
|
} else item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
|
|
100
133
|
for (const node of state.nodes ?? []) {
|
|
@@ -107,9 +140,28 @@ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root:
|
|
|
107
140
|
if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url)) item.results.push(result);
|
|
108
141
|
}
|
|
109
142
|
}
|
|
143
|
+
if (seat.status === 'done' && !item.results.some(existing => existing.seat === seat.seat)) {
|
|
144
|
+
const file = textResultFile(seat, state, root);
|
|
145
|
+
const result = file ? resultOf({ file, title: seat.title }, seat, item.id, root) : null;
|
|
146
|
+
if (result) item.results.push(result);
|
|
147
|
+
else {
|
|
148
|
+
const keys = [...new Set((state.nodes ?? []).flatMap(node => Object.keys(lastJsonObject(node.output) ?? {})))];
|
|
149
|
+
debug.log('exec.result', 'none', { seat: seat.seat, graphId: seat.graphId, keys });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
110
152
|
const summary = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)?.summary).find(value => typeof value === 'string');
|
|
111
153
|
if (typeof summary === 'string') item.summary = summary;
|
|
112
|
-
if (seat.status === 'failed') seat.reason = state
|
|
154
|
+
if (seat.status === 'failed') seat.reason = failureReason(state);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** EXEC2 — the failing node's own error (e.g. doc-draft `check` {outcome:'fail', error}) beats the terminal gate's empty one. */
|
|
158
|
+
function failureReason(state: GraphRunState): string {
|
|
159
|
+
for (const node of [...(state.nodes ?? [])].reverse()) {
|
|
160
|
+
if (typeof node.error === 'string' && node.error) return node.error;
|
|
161
|
+
const output = lastJsonObject(node.output);
|
|
162
|
+
if (output && typeof output.error === 'string' && output.error) return redactSecrets(output.error);
|
|
163
|
+
}
|
|
164
|
+
return `그래프 런 ${state.status}`;
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
const PRIOR_TEXT_LIMIT = 4_000;
|
|
@@ -129,8 +181,14 @@ export function withPriorResults(inputs: Record<string, unknown> | undefined, pr
|
|
|
129
181
|
return { ...base, context: `${previous}앞 자리 결과:\n${parts.join('\n\n')}` };
|
|
130
182
|
}
|
|
131
183
|
|
|
184
|
+
function notifyTransition(item: ExecRequest, from: ExecRequest['status'], pendingApprovals: number): void {
|
|
185
|
+
void notifyExecRequestTransition({ id: item.id, text: item.text, from, to: item.status,
|
|
186
|
+
summary: item.summary, pendingApprovals }).catch(() => { /* Push never interrupts execution. */ });
|
|
187
|
+
}
|
|
188
|
+
|
|
132
189
|
function aggregate(item: ExecRequest): void {
|
|
133
190
|
if (item.status === 'planning' || item.seats.length === 0) return;
|
|
191
|
+
const previous = item.status;
|
|
134
192
|
if (item.seats.some(seat => seat.status === 'waiting' || seat.status === 'running')) item.status = 'running';
|
|
135
193
|
else item.status = item.seats.some(seat => seat.status === 'failed') ? 'failed' : 'done';
|
|
136
194
|
// 실패면 요약은 «실패 사유»가 이긴다 — 먼저 끝난 자리의 산출 설명이 남아 있어도 덮는다(혼합 결과에서 성공처럼 보이지 않게).
|
|
@@ -138,6 +196,7 @@ function aggregate(item: ExecRequest): void {
|
|
|
138
196
|
const reasons = item.seats.filter(seat => seat.status === 'failed').map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`);
|
|
139
197
|
if (reasons.length > 0) item.summary = reasons.join('; ');
|
|
140
198
|
}
|
|
199
|
+
if (previous !== item.status && (item.status === 'done' || item.status === 'failed')) notifyTransition(item, previous, item.approvals.length);
|
|
141
200
|
}
|
|
142
201
|
|
|
143
202
|
export class ExecRequestRunner {
|
|
@@ -156,10 +215,12 @@ export class ExecRequestRunner {
|
|
|
156
215
|
if (item.status === 'planning') {
|
|
157
216
|
item.status = 'failed';
|
|
158
217
|
item.summary = '데몬 재시작으로 COO 계획이 중단됐습니다';
|
|
218
|
+
notifyTransition(item, 'planning', item.approvals.length);
|
|
159
219
|
} else {
|
|
160
220
|
if (item.seats.length === 0) {
|
|
161
221
|
item.status = 'failed';
|
|
162
222
|
item.summary = '데몬 재시작으로 자리 배정이 중단됐습니다';
|
|
223
|
+
notifyTransition(item, 'running', item.approvals.length);
|
|
163
224
|
}
|
|
164
225
|
for (const seat of item.seats) {
|
|
165
226
|
if (seat.status === 'done' || seat.status === 'failed') continue;
|
|
@@ -192,14 +253,16 @@ export class ExecRequestRunner {
|
|
|
192
253
|
}
|
|
193
254
|
}
|
|
194
255
|
|
|
195
|
-
submit(text: string): ExecRequest {
|
|
196
|
-
const item = this.store.create(text);
|
|
256
|
+
submit(text: string, attachments?: ExecAttachment[]): ExecRequest {
|
|
257
|
+
const item = this.store.create(text, attachments);
|
|
197
258
|
void this.execute(item.id).catch(error => {
|
|
198
259
|
const current = this.store.get(item.id);
|
|
199
260
|
if (!current) return;
|
|
261
|
+
const previous = current.status;
|
|
200
262
|
current.status = 'failed';
|
|
201
263
|
current.summary = error instanceof Error ? error.message : String(error);
|
|
202
264
|
this.store.save(current);
|
|
265
|
+
if (previous !== current.status) notifyTransition(current, previous, current.approvals.length);
|
|
203
266
|
debug.log('exec-requests', 'failed', { id: item.id, reason: current.summary });
|
|
204
267
|
});
|
|
205
268
|
return item;
|
|
@@ -209,8 +272,40 @@ export class ExecRequestRunner {
|
|
|
209
272
|
const item = this.store.get(id);
|
|
210
273
|
if (!item) return;
|
|
211
274
|
const graphs = await (this.deps.graphs ?? installedGraphs)();
|
|
212
|
-
const plans = await (this.deps.plan ?? ((text: string) => planExecRequest(text, { graphs: async () => graphs })))(item.text);
|
|
275
|
+
const plans = await (this.deps.plan ?? ((text: string, attachments?: readonly ExecAttachment[]) => planExecRequest(text, { graphs: async () => graphs, attachments })))(item.text, item.attachments);
|
|
213
276
|
const byId = new Map(graphs.map(graph => [graph.id, graph]));
|
|
277
|
+
// Every clause must be a question — «상황 알려줘? 카드 만들어줘» carries a work item (review r1).
|
|
278
|
+
const clauses = [...item.text.trim().matchAll(/([^??.!。\n]+)([??]?)/g)].map(m => ({ body: m[1]!.trim(), asked: m[2] !== '' })).filter(c => c.body);
|
|
279
|
+
const isQuestion = clauses.length > 0 && clauses.every(c => c.asked
|
|
280
|
+
|| /(?:알려\s*줘|알려\s*주세요|보여\s*줘|보여\s*주세요|어떻게|무엇|뭐|어떤|언제|누구|질문|인가요|나요|습니까)$/.test(c.body));
|
|
281
|
+
if (isQuestion && plans.length > 0 && plans.every(plan => !plan.graphId
|
|
282
|
+
&& !/\s—\s.+\s필요\s*$/.test(plan.title) && !/\s—\s.+\s필요\s*$/.test(plan.reason ?? '')
|
|
283
|
+
&& !/(?:초안|작성|제작|생성|게시|분석|전략|메일|보고서)/.test(plan.title)) && !(item.attachments?.length)) {
|
|
284
|
+
for (const plan of plans) {
|
|
285
|
+
const answered = await (this.deps.answer ?? answerAsSeat)(plan.seat, item.text);
|
|
286
|
+
if (!answered) {
|
|
287
|
+
item.seats.push({ seat: plan.seat, title: plan.title, status: 'failed', graphId: '', runId: '',
|
|
288
|
+
reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const seat: ExecSeat = { seat: plan.seat, title: plan.title, status: 'done', graphId: 'seat-answer', runId: randomUUID() };
|
|
292
|
+
const dir = join(this.root, 'graph-runs', seat.graphId);
|
|
293
|
+
const statePath = join(dir, `${seat.runId}.json`);
|
|
294
|
+
const artifactDir = join(dir, `${seat.runId}.json.artifacts`);
|
|
295
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
296
|
+
const file = join(artifactDir, TEXT_RESULT_NAME);
|
|
297
|
+
writeFileSync(file, redactSecrets(answered.text.slice(0, TEXT_RESULT_LIMIT)), { mode: 0o600 });
|
|
298
|
+
const state: GraphRunState = { graphId: seat.graphId, runId: seat.runId, status: 'done', path: [], executed: 0, dryRun: false, statePath, nodes: [] };
|
|
299
|
+
writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
|
|
300
|
+
item.seats.push(seat);
|
|
301
|
+
const result = resultOf({ file, title: answered.title }, seat, id, this.root);
|
|
302
|
+
if (result) item.results.push(result);
|
|
303
|
+
}
|
|
304
|
+
item.status = 'running';
|
|
305
|
+
aggregate(item);
|
|
306
|
+
this.store.save(item);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
214
309
|
item.seats = plans.map(plan => ({ seat: plan.seat, title: plan.title, status: plan.reason || !byId.has(plan.graphId) ? 'failed' : 'waiting', graphId: plan.graphId,
|
|
215
310
|
runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs, ...(plan.after?.length ? { after: plan.after } : {}),
|
|
216
311
|
...(plan.reason || !byId.has(plan.graphId) ? { reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}) }));
|
|
@@ -366,12 +461,14 @@ export class ExecRequestRunner {
|
|
|
366
461
|
for (const entry of Array.isArray(output.artifacts) ? output.artifacts : [output]) {
|
|
367
462
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
368
463
|
const artifact = entry as Record<string, unknown>;
|
|
369
|
-
const candidate =
|
|
464
|
+
const candidate = fileCandidate(artifact);
|
|
370
465
|
if (typeof candidate !== 'string') continue;
|
|
371
466
|
const path = safeFile(candidate, this.root, seat.graphId, seat.runId);
|
|
372
467
|
if (path && fileName(path, seat.runId) === name) return path;
|
|
373
468
|
}
|
|
374
469
|
}
|
|
470
|
+
const text = safeFile(join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json.artifacts`, TEXT_RESULT_NAME), this.root, seat.graphId, seat.runId);
|
|
471
|
+
if (text && fileName(text, seat.runId) === name) return text;
|
|
375
472
|
} catch { return null; }
|
|
376
473
|
return null;
|
|
377
474
|
}
|
|
@@ -22,9 +22,15 @@ export interface ExecResult {
|
|
|
22
22
|
url: string;
|
|
23
23
|
sources?: Array<{ title: string; url: string }>;
|
|
24
24
|
}
|
|
25
|
+
export interface ExecAttachment {
|
|
26
|
+
name: string;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export interface ExecRequest {
|
|
26
31
|
id: string;
|
|
27
32
|
text: string;
|
|
33
|
+
attachments?: ExecAttachment[];
|
|
28
34
|
createdAt: string;
|
|
29
35
|
status: 'planning' | 'running' | 'done' | 'failed';
|
|
30
36
|
summary: string;
|
|
@@ -39,8 +45,8 @@ export class ExecRequestStore {
|
|
|
39
45
|
readonly dir: string;
|
|
40
46
|
constructor(configDir = getElanousConfigDir()) { this.dir = join(configDir, 'exec-requests'); }
|
|
41
47
|
|
|
42
|
-
create(text: string): ExecRequest {
|
|
43
|
-
const item: ExecRequest = { id: randomUUID(), text, createdAt: new Date().toISOString(), status: 'planning', summary: '', seats: [], results: [], approvals: [] };
|
|
48
|
+
create(text: string, attachments?: ExecAttachment[]): ExecRequest {
|
|
49
|
+
const item: ExecRequest = { id: randomUUID(), text, ...(attachments !== undefined ? { attachments } : {}), createdAt: new Date().toISOString(), status: 'planning', summary: '', seats: [], results: [], approvals: [] };
|
|
44
50
|
this.save(item);
|
|
45
51
|
return item;
|
|
46
52
|
}
|
package/src/field/field-media.ts
CHANGED
|
@@ -219,19 +219,22 @@ function uniqueName(dir: string, name: string, size: number, taken: Set<string>)
|
|
|
219
219
|
|
|
220
220
|
export const FIELD_CAPTION_TAG = '#현장';
|
|
221
221
|
|
|
222
|
-
/** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event? }`, 없으면 null.
|
|
223
|
-
* `#현장 <slug>` — 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
|
|
224
|
-
export function parseFieldCaption(caption: string | undefined | null): { event?: string; text?: string } | null {
|
|
222
|
+
/** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event?, mode?, text? }`, 없으면 null.
|
|
223
|
+
* `#현장 <slug>` — 모드 토큰을 건너뛴 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
|
|
224
|
+
export function parseFieldCaption(caption: string | undefined | null): { event?: string; mode?: 'instant'; text?: string } | null {
|
|
225
225
|
if (!caption) return null;
|
|
226
226
|
const tokens = caption.trim().split(/\s+/);
|
|
227
227
|
const at = tokens.indexOf(FIELD_CAPTION_TAG);
|
|
228
228
|
if (at < 0) return null;
|
|
229
|
-
const
|
|
229
|
+
const isInstant = (token: string) => token === '즉석' || token.toLowerCase() === 'instant';
|
|
230
|
+
const mode = tokens.some(isInstant) ? 'instant' : undefined;
|
|
231
|
+
const eventAt = at + 1 + (tokens[at + 1] && isInstant(tokens[at + 1]) ? 1 : 0);
|
|
232
|
+
const next = tokens[eventAt]?.toLowerCase();
|
|
230
233
|
const hasEvent = !!next && isFieldSlug(next);
|
|
231
|
-
// 태그·행사
|
|
232
|
-
const rest = tokens.filter((
|
|
234
|
+
// 태그·행사 이름·모드를 뺀 나머지가 «글»(자막) — 태그 앞에 쓴 글도 같이.
|
|
235
|
+
const rest = tokens.filter((token, i) => i !== at && !(hasEvent && i === eventAt) && !isInstant(token)).join(' ');
|
|
233
236
|
const text = normalizeFieldCaption(rest);
|
|
234
|
-
return { ...(hasEvent ? { event: next } : {}), ...(text ? { text } : {}) };
|
|
237
|
+
return { ...(hasEvent ? { event: next } : {}), ...(mode ? { mode } : {}), ...(text ? { text } : {}) };
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
export const FIELD_CAPTIONS_FILE = 'captions.txt';
|
package/src/field/field-reel.ts
CHANGED
|
@@ -4,11 +4,12 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { spawn, execFileSync } from 'node:child_process';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
|
+
import { mergeCapturedPath } from '../shell-env-bootstrap.js';
|
|
7
8
|
import { listFieldMedia } from './field-media.js';
|
|
8
9
|
import { startFieldFeed, type FieldFeedStart } from './field-feed-auto.js';
|
|
9
10
|
|
|
10
11
|
export interface FieldReelResult { ok: boolean; file?: string; seconds: number; error?: string; feed?: FieldFeedStart }
|
|
11
|
-
export type FieldReelRunner = (dir: string, opts: { title: string; sub: string }) => Promise<FieldReelResult>;
|
|
12
|
+
export type FieldReelRunner = (dir: string, opts: { title: string; sub: string; mode?: 'standard' | 'instant' }) => Promise<FieldReelResult>;
|
|
12
13
|
export interface FieldReelStatus {
|
|
13
14
|
state: 'waiting' | 'rendering' | 'done' | 'failed';
|
|
14
15
|
items: number;
|
|
@@ -23,6 +24,7 @@ export interface FieldReelOptions {
|
|
|
23
24
|
quietMs?: number;
|
|
24
25
|
title?: string;
|
|
25
26
|
sub?: string;
|
|
27
|
+
mode?: 'standard' | 'instant';
|
|
26
28
|
runner?: FieldReelRunner;
|
|
27
29
|
onDone?: (result: FieldReelResult) => void | Promise<void>;
|
|
28
30
|
/** 같은 수신자가 앨범 항목마다 예약해도 한 렌더당 한 번만 통지한다. */
|
|
@@ -96,10 +98,12 @@ export function readFieldReelStatus(dir: string): FieldReelStatus | null {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
export const defaultFieldReelRunner
|
|
101
|
+
export const defaultFieldReelRunner = (dir: string, { title, sub, mode = 'standard' }: { title: string; sub: string; mode?: 'standard' | 'instant' }, spawnReel: typeof spawn = spawn): Promise<FieldReelResult> => new Promise((finish) => {
|
|
100
102
|
const script = resolve(import.meta.dir, '../../skills/explainer-video/engine/reel.sh');
|
|
101
103
|
const started = Date.now();
|
|
102
|
-
const
|
|
104
|
+
const env = { ...process.env };
|
|
105
|
+
mergeCapturedPath(env, { capture: true });
|
|
106
|
+
const child = spawnReel('zsh', [script, dir, '--title', title, '--sub', sub, ...(mode === 'instant' ? ['--instant'] : [])], { stdio: 'ignore', env });
|
|
103
107
|
let settled = false;
|
|
104
108
|
const done = (error?: string) => {
|
|
105
109
|
if (settled) return;
|
|
@@ -147,6 +151,7 @@ async function render(dir: string, job: Job): Promise<void> {
|
|
|
147
151
|
result = await (renderOpts.runner ?? defaultFieldReelRunner)(dir, {
|
|
148
152
|
title: titleLines[0]?.trim() || renderOpts.title || event,
|
|
149
153
|
sub: titleLines[1]?.trim() || renderOpts.sub || sub,
|
|
154
|
+
...(renderOpts.mode ? { mode: renderOpts.mode } : {}),
|
|
150
155
|
});
|
|
151
156
|
} catch (err) {
|
|
152
157
|
result = { ok: false, seconds: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -160,10 +165,15 @@ async function render(dir: string, job: Job): Promise<void> {
|
|
|
160
165
|
debug.log('field.reel', result.ok ? 'done' : 'failed', { event, items, seconds: result.seconds });
|
|
161
166
|
if (result.ok) {
|
|
162
167
|
// EV10b: 엔진이 timeline.json 에 남긴 비전 호출 수·성공·«현장 N» 강등·걸린 ms — codex 잔량과 겹치므로 관측한다(OP 10-01).
|
|
168
|
+
let cardsCache: 'hit' | 'miss' | 'n/a' = 'n/a';
|
|
163
169
|
try {
|
|
164
|
-
const t = JSON.parse(readFileSync(join(dir, 'reel', 'timeline.json'), 'utf8')) as { vision?: Record<string, unknown>; music?: unknown };
|
|
170
|
+
const t = JSON.parse(readFileSync(join(dir, 'reel', 'timeline.json'), 'utf8')) as { vision?: Record<string, unknown>; music?: unknown; cardsCache?: 'hit' | 'miss' };
|
|
165
171
|
if (t.vision) debug.log('field.reel', 'vision', { event, ...t.vision, music: t.music === true });
|
|
172
|
+
if (renderOpts.mode === 'instant' && (t.cardsCache === 'hit' || t.cardsCache === 'miss')) cardsCache = t.cardsCache;
|
|
166
173
|
} catch { debug.log('field.reel', 'vision-unread', { event }); }
|
|
174
|
+
// A custom runner without an explicit mode may render any way — say «unknown» instead of guessing the default.
|
|
175
|
+
const mode = renderOpts.mode ?? (renderOpts.runner ? 'unknown' : 'standard');
|
|
176
|
+
debug.log('field.reel', 'rendered', { mode, seconds: result.seconds, cardsCache });
|
|
167
177
|
}
|
|
168
178
|
// 뒤이은 업로드로 다시 렌더할 예정이면(dirty) 피드는 그 마지막 렌더 뒤에 한 번만 띄운다.
|
|
169
179
|
if (result.ok && !job.dirty) result = { ...result, feed: (renderOpts.startFeed ?? ((d: string) => startFieldFeed(d, { enabled: renderOpts.autoFeed })))(dir) };
|
package/src/git-fs/worktree.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the pattern; we trim the hook-based fallback (sandboxed envs)
|
|
10
10
|
// because elanous's primary surface runs native git.
|
|
11
11
|
|
|
12
|
-
import { spawnSync, execFileSync } from 'node:child_process';
|
|
12
|
+
import { spawnSync, execFileSync, type SpawnSyncReturns } from 'node:child_process';
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync, rmSync, symlinkSync, lstatSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -17,8 +17,33 @@ import { findGitDir } from './locate.js';
|
|
|
17
17
|
import { isTransientGitError } from './retry.js';
|
|
18
18
|
import { runGitCommand } from './runner.js';
|
|
19
19
|
import { debug } from '../debug/log.js';
|
|
20
|
+
import { getUserConfig } from '../user-config.js';
|
|
20
21
|
|
|
21
22
|
const GIT_TIMEOUT_MS = 30_000;
|
|
23
|
+
const WORKTREE_ADD_TIMEOUT_MS = 5 * 60_000;
|
|
24
|
+
|
|
25
|
+
export interface WorktreeAddDeps {
|
|
26
|
+
spawn?: (command: string, args: string[], options: { cwd: string; encoding: 'utf8'; timeout: number; maxBuffer: number }) => SpawnSyncReturns<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function worktreeAddTimeoutMs(): number {
|
|
30
|
+
const seconds = getUserConfig().harness?.worktreeAddTimeoutSec;
|
|
31
|
+
return typeof seconds === 'number' && Number.isSafeInteger(seconds) && seconds > 0 && seconds <= 2_147_483
|
|
32
|
+
? seconds * 1000 : WORKTREE_ADD_TIMEOUT_MS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function worktreeAddFailureLines(stderr: string): { lastProgress: string | null; details: string[] } {
|
|
36
|
+
let lastProgress: string | null = null;
|
|
37
|
+
const details: string[] = [];
|
|
38
|
+
for (const line of stderr.split(/[\r\n]+/).map((value) => value.trim()).filter(Boolean)) {
|
|
39
|
+
if (/^(?:Updating files|Checking out files|Receiving objects|Resolving deltas):\s*\d+%/.test(line)) {
|
|
40
|
+
lastProgress = line;
|
|
41
|
+
} else if (!/^Preparing worktree\b/.test(line)) {
|
|
42
|
+
details.push(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { lastProgress, details: details.slice(-3) };
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
/** Git metadata left by an interrupted operation in one checkout. */
|
|
24
49
|
/** ⚠️ 공개인 이유(무인 리뷰 질의) — `GitResidueObservation.residues` 의 **원소 타입**이라,
|
|
@@ -618,7 +643,7 @@ function worktreeHoldingBranch(repoRoot: string, branch: string): string | null
|
|
|
618
643
|
return null;
|
|
619
644
|
}
|
|
620
645
|
|
|
621
|
-
export function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult {
|
|
646
|
+
export function createWorktree(opts: CreateWorktreeOpts, deps: WorktreeAddDeps = {}): CreateWorktreeResult {
|
|
622
647
|
const { repoRoot, branch, worktreeRoot } = opts;
|
|
623
648
|
validateBranchName(branch);
|
|
624
649
|
const branchHolder = worktreeHoldingBranch(repoRoot, branch);
|
|
@@ -684,25 +709,39 @@ export function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult {
|
|
|
684
709
|
// ★ 병렬 안전 재시도(2026-07-21) — 동시 worktree add 가 git 락에서 경쟁하면 즉시 실패 → 락/일시적
|
|
685
710
|
// 에러면 백오프 재시도(패자가 승자의 락 해제를 기다렸다 성공). non-transient 는 재시도 없이 throw.
|
|
686
711
|
const MAX_ATTEMPTS = 6;
|
|
687
|
-
|
|
712
|
+
const addTimeoutMs = worktreeAddTimeoutMs();
|
|
713
|
+
const addSpawn = deps.spawn ?? spawnSync;
|
|
688
714
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
689
|
-
res =
|
|
715
|
+
const res = addSpawn(
|
|
690
716
|
'git',
|
|
691
717
|
['worktree', 'add', branchFlag, branch, wtPath, baseCheckout],
|
|
692
|
-
{ cwd: repoRoot, encoding: 'utf8', timeout:
|
|
718
|
+
{ cwd: repoRoot, encoding: 'utf8', timeout: addTimeoutMs, maxBuffer: 4 * 1024 * 1024 },
|
|
693
719
|
);
|
|
694
720
|
if (res.status === 0) break;
|
|
695
721
|
const err = (res.stderr || res.stdout || '').toString();
|
|
722
|
+
const { lastProgress, details } = worktreeAddFailureLines(err);
|
|
723
|
+
const timedOut = (res.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
|
|
724
|
+
if (timedOut) {
|
|
725
|
+
debug.log('git-fs.worktree', 'add.failed', { reason: 'timeout', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
|
|
726
|
+
throw new Error(`git worktree add timed out after ${addTimeoutMs / 1000}s (${res.signal ?? 'none'}) — ${[...details, ...(lastProgress ? [`checkout was at «${lastProgress}»`] : [])].join(' | ') || `git exited ${res.status}`}`);
|
|
727
|
+
}
|
|
728
|
+
if (res.signal) {
|
|
729
|
+
debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal, lastProgress }, { level: 'warn' });
|
|
730
|
+
throw new Error(`git worktree add failed (${res.signal}) — ${details.join(' | ') || lastProgress || `git exited ${res.status}`}`);
|
|
731
|
+
}
|
|
696
732
|
const blockingPath = blockingWorktreePath(err);
|
|
697
733
|
if (blockingPath) {
|
|
734
|
+
debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
|
|
698
735
|
debug.log('git-fs.worktree', 'add.branch-held', { attempt, branch, blockingPath }, { level: 'warn' });
|
|
699
736
|
throw new Error(`git worktree add failed — branch ${branch} is already used by worktree at ${blockingPath}`);
|
|
700
737
|
}
|
|
701
738
|
if (isUnbornHeadError(err)) {
|
|
739
|
+
debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
|
|
702
740
|
throw new Error('git worktree add failed — this repository has no commits yet; create the first commit with: git commit --allow-empty -m "Initial commit"');
|
|
703
741
|
}
|
|
704
742
|
if (!isTransientGitError(err) || attempt === MAX_ATTEMPTS) {
|
|
705
|
-
|
|
743
|
+
debug.log('git-fs.worktree', 'add.failed', { reason: 'error', ms: addTimeoutMs, signal: res.signal ?? null, lastProgress }, { level: 'warn' });
|
|
744
|
+
throw new Error(`git worktree add failed — ${details.join(' | ') || lastProgress || `git exited ${res.status}`}`);
|
|
706
745
|
}
|
|
707
746
|
debug.log('git-fs.worktree', 'add.retry', { attempt, branch, err: err.trim().slice(0, 120) });
|
|
708
747
|
sleepSyncMs(80 * attempt); // 선형 백오프(80·160·240…ms)
|