elanous 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +16 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +16 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +24 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -9,15 +9,60 @@ import { listLoops } from '../../loops/registry.js';
|
|
|
9
9
|
import { listSubSeatCharters } from './sub-seat-charters.js';
|
|
10
10
|
import { SEATS } from './ops-seats.js';
|
|
11
11
|
import { jsonResponse } from './json-response.js';
|
|
12
|
+
import { readTaskAgentState, type TaskCard as TaskAgentCard } from '../../task-agent/task-hand.js';
|
|
13
|
+
import { LogStore, logsDbPath } from '../../mss/logging/log-store.js';
|
|
14
|
+
|
|
15
|
+
/** `move` 칸에 실을 수 있는 값(allowlist) — 판단부 수 종류만, 문면은 싣지 않는다. */
|
|
16
|
+
export const LOOP_EDGE_MOVES = ['green-proposal'] as const;
|
|
12
17
|
|
|
13
18
|
export interface LoopEdge {
|
|
14
19
|
at: string;
|
|
15
|
-
kind: 'request' | 'decision' | 'report' | 'card' | 'run';
|
|
20
|
+
kind: 'request' | 'decision' | 'report' | 'card' | 'run' | 'hand' | 'launch' | 'move';
|
|
16
21
|
from: string;
|
|
17
22
|
to: string;
|
|
18
23
|
ref: string;
|
|
24
|
+
/** 체크리스트 칸 id(ID 정규식) — hand 간선만. */
|
|
25
|
+
cell?: string;
|
|
26
|
+
/** LOOP_EDGE_MOVES 중 하나 — move 간선만. */
|
|
27
|
+
move?: typeof LOOP_EDGE_MOVES[number];
|
|
28
|
+
/** hand 간선만 — 원장 행 mode, 없으면 key 접두(`shadow:orch:` = shadow · `orch:` = live). 못 정하면 생략. */
|
|
29
|
+
mode?: 'shadow' | 'live';
|
|
30
|
+
/** hand 간선만 — 그 행을 쓴 오케스트레이터 틱 runId(ID 정규식). 없으면 생략. */
|
|
31
|
+
tick?: string;
|
|
32
|
+
/** live claimed 뒤 handed·released 없이 N분 지남 = 발사 실패(끊긴 넘김). ref = 오케스트레이터 카드 id. */
|
|
33
|
+
broken?: true;
|
|
34
|
+
/** launch 간선만 — 그 카드에 묶인 런 원장 runId(ID 정규식). 못 묶으면 생략(PWA 가 10분 뒤 «유령»으로 본다). */
|
|
35
|
+
run?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 노드 접두 — 조율 루프는 레지스트리 행 `loop:orchestrator` 그대로 쓴다. `queue:` 는 대기열 대체 간선 자리(조각 A 밖). */
|
|
39
|
+
export const TASK_AGENT_NODE = 'agent:task-agent';
|
|
40
|
+
export const ORCHESTRATOR_NODE = 'loop:orchestrator';
|
|
41
|
+
|
|
42
|
+
/** 한 런 원장에서 투영한 사실 — 문면은 feature(연결 비교 전용 · 응답에 싣지 않는다)뿐. */
|
|
43
|
+
export interface RunFact {
|
|
44
|
+
runId: string;
|
|
45
|
+
startAt?: string;
|
|
46
|
+
seat?: string;
|
|
47
|
+
feature?: string;
|
|
48
|
+
goalId?: string;
|
|
49
|
+
mergedAt?: string;
|
|
50
|
+
prs: Array<{ at: string; number: number }>;
|
|
51
|
+
children: string[];
|
|
19
52
|
}
|
|
20
53
|
|
|
54
|
+
/** handed.jsonl 행 투영 — status 가 없으면 handed 로 읽는다(조각 A 주입 호환). claimed·released 는 끊긴 간선 판정에만 쓴다. */
|
|
55
|
+
export interface HandedRow {
|
|
56
|
+
at: string;
|
|
57
|
+
cardId?: string;
|
|
58
|
+
cell?: string;
|
|
59
|
+
status?: 'claimed' | 'handed' | 'released';
|
|
60
|
+
key?: string;
|
|
61
|
+
mode?: 'shadow' | 'live';
|
|
62
|
+
runId?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface PodFinish { at: string; childRunId: string; prNumber: number }
|
|
65
|
+
|
|
21
66
|
export interface LoopEdgesDeps {
|
|
22
67
|
listCoord?: typeof listCoordEvents;
|
|
23
68
|
cardRoot?: string;
|
|
@@ -28,6 +73,16 @@ export interface LoopEdgesDeps {
|
|
|
28
73
|
listOrchestratorLaunches?: () => Array<{ key: string; runId: string }>;
|
|
29
74
|
listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
|
|
30
75
|
seatIds?: () => string[];
|
|
76
|
+
/** 기본 = `<root>/loop/orchestrator/handed.jsonl` · root = cardRoot ?? effectiveInstanceRoot(). */
|
|
77
|
+
listHanded?: () => HandedRow[];
|
|
78
|
+
/** live claimed 가 handed·released 없이 이만큼 지나면 끊긴 hand 간선(기본 10분). */
|
|
79
|
+
handBrokenAfterMs?: number;
|
|
80
|
+
/** 기본 = `<root>/task-agent-actions.json` 의 tasks(readTaskAgentState). */
|
|
81
|
+
listTaskCards?: () => TaskAgentCard[];
|
|
82
|
+
/** 기본 = ledgerDir(또는 연합 원장)에서 투영. listRunStarts 를 주입하고 이것을 안 주면 [] 다. */
|
|
83
|
+
listRunFacts?: () => RunFact[];
|
|
84
|
+
/** 대체 원천 — logs `self-implement.pod` `job-finished` 의 childRunId·prUrl(PR 번호만). */
|
|
85
|
+
listPodFinishes?: (since: string) => PodFinish[];
|
|
31
86
|
}
|
|
32
87
|
|
|
33
88
|
const ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
@@ -100,9 +155,9 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
|
|
|
100
155
|
return edges;
|
|
101
156
|
}
|
|
102
157
|
|
|
103
|
-
function
|
|
158
|
+
function runFacts(dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): RunFact[] {
|
|
104
159
|
if (!existsSync(dir)) return [];
|
|
105
|
-
const result:
|
|
160
|
+
const result: RunFact[] = [];
|
|
106
161
|
let names: string[];
|
|
107
162
|
try { names = readdirSync(dir); } catch { return []; }
|
|
108
163
|
for (const name of names) {
|
|
@@ -113,20 +168,84 @@ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, strin
|
|
|
113
168
|
const runId = name.slice(0, -6);
|
|
114
169
|
let entries: RunLedgerEntry[] | null;
|
|
115
170
|
try { entries = loadRunLedger(runId, dir); } catch { continue; }
|
|
116
|
-
|
|
171
|
+
if (!entries) continue;
|
|
172
|
+
const start = entries.find(entry => entry.event === 'start' && entry.runId === runId);
|
|
117
173
|
const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
|
|
118
174
|
const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
|
|
119
175
|
?? start?.data.originAgent ?? start?.data.controller;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
176
|
+
const merged = entries.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
|
|
177
|
+
const goalId = [start, ...entries].map(entry => entry?.goalId ?? (typeof entry?.data.goalId === 'string' ? entry.data.goalId : undefined))
|
|
178
|
+
.find((value): value is string => typeof value === 'string' && /^[a-f0-9]{16}$/.test(value));
|
|
179
|
+
// pr-opened data = {url, number, branch, …} (TC 실측 10-07) — 번호는 data.number(양의 정수)만 읽는다. 못 읽으면 간선 0.
|
|
180
|
+
const prs = entries.filter(entry => entry.event === 'pr-opened' && typeof entry.timestamp === 'string'
|
|
181
|
+
&& Number.isSafeInteger(entry.data.number) && (entry.data.number as number) > 0)
|
|
182
|
+
.map(entry => ({ at: entry.timestamp!, number: entry.data.number as number }));
|
|
183
|
+
// `pod-ledger-incomplete` 첫 줄 원장은 시작이 없어도 버리지 않는다 — pr 근거로만 쓰고 run 단계는 만들지 않는다.
|
|
184
|
+
const children = [...new Set(entries.filter(entry => entry.event === 'pod-child-run' && typeof entry.data.childRunId === 'string'
|
|
185
|
+
&& ID.test(entry.data.childRunId) && entry.data.childRunId !== runId).map(entry => entry.data.childRunId as string))];
|
|
186
|
+
result.push({
|
|
187
|
+
runId, prs, children,
|
|
188
|
+
...(start?.timestamp ? { startAt: start.timestamp } : {}),
|
|
189
|
+
...(start?.timestamp && seat(launchSeat, seats) ? { seat: launchSeat } : {}),
|
|
190
|
+
...(start?.timestamp && feature ? { feature } : {}),
|
|
191
|
+
...(goalId ? { goalId } : {}),
|
|
192
|
+
...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}),
|
|
193
|
+
});
|
|
126
194
|
}
|
|
127
195
|
return result;
|
|
128
196
|
}
|
|
129
197
|
|
|
198
|
+
function runStartsFrom(facts: readonly RunFact[], since: string): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
|
|
199
|
+
return facts.filter(fact => fact.startAt !== undefined && fact.seat !== undefined
|
|
200
|
+
&& (fact.startAt >= since || (fact.mergedAt !== undefined && fact.mergedAt >= since)))
|
|
201
|
+
.map(fact => ({ runId: fact.runId, at: fact.startAt!, seat: fact.seat!, ...(fact.mergedAt ? { mergedAt: fact.mergedAt } : {}) }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** handed.jsonl(`tick.ts` ORCH-TA-HAND) — 행의 at·status·key·cardId·cell·mode·runId 만. 넘김은 handed 뿐이고 claimed·released 는 끊긴 간선 판정용이다. */
|
|
205
|
+
function handedRows(root: string): HandedRow[] {
|
|
206
|
+
const path = join(root, 'loop', 'orchestrator', 'handed.jsonl');
|
|
207
|
+
try { if (!existsSync(path) || statSync(path).size > 5 * 1024 * 1024) return []; } catch { return []; }
|
|
208
|
+
let raw: string;
|
|
209
|
+
try { raw = readFileSync(path, 'utf8'); } catch { return []; }
|
|
210
|
+
const rows: HandedRow[] = [];
|
|
211
|
+
for (const line of raw.split('\n')) {
|
|
212
|
+
const row = line.trim() ? parse(line) : null;
|
|
213
|
+
const status = row?.status;
|
|
214
|
+
if ((status !== 'handed' && status !== 'claimed' && status !== 'released') || typeof row!.at !== 'string') continue;
|
|
215
|
+
if (status === 'handed' && typeof row!.cardId !== 'string') continue;
|
|
216
|
+
rows.push({
|
|
217
|
+
at: row!.at as string, status,
|
|
218
|
+
...(typeof row!.cardId === 'string' ? { cardId: row!.cardId } : {}),
|
|
219
|
+
...(typeof row!.cell === 'string' ? { cell: row!.cell } : {}),
|
|
220
|
+
...(typeof row!.key === 'string' ? { key: row!.key } : {}),
|
|
221
|
+
...(row!.mode === 'shadow' || row!.mode === 'live' ? { mode: row!.mode } : {}),
|
|
222
|
+
...(typeof row!.runId === 'string' ? { runId: row!.runId } : {}),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return rows;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function taskCards(root: string): TaskAgentCard[] {
|
|
229
|
+
const tasks = object(readTaskAgentState<{ tasks?: unknown }>(join(root, 'task-agent-actions.json')).tasks);
|
|
230
|
+
return tasks ? Object.values(tasks).filter((card): card is TaskAgentCard => object(card) !== null) : [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 대체 원천: Pod 런은 도는 동안 원장이 Pod 안에만 있다 — 호스트 logs 의 job-finished 에서 childRunId·PR 번호만 읽는다. */
|
|
234
|
+
function podFinishes(since: string): PodFinish[] {
|
|
235
|
+
const path = logsDbPath();
|
|
236
|
+
if (!existsSync(path)) return [];
|
|
237
|
+
const store = LogStore.openReadOnly(path);
|
|
238
|
+
try {
|
|
239
|
+
const result: PodFinish[] = [];
|
|
240
|
+
for (const row of store.queryAll({ exactCategories: ['self-implement.pod'], events: ['job-finished'], sinceMs: Date.parse(since) })) {
|
|
241
|
+
const data = row.data ? parse(row.data) : null;
|
|
242
|
+
const pr = typeof data?.prUrl === 'string' ? /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/.exec(data.prUrl) : null;
|
|
243
|
+
if (typeof data?.childRunId === 'string' && pr) result.push({ at: row.ts, childRunId: data.childRunId, prNumber: Number(pr[1]) });
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
} finally { store.close(); }
|
|
247
|
+
}
|
|
248
|
+
|
|
130
249
|
/** Project only the key and runId of orchestrator launches, never their card snapshots or prose. */
|
|
131
250
|
function orchestratorLaunches(root: string): Array<{ key: string; runId: string }> {
|
|
132
251
|
const dir = join(root, 'loop', 'orchestrator');
|
|
@@ -152,17 +271,166 @@ function orchestratorLaunches(root: string): Array<{ key: string; runId: string
|
|
|
152
271
|
return launches;
|
|
153
272
|
}
|
|
154
273
|
|
|
274
|
+
type JourneyLink = { from: string; to: string; both: boolean };
|
|
275
|
+
type Journey = { links: JourneyLink[]; pieces: Map<string, string[]> };
|
|
276
|
+
|
|
277
|
+
/** 런 원장 start.feature 는 `boundReadableText(feature, 120)` 로 잘린다(orchestrator.ts RUN_START_FEATURE_MAX_CHARS) —
|
|
278
|
+
* 꼬리 표식 ` [truncated; originalChars=N]`·`[truncated]`·`…`(⊕ 손으로 쓴 `...`)을 떼고 공백을 정규화한다. */
|
|
279
|
+
const FEATURE_TRUNCATION_TAIL = /(?:\s*\[truncated(?:; originalChars=\d+)?\]|…|\.\.\.)$/;
|
|
280
|
+
const FEATURE_PREFIX_MIN = 40;
|
|
281
|
+
function normalizeText(text: string): string { return text.replace(/\s+/g, ' ').trim(); }
|
|
282
|
+
/** 잘린 feature 의 비교 접두 — 표식이 없으면 undefined(완전 일치만). 끝의 짝 잃은 서로게이트도 뗀다(UTF-16 절단). */
|
|
283
|
+
function featurePrefix(feature: string): string | undefined {
|
|
284
|
+
if (!FEATURE_TRUNCATION_TAIL.test(feature)) return undefined;
|
|
285
|
+
const prefix = normalizeText(feature.replace(FEATURE_TRUNCATION_TAIL, '').replace(/[\ud800-\udbff]$/, ''));
|
|
286
|
+
return prefix.length >= FEATURE_PREFIX_MIN ? prefix : undefined;
|
|
287
|
+
}
|
|
288
|
+
/** 카드 text ↔ 런 feature — 완전 일치, 또는 잘린 feature(≥40자)가 카드 text(공백 정규화)의 접두. */
|
|
289
|
+
function featureMatches(feature: string | undefined, text: string): boolean {
|
|
290
|
+
if (!feature || !text) return false;
|
|
291
|
+
if (feature === text) return true;
|
|
292
|
+
const prefix = featurePrefix(feature);
|
|
293
|
+
return prefix !== undefined && normalizeText(text).startsWith(prefix);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 카드 → 런 연결: 카드 runId(있으면 최우선) → goalId 일치 → feature 일치(완전 또는 잘린 접두) ∧ start ≥ launch.at — 뒤 둘은 «유일할 때만»
|
|
297
|
+
* (같은 feature 에 닿는 발사 카드가 둘이면 잇지 않는다). 자식 런(pod-child-run)은 부모로 닿는다. */
|
|
298
|
+
function linkedRun(card: TaskAgentCard, launchAt: string | undefined, launchedTexts: readonly string[], facts: readonly RunFact[], childIds: ReadonlySet<string>): string | undefined {
|
|
299
|
+
const explicitRun = (card as { runId?: unknown }).runId;
|
|
300
|
+
if (typeof explicitRun === 'string' && ID.test(explicitRun)) return explicitRun;
|
|
301
|
+
const pick = (candidates: RunFact[]): string | undefined => {
|
|
302
|
+
const top = candidates.filter(fact => !childIds.has(fact.runId));
|
|
303
|
+
return top.length === 1 ? top[0]!.runId : candidates.length === 1 ? candidates[0]!.runId : undefined;
|
|
304
|
+
};
|
|
305
|
+
if (typeof card.goalId === 'string' && card.goalId) {
|
|
306
|
+
const byGoal = pick(facts.filter(fact => fact.goalId === card.goalId));
|
|
307
|
+
if (byGoal) return byGoal;
|
|
308
|
+
}
|
|
309
|
+
const launchMs = launchAt === undefined ? NaN : Date.parse(launchAt);
|
|
310
|
+
if (!Number.isFinite(launchMs) || typeof card.text !== 'string' || !card.text) return undefined;
|
|
311
|
+
const runId = pick(facts.filter(fact => featureMatches(fact.feature, card.text) && fact.startAt !== undefined && Date.parse(fact.startAt) >= launchMs));
|
|
312
|
+
if (!runId) return undefined;
|
|
313
|
+
const feature = facts.find(fact => fact.runId === runId)!.feature;
|
|
314
|
+
return launchedTexts.filter(text => featureMatches(feature, text)).length === 1 ? runId : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** hand 행의 mode — 행의 mode 필드가 우선, 없으면 key 접두(`shadow:orch:` = shadow · `orch:` = live). */
|
|
318
|
+
function handMode(row: HandedRow): 'shadow' | 'live' | undefined {
|
|
319
|
+
if (row.mode === 'shadow' || row.mode === 'live') return row.mode;
|
|
320
|
+
if (typeof row.key !== 'string') return undefined;
|
|
321
|
+
if (row.key.startsWith('shadow:orch:')) return 'shadow';
|
|
322
|
+
if (row.key.startsWith('orch:')) return 'live';
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 넘김(hand)·발사(launch)·런→PR·green 제안(move) 간선을 더하고, ?ref= 여정에 쓸 id 연결을 돌려준다. 문면은 싣지 않는다. */
|
|
327
|
+
function taskAgentEdges(deps: LoopEdgesDeps, facts: readonly RunFact[], seats: ReadonlySet<string>, since: number, now: number, sinceIso: string, edges: LoopEdge[]): Journey {
|
|
328
|
+
// 주입된 카드 목록(시험)에 뿌리가 없으면 운영 뿌리를 읽지 않는다 — 공개본엔 bunfig preload 격리가 없다.
|
|
329
|
+
const root = deps.cardRoot ?? (deps.listCards ? undefined : effectiveInstanceRoot());
|
|
330
|
+
const journey: Journey = { links: [], pieces: new Map() };
|
|
331
|
+
let handed: HandedRow[] = [];
|
|
332
|
+
try { handed = (deps.listHanded ?? (() => root === undefined ? [] : handedRows(root)))(); } catch { /* An unreadable hand ledger hands nothing. */ }
|
|
333
|
+
const settled = new Set<string>();
|
|
334
|
+
for (const row of handed) if (row.key !== undefined && (row.status ?? 'handed') !== 'claimed') settled.add(row.key);
|
|
335
|
+
const brokenAfter = deps.handBrokenAfterMs ?? 10 * 60 * 1000;
|
|
336
|
+
const brokenKeys = new Set<string>();
|
|
337
|
+
for (const row of handed) {
|
|
338
|
+
const status = row.status ?? 'handed';
|
|
339
|
+
const mode = handMode(row);
|
|
340
|
+
const tick = typeof row.runId === 'string' && ID.test(row.runId) ? { tick: row.runId } : {};
|
|
341
|
+
if (status === 'handed') {
|
|
342
|
+
if (typeof row.cardId !== 'string' || !ID.test(row.cardId) || !inWindow(row.at, since, now)) continue;
|
|
343
|
+
edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: row.cardId,
|
|
344
|
+
...(row.cell && ID.test(row.cell) ? { cell: row.cell } : {}), ...(mode ? { mode } : {}), ...tick });
|
|
345
|
+
// 넘김 행의 cardId 는 TASK-AGENT 카드다 — 소원(wish) 카드는 key(`orch:<wish>:<칸>`)에만 있다. 둘을 잇지 않으면
|
|
346
|
+
// `?ref=<wish>` 여정이 넘김에서 끊겨 데모 띠가 1단계 뒤로 «남음»에 멈춘다(10-07 리허설 2 실측 · JOURNEY-HAND-LINK).
|
|
347
|
+
const wish = row.key === undefined ? undefined : /^orch:([^:]+):([^:]+)$/.exec(row.key)?.[1];
|
|
348
|
+
if (wish !== undefined && ID.test(wish) && wish !== row.cardId) journey.links.push({ from: wish, to: row.cardId, both: true });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// live claimed 만(handed·released 없음 · N분 지남) = 발사 실패 — 끊긴 간선 하나. shadow claimed→released 는 간선 0.
|
|
352
|
+
if (status !== 'claimed' || mode !== 'live' || row.key === undefined || settled.has(row.key) || brokenKeys.has(row.key)) continue;
|
|
353
|
+
const claimedMs = Date.parse(row.at);
|
|
354
|
+
if (!Number.isFinite(claimedMs) || now - claimedMs < brokenAfter || !inWindow(row.at, since, now)) continue;
|
|
355
|
+
const parts = /^orch:([^:]+):([^:]+)$/.exec(row.key);
|
|
356
|
+
if (!parts || !ID.test(parts[1]!) || !ID.test(parts[2]!)) continue;
|
|
357
|
+
brokenKeys.add(row.key);
|
|
358
|
+
edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: parts[1]!, cell: parts[2]!, mode, ...tick, broken: true });
|
|
359
|
+
}
|
|
360
|
+
let cards: TaskAgentCard[] = [];
|
|
361
|
+
try { cards = (deps.listTaskCards ?? (() => root === undefined ? [] : taskCards(root)))().filter(card => typeof card?.id === 'string' && ID.test(card.id)); }
|
|
362
|
+
catch { /* An unreadable task-agent state launches nothing. */ }
|
|
363
|
+
const launchesOf = (card: TaskAgentCard) => (Array.isArray(card.history) ? card.history : [])
|
|
364
|
+
.filter(event => event?.event === 'launch' && typeof event.at === 'string');
|
|
365
|
+
const launchedTexts: string[] = [];
|
|
366
|
+
for (const card of cards) {
|
|
367
|
+
if (launchesOf(card).length && typeof card.text === 'string') launchedTexts.push(card.text);
|
|
368
|
+
}
|
|
369
|
+
const childIds = new Set(facts.flatMap(fact => fact.children));
|
|
370
|
+
for (const card of cards) {
|
|
371
|
+
if (typeof card.mission === 'string' && ID.test(card.mission)) journey.links.push({ from: card.id, to: card.mission, both: false });
|
|
372
|
+
if (Array.isArray(card.pieces)) journey.pieces.set(card.id, card.pieces.filter(piece => typeof piece === 'string' && ID.test(piece)));
|
|
373
|
+
const launches = launchesOf(card);
|
|
374
|
+
const runId = linkedRun(card, launches[0]?.at, launchedTexts, facts, childIds);
|
|
375
|
+
for (const launch of launches) {
|
|
376
|
+
if (seat(card.seat, seats) && inWindow(launch.at, since, now)) edges.push({ at: launch.at, kind: 'launch', from: TASK_AGENT_NODE, to: card.seat, ref: card.id, ...(runId ? { run: runId } : {}) });
|
|
377
|
+
}
|
|
378
|
+
if (runId) journey.links.push({ from: card.id, to: runId, both: true });
|
|
379
|
+
const green = object(card.greenProposal);
|
|
380
|
+
if (green && typeof green.at === 'string' && typeof green.checklistId === 'string' && ID.test(green.checklistId) && inWindow(green.at, since, now)) {
|
|
381
|
+
edges.push({ at: green.at, kind: 'move', from: TASK_AGENT_NODE, to: `release:${green.checklistId}`, ref: card.id, move: 'green-proposal' });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const prSeen = new Set<string>();
|
|
385
|
+
const prEdge = (runId: string, at: string, number: number) => {
|
|
386
|
+
const key = `${runId}:${number}`;
|
|
387
|
+
if (!ID.test(runId) || !Number.isSafeInteger(number) || number < 1 || prSeen.has(key) || !inWindow(at, since, now)) return;
|
|
388
|
+
prSeen.add(key);
|
|
389
|
+
edges.push({ at, kind: 'run', from: `loop:${runId}`, to: `pr:${number}`, ref: runId });
|
|
390
|
+
};
|
|
391
|
+
for (const fact of facts) {
|
|
392
|
+
for (const child of fact.children) journey.links.push({ from: fact.runId, to: child, both: true });
|
|
393
|
+
for (const pr of fact.prs) prEdge(fact.runId, pr.at, pr.number);
|
|
394
|
+
}
|
|
395
|
+
let pods: PodFinish[] = [];
|
|
396
|
+
// 대체 원천은 연합 원장(운영 기본)과 짝이다 — 원장을 주입했으면(시험·한 디렉터리) logs 를 읽지 않는다.
|
|
397
|
+
const injectedLedger = deps.ledgerDir !== undefined || deps.listRunStarts !== undefined || deps.listRunFacts !== undefined;
|
|
398
|
+
try { pods = (deps.listPodFinishes ?? (injectedLedger ? () => [] : podFinishes))(sinceIso); } catch { /* logs outage — the run ledger stays the primary source. */ }
|
|
399
|
+
for (const pod of pods) prEdge(pod.childRunId, pod.at, pod.prNumber);
|
|
400
|
+
return journey;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** ?ref= 의 여정 id 집합 — 카드↔런·런↔자식 런은 양방향, 조각→미션은 위로만(형제 조각은 미션을 ref 로 줄 때만). */
|
|
404
|
+
function journeyIds(ref: string, journey: Journey): Set<string> {
|
|
405
|
+
const ids = new Set([ref, ...(journey.pieces.get(ref) ?? [])]);
|
|
406
|
+
const queue = [...ids];
|
|
407
|
+
while (queue.length) {
|
|
408
|
+
const id = queue.shift()!;
|
|
409
|
+
for (const link of journey.links) {
|
|
410
|
+
const next = link.from === id ? link.to : link.both && link.to === id ? link.from : undefined;
|
|
411
|
+
if (next !== undefined && !ids.has(next)) { ids.add(next); queue.push(next); }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return ids;
|
|
415
|
+
}
|
|
416
|
+
|
|
155
417
|
/** GET only; project an allowlist of fields from the ledgers. Never serialize ledger records. */
|
|
156
418
|
export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
|
|
157
419
|
const params = new URL(req.url).searchParams;
|
|
158
420
|
const rawSince = params.get('since');
|
|
159
|
-
const
|
|
421
|
+
const journeyRef = params.get('ref');
|
|
422
|
+
// ?mode=live|shadow — hand 간선만 그 mode 로 거른다(다른 종류 간선은 그대로).
|
|
423
|
+
const handModeFilter = params.get('mode');
|
|
424
|
+
// ?ref= 는 한 카드·런의 여정 — 60분 창과 limit 에 잘리지 않게 since 를 안 주면 처음부터 본다.
|
|
425
|
+
const since = rawSince === null ? (journeyRef === null ? Date.now() - 60 * 60 * 1000 : 0) : Date.parse(rawSince);
|
|
160
426
|
const rawLimit = params.get('limit');
|
|
161
427
|
const limit = rawLimit === null ? 100 : Number(rawLimit);
|
|
162
428
|
const now = Date.now();
|
|
163
429
|
if (rawSince !== null && !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(rawSince)
|
|
164
|
-
|| !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500
|
|
165
|
-
|
|
430
|
+
|| !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500
|
|
431
|
+
|| (journeyRef !== null && !ID.test(journeyRef))
|
|
432
|
+
|| (handModeFilter !== null && handModeFilter !== 'live' && handModeFilter !== 'shadow')) {
|
|
433
|
+
return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>&ref=<card|run|mission id>' }, 400);
|
|
166
434
|
}
|
|
167
435
|
const sinceIso = new Date(since).toISOString();
|
|
168
436
|
const seats = new Set((deps.seatIds ?? (() => [...SEATS.map(s => s.seat), ...listSubSeatCharters().map(s => s.id)]))());
|
|
@@ -189,7 +457,7 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
|
|
|
189
457
|
finally { store.close(); }
|
|
190
458
|
}
|
|
191
459
|
}
|
|
192
|
-
const
|
|
460
|
+
const readFacts = deps.listRunFacts ?? (deps.listRunStarts ? () => [] : () => {
|
|
193
461
|
const owners = new Map<string, string>();
|
|
194
462
|
try {
|
|
195
463
|
for (const entry of (deps.listLoopOwners ?? (() => listLoops()))()) {
|
|
@@ -197,8 +465,11 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
|
|
|
197
465
|
}
|
|
198
466
|
} catch { /* A registry outage does not erase explicit launch-seat evidence. */ }
|
|
199
467
|
return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
|
|
200
|
-
.flatMap(dir =>
|
|
468
|
+
.flatMap(dir => runFacts(dir, owners, seats));
|
|
201
469
|
});
|
|
470
|
+
let facts: RunFact[] = [];
|
|
471
|
+
try { facts = readFacts().filter(fact => ID.test(fact.runId)); } catch { /* An unreadable ledger set links nothing. */ }
|
|
472
|
+
const readStarts = deps.listRunStarts ?? ((from: string) => runStartsFrom(facts, from));
|
|
202
473
|
const runs = readStarts(sinceIso).filter(run => seat(run.seat, seats) && ID.test(run.runId)
|
|
203
474
|
&& (inWindow(run.at, since, now) || (run.mergedAt !== undefined && inWindow(run.mergedAt, since, now))));
|
|
204
475
|
for (const run of runs) {
|
|
@@ -224,6 +495,10 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
|
|
|
224
495
|
}
|
|
225
496
|
}
|
|
226
497
|
}
|
|
227
|
-
|
|
228
|
-
|
|
498
|
+
const links = taskAgentEdges(deps, facts, seats, since, now, sinceIso, edges);
|
|
499
|
+
const journey = journeyRef === null ? null : journeyIds(journeyRef, links);
|
|
500
|
+
const out = edges.filter(edge => (!journey || journey.has(edge.ref))
|
|
501
|
+
&& (handModeFilter === null || edge.kind !== 'hand' || edge.mode === handModeFilter));
|
|
502
|
+
out.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
503
|
+
return jsonResponse({ edges: out.slice(0, limit) });
|
|
229
504
|
}
|
package/src/nexus/api/ops-api.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { debug, redactSecretText } from '../../debug/log.js';
|
|
4
|
-
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
4
|
+
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
|
|
5
|
+
import { gateShardsPath, readGateShards, summarizeShards, type GateShardsFile, type GateShardsSummary } from '../../release-loop/gate-shards.js';
|
|
5
6
|
import { listChecklist } from '../../release-loop/checklist.js';
|
|
6
7
|
import { getSchedule } from '../../release-loop/release-schedule.js';
|
|
7
8
|
import { jsonResponse } from './json-response.js';
|
|
@@ -18,13 +19,35 @@ let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
|
|
|
18
19
|
export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
|
|
19
20
|
const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
|
|
20
21
|
|
|
21
|
-
interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
|
|
22
|
+
interface RunNode { nodeId: string; ok?: unknown; output?: unknown; startedAt?: unknown; endedAt?: unknown }
|
|
22
23
|
interface RunRecord {
|
|
23
24
|
status: string;
|
|
24
25
|
path: string[];
|
|
25
26
|
startedAt: string;
|
|
26
27
|
input?: unknown;
|
|
27
28
|
nodes: RunNode[];
|
|
29
|
+
currentNode?: { nodeId?: unknown; startedAt?: unknown };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Node times come only from the ledger (GRAPH-NODE-TIMES); older ledgers have none and the screen says so. */
|
|
33
|
+
const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
34
|
+
function isoTime(value: unknown): string | undefined {
|
|
35
|
+
return typeof value === 'string' && ISO_TIME.test(value) && Number.isFinite(Date.parse(value)) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nodeTimes(node: RunNode): { startedAt?: string; endedAt?: string } {
|
|
39
|
+
const startedAt = isoTime(node.startedAt);
|
|
40
|
+
const endedAt = isoTime(node.endedAt);
|
|
41
|
+
return { ...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The running node has no record yet — show it with its start time when the ledger names it as the path tail. */
|
|
45
|
+
function runningNode(run: RunRecord, recorded: number): Array<{ nodeId: string; ok: null; summary: string; startedAt: string }> {
|
|
46
|
+
const current = run.currentNode;
|
|
47
|
+
const startedAt = isoTime(current?.startedAt);
|
|
48
|
+
if (run.status !== 'running' || !startedAt || typeof current?.nodeId !== 'string' ||
|
|
49
|
+
run.path.length !== recorded + 1 || run.path.at(-1) !== current.nodeId) return [];
|
|
50
|
+
return [{ nodeId: redactSecretText(current.nodeId), ok: null, summary: '', startedAt }];
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
function recordAt(path: string): RunRecord | null {
|
|
@@ -66,6 +89,91 @@ function runVersion(input: unknown): string | null {
|
|
|
66
89
|
return typeof version === 'string' && VERSION.test(version) ? version : null;
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
/** Facts away mode prints for a node — only built when asked, so the HTTP shape stays as it was. */
|
|
93
|
+
export interface ReleaseNodeFacts { verdict?: string; introduced?: number; preexisting?: number; waiver?: string }
|
|
94
|
+
export interface ReleaseRunNodeView {
|
|
95
|
+
nodeId: string; ok: boolean | null; summary: string; startedAt?: string; endedAt?: string; facts?: ReleaseNodeFacts;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parsedOutput(output: unknown): Record<string, unknown> | null {
|
|
99
|
+
let value = output;
|
|
100
|
+
if (typeof value === 'string') {
|
|
101
|
+
const text = value;
|
|
102
|
+
try { value = JSON.parse(text) as unknown; }
|
|
103
|
+
catch {
|
|
104
|
+
const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
105
|
+
try { value = JSON.parse(last ?? '') as unknown; } catch { return null; }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function releaseNodeFacts(output: unknown): ReleaseNodeFacts {
|
|
112
|
+
const value = parsedOutput(output);
|
|
113
|
+
if (!value) return {};
|
|
114
|
+
const verdict = typeof value.verdict === 'string' ? value.verdict : typeof value.outcome === 'string' ? value.outcome : undefined;
|
|
115
|
+
const introduced = Array.isArray(value.introduced) ? value.introduced.length : typeof value.introduced === 'number' ? value.introduced : undefined;
|
|
116
|
+
const preexisting = typeof value.preexisting === 'number' ? value.preexisting : undefined;
|
|
117
|
+
const waiver = typeof value.waiver === 'string' && value.waiver.trim() ? redactSecretText(value.waiver).slice(0, 200) : undefined;
|
|
118
|
+
return {
|
|
119
|
+
...(verdict ? { verdict: redactSecretText(verdict).slice(0, 40) } : {}),
|
|
120
|
+
...(introduced !== undefined ? { introduced } : {}),
|
|
121
|
+
...(preexisting !== undefined ? { preexisting } : {}),
|
|
122
|
+
...(waiver ? { waiver } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export interface ReleaseRunView {
|
|
126
|
+
runId: string; status: string; startedAt: string; version: string | null; path: string[]; nodes: ReleaseRunNodeView[];
|
|
127
|
+
/** GATE-LIVE-OBS — 지금 gate 노드에 있는 런에만 싣는다(발행 원장 `gate-logs/cut/shards.json`). 파일이 없으면 생략. */
|
|
128
|
+
gateShards?: GateShardsFile & { summary: GateShardsSummary };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 도는 gate 노드의 조각 표 — 다른 판의 파일을 붙이지 않게 version 이 같을 때만. */
|
|
132
|
+
function gateShardsFor(version: string | null, status: string, nodes: ReleaseRunNodeView[], ledgerRoot: string, now: number):
|
|
133
|
+
ReleaseRunView['gateShards'] | undefined {
|
|
134
|
+
if (!version || status !== 'running' || !nodes.some((node) => node.nodeId === 'gate' && node.ok === null)) return undefined;
|
|
135
|
+
const file = readGateShards(gateShardsPath(version, ledgerRoot));
|
|
136
|
+
if (!file || file.version !== version) return undefined;
|
|
137
|
+
return { ...file, shards: file.shards.map((shard) => shard.waitReason ? { ...shard, waitReason: redactSecretText(shard.waitReason) } : shard),
|
|
138
|
+
summary: summarizeShards(file, now) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The release run ledger as `/v1/ops/release/runs` serves it (newest 20) — shared with Telegram /release and away mode
|
|
142
|
+
* so every surface reads the same verdicts. `'unavailable'` = the directory exists but cannot be read. */
|
|
143
|
+
export function readReleaseRuns(version: string | null = null,
|
|
144
|
+
dir: string = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop'),
|
|
145
|
+
opts: { facts?: boolean; ledgerRoot?: string; now?: number } = {}): ReleaseRunView[] | 'unavailable' {
|
|
146
|
+
let filenames: string[];
|
|
147
|
+
try {
|
|
148
|
+
filenames = readdirSync(dir, { withFileTypes: true })
|
|
149
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json')
|
|
150
|
+
&& IDENTIFIER.test(entry.name.slice(0, -5)))
|
|
151
|
+
.map((entry) => entry.name);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
155
|
+
return 'unavailable';
|
|
156
|
+
}
|
|
157
|
+
return filenames.flatMap((filename) => {
|
|
158
|
+
const run = recordAt(join(dir, filename));
|
|
159
|
+
if (!run) return [];
|
|
160
|
+
const v = runVersion(run.input);
|
|
161
|
+
if (version !== null && v !== version) return [];
|
|
162
|
+
const nodes: ReleaseRunNodeView[] = [...run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
|
|
163
|
+
nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
|
|
164
|
+
summary: nodeSummary(node.output), ...nodeTimes(node),
|
|
165
|
+
...(opts.facts ? { facts: releaseNodeFacts(node.output) } : {}),
|
|
166
|
+
})), ...runningNode(run, run.nodes.length)];
|
|
167
|
+
const gateShards = gateShardsFor(v, run.status, nodes, opts.ledgerRoot ?? releaseLedgerRoot(), opts.now ?? Date.now());
|
|
168
|
+
return [{
|
|
169
|
+
runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
|
|
170
|
+
startedAt: redactSecretText(run.startedAt), version: v,
|
|
171
|
+
path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
|
|
172
|
+
nodes, ...(gateShards ? { gateShards } : {}),
|
|
173
|
+
}];
|
|
174
|
+
}).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
|
|
175
|
+
}
|
|
176
|
+
|
|
69
177
|
/** The sole data gate for every read-only /v1/ops endpoint. */
|
|
70
178
|
export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
|
|
71
179
|
const url = new URL(req.url);
|
|
@@ -103,33 +211,8 @@ export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Re
|
|
|
103
211
|
}
|
|
104
212
|
if (pathname === '/v1/ops/release/runs') {
|
|
105
213
|
if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
filenames = readdirSync(dir, { withFileTypes: true })
|
|
110
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.json')
|
|
111
|
-
&& IDENTIFIER.test(entry.name.slice(0, -5)))
|
|
112
|
-
.map((entry) => entry.name);
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return served(jsonResponse([]));
|
|
116
|
-
return jsonResponse({ error: 'runs-unavailable' }, 500);
|
|
117
|
-
}
|
|
118
|
-
const runs = filenames.flatMap((filename) => {
|
|
119
|
-
const run = recordAt(join(dir, filename));
|
|
120
|
-
if (!run) return [];
|
|
121
|
-
const v = runVersion(run.input);
|
|
122
|
-
if (version !== null && v !== version) return [];
|
|
123
|
-
return [{
|
|
124
|
-
runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
|
|
125
|
-
startedAt: redactSecretText(run.startedAt), version: v,
|
|
126
|
-
path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
|
|
127
|
-
nodes: run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
|
|
128
|
-
nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
|
|
129
|
-
summary: nodeSummary(node.output),
|
|
130
|
-
})),
|
|
131
|
-
}];
|
|
132
|
-
}).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
|
|
214
|
+
const runs = readReleaseRuns(version);
|
|
215
|
+
if (runs === 'unavailable') return jsonResponse({ error: 'runs-unavailable' }, 500);
|
|
133
216
|
return served(jsonResponse(runs));
|
|
134
217
|
}
|
|
135
218
|
// WHATWG URL normalizes a literal runs/../nodes/... before routing.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { canonicalSeatId, openMsgStore, type Message, type MsgStore } from '../../msg/msg-store.js';
|
|
2
|
+
import { jsonResponse } from './json-response.js';
|
|
3
|
+
|
|
4
|
+
function ownerNotices(store: Pick<MsgStore, 'list'>): Message[] {
|
|
5
|
+
const notices: Message[] = [];
|
|
6
|
+
let after = 0;
|
|
7
|
+
let batch: Message[];
|
|
8
|
+
do {
|
|
9
|
+
batch = store.list('CEO', after, 1000);
|
|
10
|
+
notices.push(...batch.filter(message => message.from !== 'CEO'));
|
|
11
|
+
if (batch.length) after = batch[batch.length - 1]!.id;
|
|
12
|
+
} while (batch.length === 1000);
|
|
13
|
+
return notices;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const OPS_BOARD_PATH = '/v1/ops-board/notices';
|
|
17
|
+
|
|
18
|
+
export interface OpsBoardDeps {
|
|
19
|
+
authorize: (request: Request) => boolean;
|
|
20
|
+
openStore?: () => Pick<MsgStore, 'list' | 'post' | 'close'>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Owner reads the CEO inbox without acknowledging it; replies are addressed to the original sender. */
|
|
24
|
+
export async function handleOpsBoard(req: Request, deps: OpsBoardDeps): Promise<Response> {
|
|
25
|
+
if (!deps.authorize(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
26
|
+
if (req.method !== 'GET' && req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
27
|
+
const store = (deps.openStore ?? openMsgStore)();
|
|
28
|
+
try {
|
|
29
|
+
if (req.method === 'GET') {
|
|
30
|
+
const notices = ownerNotices(store);
|
|
31
|
+
return jsonResponse({ notices });
|
|
32
|
+
}
|
|
33
|
+
let body: unknown;
|
|
34
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
35
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
36
|
+
const input = body as Record<string, unknown>;
|
|
37
|
+
if (!Number.isSafeInteger(input.noticeId) || (input.noticeId as number) < 1
|
|
38
|
+
|| typeof input.body !== 'string' || !input.body.trim() || input.body.length > 3000
|
|
39
|
+
|| Object.keys(input).some(key => key !== 'noticeId' && key !== 'body')) return jsonResponse({ error: 'bad_request' }, 400);
|
|
40
|
+
const notice: Message | undefined = ownerNotices(store).find(message => message.id === input.noticeId);
|
|
41
|
+
if (!notice) return jsonResponse({ error: 'not-found' }, 404);
|
|
42
|
+
const recipient = canonicalSeatId(notice.from);
|
|
43
|
+
const reply = store.post({ from: 'CEO', to: recipient, body: input.body.trim(), kind: 'reply' });
|
|
44
|
+
return jsonResponse({ reply }, 201);
|
|
45
|
+
} finally {
|
|
46
|
+
store.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/nexus/index.ts
CHANGED
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
type DailyReflectionSchedulerHandle,
|
|
137
137
|
} from '../notes/daily-reflection-scheduler.js';
|
|
138
138
|
import { type ScheduleRunnerHandle } from '../domains/schedule-runner.js';
|
|
139
|
+
import { startWatchDefaultScheduler } from '../domains/watch-default-scheduler.js';
|
|
139
140
|
import { resolvePwaStaticDir } from './static-dir-resolve.js';
|
|
140
141
|
import {
|
|
141
142
|
createHitlPendingCallbacks,
|
|
@@ -1310,6 +1311,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
1310
1311
|
// the daily-reflection scheduler regardless of which branch booted
|
|
1311
1312
|
// it. Tests with detachForTesting set leave this undefined.
|
|
1312
1313
|
let dailyReflectionScheduler: DailyReflectionSchedulerHandle | undefined;
|
|
1314
|
+
let watchDefaultScheduler: ReturnType<typeof startWatchDefaultScheduler> | undefined;
|
|
1313
1315
|
// RFC #2161 FU A8 (2026-05-11) — discovery cron via NEXUS. Dormant
|
|
1314
1316
|
// by default (opt-in via `ELANOUS_DISCOVERY_CRON_INTERVAL_MS`); when
|
|
1315
1317
|
// configured, fires runDiscovery every interval and pushes the
|
|
@@ -3198,6 +3200,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
3198
3200
|
// schedule migrate 로 이관.) 롤백: 아래 한 줄 복원.
|
|
3199
3201
|
// try { scheduleRunner = startScheduleRunner(); } catch { /* fail-soft */ }
|
|
3200
3202
|
void scheduleRunner; // 은퇴(undefined 유지)
|
|
3203
|
+
try { watchDefaultScheduler = startWatchDefaultScheduler(); }
|
|
3204
|
+
catch (error) { debug.log('watch.default', 'failed', { stage: 'boot', reason: error instanceof Error ? error.message : String(error) }); }
|
|
3201
3205
|
}
|
|
3202
3206
|
}
|
|
3203
3207
|
|
|
@@ -3335,6 +3339,9 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
|
|
|
3335
3339
|
}
|
|
3336
3340
|
// S2 (2026-07-07) · stop the schedule runner so its node-cron tasks +
|
|
3337
3341
|
// reconcile interval don't keep firing/holding the loop past shutdown.
|
|
3342
|
+
if (watchDefaultScheduler) {
|
|
3343
|
+
try { watchDefaultScheduler.stop(); } catch { /* swallow */ }
|
|
3344
|
+
}
|
|
3338
3345
|
if (scheduleRunner) {
|
|
3339
3346
|
try { scheduleRunner.stop(); } catch { /* swallow */ }
|
|
3340
3347
|
}
|