elanous 0.2.12 → 0.2.14
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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.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/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.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-74383f890f075511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.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-fd7e200d4d83fbd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.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-b3759cc14da80908.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.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-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +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/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/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 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- 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/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/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +2 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +186 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/coord-post.sh +11 -1
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +163 -0
- package/scripts/heal/run-role.ts +14 -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 +223 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lessons/handbook-page.ts +85 -0
- package/scripts/release-loop/gate-node.ts +62 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +196 -0
- package/scripts/release-story/draft.ts +172 -0
- package/scripts/rhythm/daily.ts +383 -0
- package/scripts/rhythm/weekly.ts +294 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/test-deterministic.ts +57 -11
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- 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/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +15 -10
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/build/llm-conflict-merge.ts +45 -8
- package/src/autopilot/build/merge-intent.ts +53 -0
- 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 +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +157 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +84 -20
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +44 -2
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +248 -45
- package/src/cli/rfc-status.ts +91 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +189 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-public.ts +86 -0
- package/src/context-bus/context-now-surfaces.ts +70 -4
- package/src/context-bus/context-now.ts +4 -3
- package/src/context-bus/long-term-memory.ts +201 -24
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +83 -18
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -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.ts +64 -11
- package/src/graph-runner/graph-tick.ts +13 -2
- package/src/graph-runner/runner.ts +111 -22
- package/src/harness/harness-cli-command.ts +178 -27
- package/src/harness/harness-incidents.ts +43 -12
- package/src/harness/harness-pod-dispatch.ts +2 -0
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +494 -0
- package/src/harness/helper-scan.ts +139 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- 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/hq/promote.ts +301 -0
- package/src/index.ts +281 -27
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +72 -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-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +32 -0
- package/src/loops/checker.ts +180 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +72 -6
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +59 -0
- package/src/loops/orchestrator/author-depth.ts +164 -0
- package/src/loops/orchestrator/author-input-check.ts +77 -0
- package/src/loops/orchestrator/author-ledger.ts +230 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +180 -0
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/tick.ts +302 -0
- package/src/loops/orchestrator/traffic.ts +165 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/registry.ts +454 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +34 -0
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +65 -2
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/schedules-read.ts +5 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/auto-start.ts +48 -0
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +28 -16
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/placement.ts +242 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +73 -15
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-loop/seat-loop.ts +495 -46
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/dev-pipeline.ts +6 -2
- package/src/self-dev/draft-sweep.ts +76 -0
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +8 -0
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +294 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +79 -0
- package/src/self-implement/host-regate.ts +25 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +114 -10
- package/src/self-implement/revert-guard.ts +62 -0
- package/src/self-implement/run-ledger.ts +1 -1
- package/src/self-implement/seams.ts +28 -8
- package/src/session/index.ts +2 -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 +86 -0
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +204 -16
- 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/storage.ts +31 -13
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { chmodSync, copyFileSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import type { Tier } from './standby-snapshot.js';
|
|
9
|
+
|
|
10
|
+
interface Generation { tier: Tier; name: string; dir: string; files: string[] }
|
|
11
|
+
interface Options { to: string; standby: string; tiers: Tier[]; hqConfig?: string; dryRun: boolean; json: boolean }
|
|
12
|
+
|
|
13
|
+
function contained(base: string, name: string): string {
|
|
14
|
+
if (!name || isAbsolute(name) || name.includes('\\')) throw new Error(`invalid relative path: ${name}`);
|
|
15
|
+
const path = resolve(base, name);
|
|
16
|
+
const rel = relative(base, path);
|
|
17
|
+
if (!rel || rel === '..' || rel.startsWith(`..${sep}`)) throw new Error(`path escapes generation: ${name}`);
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function expand(path: string): string { return path.startsWith('~/') ? join(homedir(), path.slice(2)) : path; }
|
|
22
|
+
|
|
23
|
+
export function parsePromoteArgs(argv: string[]): Options {
|
|
24
|
+
const options: Options = { to: '', standby: join(homedir(), '.elanous-standby'), tiers: ['core', 'big'], dryRun: false, json: false };
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const flag = argv[i];
|
|
27
|
+
if (flag === '--dry-run') { options.dryRun = true; continue; }
|
|
28
|
+
if (flag === '--json') { options.json = true; continue; }
|
|
29
|
+
if (!['--to', '--standby', '--tiers', '--hq-config'].includes(flag)) throw new Error(`unknown option: ${flag}`);
|
|
30
|
+
const value = argv[++i];
|
|
31
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
32
|
+
if (flag === '--to') options.to = resolve(expand(value));
|
|
33
|
+
if (flag === '--standby') options.standby = resolve(expand(value));
|
|
34
|
+
if (flag === '--hq-config') options.hqConfig = resolve(expand(value));
|
|
35
|
+
if (flag === '--tiers') {
|
|
36
|
+
const tiers = value.split(',');
|
|
37
|
+
if (tiers.some(t => !['core', 'big', 'obs'].includes(t)) || new Set(tiers).size !== tiers.length) throw new Error('--tiers must be core,big,obs without duplicates');
|
|
38
|
+
options.tiers = tiers as Tier[];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!options.to) throw new Error('--to is required');
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isInside(base: string, path: string): boolean {
|
|
46
|
+
const rel = relative(base, path);
|
|
47
|
+
return !rel || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function sha256(path: string): Promise<string> {
|
|
51
|
+
const hash = createHash('sha256');
|
|
52
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
53
|
+
return hash.digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkedFile(dir: string, name: string): string {
|
|
57
|
+
const path = contained(dir, name);
|
|
58
|
+
let current = dir;
|
|
59
|
+
for (const part of relative(dir, path).split(sep)) {
|
|
60
|
+
current = join(current, part);
|
|
61
|
+
if (lstatSync(current).isSymbolicLink()) throw new Error(`symlink in generation: ${name}`);
|
|
62
|
+
}
|
|
63
|
+
if (!lstatSync(path).isFile()) throw new Error(`not a file: ${name}`);
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readGeneration(standby: string, tier: Tier): Promise<Generation> {
|
|
68
|
+
const base = join(standby, tier);
|
|
69
|
+
const name = readlinkSync(join(base, 'latest'));
|
|
70
|
+
const dir = contained(base, name);
|
|
71
|
+
if (relative(base, dir) !== name || !lstatSync(dir).isDirectory()) throw new Error(`${tier}: invalid latest`);
|
|
72
|
+
const manifest = JSON.parse(readFileSync(checkedFile(dir, 'MANIFEST.json'), 'utf8')) as { generation?: string; tier?: string; entries?: Array<{ path: string; sha256: string }> };
|
|
73
|
+
if (manifest.generation !== name || manifest.tier !== tier || !Array.isArray(manifest.entries)) throw new Error(`${tier}: invalid manifest`);
|
|
74
|
+
const lines = readFileSync(checkedFile(dir, 'SHA256SUMS'), 'utf8').trimEnd().split('\n');
|
|
75
|
+
const files: string[] = [];
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const match = /^([a-fA-F0-9]{64}) (.+)$/.exec(line);
|
|
79
|
+
if (!match || seen.has(match[2]) || ['MANIFEST.json', 'SHA256SUMS'].includes(match[2])) throw new Error(`${tier}: invalid SHA256SUMS`);
|
|
80
|
+
seen.add(match[2]);
|
|
81
|
+
const file = checkedFile(dir, match[2]);
|
|
82
|
+
if (await sha256(file) !== match[1].toLowerCase()) throw new Error(`${tier}: checksum mismatch: ${match[2]}`);
|
|
83
|
+
files.push(match[2]);
|
|
84
|
+
}
|
|
85
|
+
if (manifest.entries.length !== files.length || manifest.entries.some(e => !e || !seen.has(e.path) || !lines.includes(`${e.sha256} ${e.path}`))) throw new Error(`${tier}: manifest/checksum list mismatch`);
|
|
86
|
+
return { tier, name, dir, files };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function objectConfig(path: string): Record<string, unknown> {
|
|
90
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`config is not an object: ${path}`);
|
|
92
|
+
return value as Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pollerOff(value: unknown): Record<string, unknown> {
|
|
96
|
+
if (value === undefined) return { enabled: false };
|
|
97
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('poller config is not an object');
|
|
98
|
+
return { ...value, enabled: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function promote(options: Options, writeConfig: (path: string, contents: string) => void = writeFileSync): Promise<{ to: string; tiers: Record<string, string>; files: number; features: 'yes' | 'no'; pollers: 'off'; hq: string; seconds: number; dryRun: boolean }> {
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
const to = resolve(options.to);
|
|
104
|
+
const parent = realpathSync(dirname(to));
|
|
105
|
+
const targetPath = join(parent, relative(dirname(to), to));
|
|
106
|
+
if (isInside(realpathSync(options.standby), targetPath)) throw new Error('--to must be outside --standby');
|
|
107
|
+
if (options.hqConfig && isInside(realpathSync(options.hqConfig), targetPath)) throw new Error('--to must be outside --hq-config');
|
|
108
|
+
// Verify each tier before creating the target, so a bad big copy cannot leave a promoted core.
|
|
109
|
+
const generations: Generation[] = [];
|
|
110
|
+
for (const tier of options.tiers) generations.push(await readGeneration(options.standby, tier));
|
|
111
|
+
try { lstatSync(to); throw new Error(`target already exists: ${to}`); }
|
|
112
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
113
|
+
const files = generations.reduce((n, g) => n + g.files.length, 0);
|
|
114
|
+
const tiers = Object.fromEntries(generations.map(g => [g.tier, g.name]));
|
|
115
|
+
const configSource = [...generations].reverse().find(g => g.files.includes('config.json'));
|
|
116
|
+
let config: Record<string, unknown> | undefined;
|
|
117
|
+
let hq = '-';
|
|
118
|
+
if (!options.dryRun && configSource) {
|
|
119
|
+
config = objectConfig(checkedFile(configSource.dir, 'config.json'));
|
|
120
|
+
config.telegram = pollerOff(config.telegram);
|
|
121
|
+
config.discord = pollerOff(config.discord);
|
|
122
|
+
if (options.hqConfig) {
|
|
123
|
+
const hostConfig = objectConfig(join(options.hqConfig, 'config.json'));
|
|
124
|
+
const block = hostConfig.hq;
|
|
125
|
+
if (!block || typeof block !== 'object' || Array.isArray(block) || typeof (block as Record<string, unknown>).hostName !== 'string' || !(block as Record<string, unknown>).hostName) throw new Error('--hq-config has no hq.hostName');
|
|
126
|
+
config.hq = block;
|
|
127
|
+
hq = (block as { hostName: string }).hostName;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const summary = { to, tiers, files, features: generations.some(g => g.files.includes('release/features.sqlite')) ? 'yes' as const : 'no' as const, pollers: 'off' as const, hq, seconds: 0, dryRun: options.dryRun };
|
|
131
|
+
if (options.dryRun) return summary;
|
|
132
|
+
mkdirSync(to, { mode: 0o700 });
|
|
133
|
+
try {
|
|
134
|
+
chmodSync(to, 0o700);
|
|
135
|
+
for (const generation of generations) {
|
|
136
|
+
for (const name of generation.files) {
|
|
137
|
+
const source = checkedFile(generation.dir, name);
|
|
138
|
+
const dest = contained(to, name);
|
|
139
|
+
mkdirSync(dirname(dest), { recursive: true, mode: 0o700 });
|
|
140
|
+
copyFileSync(source, dest);
|
|
141
|
+
const stats = statSync(source);
|
|
142
|
+
chmodSync(dest, stats.mode);
|
|
143
|
+
utimesSync(dest, stats.atime, stats.mtime);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (config && configSource) {
|
|
147
|
+
copyFileSync(join(to, 'config.json'), join(to, 'config.json.pre-promote'));
|
|
148
|
+
writeConfig(join(to, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
149
|
+
}
|
|
150
|
+
summary.seconds = Math.round((Date.now() - started) / 1000);
|
|
151
|
+
return summary;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
rmSync(to, { recursive: true, force: true });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (import.meta.main) {
|
|
159
|
+
let options: Options | undefined;
|
|
160
|
+
try {
|
|
161
|
+
options = parsePromoteArgs(process.argv.slice(2));
|
|
162
|
+
const result = await promote(options);
|
|
163
|
+
if (!result.dryRun) debug.log('hq.standby', 'promoted', { tiers: result.tiers, files: result.files, reason: 'promoted' });
|
|
164
|
+
console.log(options.json ? JSON.stringify(result) : result.dryRun
|
|
165
|
+
? `promotion plan ${result.to} ${Object.entries(result.tiers).map(([t, g]) => `${t}=${g}`).join(' ')} files=${result.files}`
|
|
166
|
+
: `promoted ${result.to} core=${result.tiers.core ?? '-'} big=${result.tiers.big ?? '-'} files=${result.files} features=${result.features} pollers=off hq=${result.hq} ${result.seconds}s`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const reason = String(error);
|
|
169
|
+
debug.log('hq.standby', 'promote-refused', { tiers: options?.tiers ?? [], files: 0, reason });
|
|
170
|
+
console.error(`promotion refused: ${reason}`);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// HQ-REP direction — which way does this host replicate? (OP 10-04 10:09 · «되돌리기 방향»)
|
|
3
|
+
// The lease holder is the source and the other host is the standby: once node-b holds the lease, node-b → mbp.
|
|
4
|
+
// · a lease record exists, or the arbiter cannot be read → `fenced`: the caller runs the copy under
|
|
5
|
+
// `elanous hq fence --role cron`, which alone decides (holder · generation · arbiter-down rules, src/hq/lease.ts).
|
|
6
|
+
// · no lease record yet (before HQ-HB acquires one) → `direct` on the default source host only, `skip` elsewhere,
|
|
7
|
+
// so the mbp → node-b copy keeps running until the first lease exists (OP 10:12 bootstrap rule).
|
|
8
|
+
// · TC 10:12: a host that has ever seen a lease generation treats a missing record as «unknown» and skips — otherwise
|
|
9
|
+
// a lost arbiter file after the drill would turn mbp back into the source while node-b is HQ (split-brain).
|
|
10
|
+
// «Seen» = `~/.elanous-hq/seen-generation` (HQ-HB writes it) or a generation in the local lease state (hq/local.json).
|
|
11
|
+
// Usage: bun scripts/hq/standby-role.ts --default-source <host> [--config-dir <dir>] [--me <host>] [--json] → prints `direct` | `fenced` | `skip`
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { homedir, hostname } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { debug } from '../../src/debug/log.js';
|
|
16
|
+
import { getElanousConfigDir, setElanousConfigDir } from '../../src/elanous-config-dir.js';
|
|
17
|
+
import { hqLease, readLocal } from '../../src/hq/hq.js';
|
|
18
|
+
import { getUserConfig } from '../../src/user-config.js';
|
|
19
|
+
import type { LeaseRecord } from '../../src/hq/lease.js';
|
|
20
|
+
|
|
21
|
+
export type ReplicationRole = 'direct' | 'fenced' | 'skip';
|
|
22
|
+
export interface RoleDecision { role: ReplicationRole; reason: 'lease-exists' | 'lease-unreadable' | 'no-lease-default-source' | 'no-lease-not-default' | 'no-lease-after-seen'; holder?: string }
|
|
23
|
+
|
|
24
|
+
/** `defaultSource` may list aliases (`MacBookProM5,mbp`) — the lease name is `hq.hostName` when set, else the OS host name. */
|
|
25
|
+
export function decideReplicationRole(input: { me: string; defaultSource: string; record: LeaseRecord | null | 'unreadable'; seenGeneration?: boolean }): RoleDecision {
|
|
26
|
+
const { me, defaultSource, record } = input;
|
|
27
|
+
if (record === 'unreadable') return { role: 'fenced', reason: 'lease-unreadable' };
|
|
28
|
+
if (record) return { role: 'fenced', reason: 'lease-exists', holder: record.holder };
|
|
29
|
+
if (input.seenGeneration) return { role: 'skip', reason: 'no-lease-after-seen' };
|
|
30
|
+
const aliases = defaultSource.split(',').map(s => s.trim()).filter(Boolean);
|
|
31
|
+
return aliases.includes(me) ? { role: 'direct', reason: 'no-lease-default-source' } : { role: 'skip', reason: 'no-lease-not-default' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Has this host ever seen a lease generation? (TC 10:12 · see the header.) */
|
|
35
|
+
export function hasSeenGeneration(home: string = homedir(), localPath: string = join(getElanousConfigDir(), 'hq', 'local.json')): boolean {
|
|
36
|
+
if (existsSync(join(home, '.elanous-hq', 'seen-generation'))) return true;
|
|
37
|
+
return typeof readLocal(localPath).generation === 'number';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Same host name the lease uses (src/hq/hq.ts resolve): `hq.hostName`, else the OS host name without `.local`. */
|
|
41
|
+
export function leaseHostName(configured: string | undefined = getUserConfig().hq?.hostName, raw: string = hostname()): string {
|
|
42
|
+
return configured ?? raw.replace(/\.local$/, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
const arg = (name: string) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; };
|
|
48
|
+
const defaultSource = arg('--default-source');
|
|
49
|
+
if (!defaultSource) { console.error('standby-role: --default-source <host> required'); process.exit(2); }
|
|
50
|
+
// Same lease name and local state as HQ-HB: the config dir moves only by this setter (an env var is ignored).
|
|
51
|
+
const configDir = arg('--config-dir');
|
|
52
|
+
if (configDir) setElanousConfigDir(configDir);
|
|
53
|
+
const me = arg('--me') ?? leaseHostName();
|
|
54
|
+
let record: LeaseRecord | null | 'unreadable';
|
|
55
|
+
try { record = hqLease('status').record; } catch { record = 'unreadable'; }
|
|
56
|
+
const decided = decideReplicationRole({ me, defaultSource, record, seenGeneration: hasSeenGeneration() });
|
|
57
|
+
debug.log('hq.standby', 'role', { me, defaultSource, ...decided });
|
|
58
|
+
console.log(argv.includes('--json') ? JSON.stringify({ me, defaultSource, ...decided }) : decided.role);
|
|
59
|
+
}
|
|
@@ -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
|
+
}
|