elanous 0.2.11 → 0.2.13
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/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -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/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// HQ-REP — standby snapshots for the HQ move (mbp → node-b). OP design 10-04 08:33 (channel #23032):
|
|
3
|
+
// ① canonical ledgers every 10 min · ② observation DBs every hour · ③ secrets host-to-host only · ④ runtime is not copied.
|
|
4
|
+
// Never `cp` a live SQLite file: take `.backup` snapshots, number the generation, list sha256 for every file,
|
|
5
|
+
// then rsync into `<host>:~/.elanous-standby/<tier>/<generation>/` (not the live folder) and move `latest` only
|
|
6
|
+
// after the receiver verifies the checksums.
|
|
7
|
+
// Usage:
|
|
8
|
+
// bun scripts/hq/standby-snapshot.ts --tier core|big|obs [--push <host>] [--survival <host>,s3://bucket/prefix] [--keep N] [--dry-run] [--json]
|
|
9
|
+
// 대표 10-04 08:4x: no whole-copy of big DBs every 10 minutes — core (≤20MB) every 10 min, big and obs four times a day,
|
|
10
|
+
// and each push sends only changed blocks (`rsync --inplace` into `current/`, then a clone becomes the generation).
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir, hostname } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { debug } from '../../src/debug/log.js';
|
|
17
|
+
|
|
18
|
+
export type Tier = 'core' | 'big' | 'obs';
|
|
19
|
+
/** 대표 10-04 08:4x: only ledgers at or under this size go every 10 minutes; larger canonical DBs go four times a day. */
|
|
20
|
+
export const SMALL_LEDGER_BYTES = 20 * 1024 * 1024;
|
|
21
|
+
export interface PlanItem { rel: string; kind: 'sqlite' | 'file'; survival: boolean }
|
|
22
|
+
export interface ManifestEntry { path: string; bytes: number; sha256: string; kind: PlanItem['kind']; reused?: boolean }
|
|
23
|
+
export interface Manifest { generation: string; tier: Tier; createdAt: string; source: string; host: string; entries: ManifestEntry[] }
|
|
24
|
+
|
|
25
|
+
/** Secrets never leave the host pair through a survival copy (cloud-vm · bucket), whatever the tier. */
|
|
26
|
+
export function isSecretPath(rel: string): boolean {
|
|
27
|
+
const lower = rel.toLowerCase();
|
|
28
|
+
return /(^|\/)(secrets?|backup-key|acp-token)(\/|$|\.)/.test(lower)
|
|
29
|
+
|| /(^|\/)auth\.json/.test(lower)
|
|
30
|
+
|| /\.(p8|pem|key|env)$/.test(lower)
|
|
31
|
+
|| /(token|credential|password|session)[^/]*$/.test(lower);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ① canonical — small, consistency matters. `survival` = also kept on cloud-vm and the ops bucket.
|
|
35
|
+
const CORE_FILES: Array<[string, boolean]> = [
|
|
36
|
+
['release/features.sqlite', true], ['schedules.db', true], ['tasks/tasks.db', false], ['directives/directives.db', true],
|
|
37
|
+
['scheduler/scheduler.db', false], ['task-cards/index.db', false], ['knowledge.db', false], ['memory/knowledge.db', false],
|
|
38
|
+
['config.json', false],
|
|
39
|
+
];
|
|
40
|
+
const CORE_DIRS: Array<[string, boolean]> = [
|
|
41
|
+
['decisions', true], ['budget', false], ['seat-loop', false], ['seat-requests', false], ['test-diet', false],
|
|
42
|
+
['consult-requests', false], ['outputs', false],
|
|
43
|
+
// HQ drill round-trip marker (runbook «되돌리기»): one line written on the new HQ must come back to the old one.
|
|
44
|
+
['hq-drill', false],
|
|
45
|
+
];
|
|
46
|
+
// ② observation — large, append-only, losable.
|
|
47
|
+
const OBS_FILES = ['logs/logs.db', 'memory/surface_events.db'];
|
|
48
|
+
|
|
49
|
+
function walkFiles(root: string, dir: string): string[] {
|
|
50
|
+
if (!existsSync(join(root, dir))) return [];
|
|
51
|
+
const out: string[] = [];
|
|
52
|
+
for (const name of readdirSync(join(root, dir))) {
|
|
53
|
+
const rel = join(dir, name);
|
|
54
|
+
const st = lstatSync(join(root, rel));
|
|
55
|
+
if (st.isSymbolicLink()) continue;
|
|
56
|
+
if (st.isDirectory()) out.push(...walkFiles(root, rel));
|
|
57
|
+
else if (st.isFile() && !/\.(tmp|lock)$|-wal$|-shm$|-journal$/.test(name)) out.push(rel);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isSqlite = (rel: string) => /\.(db|sqlite|sqlite3)$/.test(rel);
|
|
63
|
+
|
|
64
|
+
export function planSnapshot(root: string, tier: Tier): PlanItem[] {
|
|
65
|
+
const items: PlanItem[] = [];
|
|
66
|
+
const add = (rel: string, survival: boolean) => {
|
|
67
|
+
if (!existsSync(join(root, rel))) return;
|
|
68
|
+
items.push({ rel, kind: isSqlite(rel) ? 'sqlite' : 'file', survival: survival && !isSecretPath(rel) });
|
|
69
|
+
};
|
|
70
|
+
if (tier === 'obs') { for (const rel of OBS_FILES) add(rel, false); return items; }
|
|
71
|
+
for (const [rel, survival] of CORE_FILES) add(rel, survival);
|
|
72
|
+
for (const [dir, survival] of CORE_DIRS) for (const rel of walkFiles(root, dir)) add(rel, survival);
|
|
73
|
+
// Trading ledgers are canonical too (OP 08:4x) — FENCE keeps execution on one host only.
|
|
74
|
+
for (const rel of walkFiles(root, 'conatus')) if (isSqlite(rel)) add(rel, false);
|
|
75
|
+
// Release ledgers: per-version checklist and manifest only — gate logs are rebuildable and large.
|
|
76
|
+
if (existsSync(join(root, 'release'))) {
|
|
77
|
+
for (const version of readdirSync(join(root, 'release'))) {
|
|
78
|
+
for (const name of ['checklist.json', 'manifest.json']) add(join('release', version, name), true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const sized = items.filter(item => !isSecretPath(item.rel) || !item.survival);
|
|
82
|
+
const big = (item: PlanItem) => statSync(join(root, item.rel)).size > SMALL_LEDGER_BYTES;
|
|
83
|
+
return tier === 'big' ? sized.filter(big) : sized.filter(item => !big(item));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sha256 = (path: string) => createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
87
|
+
|
|
88
|
+
/** Consistent copy: SQLite through `.backup` (never a live-file copy), other files byte-for-byte. */
|
|
89
|
+
/** Host-specific settings that must never ride a replicated `config.json` (OP 10-04 12:02 · drill step 5:
|
|
90
|
+
* mbp's `hq.hostName=mbp` reached node-b's promoted universe and made node-b call itself mbp). */
|
|
91
|
+
export const HOST_LOCAL_CONFIG_KEYS = ['hq'] as const;
|
|
92
|
+
|
|
93
|
+
/** `config.json` without the host-local blocks; unparsable text is copied as is (the copy must not fail on it). */
|
|
94
|
+
export function stripHostLocalConfig(text: string): string {
|
|
95
|
+
let parsed: unknown;
|
|
96
|
+
try { parsed = JSON.parse(text); } catch { return text; }
|
|
97
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return text;
|
|
98
|
+
const obj = { ...(parsed as Record<string, unknown>) };
|
|
99
|
+
let removed = false;
|
|
100
|
+
for (const key of HOST_LOCAL_CONFIG_KEYS) if (key in obj) { delete obj[key]; removed = true; }
|
|
101
|
+
return removed ? `${JSON.stringify(obj, null, 2)}\n` : text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function snapshotItem(root: string, out: string, item: PlanItem, sqlite3 = 'sqlite3'): void {
|
|
105
|
+
const dest = join(out, item.rel);
|
|
106
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
107
|
+
if (item.kind === 'sqlite') {
|
|
108
|
+
const r = spawnSync(sqlite3, [join(root, item.rel), `.backup '${dest.replace(/'/g, "''")}'`], { encoding: 'utf8' });
|
|
109
|
+
if (r.status !== 0) throw new Error(`sqlite backup failed: ${item.rel}: ${(r.stderr || '').trim()}`);
|
|
110
|
+
} else if (item.rel === 'config.json') {
|
|
111
|
+
const original = readFileSync(join(root, item.rel), 'utf8');
|
|
112
|
+
const stripped = stripHostLocalConfig(original);
|
|
113
|
+
if (stripped !== original) debug.log('hq.standby', 'config-host-local-stripped', { keys: [...HOST_LOCAL_CONFIG_KEYS] });
|
|
114
|
+
writeFileSync(dest, stripped);
|
|
115
|
+
} else cpSync(join(root, item.rel), dest);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function generationId(now = new Date()): string {
|
|
119
|
+
return now.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Build one generation in `out`. SQLite files whose source is unchanged since `previous` are reused (hard link target). */
|
|
123
|
+
export function buildGeneration(opts: { root: string; out: string; tier: Tier; previous?: Manifest | null; previousDir?: string; host?: string; now?: Date; sqlite3?: string }): Manifest {
|
|
124
|
+
const { root, out, tier } = opts;
|
|
125
|
+
mkdirSync(out, { recursive: true });
|
|
126
|
+
const entries: ManifestEntry[] = [];
|
|
127
|
+
const prev = new Map((opts.previous?.entries ?? []).map(e => [e.path, e]));
|
|
128
|
+
const prevAt = opts.previous ? Date.parse(opts.previous.createdAt) : NaN;
|
|
129
|
+
for (const item of planSnapshot(root, tier)) {
|
|
130
|
+
const old = prev.get(item.rel);
|
|
131
|
+
const mtime = statSync(join(root, item.rel)).mtimeMs;
|
|
132
|
+
if (item.kind === 'sqlite' && old && opts.previousDir && Number.isFinite(prevAt) && mtime < prevAt && existsSync(join(opts.previousDir, item.rel))) {
|
|
133
|
+
mkdirSync(dirname(join(out, item.rel)), { recursive: true });
|
|
134
|
+
cpSync(join(opts.previousDir, item.rel), join(out, item.rel));
|
|
135
|
+
entries.push({ ...old, reused: true });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
snapshotItem(root, out, item, opts.sqlite3);
|
|
139
|
+
const dest = join(out, item.rel);
|
|
140
|
+
entries.push({ path: item.rel, bytes: statSync(dest).size, sha256: sha256(dest), kind: item.kind });
|
|
141
|
+
}
|
|
142
|
+
const manifest: Manifest = { generation: generationId(opts.now), tier, createdAt: (opts.now ?? new Date()).toISOString(), source: root, host: opts.host ?? hostname().replace(/\.local$/, ''), entries };
|
|
143
|
+
writeFileSync(join(out, 'MANIFEST.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
144
|
+
writeFileSync(join(out, 'SHA256SUMS'), entries.map(e => `${e.sha256} ${e.path}`).join('\n') + '\n');
|
|
145
|
+
return manifest;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Survival subset: what cloud-vm and the bucket get. Secrets are excluded by construction. */
|
|
149
|
+
export function survivalPaths(manifest: Manifest, root: string): string[] {
|
|
150
|
+
const plan = new Map(planSnapshot(root, manifest.tier).map(i => [i.rel, i]));
|
|
151
|
+
return manifest.entries.map(e => e.path).filter(p => plan.get(p)?.survival === true && !isSecretPath(p));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function run(cmd: string, args: string[]): { ok: boolean; out: string } {
|
|
155
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
156
|
+
return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Delta push: `rsync --inplace` into a fixed `current/` (only changed blocks travel), verify, then clone it to `<gen>/`. */
|
|
160
|
+
function pushToHost(host: string, local: string, tier: Tier, gen: string, keep: number): void {
|
|
161
|
+
const base = `.elanous-standby/${tier}`;
|
|
162
|
+
run('ssh', ['-o', 'BatchMode=yes', host, `mkdir -p ${base}/current`]);
|
|
163
|
+
const r = run('rsync', ['-a', '--inplace', '--no-whole-file', '--delete', `${local}/`, `${host}:${base}/current/`]);
|
|
164
|
+
if (!r.ok) throw new Error(`rsync to ${host} failed: ${r.out}`);
|
|
165
|
+
const verify = run('ssh', ['-o', 'BatchMode=yes', host, `cd ${base}/current && (shasum -a 256 -c SHA256SUMS >/dev/null 2>&1 || sha256sum -c SHA256SUMS >/dev/null 2>&1) && echo verified`]);
|
|
166
|
+
if (verify.out !== 'verified') throw new Error(`checksum verify failed on ${host}:${base}/current`);
|
|
167
|
+
// APFS clone on macOS (no extra space until files diverge), hard links on Linux.
|
|
168
|
+
const clone = run('ssh', ['-o', 'BatchMode=yes', host, `cd ${base} && (cp -cR current ${gen} 2>/dev/null || cp -al current ${gen}) && ln -sfn ${gen} latest.new && (mv -fh latest.new latest 2>/dev/null || mv -fT latest.new latest) && ls -1d 2*Z | sort | awk -v k=${keep} '{a[NR]=$0} END {for (i=1;i<=NR-k;i++) print a[i]}' | xargs rm -rf; echo cloned`]);
|
|
169
|
+
if (!clone.out.endsWith('cloned')) throw new Error(`generation clone failed on ${host}: ${clone.out}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pushSurvival(target: string, local: string, manifest: Manifest, root: string): void {
|
|
173
|
+
const paths = survivalPaths(manifest, root);
|
|
174
|
+
const staging = `${local}.survival`;
|
|
175
|
+
rmSync(staging, { recursive: true, force: true });
|
|
176
|
+
for (const p of paths) { mkdirSync(dirname(join(staging, p)), { recursive: true }); cpSync(join(local, p), join(staging, p)); }
|
|
177
|
+
const sub = manifest.entries.filter(e => paths.includes(e.path));
|
|
178
|
+
writeFileSync(join(staging, 'SHA256SUMS'), sub.map(e => `${e.sha256} ${e.path}`).join('\n') + '\n');
|
|
179
|
+
writeFileSync(join(staging, 'MANIFEST.json'), `${JSON.stringify({ ...manifest, entries: sub }, null, 2)}\n`);
|
|
180
|
+
if (target.startsWith('s3://')) {
|
|
181
|
+
const r = run('aws', ['s3', 'sync', staging, `${target.replace(/\/$/, '')}/${manifest.generation}/`, '--sse', 'AES256', '--only-show-errors']);
|
|
182
|
+
if (!r.ok) throw new Error(`s3 sync failed: ${r.out}`);
|
|
183
|
+
} else {
|
|
184
|
+
const base = `.elanous-standby/survival`;
|
|
185
|
+
run('ssh', ['-o', 'BatchMode=yes', target, `mkdir -p ${base}`]);
|
|
186
|
+
const r = run('rsync', ['-a', `${staging}/`, `${target}:${base}/${manifest.generation}/`]);
|
|
187
|
+
if (!r.ok) throw new Error(`rsync survival to ${target} failed: ${r.out}`);
|
|
188
|
+
}
|
|
189
|
+
rmSync(staging, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (import.meta.main) {
|
|
193
|
+
const argv = process.argv.slice(2);
|
|
194
|
+
const val = (k: string) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : undefined; };
|
|
195
|
+
const tier = (val('--tier') ?? 'core') as Tier;
|
|
196
|
+
if (tier !== 'core' && tier !== 'big' && tier !== 'obs') { console.error('--tier core|big|obs'); process.exit(2); }
|
|
197
|
+
const root = process.env.ELANOUS_STATE_DIR ?? join(homedir(), '.elanous');
|
|
198
|
+
const stagingRoot = join(homedir(), '.elanous-standby-staging', tier);
|
|
199
|
+
const started = Date.now();
|
|
200
|
+
try {
|
|
201
|
+
if (argv.includes('--dry-run')) {
|
|
202
|
+
const plan = planSnapshot(root, tier);
|
|
203
|
+
console.log(JSON.stringify({ tier, items: plan.length, survival: plan.filter(i => i.survival).length, plan }, null, argv.includes('--json') ? 0 : 2));
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
mkdirSync(stagingRoot, { recursive: true });
|
|
207
|
+
// One clock reading names both the pushed folder and MANIFEST.generation — a big tier takes seconds to snapshot,
|
|
208
|
+
// and two readings drifted apart (10-04 12:13 big: folder 031304Z · manifest 031309Z · caught by standby-verify).
|
|
209
|
+
const now = new Date();
|
|
210
|
+
const gen = generationId(now);
|
|
211
|
+
const out = join(stagingRoot, 'current');
|
|
212
|
+
const manifest = buildGeneration({ root, out, tier, now });
|
|
213
|
+
const host = val('--push');
|
|
214
|
+
const keep = Number(val('--keep') ?? (tier === 'core' ? 36 : 8));
|
|
215
|
+
if (host) pushToHost(host, out, tier, gen, keep);
|
|
216
|
+
for (const target of (val('--survival') ?? '').split(',').filter(Boolean)) pushSurvival(target, out, manifest, root);
|
|
217
|
+
const bytes = manifest.entries.reduce((n, e) => n + e.bytes, 0);
|
|
218
|
+
const summary = { generation: gen, tier, files: manifest.entries.length, reused: manifest.entries.filter(e => e.reused).length, bytes, pushed: host ?? null, survival: (val('--survival') ?? '').split(',').filter(Boolean), seconds: Math.round((Date.now() - started) / 1000) };
|
|
219
|
+
debug.log('hq.standby', 'generation', summary);
|
|
220
|
+
console.log(argv.includes('--json') ? JSON.stringify(summary) : `standby ${tier} ${gen}: ${summary.files} files (${summary.reused} reused) · ${(bytes / 1e6).toFixed(1)} MB · push ${host ?? '-'} · survival ${summary.survival.join(',') || '-'} · ${summary.seconds}s`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
debug.log('hq.standby', 'failed', { tier, error: String(error).slice(0, 300) });
|
|
223
|
+
console.error(`standby ${tier} FAILED: ${String(error)}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { readFile, readlink, stat } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import type { Manifest, Tier } from './standby-snapshot.js';
|
|
9
|
+
|
|
10
|
+
export interface TierVerification {
|
|
11
|
+
tier: Tier;
|
|
12
|
+
generation: string | null;
|
|
13
|
+
ageMin: number | null;
|
|
14
|
+
host: string | null;
|
|
15
|
+
entries: number | null;
|
|
16
|
+
checked: number;
|
|
17
|
+
mismatches: number;
|
|
18
|
+
missing: number;
|
|
19
|
+
unreadable: boolean;
|
|
20
|
+
reasons: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StandbyVerification {
|
|
24
|
+
tiers: TierVerification[];
|
|
25
|
+
marker?: number | 'missing' | 'unreadable';
|
|
26
|
+
ok: boolean;
|
|
27
|
+
reasons: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VerifyOptions { root: string; tiers: Tier[]; marker?: string; maxAgeMin?: number | Partial<Record<Tier, number>>; now?: Date }
|
|
31
|
+
|
|
32
|
+
function errorCode(error: unknown): unknown {
|
|
33
|
+
return typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeFile(base: string, name: string): string | null {
|
|
37
|
+
if (!name || isAbsolute(name)) return null;
|
|
38
|
+
const path = resolve(base, name);
|
|
39
|
+
const rel = relative(base, path);
|
|
40
|
+
return !rel || rel === '..' || rel.startsWith(`..${sep}`) ? null : path;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function hashFile(path: string): Promise<string> {
|
|
44
|
+
const hash = createHash('sha256');
|
|
45
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefined, now: Date): Promise<TierVerification> {
|
|
50
|
+
const result: TierVerification = { tier, generation: null, ageMin: null, host: null, entries: null, checked: 0, mismatches: 0, missing: 0, unreadable: false, reasons: [] };
|
|
51
|
+
const base = join(root, tier);
|
|
52
|
+
try {
|
|
53
|
+
await stat(base);
|
|
54
|
+
} catch {
|
|
55
|
+
result.unreadable = true;
|
|
56
|
+
result.reasons.push(`${tier}:unreadable`);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
let target: string;
|
|
60
|
+
try {
|
|
61
|
+
target = await readlink(join(base, 'latest'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (errorCode(error) === 'ENOENT' || errorCode(error) === 'EINVAL') result.reasons.push(`${tier}:latest missing`);
|
|
64
|
+
else {
|
|
65
|
+
result.unreadable = true;
|
|
66
|
+
result.reasons.push(`${tier}:unreadable`);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
const dir = safeFile(base, target);
|
|
71
|
+
if (!dir || relative(base, dir) !== target) {
|
|
72
|
+
result.reasons.push(`${tier}:invalid latest`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
result.generation = target;
|
|
76
|
+
let manifest: Manifest;
|
|
77
|
+
let sums: string;
|
|
78
|
+
try {
|
|
79
|
+
[manifest, sums] = await Promise.all([
|
|
80
|
+
readFile(join(dir, 'MANIFEST.json'), 'utf8').then(text => JSON.parse(text) as Manifest),
|
|
81
|
+
readFile(join(dir, 'SHA256SUMS'), 'utf8'),
|
|
82
|
+
]);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof SyntaxError) result.reasons.push(`${tier}:invalid manifest`);
|
|
85
|
+
else {
|
|
86
|
+
result.unreadable = true;
|
|
87
|
+
result.reasons.push(`${tier}:unreadable`);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
if (manifest?.generation !== target || manifest.tier !== tier || !Array.isArray(manifest.entries)
|
|
92
|
+
|| typeof manifest.host !== 'string' || typeof manifest.createdAt !== 'string' || !Number.isFinite(Date.parse(manifest.createdAt))) {
|
|
93
|
+
result.reasons.push(`${tier}:invalid manifest`);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
result.host = manifest.host;
|
|
97
|
+
result.entries = manifest.entries.length;
|
|
98
|
+
const elapsedMs = now.getTime() - Date.parse(manifest.createdAt);
|
|
99
|
+
result.ageMin = Math.floor(elapsedMs / 60_000);
|
|
100
|
+
if (maxAgeMin !== undefined && elapsedMs > maxAgeMin * 60_000) result.reasons.push(`${tier}:age exceeded`);
|
|
101
|
+
const expected = new Map<string, string>();
|
|
102
|
+
for (const line of sums.split('\n').filter(Boolean)) {
|
|
103
|
+
const match = /^([a-fA-F0-9]{64}) (.+)$/.exec(line);
|
|
104
|
+
if (!match || !safeFile(dir, match[2]) || expected.has(match[2])) {
|
|
105
|
+
result.reasons.push(`${tier}:invalid SHA256SUMS`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
expected.set(match[2], match[1].toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
if (expected.size !== manifest.entries.length || manifest.entries.some(e => !e || typeof e.path !== 'string' || typeof e.sha256 !== 'string' || expected.get(e.path) !== e.sha256.toLowerCase())) {
|
|
111
|
+
result.reasons.push(`${tier}:manifest/checksum list mismatch`);
|
|
112
|
+
}
|
|
113
|
+
for (const [name, digest] of expected) {
|
|
114
|
+
const path = safeFile(dir, name)!;
|
|
115
|
+
try {
|
|
116
|
+
if ((await hashFile(path)) !== digest) result.mismatches++;
|
|
117
|
+
else result.checked++;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (errorCode(error) === 'ENOENT') result.missing++;
|
|
120
|
+
else result.unreadable = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (result.mismatches) result.reasons.push(`${tier}:checksum mismatch ${result.mismatches}`);
|
|
124
|
+
if (result.missing) result.reasons.push(`${tier}:missing files ${result.missing}`);
|
|
125
|
+
if (result.unreadable) result.reasons.push(`${tier}:unreadable`);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function verifyStandby(opts: VerifyOptions): Promise<StandbyVerification> {
|
|
130
|
+
const tiers = await Promise.all(opts.tiers.map(tier => verifyTier(opts.root, tier,
|
|
131
|
+
typeof opts.maxAgeMin === 'number' ? opts.maxAgeMin : opts.maxAgeMin?.[tier], opts.now ?? new Date())));
|
|
132
|
+
const reasons = tiers.flatMap(tier => tier.reasons);
|
|
133
|
+
let marker: StandbyVerification['marker'];
|
|
134
|
+
if (opts.marker !== undefined) {
|
|
135
|
+
const core = tiers.find(tier => tier.tier === 'core');
|
|
136
|
+
if (!core || core.unreadable) marker = 'unreadable';
|
|
137
|
+
else if (!core.generation) marker = 'missing';
|
|
138
|
+
else {
|
|
139
|
+
try {
|
|
140
|
+
const lines = (await readFile(join(opts.root, 'core', core.generation, 'hq-drill', 'round-trip.jsonl'), 'utf8')).split('\n');
|
|
141
|
+
marker = lines.filter(line => line.includes(opts.marker!)).length;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
marker = errorCode(error) === 'ENOENT' ? 'missing' : 'unreadable';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (marker !== 1) reasons.push(`marker:${marker === 0 ? 'not found' : typeof marker === 'number' ? `count ${marker}` : marker}`);
|
|
147
|
+
}
|
|
148
|
+
return { tiers, ...(opts.marker === undefined ? {} : { marker }), ok: reasons.length === 0, reasons };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatVerification(result: StandbyVerification): string {
|
|
152
|
+
const parts = result.tiers.map(t => `${t.tier}=${t.unreadable && !t.generation ? 'unreadable' : t.generation ?? 'missing'}(${t.ageMin === null ? '?' : t.ageMin}m · ${t.host ?? '?'} · ok ${t.checked}/${t.entries ?? '?'}${t.mismatches ? ` · mismatch ${t.mismatches}` : ''}${t.missing ? ` · missing ${t.missing}` : ''}${t.unreadable ? ' · unreadable' : ''})`);
|
|
153
|
+
if (result.marker !== undefined) parts.push(`marker=${result.marker}`);
|
|
154
|
+
return `hq-standby verify ${parts.join(' ')} ${result.ok ? 'OK' : `FAIL ${result.reasons.join(', ')}`}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseVerifyArgs(argv: string[]): { options: VerifyOptions; json: boolean } {
|
|
158
|
+
let root = join(homedir(), '.elanous-standby');
|
|
159
|
+
let tiers: Tier[] = ['core', 'big', 'obs'];
|
|
160
|
+
let marker: string | undefined;
|
|
161
|
+
let maxAgeMin: VerifyOptions['maxAgeMin'];
|
|
162
|
+
let json = false;
|
|
163
|
+
for (let i = 0; i < argv.length; i++) {
|
|
164
|
+
const flag = argv[i];
|
|
165
|
+
if (flag === '--json') { json = true; continue; }
|
|
166
|
+
if (!['--root', '--tier', '--marker', '--max-age-min'].includes(flag)) throw new Error(`unknown option: ${flag}`);
|
|
167
|
+
const value = argv[++i];
|
|
168
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
169
|
+
if (flag === '--root') root = value.startsWith('~/') ? join(homedir(), value.slice(2)) : value;
|
|
170
|
+
if (flag === '--tier') {
|
|
171
|
+
const parsed = value.split(',');
|
|
172
|
+
if (!parsed.length || parsed.some(t => !['core', 'big', 'obs'].includes(t)) || new Set(parsed).size !== parsed.length) throw new Error('--tier must be core,big,obs (no duplicates)');
|
|
173
|
+
tiers = parsed as Tier[];
|
|
174
|
+
}
|
|
175
|
+
if (flag === '--marker') marker = value;
|
|
176
|
+
if (flag === '--max-age-min') {
|
|
177
|
+
if (!value.includes('=') && !value.includes(',')) {
|
|
178
|
+
maxAgeMin = Number(value);
|
|
179
|
+
if (!Number.isFinite(maxAgeMin) || maxAgeMin < 0) throw new Error('--max-age-min must be a non-negative number');
|
|
180
|
+
} else {
|
|
181
|
+
const limits: Partial<Record<Tier, number>> = {};
|
|
182
|
+
for (const item of value.split(',')) {
|
|
183
|
+
const match = /^(core|big|obs)=(.+)$/.exec(item);
|
|
184
|
+
if (!match || Object.hasOwn(limits, match[1]) || !match[2].trim() || !Number.isFinite(Number(match[2])) || Number(match[2]) < 0)
|
|
185
|
+
throw new Error('--max-age-min must be a non-negative number or tier=minutes comma list (no duplicates)');
|
|
186
|
+
limits[match[1] as Tier] = Number(match[2]);
|
|
187
|
+
}
|
|
188
|
+
maxAgeMin = limits;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (marker !== undefined && !tiers.includes('core')) tiers = [...tiers, 'core'];
|
|
193
|
+
return { options: { root, tiers, ...(marker === undefined ? {} : { marker }), ...(maxAgeMin === undefined ? {} : { maxAgeMin }) }, json };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (import.meta.main) {
|
|
197
|
+
let parsed: ReturnType<typeof parseVerifyArgs> | undefined;
|
|
198
|
+
try {
|
|
199
|
+
parsed = parseVerifyArgs(process.argv.slice(2));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(`hq-standby verify: ${String(error)}`);
|
|
202
|
+
process.exitCode = 2;
|
|
203
|
+
}
|
|
204
|
+
if (parsed) {
|
|
205
|
+
try {
|
|
206
|
+
const result = await verifyStandby(parsed.options);
|
|
207
|
+
debug.log('hq.standby', 'verify', { tiers: result.tiers, ok: result.ok, reasons: result.reasons });
|
|
208
|
+
if (!result.ok) debug.log('hq.standby', 'verify-failed', { reasons: result.reasons });
|
|
209
|
+
console.log(parsed.json ? JSON.stringify(result) : formatVerification(result));
|
|
210
|
+
if (!result.ok) process.exitCode = 1;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const reasons = [`unreadable:${String(error)}`];
|
|
213
|
+
debug.log('hq.standby', 'verify', { tiers: parsed.options.tiers, ok: false, reasons });
|
|
214
|
+
debug.log('hq.standby', 'verify-failed', { reasons });
|
|
215
|
+
console.log(parsed.json ? JSON.stringify({ tiers: [], ok: false, reasons }) : `hq-standby verify FAIL ${reasons[0]}`);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|