elanous 0.2.19 → 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.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/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.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/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
- 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-7c058069c5e7fa63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
- 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-db03ae3589a81c9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
- 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-31d5b875760bb62d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
- 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-0fd895fab9b62c1b.js +1 -0
- 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-34882c84f84021c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/demo/editor-recipes.yaml +22 -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/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +2 -1
- package/scripts/release-loop/gate-node.ts +296 -25
- package/scripts/release-loop/gate-timing.ts +116 -7
- 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 +4 -1
- package/scripts/release-loop/unattended-release.ts +10 -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/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/release-watch.ts +20 -3
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/release-cli.ts +20 -5
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/tasks-cli.ts +94 -8
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
- package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +11 -1
- package/src/harness/harness-cli-command.ts +419 -52
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +62 -0
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +11 -1
- package/src/index.ts +69 -6
- package/src/maturity/feature-maturity.ts +2 -2
- 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 +38 -3
- package/src/nexus/api/loop-edges.ts +4 -2
- package/src/nexus/api/ops-api.ts +22 -7
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/feature-store.ts +14 -11
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/release-schedule.ts +6 -2
- 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/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/pr-preview.ts +84 -16
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/orchestrator.ts +218 -5
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -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/board.ts +1 -1
- package/src/task-agent/card-evidence.ts +99 -10
- package/src/task-agent/judge.ts +7 -0
- package/src/task-agent/live-moves.ts +120 -20
- package/src/task-agent/mission.ts +1 -1
- package/src/task-agent/shadow.ts +10 -1
- package/src/task-agent/task-hand.ts +7 -2
- package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
- 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 +93 -10
- package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
- package/src/telegram-commands.ts +41 -0
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/user-config.ts +56 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
- /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// LOOPCHECK-FENCE (0.2.20): an hq-fence cron execution is one process, not a loop.
|
|
2
|
+
// Each wrapped run leaves its rc in a per-role streak file; the wrapper's owner alert
|
|
3
|
+
// raises a seat request only when the same role failed in consecutive runs, keyed per
|
|
4
|
+
// role so the seat request journal holds at most one `loopcheck:hq-fence:<role>` per day.
|
|
5
|
+
// No loops/ import here: hq.ts records through this module and loops/registry imports hq.ts.
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { Database } from 'bun:sqlite';
|
|
9
|
+
import { debug } from '../debug/log.js';
|
|
10
|
+
|
|
11
|
+
/** The role is the file name; `lastAt` is shown in the owner alert so TC sees when the streak last grew. */
|
|
12
|
+
export interface FenceOutcome { consecutiveFailures: number; lastRc: number; lastAt: string }
|
|
13
|
+
|
|
14
|
+
/** Two failures in a row: one rc=1 from a job that recovers on the next fire is not a fence fault. */
|
|
15
|
+
export const FENCE_FAILURE_STREAK = 2;
|
|
16
|
+
|
|
17
|
+
const safeRole = (role: string): string => role.replace(/[^\w.-]/g, '_') || 'unknown';
|
|
18
|
+
|
|
19
|
+
export function fenceOutcomePath(dir: string, role: string): string {
|
|
20
|
+
return join(dir, `${safeRole(role)}.json`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Default directory next to the host-local lease view (`<config>/hq/fence-outcomes`). */
|
|
24
|
+
export function fenceOutcomeDir(localPath: string): string {
|
|
25
|
+
return join(dirname(localPath), 'fence-outcomes');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readFenceOutcome(dir: string, role: string): FenceOutcome | null {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(fenceOutcomePath(dir, role), 'utf8')) as Partial<FenceOutcome>;
|
|
31
|
+
return typeof parsed.consecutiveFailures === 'number' ? parsed as FenceOutcome : null;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
34
|
+
debug.log('hq.fence-outcome', 'read-failed', { role, error: String(error) });
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** rc 0 resets the streak; any other rc extends it. A skipped (not-holder) run is not recorded.
|
|
40
|
+
* Overlapping runs of one role serialize the read-modify-write on an SQLite lock (released even on SIGKILL). */
|
|
41
|
+
export function recordFenceOutcome(dir: string, role: string, rc: number, now: Date = new Date()): FenceOutcome {
|
|
42
|
+
const path = fenceOutcomePath(dir, role);
|
|
43
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
44
|
+
const lock = new Database(`${path}.lock.sqlite`);
|
|
45
|
+
try {
|
|
46
|
+
lock.exec('PRAGMA busy_timeout = 10000');
|
|
47
|
+
lock.exec('BEGIN EXCLUSIVE');
|
|
48
|
+
try {
|
|
49
|
+
const previous = readFenceOutcome(dir, role);
|
|
50
|
+
const outcome: FenceOutcome = { consecutiveFailures: rc === 0 ? 0 : (previous?.consecutiveFailures ?? 0) + 1, lastRc: rc, lastAt: now.toISOString() };
|
|
51
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
52
|
+
writeFileSync(tmp, JSON.stringify(outcome));
|
|
53
|
+
renameSync(tmp, path);
|
|
54
|
+
lock.exec('COMMIT');
|
|
55
|
+
return outcome;
|
|
56
|
+
} catch (error) { lock.exec('ROLLBACK'); throw error; }
|
|
57
|
+
} finally { lock.close(); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `hq-fence: fence failed (rc=1, role=seat-loop)` → `seat-loop`; other wrapper reasons have no role. */
|
|
61
|
+
export function fenceAlertRole(reason: string): string | null {
|
|
62
|
+
return /\brole=([\w.-]+)/.exec(reason)?.[1] ?? null;
|
|
63
|
+
}
|
package/src/hq/fence-wrapper.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { chmodSync, constants, copyFileSync, lstatSync, mkdirSync, mkdtempSync,
|
|
|
3
3
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { createPatch } from 'diff';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { notifyOwners } from '../loops/checker.js';
|
|
8
|
+
import { FENCE_FAILURE_STREAK, fenceAlertRole, readFenceOutcome } from './fence-outcomes.js';
|
|
6
9
|
|
|
7
10
|
const quote = (value: string): string => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
8
11
|
|
|
@@ -121,3 +124,33 @@ export function installHqFenceWrapper(configDir: string, body: string, yes: bool
|
|
|
121
124
|
} finally { rmSync(tempDir, { recursive: true, force: true }); }
|
|
122
125
|
return { path, preview, ...(backup ? { backup } : {}) };
|
|
123
126
|
}
|
|
127
|
+
|
|
128
|
+
export interface FenceAlertDeps { root: string; outcomeDir: string; now?: Date }
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The wrapper's owner alert. Returns the number of seat requests queued (0 or 1).
|
|
132
|
+
* - `fence failed (rc, role)`: queued only when that role's streak reached FENCE_FAILURE_STREAK
|
|
133
|
+
* (an unknown streak is not read as «first failure» — it is queued, still capped per role/day).
|
|
134
|
+
* - Any other wrapper failure (entry missing, bun missing, role missing): queued under `hq-fence:wrapper`.
|
|
135
|
+
*/
|
|
136
|
+
export function hqFenceAlert(reason: string, deps: FenceAlertDeps): number {
|
|
137
|
+
const now = deps.now ?? new Date();
|
|
138
|
+
const fenceFailure = /fence failed \(rc=/.test(reason);
|
|
139
|
+
const role = fenceAlertRole(reason);
|
|
140
|
+
let streak: number | null = null;
|
|
141
|
+
let lastAt: string | undefined;
|
|
142
|
+
if (fenceFailure && role) {
|
|
143
|
+
const outcome = readFenceOutcome(deps.outcomeDir, role);
|
|
144
|
+
streak = outcome?.consecutiveFailures ?? null;
|
|
145
|
+
lastAt = outcome?.lastAt;
|
|
146
|
+
if (streak !== null && streak < FENCE_FAILURE_STREAK) {
|
|
147
|
+
debug.log('hq.fence-wrapper', 'alert-suppressed', { role, streak, threshold: FENCE_FAILURE_STREAK, reason });
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const id = `hq-fence:${fenceFailure && role ? role : 'wrapper'}`;
|
|
152
|
+
const text = streak !== null ? `${reason} · ${streak} consecutive runs (last ${lastAt ?? 'unknown'})` : reason;
|
|
153
|
+
const added = notifyOwners([{ id, owner: 'TC', enabled: true, registered: true, state: 'failing', reason: text }], { root: deps.root, now });
|
|
154
|
+
debug.log('hq.fence-wrapper', added ? 'alert-queued' : 'alert-deduplicated', { id, role, streak, reason });
|
|
155
|
+
return added;
|
|
156
|
+
}
|
package/src/hq/hq.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { debug } from '../debug/log.js';
|
|
|
10
10
|
import { DEFAULT_NEXUS_HTTP_PORT } from '../nexus/default-port.js';
|
|
11
11
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
12
12
|
import { getUserConfig, type HqConfig, type HqFenceRole } from '../user-config.js';
|
|
13
|
+
import { fenceOutcomeDir, recordFenceOutcome } from './fence-outcomes.js';
|
|
13
14
|
import { promoteHq, proposeHqPromotion, writeOpSeatRequest } from './promote.js';
|
|
14
15
|
import type { HqOpRequestWriter, PromoteResult } from './promote.js';
|
|
15
16
|
import {
|
|
@@ -42,6 +43,8 @@ export interface HqDeps {
|
|
|
42
43
|
/** Holder/standby views and the arbiter's check use this tailnet probe — never ssh (OP 10-04 09:04). */
|
|
43
44
|
probe?: HostProbe;
|
|
44
45
|
localPath?: string;
|
|
46
|
+
/** Per-role streak files of wrapped-run rc (LOOPCHECK-FENCE · default next to localPath). */
|
|
47
|
+
outcomeDir?: string;
|
|
45
48
|
/** Host-local identity, deliberately outside the replicated config/state directory. */
|
|
46
49
|
hostPath?: string;
|
|
47
50
|
/** Seen-generation marker (default hq.seenGenerationFile · ~/.elanous-hq/seen-generation). */
|
|
@@ -391,5 +394,12 @@ export function hqFenceRun(role: HqFenceRole, command: string[], deps: HqDeps =
|
|
|
391
394
|
const [cmd, ...args] = command;
|
|
392
395
|
if (!cmd) throw new Error('hq fence: command required after --');
|
|
393
396
|
const r = spawnSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ELANOUS_HQ_GENERATION: String(decided.generation ?? '') } });
|
|
394
|
-
|
|
397
|
+
const rc = r.status ?? 1;
|
|
398
|
+
// LOOPCHECK-FENCE: the wrapper's alert reads this streak, so a lone failure is not a seat request.
|
|
399
|
+
try {
|
|
400
|
+
const dir = deps.outcomeDir ?? fenceOutcomeDir(deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'));
|
|
401
|
+
const outcome = recordFenceOutcome(dir, role, rc, new Date(deps.now?.() ?? Date.now()));
|
|
402
|
+
if (rc !== 0) debug.log('hq.fence', 'run-failed', { role, rc, consecutiveFailures: outcome.consecutiveFailures });
|
|
403
|
+
} catch (error) { try { debug.log('hq.fence', 'outcome-record-failed', { role, rc, error: String(error) }); } catch { /* fail-soft */ } }
|
|
404
|
+
return rc;
|
|
395
405
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SelfDevRunParticipant, SelfDevRunState } from './self-dev/run-stor
|
|
|
7
7
|
import { applyConfigDirFlagFromArgv } from './cli/config-dir-flag.js';
|
|
8
8
|
import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
|
|
9
9
|
import { registerDocsCommands } from './cli/docs-cli.js';
|
|
10
|
+
import { registerRetroCommands } from './cli/retro-cli.js';
|
|
10
11
|
import { registerA2ACommands } from './cli/a2a-cli.js';
|
|
11
12
|
import { registerPluginCommands } from './cli/plugin-cli.js';
|
|
12
13
|
import { registerMakeCommand } from './cli/make-cli.js';
|
|
@@ -219,13 +220,14 @@ program.enablePositionalOptions();
|
|
|
219
220
|
|
|
220
221
|
program.command('preview <target>')
|
|
221
222
|
.description('Isolated PR/branch-head TUI + PWA preview; publish the TUI-COMFORT friction table to a PR')
|
|
222
|
-
.option('--scenario <id>', 'TUI-COMFORT scenario (default S2)', 'S2')
|
|
223
|
+
.option('--scenario <id>', 'TUI-COMFORT scenario IDs (comma-separated; default S2)', 'S2')
|
|
223
224
|
.option('--flag <path>', 'Enable an existing config feature flag in main-based preview (repeatable)', (value: string, all: string[]) => [...all, value], [] as string[])
|
|
225
|
+
.option('--bundle-pr <number>', 'Merge PR head into isolated main preview (repeatable; no flags)', (value: string, all: number[]) => [...all, Number(value)], [] as number[])
|
|
224
226
|
.option('--comment-pr <number>', 'PR to receive a branch/main preview report', (value: string) => Number(value))
|
|
225
|
-
.action(async (target: string, opts: { scenario: string; flag: string[]; commentPr?: number }) => {
|
|
227
|
+
.action(async (target: string, opts: { scenario: string; flag: string[]; bundlePr: number[]; commentPr?: number }) => {
|
|
226
228
|
try {
|
|
227
229
|
const { runPrPreview } = await import('./self-dev/pr-preview.js');
|
|
228
|
-
const result = await runPrPreview({ target, repoRoot: process.cwd(), scenario: opts.scenario, flags: opts.flag, ...(opts.commentPr === undefined ? {} : { commentPr: opts.commentPr }) });
|
|
230
|
+
const result = await runPrPreview({ target, repoRoot: process.cwd(), scenario: opts.scenario, flags: opts.flag, bundlePrs: opts.bundlePr, ...(opts.commentPr === undefined ? {} : { commentPr: opts.commentPr }) });
|
|
229
231
|
console.log(`preview ${result.status} · ${result.head} · PR #${result.prNumber} · comment posted`);
|
|
230
232
|
if (result.status !== 'passed') process.exitCode = 1;
|
|
231
233
|
} catch (error) {
|
|
@@ -1059,10 +1061,12 @@ fenceWrapperCmd.command('install').description('래퍼 제안 또는 백업 후
|
|
|
1059
1061
|
fenceWrapperCmd.command('alert <reason>').description('HQ 울타리 실패를 기존 루프 소유자 요청 원장에 적재')
|
|
1060
1062
|
.action(async (reason: string) => {
|
|
1061
1063
|
try {
|
|
1062
|
-
|
|
1064
|
+
// LOOPCHECK-FENCE: keyed per role (not per pid) and only after consecutive failures of that role.
|
|
1065
|
+
const { fenceOutcomeDir } = await import('./hq/fence-outcomes.js');
|
|
1066
|
+
const { hqFenceAlert } = await import('./hq/fence-wrapper.js');
|
|
1063
1067
|
const { getElanousConfigDir } = await import('./elanous-config-dir.js');
|
|
1064
|
-
|
|
1065
|
-
|
|
1068
|
+
const root = getElanousConfigDir();
|
|
1069
|
+
hqFenceAlert(reason, { root, outcomeDir: fenceOutcomeDir(_joinPath(root, 'hq', 'local.json')) });
|
|
1066
1070
|
} catch (error) { console.error(`hq fence-wrapper alert: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
|
|
1067
1071
|
});
|
|
1068
1072
|
hqCmd.command('fence-audit').description('운영 crontab 의 HQ 울타리 밖 쓰기 잡 목록과 감싼 줄 제안(읽기 전용)')
|
|
@@ -1905,6 +1909,7 @@ program
|
|
|
1905
1909
|
});
|
|
1906
1910
|
|
|
1907
1911
|
registerAutopilotCommands(program);
|
|
1912
|
+
registerRetroCommands(program);
|
|
1908
1913
|
|
|
1909
1914
|
// ── memory ──
|
|
1910
1915
|
const memCmd = program.command('memory').description('Persistent memories injected into every chat turn (user / feedback / project / reference)');
|
|
@@ -7679,6 +7684,11 @@ configCmd
|
|
|
7679
7684
|
.description('Print all config or one dotted path, e.g. llm.provider (secrets redacted by default)')
|
|
7680
7685
|
.option('--reveal', 'Print unredacted output and record the reveal')
|
|
7681
7686
|
.action(async (path: string | undefined, opts: { reveal?: boolean }) => {
|
|
7687
|
+
const { resolveCurrentInstance } = await import('./instance/current.js');
|
|
7688
|
+
const instance = resolveCurrentInstance();
|
|
7689
|
+
if (instance.kind === 'test' && instance.layer === 'tree-derived') {
|
|
7690
|
+
console.error('⚠️ test 우주의 config를 읽습니다 — 운영 값은 --config-dir ~/.elanous');
|
|
7691
|
+
}
|
|
7682
7692
|
const cfg = getUserConfig() as unknown as Record<string, unknown>;
|
|
7683
7693
|
const value = path ? getConfigPath(cfg, path) : cfg;
|
|
7684
7694
|
if (value === undefined) {
|
|
@@ -7773,6 +7783,59 @@ configCmd
|
|
|
7773
7783
|
ui.info(`unset ${path}`);
|
|
7774
7784
|
});
|
|
7775
7785
|
|
|
7786
|
+
// ── config drift / explain (CFG-STORE1 · 0.2.20 · 2026-10-08) — 읽기 전용 «유효 config ⊕ 출처» ──
|
|
7787
|
+
// 집(~/.elanous)과 파생 시험 우주(<트리>/.elanous-test) config 사본의 드리프트를 한 명령으로.
|
|
7788
|
+
// ⛔ 쓰기 0 · 못 읽음 ≠ 같음 · 비밀 값 가림. 계산 = src/cli/config-drift.ts.
|
|
7789
|
+
async function configDriftDiscover(o: { registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) {
|
|
7790
|
+
const { discoverUniverses } = await import('./cli/config-drift.js');
|
|
7791
|
+
return discoverUniverses({
|
|
7792
|
+
...(o.registry === false ? { registryPath: null } : typeof o.registry === 'string' ? { registryPath: o.registry } : {}),
|
|
7793
|
+
...(o.treeScan === false ? { treeScanRoots: [] } : o.scanRoot?.length ? { treeScanRoots: o.scanRoot } : {}),
|
|
7794
|
+
});
|
|
7795
|
+
}
|
|
7796
|
+
|
|
7797
|
+
configCmd
|
|
7798
|
+
.command('drift')
|
|
7799
|
+
.description('읽기 전용 — 운영 config 와 파생 시험 우주(.elanous-test) config 사본이 다른 키 표 (CFG-STORE1)')
|
|
7800
|
+
.option('--key <key>', '이 키(또는 그 아래)만')
|
|
7801
|
+
.option('--json', 'JSON 출력')
|
|
7802
|
+
.option('--limit <n>', '표시할 키 수 (0=전부 · 기본 50)', '50')
|
|
7803
|
+
.option('--registry <path>', '인스턴스 레지스트리 경로 (기본 ~/.elanous/logs/instances.json)')
|
|
7804
|
+
.option('--scan-root <dir...>', '명시 트리 뿌리 (기본 = config-drift.ts defaultTreeScanRoots)')
|
|
7805
|
+
.option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
|
|
7806
|
+
.option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리(자리·작업 트리)만')
|
|
7807
|
+
.option('--include-expected', 'sync-test 의 test-safe 변환(비밀 제거·발송 끔)과 _test 메타 키 차이도 드리프트로 센다')
|
|
7808
|
+
.action(async (o: { key?: string; json?: boolean; limit: string; includeExpected?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
|
|
7809
|
+
const { computeDrift, renderDrift } = await import('./cli/config-drift.js');
|
|
7810
|
+
const report = computeDrift(await configDriftDiscover(o), { key: o.key, includeExpected: o.includeExpected });
|
|
7811
|
+
debug.log('config.drift', 'computed', {
|
|
7812
|
+
key: o.key ?? null, rows: report.rows.length, prodStatus: report.prodStatus, ...report.universes,
|
|
7813
|
+
});
|
|
7814
|
+
debug.flush();
|
|
7815
|
+
console.log(o.json ? JSON.stringify(report, null, 2) : renderDrift(report, { limit: Number(o.limit) || 0 }));
|
|
7816
|
+
if (report.prodStatus !== 'ok') process.exit(1);
|
|
7817
|
+
});
|
|
7818
|
+
|
|
7819
|
+
configCmd
|
|
7820
|
+
.command('explain <key>')
|
|
7821
|
+
.description('읽기 전용 — 운영 값 ⊕ 출처 층 ⊕ 파생 시험 우주별 값 (CFG-STORE1)')
|
|
7822
|
+
.option('--json', 'JSON 출력')
|
|
7823
|
+
.option('--registry <path>', '인스턴스 레지스트리 경로')
|
|
7824
|
+
.option('--scan-root <dir...>', '명시 트리 뿌리')
|
|
7825
|
+
.option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
|
|
7826
|
+
.option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리만')
|
|
7827
|
+
.action(async (key: string, o: { json?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
|
|
7828
|
+
const { computeExplain, renderExplain } = await import('./cli/config-drift.js');
|
|
7829
|
+
const { buildUserConfig } = await import('./user-config.js');
|
|
7830
|
+
const report = computeExplain(await configDriftDiscover(o), key, {
|
|
7831
|
+
codeDefaults: () => buildUserConfig('/nonexistent/elanous-config-drift/config.json'),
|
|
7832
|
+
});
|
|
7833
|
+
debug.log('config.explain', 'computed', { key, layer: report.prod.layer, ...report.universes });
|
|
7834
|
+
debug.flush();
|
|
7835
|
+
console.log(o.json ? JSON.stringify(report, null, 2) : renderExplain(report));
|
|
7836
|
+
if (report.prod.layer === 'prod-unreadable') process.exit(1);
|
|
7837
|
+
});
|
|
7838
|
+
|
|
7776
7839
|
// ── config 격리 sync (ISO-1 · 2026-07-13) — 운영→테스트 물질화 동기화 ──
|
|
7777
7840
|
// 격리 테스트 인스턴스는 운영 config 를 공유하지 않는다(overlay 은퇴 방향).
|
|
7778
7841
|
configCmd
|
|
@@ -74,7 +74,7 @@ export const FEATURE_MATURITY = {
|
|
|
74
74
|
loops: 'beta', buzz: 'beta', harness: 'stable', self: 'beta', factcheck: 'beta',
|
|
75
75
|
provider: 'stable', 'provider:set': 'beta', 'provider:restore': 'beta',
|
|
76
76
|
'provider:rotate': 'beta', 'provider:use': 'beta', 'status-bar': 'beta',
|
|
77
|
-
schedule: 'beta', guardian: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
|
|
77
|
+
schedule: 'beta', guardian: 'beta', intake: 'beta', logs: 'stable', retro: 'beta', ad: 'beta', docs: 'beta',
|
|
78
78
|
decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
|
|
79
79
|
env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
|
|
80
80
|
config: 'stable', tasks: 'stable', brief: 'beta', notify: 'beta', away: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
@@ -160,7 +160,7 @@ export const FEATURE_MATURITY = {
|
|
|
160
160
|
ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
|
|
161
161
|
decisions: 'stable', work: 'stable', cancel: 'stable',
|
|
162
162
|
skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
|
|
163
|
-
taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
|
|
163
|
+
taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
|
|
164
164
|
brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
|
|
165
165
|
harness: 'ops',
|
|
166
166
|
// Existing commands outside the CXO four-grade menu list are owner-only.
|
|
@@ -58,12 +58,18 @@ export async function handleGraphsValidatePost(req: Request, opts: MetaApiOpts):
|
|
|
58
58
|
return jsonResponse({ error: 'bad_request', reason: 'graph (harness|workflow) and yaml (string) required' }, 400);
|
|
59
59
|
}
|
|
60
60
|
const { graph, yaml } = body as { graph: GraphKind; yaml: string };
|
|
61
|
+
const { ok, errors, ignoredKeys } = validateGraphYaml(graph, yaml);
|
|
62
|
+
debug.log('graph.kinds', 'validate', { graph, ok, errors, ignored: ignoredKeys });
|
|
63
|
+
return jsonResponse({ ok, errors, ignoredKeys }, ok ? 200 : 422);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The one validator behind `POST /v1/graphs/validate` and the graph save path. */
|
|
67
|
+
export function validateGraphYaml(graph: GraphKind, yaml: string): { ok: boolean; errors: readonly unknown[]; ignoredKeys: string[]; graphId?: string } {
|
|
61
68
|
const result = graph === 'harness' ? parseGraphTemplateYaml(yaml) : parseWorkflowYaml(yaml);
|
|
62
|
-
const errors = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).errors : (result as ReturnType<typeof parseWorkflowYaml>).issues;
|
|
69
|
+
const errors: readonly unknown[] = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).errors : (result as ReturnType<typeof parseWorkflowYaml>).issues;
|
|
63
70
|
let input: unknown;
|
|
64
71
|
try { input = parseYaml(yaml); } catch { /* The parser already reports the YAML error. */ }
|
|
65
72
|
const ignoredKeys = ignoredPaths(input, graph === 'harness' ? harnessShape : workflowShape);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
return jsonResponse({ ok, errors, ignoredKeys }, ok ? 200 : 422);
|
|
73
|
+
const graphId = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).template?.graphId : undefined;
|
|
74
|
+
return { ok: errors.length === 0, errors, ignoredKeys, ...(graphId === undefined ? {} : { graphId }) };
|
|
69
75
|
}
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
6
7
|
import { defaultGraphsDir, loadGraphTemplatesFrom } from '../../self-implement/graph-templates.js';
|
|
7
8
|
import { loadGraphTemplates } from '../../self-implement/graph-yaml.js';
|
|
9
|
+
import { validateGraphYaml } from './graph-kinds.js';
|
|
8
10
|
import { jsonResponse } from './json-response.js';
|
|
9
11
|
|
|
10
12
|
/** Declared graph ids only. A user string is never joined onto a filesystem path. */
|
|
11
13
|
const GRAPH_ID = /^[a-z0-9-]+$/;
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Sidecar names a graph directory loads specially. `recipes.yaml` next to a graph is what
|
|
17
|
+
* the graph runner executes (`cmd:` recipes), so an HTTP write to `<mine>/recipes.yaml`
|
|
18
|
+
* would be a remote command path. These ids are never created, saved or reverted, and
|
|
19
|
+
* these files are never indexed as graphs.
|
|
20
|
+
*/
|
|
21
|
+
export const RESERVED_GRAPH_IDS: ReadonlySet<string> = new Set(['recipes', 'overlay', 'overlays']);
|
|
22
|
+
|
|
23
|
+
function reserved(id: string): Response | null {
|
|
24
|
+
if (!RESERVED_GRAPH_IDS.has(id)) return null;
|
|
25
|
+
debug.log('nexus.graphs', 'rejected-reserved', { id });
|
|
26
|
+
return jsonResponse({ error: 'reserved-id', id }, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export interface GraphCatalogEntry {
|
|
14
30
|
id: string;
|
|
15
31
|
source: 'core' | 'mine';
|
|
@@ -45,6 +61,7 @@ function readDirYaml(dir: string): string[] {
|
|
|
45
61
|
function indexByDeclaredId(dir: string): Map<string, { file: string; text: string }> {
|
|
46
62
|
const found = new Map<string, { file: string; text: string }>();
|
|
47
63
|
for (const file of readDirYaml(dir)) {
|
|
64
|
+
if (RESERVED_GRAPH_IDS.has(file.replace(/\.ya?ml$/, ''))) continue;
|
|
48
65
|
let text: string;
|
|
49
66
|
try { text = readFileSync(join(dir, file), 'utf8'); }
|
|
50
67
|
catch { continue; }
|
|
@@ -125,6 +142,21 @@ function validateInTemp(text: string, expectedId: string): { ok: true } | { ok:
|
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
/** Same validator as POST /v1/graphs/validate. Invalid → 422; declared graph_id ≠ id → 400. Null = ok. */
|
|
146
|
+
function rejectGraphYaml(text: string, id: string, version?: string): Response | null {
|
|
147
|
+
const checked = validateGraphYaml('harness', text);
|
|
148
|
+
if (!checked.ok) {
|
|
149
|
+
debug.log('nexus.graphs', 'rejected-invalid', { id, ...(version ? { version } : {}), issues: checked.errors });
|
|
150
|
+
return jsonResponse({ error: 'invalid-graph', errors: checked.errors, ignoredKeys: checked.ignoredKeys }, 422);
|
|
151
|
+
}
|
|
152
|
+
if (checked.graphId !== id) {
|
|
153
|
+
const errors = [{ path: `${id}/graph_id`, message: 'graph_id 가 요청 id 와 다르다' }];
|
|
154
|
+
debug.log('nexus.graphs', 'rejected-invalid', { id, issues: errors });
|
|
155
|
+
return jsonResponse({ error: 'graph-id-mismatch', errors }, 400);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
/** Core graphs are read from the same YAML loader as the harness, never rewritten by this API. */
|
|
129
161
|
export function handleGraphsGet(pathname: string, dir = defaultGraphsDir(), deps: GraphsApiDeps = {}): Response {
|
|
130
162
|
const coreDir = deps.coreDir ?? dir;
|
|
@@ -168,6 +200,8 @@ export async function handleGraphsPut(pathname: string, req: Request, deps: Grap
|
|
|
168
200
|
if (!match) return jsonResponse({ error: 'not-found' }, 404);
|
|
169
201
|
const id = resolveId(match[1]!);
|
|
170
202
|
if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
|
|
203
|
+
const reservedId = reserved(id);
|
|
204
|
+
if (reservedId) return reservedId;
|
|
171
205
|
const coreDir = deps.coreDir ?? defaultGraphsDir();
|
|
172
206
|
const mineDir = deps.mineDir ?? mineGraphsDir();
|
|
173
207
|
const core = coreCatalog(coreDir);
|
|
@@ -178,12 +212,146 @@ export async function handleGraphsPut(pathname: string, req: Request, deps: Grap
|
|
|
178
212
|
let body: { yaml?: unknown };
|
|
179
213
|
try { body = await req.json() as { yaml?: unknown }; }
|
|
180
214
|
catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
|
|
181
|
-
if (typeof body
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
215
|
+
if (typeof body?.yaml !== 'string') return jsonResponse({ error: 'bad_request', reason: 'yaml string required' }, 400);
|
|
216
|
+
const rejected = rejectGraphYaml(body.yaml, id);
|
|
217
|
+
if (rejected) return rejected;
|
|
184
218
|
const existing = indexByDeclaredId(mineDir).get(id)!;
|
|
185
|
-
|
|
186
|
-
return jsonResponse({
|
|
219
|
+
// Writes go only to <mine>/<id>.yaml; a hand-placed file under another name is not edited over HTTP.
|
|
220
|
+
if (existing.file !== `${id}.yaml`) return jsonResponse({ error: 'file-name-mismatch', id, file: existing.file }, 409);
|
|
221
|
+
const { version, previous } = writeWithVersion(mineDir, id, existing, body.yaml);
|
|
222
|
+
debug.log('nexus.graphs', 'saved', { id, version, previous });
|
|
223
|
+
return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const VERSION_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z(?:-\d{4})?$/;
|
|
227
|
+
export const GRAPH_VERSION_CAP = 20;
|
|
228
|
+
|
|
229
|
+
function versionsDir(mineDir: string, id: string): string {
|
|
230
|
+
return join(mineDir, '.versions', id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Version ids are ISO timestamps (`:`/`.` → `-`), so name order is save order. */
|
|
234
|
+
export function listGraphVersions(mineDir: string, id: string): Array<{ version: string; savedAt: string; bytes: number }> {
|
|
235
|
+
const dir = versionsDir(mineDir, id);
|
|
236
|
+
// Only `.yaml` is ever written, read and reverted — a stray `.yml` is not a version.
|
|
237
|
+
return readDirYaml(dir)
|
|
238
|
+
.filter((file) => file.endsWith('.yaml'))
|
|
239
|
+
.map((file) => file.replace(/\.yaml$/, ''))
|
|
240
|
+
.filter((version) => VERSION_ID.test(version))
|
|
241
|
+
.sort()
|
|
242
|
+
.map((version) => {
|
|
243
|
+
const iso = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(version);
|
|
244
|
+
let bytes = 0;
|
|
245
|
+
try { bytes = statSync(join(dir, `${version}.yaml`)).size; } catch { /* listed above; raced away */ }
|
|
246
|
+
return { version, savedAt: iso ? `${iso[1]}T${iso[2]}:${iso[3]}:${iso[4]}.${iso[5]}Z` : version, bytes };
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function recordVersion(mineDir: string, id: string, text: string): string {
|
|
251
|
+
const dir = versionsDir(mineDir, id);
|
|
252
|
+
mkdirSync(dir, { recursive: true });
|
|
253
|
+
const base = new Date().toISOString().replace(/[:.]/g, '-');
|
|
254
|
+
let version = base;
|
|
255
|
+
for (let n = 1; existsSync(join(dir, `${version}.yaml`)); n += 1) version = `${base}-${String(n).padStart(4, '0')}`;
|
|
256
|
+
writeFileSync(join(dir, `${version}.yaml`), text);
|
|
257
|
+
const all = listGraphVersions(mineDir, id);
|
|
258
|
+
for (const old of all.slice(0, Math.max(0, all.length - GRAPH_VERSION_CAP))) {
|
|
259
|
+
rmSync(join(dir, `${old.version}.yaml`), { force: true });
|
|
260
|
+
}
|
|
261
|
+
return version;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Every saved state is a version. A graph with no history yet (e.g. a fresh clone)
|
|
266
|
+
* first records its current bytes, so `previous` always points at restorable bytes.
|
|
267
|
+
*/
|
|
268
|
+
function writeWithVersion(mineDir: string, id: string, existing: { file: string; text: string }, text: string): { version: string; previous: string } {
|
|
269
|
+
const history = listGraphVersions(mineDir, id);
|
|
270
|
+
const previous = history.at(-1)?.version ?? recordVersion(mineDir, id, existing.text);
|
|
271
|
+
// Version first, then an atomic replace of the current file: a crash in between leaves
|
|
272
|
+
// the old current bytes intact (they are `previous`), never bytes that no version holds.
|
|
273
|
+
const version = recordVersion(mineDir, id, text);
|
|
274
|
+
const target = join(mineDir, `${id}.yaml`);
|
|
275
|
+
const temp = join(mineDir, `.${id}.yaml.${process.pid}.tmp`);
|
|
276
|
+
writeFileSync(temp, text);
|
|
277
|
+
renameSync(temp, target);
|
|
278
|
+
return { version, previous };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mineOwnership(id: string, deps: GraphsApiDeps): { mineDir: string; existing: { file: string; text: string } } | Response {
|
|
282
|
+
const reservedId = reserved(id);
|
|
283
|
+
if (reservedId) return reservedId;
|
|
284
|
+
const coreDir = deps.coreDir ?? defaultGraphsDir();
|
|
285
|
+
const mineDir = deps.mineDir ?? mineGraphsDir();
|
|
286
|
+
const existing = indexByDeclaredId(mineDir).get(id);
|
|
287
|
+
if (!existing) {
|
|
288
|
+
const core = coreCatalog(coreDir);
|
|
289
|
+
if (core.ok && indexByDeclaredId(coreDir).has(id)) return jsonResponse({ error: 'core-read-only', id }, 403);
|
|
290
|
+
return jsonResponse({ error: 'not-found', id }, 404);
|
|
291
|
+
}
|
|
292
|
+
return { mineDir, existing };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** GET /v1/graphs/<id>/versions — only «mine» graphs have history. */
|
|
296
|
+
export function handleGraphVersionsGet(pathname: string, deps: GraphsApiDeps = {}): Response {
|
|
297
|
+
const match = /^\/v1\/graphs\/([^/]+)\/versions$/.exec(pathname);
|
|
298
|
+
if (!match) return jsonResponse({ error: 'not-found' }, 404);
|
|
299
|
+
const id = resolveId(match[1]!);
|
|
300
|
+
if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
|
|
301
|
+
const owned = mineOwnership(id, deps);
|
|
302
|
+
if (owned instanceof Response) return owned;
|
|
303
|
+
return jsonResponse({ id, versions: listGraphVersions(owned.mineDir, id) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** POST /v1/graphs/<id>/revert {version} — restores that version; the restore is itself a new version. */
|
|
307
|
+
export async function handleGraphsRevert(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
|
|
308
|
+
const match = /^\/v1\/graphs\/([^/]+)\/revert$/.exec(pathname);
|
|
309
|
+
if (!match) return jsonResponse({ error: 'not-found' }, 404);
|
|
310
|
+
const id = resolveId(match[1]!);
|
|
311
|
+
if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
|
|
312
|
+
const owned = mineOwnership(id, deps);
|
|
313
|
+
if (owned instanceof Response) return owned;
|
|
314
|
+
if (owned.existing.file !== `${id}.yaml`) return jsonResponse({ error: 'file-name-mismatch', id, file: owned.existing.file }, 409);
|
|
315
|
+
let body: { version?: unknown };
|
|
316
|
+
try { body = await req.json() as { version?: unknown }; }
|
|
317
|
+
catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
|
|
318
|
+
if (typeof body?.version !== 'string' || !VERSION_ID.test(body.version)) {
|
|
319
|
+
return jsonResponse({ error: 'bad_request', reason: 'version string required' }, 400);
|
|
320
|
+
}
|
|
321
|
+
const target = body.version;
|
|
322
|
+
// Only a listed version id is ever joined onto a path.
|
|
323
|
+
if (!listGraphVersions(owned.mineDir, id).some((entry) => entry.version === target)) {
|
|
324
|
+
return jsonResponse({ error: 'version-not-found', id, version: target }, 404);
|
|
325
|
+
}
|
|
326
|
+
const text = readFileSync(join(versionsDir(owned.mineDir, id), `${target}.yaml`), 'utf8');
|
|
327
|
+
const rejected = rejectGraphYaml(text, id, target);
|
|
328
|
+
if (rejected) return rejected;
|
|
329
|
+
const { version, previous } = writeWithVersion(owned.mineDir, id, owned.existing, text);
|
|
330
|
+
debug.log('nexus.graphs', 'reverted', { id, version, previous, restoredFrom: target });
|
|
331
|
+
return jsonResponse({ id, source: 'mine', editable: true, reverted: true, version, previous, restoredFrom: target });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** POST /v1/graphs {id, yaml} — a brand-new «mine» graph (blank canvas → save). Records version v1. */
|
|
335
|
+
export async function handleGraphsCreate(req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
|
|
336
|
+
let body: { id?: unknown; yaml?: unknown };
|
|
337
|
+
try { body = await req.json() as { id?: unknown; yaml?: unknown }; }
|
|
338
|
+
catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
|
|
339
|
+
const id = typeof body?.id === 'string' ? resolveId(body.id) : null;
|
|
340
|
+
if (!id) return jsonResponse({ error: 'bad_request', reason: 'id must match [a-z0-9-]' }, 400);
|
|
341
|
+
const reservedId = reserved(id);
|
|
342
|
+
if (reservedId) return reservedId;
|
|
343
|
+
if (typeof body.yaml !== 'string') return jsonResponse({ error: 'bad_request', reason: 'yaml string required' }, 400);
|
|
344
|
+
const coreDir = deps.coreDir ?? defaultGraphsDir();
|
|
345
|
+
const mineDir = deps.mineDir ?? mineGraphsDir();
|
|
346
|
+
if (indexByDeclaredId(coreDir).has(id)) return jsonResponse({ error: 'core-read-only', id }, 403);
|
|
347
|
+
if (indexByDeclaredId(mineDir).has(id) || existsSync(join(mineDir, `${id}.yaml`))) return jsonResponse({ error: 'exists', id }, 409);
|
|
348
|
+
const rejected = rejectGraphYaml(body.yaml, id);
|
|
349
|
+
if (rejected) return rejected;
|
|
350
|
+
mkdirSync(mineDir, { recursive: true });
|
|
351
|
+
writeFileSync(join(mineDir, `${id}.yaml`), body.yaml, { flag: 'wx' });
|
|
352
|
+
const version = recordVersion(mineDir, id, body.yaml);
|
|
353
|
+
debug.log('nexus.graphs', 'saved', { id, version, created: true });
|
|
354
|
+
return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous: null }, 201);
|
|
187
355
|
}
|
|
188
356
|
|
|
189
357
|
/** Copy core bytes into «my graphs», changing only the declared graph_id. */
|
|
@@ -198,6 +366,8 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
|
|
|
198
366
|
if (typeof body.newId !== 'string' || !GRAPH_ID.test(body.newId) || body.newId === id) {
|
|
199
367
|
return jsonResponse({ error: 'bad_request', reason: 'newId must match [a-z0-9-]' }, 400);
|
|
200
368
|
}
|
|
369
|
+
const reservedId = reserved(body.newId);
|
|
370
|
+
if (reservedId) return reservedId;
|
|
201
371
|
const coreDir = deps.coreDir ?? defaultGraphsDir();
|
|
202
372
|
const mineDir = deps.mineDir ?? mineGraphsDir();
|
|
203
373
|
const source = indexByDeclaredId(coreDir).get(id) ?? indexByDeclaredId(mineDir).get(id);
|
|
@@ -215,7 +385,9 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
|
|
|
215
385
|
}
|
|
216
386
|
|
|
217
387
|
export async function handleGraphsMutation(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response | null> {
|
|
388
|
+
if (req.method === 'POST' && pathname === '/v1/graphs') return handleGraphsCreate(req, deps);
|
|
218
389
|
if (req.method === 'PUT' && /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname)) return handleGraphsPut(pathname, req, deps);
|
|
219
390
|
if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/clone$/.test(pathname)) return handleGraphsClone(pathname, req, deps);
|
|
391
|
+
if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/revert$/.test(pathname)) return handleGraphsRevert(pathname, req, deps);
|
|
220
392
|
return null;
|
|
221
393
|
}
|
|
@@ -24,6 +24,7 @@ import { createProject, listProjects } from '../../project/project-store.js';
|
|
|
24
24
|
import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
|
|
25
25
|
import { buildGridData, type GridDeps } from './grid.js';
|
|
26
26
|
import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
|
|
27
|
+
import { OPS_BOARD_PATH, handleOpsBoard, type OpsBoardDeps } from './ops-board-route.js';
|
|
27
28
|
import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
|
|
28
29
|
import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
|
|
29
30
|
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
@@ -166,7 +167,8 @@ import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEvents
|
|
|
166
167
|
import { handleLoopEdgesGet } from './loop-edges.js';
|
|
167
168
|
import { handleFabricPlans, type FabricPlansRouteOpts } from './fabric-plans.js';
|
|
168
169
|
import { dispatchPersonaRoute } from './personas.js';
|
|
169
|
-
import { handleMe } from './operator.js';
|
|
170
|
+
import { handleMe, operatorSignal } from './operator.js';
|
|
171
|
+
import { handleGraphRunRoute } from '../../graph-runner/graph-run-api.js';
|
|
170
172
|
import { handleOpsApi } from './ops-api.js';
|
|
171
173
|
import { handleRoleJudge } from './role-judge.js';
|
|
172
174
|
import { handleAudioStt } from './audio-stt.js';
|
|
@@ -363,7 +365,7 @@ import {
|
|
|
363
365
|
} from './workflows.js';
|
|
364
366
|
import { handleTriggersSnapshot } from './triggers.js';
|
|
365
367
|
import { handleWorkflowTemplatesList } from './workflow-templates.js';
|
|
366
|
-
import { handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
368
|
+
import { handleGraphVersionsGet, handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
|
|
367
369
|
import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
|
|
368
370
|
import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
|
|
369
371
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
@@ -484,6 +486,7 @@ export interface NexusHttpServerOpts {
|
|
|
484
486
|
/** Read-only grid measurement seam; omitted in production for live HQ and Pod pool reads. */
|
|
485
487
|
grid?: GridDeps;
|
|
486
488
|
decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
|
|
489
|
+
opsBoard?: Pick<OpsBoardDeps, 'openStore'>;
|
|
487
490
|
cardFollowup?: CardFollowupJobs;
|
|
488
491
|
skillExec?: SkillExecJobs;
|
|
489
492
|
execRequests?: ExecRequestRunner;
|
|
@@ -938,6 +941,20 @@ export async function routeRequest(
|
|
|
938
941
|
}
|
|
939
942
|
return pitch.handlePitchApi(req, opts.metaApi);
|
|
940
943
|
}
|
|
944
|
+
// CGE-RUN — 편집기 «실행»은 운영자 전용이다: 사용자 그래프가 셸 노드를 부를 수 있으므로 같은 출처 무토큰 통과는 받지 않는다
|
|
945
|
+
// (운영자 프록시 헤더 · owner bearer 일치 · owner 가 발급한 짧은 임시 토큰만 — 명시 토큰은 언제나 필요).
|
|
946
|
+
if (/^\/v1\/graphs\/[^/]+\/(?:run|runs\/[^/]+)$/.test(pathname)) {
|
|
947
|
+
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
948
|
+
const signal = operatorSignal(req, opts.metaApi);
|
|
949
|
+
const credential = bearerCredential(req, opts.metaApi);
|
|
950
|
+
if (!signal.operator || (signal.operatorSource !== 'op-proxy' && credential !== 'bearer-match' && credential !== 'temp-token')) {
|
|
951
|
+
debug.log('graphs.run', 'refused', { path: pathname, source: signal.operatorSource });
|
|
952
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
953
|
+
}
|
|
954
|
+
const routed = handleGraphRunRoute(method, pathname);
|
|
955
|
+
if (routed) return routed;
|
|
956
|
+
return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
957
|
+
}
|
|
941
958
|
|
|
942
959
|
// Default-deny for every `/v1/` path that is not on PUBLIC_ROUTES.
|
|
943
960
|
// Reuses checkAuth (same-origin exemption + constant-time bearer).
|
|
@@ -1392,6 +1409,21 @@ export async function routeRequest(
|
|
|
1392
1409
|
return (opts.cardFollowup ?? defaultCardFollowupJobs()).get(pathname.slice(CARD_FOLLOWUP_PATH.length + 1));
|
|
1393
1410
|
}
|
|
1394
1411
|
|
|
1412
|
+
if (pathname === OPS_BOARD_PATH && (method === 'GET' || method === 'POST')) {
|
|
1413
|
+
const peerAddress = (server as BunServerLike & { requestIP?: (request: Request) => { address: string } | null }).requestIP?.(req)?.address;
|
|
1414
|
+
const host = req.headers.get('host') ?? new URL(req.url).host;
|
|
1415
|
+
const tailnetPeer = peerAddress !== undefined && (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(peerAddress)
|
|
1416
|
+
|| /^fd7a:115c:a1e0:/i.test(peerAddress));
|
|
1417
|
+
const localPeer = peerAddress === '127.0.0.1' || peerAddress === '::1';
|
|
1418
|
+
const tailnetHost = /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}(?::\d+)?$/.test(host)
|
|
1419
|
+
|| /^\[fd7a:115c:a1e0:[0-9a-f:]+\](?::\d+)?$/i.test(host) || /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.ts\.net(?::\d+)?$/i.test(host);
|
|
1420
|
+
const localHost = /^(?:localhost|127\.0\.0\.1)(?::\d+)?$/.test(host) || /^\[::1\](?::\d+)?$/.test(host);
|
|
1421
|
+
if ((!tailnetPeer && !localPeer) || (!tailnetHost && !localHost)) return jsonResponse({ error: 'forbidden' }, 403);
|
|
1422
|
+
return handleOpsBoard(req, {
|
|
1423
|
+
authorize: request => !!opts.metaApi && opts.metaApi.noAuth !== true && checkAuth(request, opts.metaApi),
|
|
1424
|
+
...opts.opsBoard,
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1395
1427
|
if (method === 'GET' && pathname === DECISIONS_PATH) {
|
|
1396
1428
|
return handleDecisions(req, {
|
|
1397
1429
|
authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
@@ -1471,7 +1503,7 @@ export async function routeRequest(
|
|
|
1471
1503
|
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1472
1504
|
return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
|
|
1473
1505
|
}
|
|
1474
|
-
if ((method === 'PUT' || method === 'POST') && pathname.startsWith('/v1/graphs/')) {
|
|
1506
|
+
if ((method === 'PUT' || method === 'POST') && (pathname.startsWith('/v1/graphs/') || pathname === '/v1/graphs')) {
|
|
1475
1507
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
|
|
1476
1508
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1477
1509
|
const mutated = await handleGraphsMutation(pathname, req);
|
|
@@ -2785,6 +2817,9 @@ export async function routeRequest(
|
|
|
2785
2817
|
if (method === 'GET' && (pathname === '/v1/graphs' || /^\/v1\/graphs\/[^/]+$/.test(pathname) || /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname))) {
|
|
2786
2818
|
return handleGraphsGet(pathname);
|
|
2787
2819
|
}
|
|
2820
|
+
if (method === 'GET' && /^\/v1\/graphs\/[^/]+\/versions$/.test(pathname)) {
|
|
2821
|
+
return handleGraphVersionsGet(pathname);
|
|
2822
|
+
}
|
|
2788
2823
|
// Archon-port T2.3 (2026-05-08) — workflow GET surface.
|
|
2789
2824
|
// (POST /validate · POST /:name/run · PUT/DELETE /:name landed
|
|
2790
2825
|
// inside the mutation block above.)
|