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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { decideGraphApproval, type GraphRunState } from './runner.js';
|
|
7
|
+
|
|
8
|
+
export interface GraphApprovalCardDeps {
|
|
9
|
+
root?: string;
|
|
10
|
+
ledger?: Pick<DecisionLedger, 'list' | 'raise'>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Pending = NonNullable<GraphRunState['pending']>;
|
|
14
|
+
const REF = /^graph-approval:([A-Za-z0-9][A-Za-z0-9._-]*):([A-Za-z0-9][A-Za-z0-9._-]*):([A-Za-z0-9][A-Za-z0-9._-]*):([1-9]\d*)$/;
|
|
15
|
+
|
|
16
|
+
function ledgerFor(deps: GraphApprovalCardDeps, root: string): Pick<DecisionLedger, 'list' | 'raise'> {
|
|
17
|
+
return deps.ledger ?? new DecisionLedger({ stateDir: root });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function observation(event: 'raised' | 'dedup' | 'applied' | 'skipped', state: Pick<GraphRunState, 'graphId' | 'runId'> & { nodeId: string }, decisionId?: string, outcome?: string, reason?: string): void {
|
|
21
|
+
debug.log('graph.approval-card', event, { graphId: state.graphId, runId: state.runId, nodeId: state.nodeId, decisionId, outcome, ...(reason ? { reason } : {}) });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function raiseApprovalCard(state: GraphRunState, pending: Pending, deps: GraphApprovalCardDeps = {}): DecisionEntry | undefined {
|
|
25
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
26
|
+
const nodeId = pending.nodeId;
|
|
27
|
+
const identity = { graphId: state.graphId, runId: state.runId, nodeId };
|
|
28
|
+
const visit = state.path.filter(node => node === nodeId).length;
|
|
29
|
+
if (state.dryRun || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
|
|
30
|
+
state.pending.decision || state.path.at(-1) !== nodeId || visit < 1 ||
|
|
31
|
+
existsSync(`${join(root, 'graph-runs', state.graphId, `${state.runId}.json`)}.${state.path.length}.decision.json`)) {
|
|
32
|
+
observation('skipped', identity);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const ref = `graph-approval:${state.graphId}:${state.runId}:${nodeId}:${visit}`;
|
|
36
|
+
const ledger = ledgerFor(deps, root);
|
|
37
|
+
const existing = ledger.list({ status: 'all' }).find(entry => entry.refs?.includes(ref));
|
|
38
|
+
if (existing) {
|
|
39
|
+
observation('dedup', identity, existing.id, existing.choice);
|
|
40
|
+
return existing;
|
|
41
|
+
}
|
|
42
|
+
const publish = state.graphId === 'release-loop' && nodeId === 'approve-publish';
|
|
43
|
+
const firstLine = pending.message.split(/\r?\n/, 1)[0]?.trim();
|
|
44
|
+
const version = state.input && typeof state.input === 'object' && 'version' in state.input && typeof state.input.version === 'string'
|
|
45
|
+
? state.input.version : state.runId;
|
|
46
|
+
const title = publish && firstLine ? firstLine : `${state.graphId} ${version} — ${nodeId} 승인`;
|
|
47
|
+
const entry = ledger.raise({ title, category: publish ? 'publish' : 'scope',
|
|
48
|
+
scqa: { s: `${state.graphId} / ${state.runId} 런이 ${nodeId} 승인에서 대기 중이다.`, c: pending.message.slice(0, 220) || '승인 대기 중이다.' },
|
|
49
|
+
options: [{ key: 'a', label: '승인', consequence: `${nodeId} 승인 후 런을 계속할 수 있다` },
|
|
50
|
+
{ key: 'b', label: '거절', consequence: `${nodeId} 거절로 기록한다` }],
|
|
51
|
+
recommendation: { skipped: true, reason: '사람 판단' }, raisedBy: { agent: 'graph-runner' }, refs: [ref] });
|
|
52
|
+
observation('raised', identity, entry.id);
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A card answer only claims the same pending visit; runner owns the atomic decision file. */
|
|
57
|
+
export function applyDecidedApprovals(deps: GraphApprovalCardDeps = {}): number {
|
|
58
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
59
|
+
let applied = 0;
|
|
60
|
+
for (const entry of ledgerFor(deps, root).list({ status: 'decided' })) {
|
|
61
|
+
for (const ref of entry.refs ?? []) {
|
|
62
|
+
const match = REF.exec(ref);
|
|
63
|
+
if (!match) continue;
|
|
64
|
+
const [, graphId, runId, nodeId, rawVisit] = match;
|
|
65
|
+
const identity = { graphId: graphId!, runId: runId!, nodeId: nodeId! };
|
|
66
|
+
// One unreadable run must not block every later card (L11 review round 1 should-fix).
|
|
67
|
+
try { if (applyOne(entry, identity, Number(rawVisit), root)) applied++; }
|
|
68
|
+
catch (error) {
|
|
69
|
+
observation('skipped', identity, entry.id, entry.choice, error instanceof Error ? error.message : String(error));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return applied;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyOne(entry: DecisionEntry, identity: { graphId: string; runId: string; nodeId: string }, visit: number, root: string): boolean {
|
|
77
|
+
const { graphId, runId, nodeId } = identity;
|
|
78
|
+
// Graph approvals gate irreversible steps (publish) — only a person's answer approves or rejects, never a delegated auto decision.
|
|
79
|
+
if ((entry.choice !== 'a' && entry.choice !== 'b') || entry.decidedBy?.kind !== 'human' || !Number.isSafeInteger(visit)) {
|
|
80
|
+
observation('skipped', identity, entry.id, entry.choice);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const statePath = join(root, 'graph-runs', graphId, `${runId}.json`);
|
|
84
|
+
let state: GraphRunState;
|
|
85
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf8')) as GraphRunState; }
|
|
86
|
+
catch (error) {
|
|
87
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
88
|
+
observation('skipped', identity, entry.id, entry.choice);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.status !== 'awaiting-approval' ||
|
|
92
|
+
!state.pending || state.pending.nodeId !== nodeId || state.pending.decision || state.path.at(-1) !== nodeId ||
|
|
93
|
+
state.path.filter(node => node === nodeId).length !== visit ||
|
|
94
|
+
existsSync(`${statePath}.${state.path.length}.decision.json`)) {
|
|
95
|
+
observation('skipped', identity, entry.id, entry.choice);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const outcome = entry.choice === 'a' ? 'approved' : 'rejected';
|
|
99
|
+
try { decideGraphApproval(graphId, runId, outcome, `owner:${entry.decidedBy.kind}`, root,
|
|
100
|
+
{ nodeId, visit: visit }); }
|
|
101
|
+
catch (error) {
|
|
102
|
+
// A CLI approver (or another notifier) may win the atomic hard-link claim.
|
|
103
|
+
// A resumed run may also have left this visit before the writer reads it.
|
|
104
|
+
const current = JSON.parse(readFileSync(statePath, 'utf8')) as GraphRunState;
|
|
105
|
+
if (!existsSync(`${statePath}.${state.path.length}.decision.json`) &&
|
|
106
|
+
current.status === 'awaiting-approval' && current.pending?.nodeId === nodeId && !current.pending.decision &&
|
|
107
|
+
current.path.at(-1) === nodeId && current.path.filter(node => node === nodeId).length === visit) throw error;
|
|
108
|
+
observation('skipped', identity, entry.id, outcome);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
observation('applied', identity, entry.id, outcome);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
5
|
import { writeStdoutJson } from '../cli/stdout-json.js';
|
|
3
|
-
import {
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { decideGraphApproval, graphRunAlive, lastJsonObject, latestGraphRun, listGraphRuns, runGraph, type GraphRunState } from './runner.js';
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
function findGraphFile(graphId: string): string | undefined {
|
|
10
|
+
const roots = [resolve('graphs'), resolve(import.meta.dir, '../../graphs')];
|
|
11
|
+
for (const root of roots) {
|
|
12
|
+
for (const dir of [root, ...['release', 'ops'].map((part) => join(root, part))]) {
|
|
13
|
+
if (!existsSync(dir)) continue;
|
|
14
|
+
for (const name of readdirSync(dir)) {
|
|
15
|
+
if (!name.endsWith('.yaml') || name === 'recipes.yaml') continue;
|
|
16
|
+
const file = join(dir, name);
|
|
17
|
+
if (!existsSync(join(dir, 'recipes.yaml'))) continue;
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = parseYaml(readFileSync(file, 'utf8'));
|
|
20
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && (parsed as Record<string, unknown>).graph_id === graphId) return file;
|
|
21
|
+
} catch { continue; }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerGraphCommands(program: Command, runsDeps: { root?: string; processStartMs?: (pid: number) => number | null } = {}): void {
|
|
6
29
|
const graph = program.command('graph').description('Run a declared command graph or inspect its latest run');
|
|
7
30
|
graph.command('run <file>')
|
|
8
31
|
.option('--dry-run', 'Walk the success path without executing commands')
|
|
9
32
|
.option('--json', 'Print run state as JSON')
|
|
10
33
|
.option('--resume <run_id>', 'Resume a persisted run awaiting approval or restart a failed path with --from')
|
|
11
34
|
.option('--from <node_id>', 'Restart a failed run at a node on its saved path (requires --resume)')
|
|
35
|
+
.option('--use-current-graph', 'Use the installed graph rather than the run snapshot')
|
|
12
36
|
.option('--input <json>', 'JSON object passed to the graph run')
|
|
13
|
-
.action(async (file: string, opts: { dryRun?: boolean; json?: boolean; resume?: string; from?: string; input?: string }) => {
|
|
37
|
+
.action(async (file: string, opts: { dryRun?: boolean; json?: boolean; resume?: string; from?: string; input?: string; useCurrentGraph?: boolean }) => {
|
|
14
38
|
try {
|
|
15
39
|
await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
|
|
16
40
|
if (opts.resume && opts.dryRun) throw new Error('--resume cannot be combined with --dry-run');
|
|
@@ -23,7 +47,7 @@ export function registerGraphCommands(program: Command): void {
|
|
|
23
47
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('--input must be a JSON object');
|
|
24
48
|
input = parsed as Record<string, unknown>;
|
|
25
49
|
}
|
|
26
|
-
const state = await runGraph(file, { ...(opts.resume ? { resumeRunId: opts.resume } : { dryRun: opts.dryRun }), ...(opts.from ? { fromNodeId: opts.from } : {}), ...(input === undefined ? {} : { input }) });
|
|
50
|
+
const state = await runGraph(file, { ...(opts.resume ? { resumeRunId: opts.resume } : { dryRun: opts.dryRun }), ...(opts.from ? { fromNodeId: opts.from } : {}), useCurrentGraph: opts.useCurrentGraph, ...(input === undefined ? {} : { input }) });
|
|
27
51
|
if (opts.json) await writeStdoutJson(JSON.stringify(state) + '\n');
|
|
28
52
|
else console.log(`${state.graphId} ${state.runId}: ${state.status} (${state.path.join(' → ')}, executed: ${state.executed})${state.pending ? ` — ${state.pending.message}` : ''}`);
|
|
29
53
|
if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
|
|
@@ -87,6 +111,107 @@ export function registerGraphCommands(program: Command): void {
|
|
|
87
111
|
process.exitCode = 1;
|
|
88
112
|
}
|
|
89
113
|
});
|
|
114
|
+
const runs = graph.command('runs').description('List and inspect persisted graph runs');
|
|
115
|
+
const alive = (state: GraphRunState) => graphRunAlive(state, runsDeps.processStartMs);
|
|
116
|
+
const row = (state: GraphRunState) => ({
|
|
117
|
+
runId: state.runId, graphId: state.graphId, status: state.status,
|
|
118
|
+
lastNode: state.path.at(-1) ?? null, startedAt: state.startedAt ?? null,
|
|
119
|
+
finishedAt: state.finishedAt ?? null, alive: alive(state),
|
|
120
|
+
});
|
|
121
|
+
const warning = (unreadable: number) => { if (unreadable) console.error(`⚠ 못 읽은 원장 ${unreadable}`); };
|
|
122
|
+
runs.command('list')
|
|
123
|
+
.option('--graph <id>', 'Filter by graph id')
|
|
124
|
+
.option('--state <status>', 'Filter by run status')
|
|
125
|
+
.option('--limit <N>', 'Maximum number of runs')
|
|
126
|
+
.option('--json', 'Print runs as JSON')
|
|
127
|
+
.action(async (opts: { graph?: string; state?: string; limit?: string; json?: boolean }) => {
|
|
128
|
+
try {
|
|
129
|
+
if (opts.limit !== undefined && (!/^[1-9]\d*$/.test(opts.limit) || !Number.isSafeInteger(Number(opts.limit)))) throw new Error('--limit must be a positive integer');
|
|
130
|
+
const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
|
|
131
|
+
const selected = all.filter((state) => (!opts.graph || state.graphId === opts.graph) && (!opts.state || state.status === opts.state))
|
|
132
|
+
.slice(0, opts.limit === undefined ? undefined : Number(opts.limit)).map(row);
|
|
133
|
+
debug.log('graph.runs', 'list', { count: selected.length, unreadable, alive: selected.filter((state) => state.alive === true).length });
|
|
134
|
+
if (opts.json) await writeStdoutJson(JSON.stringify(selected) + '\n');
|
|
135
|
+
else for (const state of selected) console.log(`${state.runId} · ${state.graphId} · ${state.status} · ${state.lastNode ?? '-'} · ${state.startedAt ?? '-'} · ${state.finishedAt ?? '-'} · alive=${state.alive}`);
|
|
136
|
+
warning(unreadable);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`graph runs list: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
runs.command('resume <run_id>')
|
|
143
|
+
.option('--from <node_id>', 'Restart at this node on the saved path')
|
|
144
|
+
.option('--use-current-graph', 'Use the installed graph and enforce the original hash')
|
|
145
|
+
.option('--file <path>', 'Graph file for a legacy run without a recorded graph path')
|
|
146
|
+
.option('--json', 'Print run state as JSON')
|
|
147
|
+
.action(async (runId: string, opts: { from?: string; useCurrentGraph?: boolean; file?: string; json?: boolean }) => {
|
|
148
|
+
try {
|
|
149
|
+
await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
|
|
150
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') throw new Error(`invalid run id: ${runId}`);
|
|
151
|
+
const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
|
|
152
|
+
const exact = all.filter((state) => state.runId === runId);
|
|
153
|
+
const matches = exact.length ? exact : all.filter((state) => state.runId.startsWith(runId));
|
|
154
|
+
if (matches.length !== 1) {
|
|
155
|
+
if (matches.length) console.error(`ambiguous run id: ${runId}\n${matches.map((state) => `${state.graphId}/${state.runId}`).join('\n')}`);
|
|
156
|
+
else console.error(`no run: ${runId}`);
|
|
157
|
+
warning(unreadable);
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const saved = matches[0]!;
|
|
162
|
+
if (saved.graphSnapshot && opts.file && (!saved.graphPath || resolve(opts.file) !== resolve(saved.graphPath))) {
|
|
163
|
+
throw new Error('--file cannot change the graph path of a snapshot run (이 런은 시작 때 그래프 사본이 있다 — --file 대신 --use-current-graph)');
|
|
164
|
+
}
|
|
165
|
+
const from = opts.from ?? (saved.status === 'failed' ? [...saved.nodes].reverse().find((node) => !node.ok)?.nodeId : undefined);
|
|
166
|
+
const file = saved.graphSnapshot
|
|
167
|
+
? saved.graphPath
|
|
168
|
+
: opts.file ?? (saved.graphPath && existsSync(saved.graphPath) ? saved.graphPath : undefined) ?? findGraphFile(saved.graphId);
|
|
169
|
+
if (!file) throw new Error(`graph file not found for ${saved.graphId}/${saved.runId}; provide --file`);
|
|
170
|
+
const state = await runGraph(file, { resumeRunId: saved.runId, resumeGraphId: saved.graphId, fromNodeId: from,
|
|
171
|
+
useCurrentGraph: opts.useCurrentGraph, deps: { root: runsDeps.root } });
|
|
172
|
+
if (opts.json) await writeStdoutJson(JSON.stringify(state) + '\n');
|
|
173
|
+
else console.log(`${state.graphId} ${state.runId}: ${state.status} (${state.path.join(' → ')}, executed: ${state.executed})`);
|
|
174
|
+
if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
|
|
175
|
+
warning(unreadable);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`graph runs resume: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
runs.command('status <run_id>')
|
|
182
|
+
.option('--json', 'Print run detail as JSON')
|
|
183
|
+
.action(async (runId: string, opts: { json?: boolean }) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') throw new Error(`invalid run id: ${runId}`);
|
|
186
|
+
const { runs: all, unreadable } = listGraphRuns(runsDeps.root);
|
|
187
|
+
const exact = all.filter((state) => state.runId === runId);
|
|
188
|
+
const matches = exact.length ? exact : all.filter((state) => state.runId.startsWith(runId));
|
|
189
|
+
debug.log('graph.runs', 'status', { count: matches.length, unreadable, alive: matches.filter((state) => alive(state) === true).length });
|
|
190
|
+
if (matches.length !== 1) {
|
|
191
|
+
if (matches.length) console.error(`ambiguous run id: ${runId}\n${matches.map((state) => `${state.graphId}/${state.runId}`).join('\n')}`);
|
|
192
|
+
else console.error(`no run: ${runId}`);
|
|
193
|
+
warning(unreadable);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const state = matches[0]!;
|
|
198
|
+
const detail = { ...row(state), nodes: state.nodes.map((node) => {
|
|
199
|
+
const summary = lastJsonObject(node.output)?.summary;
|
|
200
|
+
return { nodeId: node.nodeId, ok: node.ok, summary: typeof summary === 'string' ? summary.slice(0, 300) : null };
|
|
201
|
+
}), pending: state.pending ?? null, resume: state.resume ?? null };
|
|
202
|
+
if (opts.json) await writeStdoutJson(JSON.stringify(detail) + '\n');
|
|
203
|
+
else {
|
|
204
|
+
console.log(`${detail.runId} · ${detail.graphId} · ${detail.status} · alive=${detail.alive}`);
|
|
205
|
+
for (const node of detail.nodes) console.log(` ${node.nodeId} · ok=${node.ok} · ${node.summary ?? '-'}`);
|
|
206
|
+
console.log(`pending: ${JSON.stringify(detail.pending)}`);
|
|
207
|
+
console.log(`resume: ${JSON.stringify(detail.resume)}`);
|
|
208
|
+
}
|
|
209
|
+
warning(unreadable);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(`graph runs status: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
90
215
|
graph.command('status <graph_id>')
|
|
91
216
|
.option('--json', 'Print latest run state as JSON')
|
|
92
217
|
.action(async (graphId: string, opts: { json?: boolean }) => {
|
|
@@ -2,6 +2,8 @@ import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, readdirSy
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { applyDecidedApprovals, raiseApprovalCard, type GraphApprovalCardDeps } from './graph-approval-card.js';
|
|
5
7
|
import type { GraphRunState } from './runner.js';
|
|
6
8
|
|
|
7
9
|
export interface GraphNotificationEvent {
|
|
@@ -20,6 +22,7 @@ export interface GraphNotifyOptions {
|
|
|
20
22
|
graphsDir?: string;
|
|
21
23
|
dryRun?: boolean;
|
|
22
24
|
send?: (message: string, event: GraphNotificationEvent) => Promise<void> | void;
|
|
25
|
+
approvalCard?: Pick<GraphApprovalCardDeps, 'ledger'>;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
type NotificationSetting = boolean | string | { on?: string | string[]; enabled?: boolean };
|
|
@@ -177,16 +180,35 @@ export async function notifyGraphEvents(options: GraphNotifyOptions = {}): Promi
|
|
|
177
180
|
return collectGraphEvents({ root, graphsDir: options.graphsDir }).filter((event) => !sent.has(event.id));
|
|
178
181
|
};
|
|
179
182
|
if (options.dryRun) return collectFresh();
|
|
183
|
+
const syncApprovals = () => {
|
|
184
|
+
const deps = { root, ...options.approvalCard };
|
|
185
|
+
try { applyDecidedApprovals(deps); }
|
|
186
|
+
catch (error) {
|
|
187
|
+
debug.log('graph.approval-card', 'skipped', { graphId: undefined, runId: undefined, nodeId: undefined,
|
|
188
|
+
decisionId: undefined, outcome: undefined, reason: error instanceof Error ? error.message : String(error) });
|
|
189
|
+
}
|
|
190
|
+
for (const event of collectGraphEvents({ root, graphsDir: options.graphsDir })) {
|
|
191
|
+
if (event.kind !== 'approval') continue;
|
|
192
|
+
try {
|
|
193
|
+
const state = JSON.parse(readFileSync(join(root, 'graph-runs', event.graphId, `${event.runId}.json`), 'utf8')) as GraphRunState;
|
|
194
|
+
if (state.pending?.nodeId === event.nodeId) raiseApprovalCard(state, state.pending, deps);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
debug.log('graph.approval-card', 'skipped', { graphId: event.graphId, runId: event.runId, nodeId: event.nodeId,
|
|
197
|
+
decisionId: undefined, outcome: undefined, reason: error instanceof Error ? error.message : String(error) });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
180
201
|
// The former .lock path was a directory; use a separate, stable inode instead of trying to reclaim it.
|
|
181
202
|
const lock = `${ledgerPath}.flock`;
|
|
182
203
|
mkdirSync(join(root, 'graph-runs'), { recursive: true });
|
|
183
204
|
const flock = await loadFlock();
|
|
184
|
-
if (!flock) return notifyWithExclusiveFile(`${ledgerPath}.owner`, collectFresh, options.send, ledgerPath);
|
|
205
|
+
if (!flock) return notifyWithExclusiveFile(`${ledgerPath}.owner`, collectFresh, options.send, ledgerPath, syncApprovals);
|
|
185
206
|
const fd = openSync(lock, 'a');
|
|
186
207
|
let acquired = false;
|
|
187
208
|
try {
|
|
188
209
|
if (flock(fd, LOCK_EX_NB) !== 0) throw new Error('graph notification is already running');
|
|
189
210
|
acquired = true;
|
|
211
|
+
syncApprovals();
|
|
190
212
|
return await sendFresh(collectFresh(), options.send, ledgerPath);
|
|
191
213
|
} finally {
|
|
192
214
|
try {
|
|
@@ -209,12 +231,12 @@ async function sendFresh(fresh: GraphNotificationEvent[], sendOption: GraphNotif
|
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
/** Platforms without flock (Windows, musl): an exclusive owner file. A crash leaves it behind, and the next run names it instead of double-sending. */
|
|
212
|
-
async function notifyWithExclusiveFile(ownerPath: string, collectFresh: () => GraphNotificationEvent[], send: GraphNotifyOptions['send'], ledgerPath: string): Promise<GraphNotificationEvent[]> {
|
|
234
|
+
async function notifyWithExclusiveFile(ownerPath: string, collectFresh: () => GraphNotificationEvent[], send: GraphNotifyOptions['send'], ledgerPath: string, syncApprovals: () => void): Promise<GraphNotificationEvent[]> {
|
|
213
235
|
try { writeFileSync(ownerPath, String(process.pid), { flag: 'wx' }); }
|
|
214
236
|
catch (error) {
|
|
215
237
|
if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`graph notification is already running (or a crashed run left ${ownerPath} — remove it if no notifier is running)`);
|
|
216
238
|
throw error;
|
|
217
239
|
}
|
|
218
|
-
try { return await sendFresh(collectFresh(), send, ledgerPath); }
|
|
240
|
+
try { syncApprovals(); return await sendFresh(collectFresh(), send, ledgerPath); }
|
|
219
241
|
finally { rmSync(ownerPath, { force: true }); }
|
|
220
242
|
}
|
|
@@ -11,6 +11,7 @@ import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentia
|
|
|
11
11
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
12
12
|
import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
|
|
13
13
|
import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec } from '../self-implement/graph-yaml.js';
|
|
14
|
+
import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js';
|
|
14
15
|
import { executeBashNode } from '../workflow-runtime/nodes/bash.js';
|
|
15
16
|
import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtime/types.js';
|
|
16
17
|
|
|
@@ -21,6 +22,8 @@ export interface GraphRunState {
|
|
|
21
22
|
runId: string;
|
|
22
23
|
startedAt?: string;
|
|
23
24
|
finishedAt?: string;
|
|
25
|
+
pid?: number;
|
|
26
|
+
pidStartedAt?: string;
|
|
24
27
|
status: 'running' | 'done' | 'failed' | 'budget-exceeded' | 'awaiting-approval';
|
|
25
28
|
path: string[];
|
|
26
29
|
nodes: Array<{ nodeId: string; ok: boolean; exit: number | null; executed: boolean; output?: unknown; error?: string; decidedBy?: string; decidedAt?: string }>;
|
|
@@ -29,7 +32,9 @@ export interface GraphRunState {
|
|
|
29
32
|
approvalSourceHash?: string;
|
|
30
33
|
/** Hash of the graph and recipes used for this run; required to safely restart a failed path. */
|
|
31
34
|
sourceHash?: string;
|
|
32
|
-
|
|
35
|
+
graphSnapshot?: { graphSha: string; recipesSha: string };
|
|
36
|
+
graphPath?: string;
|
|
37
|
+
resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
|
|
33
38
|
executed: number;
|
|
34
39
|
dryRun: boolean;
|
|
35
40
|
statePath: string;
|
|
@@ -40,8 +45,10 @@ export interface GraphRunOptions {
|
|
|
40
45
|
dryRun?: boolean;
|
|
41
46
|
runId?: string;
|
|
42
47
|
resumeRunId?: string;
|
|
48
|
+
resumeGraphId?: string;
|
|
43
49
|
fromNodeId?: string;
|
|
44
|
-
|
|
50
|
+
useCurrentGraph?: boolean;
|
|
51
|
+
deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null };
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
function safeSegment(value: string): string {
|
|
@@ -157,13 +164,19 @@ function persistGraphRun(state: GraphRunState): void {
|
|
|
157
164
|
renameSync(temporary, state.statePath);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
export
|
|
167
|
+
export interface ExpectedGraphApprovalVisit { nodeId: string; visit: number }
|
|
168
|
+
|
|
169
|
+
export function decideGraphApproval(graphId: string, runId: string, decision: 'approved' | 'rejected', by?: string, root = effectiveInstanceRoot(), expected?: ExpectedGraphApprovalVisit): GraphRunState {
|
|
161
170
|
const state = readGraphRun(graphId, runId, root);
|
|
162
|
-
if (state.status !== 'awaiting-approval' || !state.pending || state.pending.decision
|
|
171
|
+
if (state.status !== 'awaiting-approval' || !state.pending || state.pending.decision ||
|
|
172
|
+
(expected && (state.dryRun || state.pending.nodeId !== expected.nodeId || state.path.at(-1) !== expected.nodeId ||
|
|
173
|
+
!Number.isSafeInteger(expected.visit) || expected.visit < 1 ||
|
|
174
|
+
state.path.filter(node => node === expected.nodeId).length !== expected.visit))) {
|
|
163
175
|
throw new Error(`run is not awaiting an undecided approval: ${graphId}/${runId}`);
|
|
164
176
|
}
|
|
165
177
|
if (by !== undefined && !by.trim()) throw new Error('approver name must not be empty');
|
|
166
178
|
const recorded = { nodeId: state.pending.nodeId, decision, ...(by === undefined ? {} : { decidedBy: by }), decidedAt: new Date().toISOString() };
|
|
179
|
+
// A card can claim only the visit it names; never derive a later visit's claim path from a fresh read.
|
|
167
180
|
const decisionPath = approvalDecisionPath(state);
|
|
168
181
|
const temporary = `${decisionPath}.${randomUUID()}.tmp`;
|
|
169
182
|
writeFileSync(temporary, JSON.stringify(recorded) + '\n', { flag: 'wx' });
|
|
@@ -206,7 +219,26 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
|
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
export async function runGraph(path: string, options: GraphRunOptions = {}): Promise<GraphRunState> {
|
|
209
|
-
|
|
222
|
+
if (options.resumeRunId && options.runId) throw new Error('runId and resumeRunId cannot be combined');
|
|
223
|
+
if (options.fromNodeId && !options.resumeRunId) throw new Error('--from requires --resume');
|
|
224
|
+
if (options.useCurrentGraph && !options.resumeRunId) throw new Error('--use-current-graph requires --resume');
|
|
225
|
+
const root = options.deps?.root ?? effectiveInstanceRoot();
|
|
226
|
+
const matchingRuns = options.resumeRunId && !options.resumeGraphId
|
|
227
|
+
? listGraphRuns(root).runs.filter((run) => run.runId === options.resumeRunId && run.graphPath === resolve(path)) : [];
|
|
228
|
+
if (matchingRuns.length > 1) throw new Error(`ambiguous run id: ${options.resumeRunId}`);
|
|
229
|
+
const currentSource = options.resumeRunId && (options.resumeGraphId || matchingRuns.length) && !options.useCurrentGraph
|
|
230
|
+
? undefined : readFileSync(path, 'utf8');
|
|
231
|
+
const currentHeader: unknown = options.resumeRunId && !options.resumeGraphId && !matchingRuns.length ? parseYaml(currentSource!) : undefined;
|
|
232
|
+
const resumeGraphId = options.resumeGraphId ?? matchingRuns[0]?.graphId ?? (currentHeader && typeof currentHeader === 'object' && !Array.isArray(currentHeader)
|
|
233
|
+
? (currentHeader as Record<string, unknown>).graph_id : undefined);
|
|
234
|
+
const saved = options.resumeRunId && typeof resumeGraphId === 'string'
|
|
235
|
+
? readGraphRun(safeSegment(resumeGraphId), safeSegment(options.resumeRunId), root) : undefined;
|
|
236
|
+
if (saved?.graphSnapshot && (!saved.graphPath || resolve(path) !== resolve(saved.graphPath))) {
|
|
237
|
+
throw new Error('--file cannot change the graph path of a snapshot run (이 런은 시작 때 그래프 사본이 있다 — --file 대신 --use-current-graph)');
|
|
238
|
+
}
|
|
239
|
+
const snapshotDir = saved ? `${saved.statePath}.graph` : undefined;
|
|
240
|
+
const graphMode: 'snapshot' | 'current' = snapshotDir && saved?.graphSnapshot && !options.useCurrentGraph ? 'snapshot' : 'current';
|
|
241
|
+
const source = graphMode === 'snapshot' ? readFileSync(join(snapshotDir!, 'graph.yaml'), 'utf8') : currentSource ?? readFileSync(path, 'utf8');
|
|
210
242
|
// The shared YAML parser requires a recipe string. A command-less node in
|
|
211
243
|
// this runner is represented internally as `none`, without changing that parser.
|
|
212
244
|
const raw: unknown = parseYaml(source);
|
|
@@ -220,7 +252,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
220
252
|
if (!parsed.template || parsed.errors.length) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
221
253
|
const graph = parsed.template;
|
|
222
254
|
const pluginRoot = elanousStateRoot();
|
|
223
|
-
const graphFile = realpathSync(path);
|
|
255
|
+
const graphFile = graphMode === 'snapshot' && !existsSync(path) ? resolve(path) : realpathSync(path);
|
|
224
256
|
const installed = listInstalledPlugins(pluginRoot);
|
|
225
257
|
const owner = installed.find(item => {
|
|
226
258
|
const graphsDir = join(item.path, 'graphs');
|
|
@@ -235,7 +267,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
235
267
|
throw new Error(`unsupported terminal node: ${terminal} (expected done or failed)`);
|
|
236
268
|
}
|
|
237
269
|
}
|
|
238
|
-
const recipeSource = readFileSync(join(dirname(path), 'recipes.yaml'), 'utf8');
|
|
270
|
+
const recipeSource = readFileSync(graphMode === 'snapshot' ? join(snapshotDir!, 'recipes.yaml') : join(dirname(path), 'recipes.yaml'), 'utf8');
|
|
271
|
+
if (graphMode === 'snapshot' && saved?.graphSnapshot &&
|
|
272
|
+
(createHash('sha256').update(source).digest('hex') !== saved.graphSnapshot.graphSha ||
|
|
273
|
+
createHash('sha256').update(recipeSource).digest('hex') !== saved.graphSnapshot.recipesSha)) {
|
|
274
|
+
throw new Error('graph snapshot changed since run started');
|
|
275
|
+
}
|
|
239
276
|
const recipes = recipesFor(path, recipeSource);
|
|
240
277
|
const catalogRoles = new Set(loadNodeCatalog().roles.keys());
|
|
241
278
|
// Bind a human decision to the exact graph and recipe contents, not just its id or approval prompt.
|
|
@@ -247,10 +284,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
247
284
|
if (!resolved.command && !resolved.approval) throw new Error(`unknown command recipe for ${node.nodeId}: ${node.recipe}`);
|
|
248
285
|
}
|
|
249
286
|
const graphId = safeSegment(graph.graphId);
|
|
250
|
-
if (
|
|
251
|
-
|
|
287
|
+
if (saved && graphId !== saved.graphId) {
|
|
288
|
+
if (options.fromNodeId && graphMode === 'current') throw new Error('graph or recipes changed since failed run');
|
|
289
|
+
throw new Error(`run identity mismatch: ${saved.graphId}/${saved.runId}`);
|
|
290
|
+
}
|
|
252
291
|
const runId = safeSegment(options.resumeRunId ?? options.runId ?? randomUUID());
|
|
253
|
-
const statePath = graphRunPath(graphId, runId,
|
|
292
|
+
const statePath = graphRunPath(graphId, runId, root);
|
|
254
293
|
// A resume owns the run from the first state read through its last write and command.
|
|
255
294
|
// Fail closed on a concurrent (or interrupted) owner rather than replaying a command.
|
|
256
295
|
const resumeLock = `${statePath}.resume.lock`;
|
|
@@ -263,12 +302,20 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
263
302
|
}
|
|
264
303
|
}
|
|
265
304
|
try {
|
|
305
|
+
const startMs = (options.deps?.processStartMs ?? psProcessStartMs)(process.pid);
|
|
306
|
+
const runnerOwner = { pid: process.pid, pidStartedAt: new Date(startMs ?? Date.now() - process.uptime() * 1_000).toISOString() };
|
|
266
307
|
const state: GraphRunState = options.resumeRunId
|
|
267
|
-
? readGraphRun(graphId, runId,
|
|
268
|
-
: { graphId, runId, startedAt: new Date().toISOString(), status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash,
|
|
308
|
+
? readGraphRun(graphId, runId, root)
|
|
309
|
+
: { graphId, runId, startedAt: new Date().toISOString(), ...runnerOwner, status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash,
|
|
310
|
+
graphSnapshot: { graphSha: createHash('sha256').update(source).digest('hex'), recipesSha: createHash('sha256').update(recipeSource).digest('hex') },
|
|
311
|
+
graphPath: resolve(path), ...(options.input === undefined ? {} : { input: options.input }), executed: 0, dryRun: options.dryRun === true, statePath };
|
|
312
|
+
if (options.resumeRunId && graphMode === 'snapshot' && state.sourceHash && state.sourceHash !== approvalSourceHash) {
|
|
313
|
+
throw new Error('graph snapshot changed since run started');
|
|
314
|
+
}
|
|
315
|
+
if (options.resumeRunId && options.dryRun !== undefined && options.dryRun !== state.dryRun) throw new Error('cannot change dryRun when resuming');
|
|
269
316
|
if (options.fromNodeId) {
|
|
270
317
|
if (state.status !== 'failed' || state.pending || state.dryRun) throw new Error('--from requires a failed, non-dry run without pending approval');
|
|
271
|
-
if (!state.sourceHash || state.sourceHash !== approvalSourceHash) throw new Error('graph or recipes changed since failed run');
|
|
318
|
+
if (graphMode === 'current' && (!state.sourceHash || state.sourceHash !== approvalSourceHash)) throw new Error('graph or recipes changed since failed run');
|
|
272
319
|
const from = state.path.indexOf(options.fromNodeId);
|
|
273
320
|
if (from < 0 || state.path.lastIndexOf(options.fromNodeId) !== from || graph.terminalNodes.includes(options.fromNodeId)) throw new Error(`--from node is not unique on the saved executable path: ${options.fromNodeId}`);
|
|
274
321
|
if (state.path.length !== state.nodes.length || state.nodes.some((record, i) => record.nodeId !== state.path[i]) ||
|
|
@@ -306,10 +353,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
306
353
|
state.nodes = state.nodes.slice(0, from);
|
|
307
354
|
state.executed = state.nodes.filter((node) => node.executed).length;
|
|
308
355
|
state.status = 'running';
|
|
309
|
-
state
|
|
356
|
+
Object.assign(state, runnerOwner);
|
|
357
|
+
state.resume = { from: options.fromNodeId, at: new Date().toISOString(), previousStatus: 'failed', graph: graphMode };
|
|
310
358
|
// Persist the restart boundary before executing it, so previous outputs remain durable.
|
|
311
359
|
persistGraphRun(state);
|
|
312
|
-
debug.log('graph.run', 'resume', { graphId, runId, from: options.fromNodeId });
|
|
313
360
|
}
|
|
314
361
|
if (options.resumeRunId && state.status === 'running' && state.pending) {
|
|
315
362
|
throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
|
|
@@ -323,10 +370,16 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
323
370
|
if (options.resumeRunId && (state.graphId !== graphId || state.runId !== runId)) {
|
|
324
371
|
throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
|
|
325
372
|
}
|
|
326
|
-
if (options.resumeRunId && options.dryRun !== undefined && options.dryRun !== state.dryRun) throw new Error('cannot change dryRun when resuming');
|
|
327
373
|
if (options.resumeRunId && state.status === 'awaiting-approval' && state.approvalSourceHash !== approvalSourceHash) {
|
|
328
374
|
throw new Error(`approval source changed since run was paused: ${graphId}/${runId}`);
|
|
329
375
|
}
|
|
376
|
+
if (options.resumeRunId && !options.fromNodeId) {
|
|
377
|
+
const previousStatus = state.status;
|
|
378
|
+
Object.assign(state, runnerOwner);
|
|
379
|
+
state.resume = { from: state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode, at: new Date().toISOString(), previousStatus, graph: graphMode };
|
|
380
|
+
persistGraphRun(state);
|
|
381
|
+
}
|
|
382
|
+
if (options.resumeRunId) debug.log('graph.runs', 'resume', { graphId, runId, from: state.resume?.from, graph: graphMode });
|
|
330
383
|
const persist = () => {
|
|
331
384
|
if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded') state.finishedAt ??= new Date().toISOString();
|
|
332
385
|
persistGraphRun(state);
|
|
@@ -345,6 +398,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
345
398
|
if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`run id already exists: ${graphId}/${runId}`);
|
|
346
399
|
throw error;
|
|
347
400
|
}
|
|
401
|
+
mkdirSync(`${statePath}.graph`);
|
|
402
|
+
writeFileSync(join(`${statePath}.graph`, 'graph.yaml'), source, { flag: 'wx' });
|
|
403
|
+
writeFileSync(join(`${statePath}.graph`, 'recipes.yaml'), recipeSource, { flag: 'wx' });
|
|
348
404
|
}
|
|
349
405
|
while (current !== undefined) {
|
|
350
406
|
const node = graph.nodes.find((n) => n.nodeId === current);
|
|
@@ -474,6 +530,76 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
474
530
|
}
|
|
475
531
|
}
|
|
476
532
|
|
|
533
|
+
export type GraphRunAlive = boolean | 'unknown';
|
|
534
|
+
|
|
535
|
+
export function graphRunAlive(state: GraphRunState, processStartMs: (pid: number) => number | null = psProcessStartMs): GraphRunAlive {
|
|
536
|
+
if (state.pid === undefined) return 'unknown';
|
|
537
|
+
if (!Number.isSafeInteger(state.pid) || state.pid <= 0 || typeof state.pidStartedAt !== 'string') return false;
|
|
538
|
+
const recorded = Date.parse(state.pidStartedAt);
|
|
539
|
+
if (!Number.isFinite(recorded)) return false;
|
|
540
|
+
try {
|
|
541
|
+
const actual = processStartMs(state.pid);
|
|
542
|
+
return actual !== null && Number.isFinite(actual) && Math.abs(actual - recorded) <= START_TOLERANCE_MS;
|
|
543
|
+
} catch { return false; }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function isDecisionClaim(raw: unknown): boolean {
|
|
547
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return false;
|
|
548
|
+
const value = raw as Record<string, unknown>;
|
|
549
|
+
return typeof value.nodeId === 'string' && (value.decision === 'approved' || value.decision === 'rejected')
|
|
550
|
+
&& typeof value.decidedAt === 'string' && (value.decidedBy === undefined || typeof value.decidedBy === 'string')
|
|
551
|
+
&& !('graphId' in value) && !('runId' in value);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function listGraphRuns(root = effectiveInstanceRoot()): { runs: GraphRunState[]; unreadable: number } {
|
|
555
|
+
const base = join(root, 'graph-runs');
|
|
556
|
+
let graphs: string[];
|
|
557
|
+
try { graphs = readdirSync(base); }
|
|
558
|
+
catch (error) {
|
|
559
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { runs: [], unreadable: 0 };
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
const runs: GraphRunState[] = [];
|
|
563
|
+
let unreadable = 0;
|
|
564
|
+
for (const graphId of graphs) {
|
|
565
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(graphId) || graphId === '.' || graphId === '..') continue;
|
|
566
|
+
const dir = join(base, graphId);
|
|
567
|
+
let files: string[];
|
|
568
|
+
try {
|
|
569
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
570
|
+
files = readdirSync(dir);
|
|
571
|
+
} catch { unreadable++; continue; }
|
|
572
|
+
for (const file of files) {
|
|
573
|
+
if (!file.endsWith('.json')) continue;
|
|
574
|
+
const runId = file.slice(0, -5);
|
|
575
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId) || runId === '.' || runId === '..') continue;
|
|
576
|
+
// Decision claims are named <runId>.json.<visit>.decision.json; a run
|
|
577
|
+
// may itself end in .decision, so check the ledger identity before skipping.
|
|
578
|
+
const decisionParent = file.match(/^(.*\.json)\.\d+\.decision\.json$/)?.[1];
|
|
579
|
+
try {
|
|
580
|
+
const raw: unknown = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
|
581
|
+
// Skip only a VALID decision claim (decideGraphApproval writes { nodeId, decision, decidedBy?, decidedAt });
|
|
582
|
+
// anything else under a claim-like name is judged as a ledger and counts as unreadable when broken.
|
|
583
|
+
if (decisionParent && isDecisionClaim(raw)) continue;
|
|
584
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
|
|
585
|
+
(raw as Partial<GraphRunState>).graphId !== graphId || (raw as Partial<GraphRunState>).runId !== runId) {
|
|
586
|
+
unreadable++;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const state = readGraphRun(graphId, runId, root);
|
|
590
|
+
if (typeof state.startedAt !== 'string' || !Number.isFinite(Date.parse(state.startedAt)) ||
|
|
591
|
+
!Array.isArray(state.path) || !state.path.every((node) => typeof node === 'string') ||
|
|
592
|
+
!Array.isArray(state.nodes) || !state.nodes.every((node) => node && typeof node.nodeId === 'string' && typeof node.ok === 'boolean')) {
|
|
593
|
+
throw new Error('invalid run state');
|
|
594
|
+
}
|
|
595
|
+
runs.push(state);
|
|
596
|
+
} catch { unreadable++; }
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
runs.sort((a, b) => Date.parse(b.startedAt!) - Date.parse(a.startedAt!) || a.graphId.localeCompare(b.graphId) || a.runId.localeCompare(b.runId));
|
|
600
|
+
return { runs, unreadable };
|
|
601
|
+
}
|
|
602
|
+
|
|
477
603
|
export function latestGraphRun(graphId: string, root = effectiveInstanceRoot()): GraphRunState | null {
|
|
478
604
|
const dir = join(root, 'graph-runs', safeSegment(graphId));
|
|
479
605
|
let files: string[];
|