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
|
@@ -1,24 +1,46 @@
|
|
|
1
|
+
// Bounded Claude Code hook extractor: only seat folders (marked by `.claude/seat`) emit, and only fixed metadata.
|
|
1
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SEATS = new Set(['OP', 'TC', 'MK', 'UX']);
|
|
7
|
+
const MAX_LEVELS = 6;
|
|
8
|
+
|
|
9
|
+
// Walks up from the session cwd to the git root (or MAX_LEVELS); no or invalid marker → null.
|
|
10
|
+
function findSeat(start: string): string | null {
|
|
11
|
+
let dir = start;
|
|
12
|
+
for (let level = 0; level <= MAX_LEVELS; level++) {
|
|
13
|
+
const marker = join(dir, '.claude', 'seat');
|
|
14
|
+
if (existsSync(marker)) {
|
|
15
|
+
const seat = readFileSync(marker, 'utf8').trim();
|
|
16
|
+
return SEATS.has(seat) ? seat : null;
|
|
17
|
+
}
|
|
18
|
+
if (existsSync(join(dir, '.git'))) return null;
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) return null;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
2
25
|
|
|
3
26
|
const event = process.argv[2];
|
|
4
27
|
if (event !== 'stop' && event !== 'post-tool-use') process.exit(0);
|
|
5
|
-
|
|
6
28
|
try {
|
|
7
29
|
const payload = JSON.parse(await Bun.stdin.text()) as Record<string, unknown>;
|
|
8
|
-
// A locator is useful only when it is a session ID, not arbitrary hook input.
|
|
9
30
|
const session = payload.session_id;
|
|
10
31
|
if (typeof session !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(session)) process.exit(0);
|
|
32
|
+
const cwd = typeof payload.cwd === 'string' && isAbsolute(payload.cwd) ? payload.cwd : process.cwd();
|
|
33
|
+
const seat = findSeat(cwd);
|
|
34
|
+
if (seat === null) process.exit(0);
|
|
35
|
+
if (event === 'stop') {
|
|
36
|
+
spawnSync('elanous', ['context', 'emit', 'task-done', '--seat', seat, '--text', 'Claude Code session stopped', '--ref', session],
|
|
37
|
+
{ stdio: 'ignore', timeout: 3000 });
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
11
40
|
const tool = payload.tool_name;
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
'Task', 'WebFetch', 'WebSearch', 'NotebookEdit', 'TodoWrite', 'AskUserQuestion']);
|
|
16
|
-
const summary = event === 'stop' ? 'Claude Code session stopped'
|
|
17
|
-
: `Claude Code tool ${knownTools.has(tool as string) ? tool : 'other'} completed`;
|
|
41
|
+
if (typeof tool !== 'string' || !/^[a-z][a-z0-9_.:-]{0,63}$/i.test(tool)) process.exit(0);
|
|
42
|
+
const knownTools = new Set(['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'LS', 'Task', 'WebFetch', 'WebSearch', 'NotebookEdit', 'TodoWrite', 'AskUserQuestion']);
|
|
43
|
+
const summary = `Claude Code tool ${knownTools.has(tool) ? tool : 'other'} completed`;
|
|
18
44
|
const source = `elanous://context/claude-code/${session}`;
|
|
19
|
-
spawnSync('elanous', ['context', 'emit', '--kind', 'done', '--summary', summary, '--source', source], {
|
|
20
|
-
|
|
21
|
-
});
|
|
22
|
-
} catch {
|
|
23
|
-
// Hooks must not interrupt the seat session on malformed input or unavailable CLI.
|
|
24
|
-
}
|
|
45
|
+
spawnSync('elanous', ['context', 'emit', '--kind', 'done', '--summary', summary, '--source', source], { stdio: 'ignore', timeout: 3000 });
|
|
46
|
+
} catch { /* never interrupt the seat session */ }
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
# 환경 노브: CH_PR · CH_REPO · CH_INTERVAL(기본 60) · CH_BODY_CHARS(기본 1500) · CH_WATCH_BODY(기본 1)
|
|
48
48
|
|
|
49
49
|
set -u
|
|
50
|
+
set -o pipefail
|
|
50
51
|
|
|
51
52
|
PR_NUM="${CH_PR:-23032}" # ⛔ 2026-10-03: #20798 가 2,500 상한 → 채널 «6»(OP) # ⛔ 2026-09-27: #16815 가 2,500 상한에 닿아 채널 «5» 로 옮김(🅞 · 벽에 «닿은 뒤» — 미리 못 옮겼다)
|
|
52
53
|
# 📏 이력: #5730(07-28~08-12) → #8328(08-11~08-25) → #12577(08-25~09-10) → #16815(09-10~09-27) → #20798(09-27~10-03) → #23032.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { discordDecisionOwner } from '../src/decisions/discord-decision-cards.js';
|
|
5
|
+
import { handleDiscordSeatWork, type DiscordSeatWorkDeps } from '../src/intake-plane/discord-seat-work.js';
|
|
6
|
+
import { submitIntakeWork } from '../src/intake-plane/submit-intake-work.js';
|
|
7
|
+
import type { MsgStore } from '../src/msg/msg-store.js';
|
|
8
|
+
import { dispatchCeoTask } from '../src/seat-dispatch/ceo-commands.js';
|
|
9
|
+
import { resolveSeat } from '../src/seat-address/seat-address.js';
|
|
10
|
+
import { getUserConfig, type UserConfig } from '../src/user-config.js';
|
|
11
|
+
|
|
12
|
+
export type ProbeResult = {
|
|
13
|
+
text: string;
|
|
14
|
+
ms: number;
|
|
15
|
+
route: 'dispatch' | 'ask' | 'submit' | 'none';
|
|
16
|
+
seat: string | null;
|
|
17
|
+
body: string | null;
|
|
18
|
+
reply: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ProbeOptions = { config?: UserConfig; dm?: boolean; live?: boolean; liveDeps?: DiscordSeatWorkDeps; messageId?: string };
|
|
22
|
+
|
|
23
|
+
/** Execute the real Discord seat router; dry dependencies never open a live store or delivery channel. */
|
|
24
|
+
export async function probeDiscordSeatWork(text: string, options: ProbeOptions = {}): Promise<ProbeResult> {
|
|
25
|
+
const config = options.config ?? getUserConfig();
|
|
26
|
+
const ownerId = discordDecisionOwner(config);
|
|
27
|
+
if (options.dm && !ownerId) throw new Error('Discord 소유자 ID가 없어 DM을 재현할 수 없습니다.');
|
|
28
|
+
const messageId = options.messageId ?? 'probe-1';
|
|
29
|
+
const msg = { channelId: options.dm ? `probe-dm-${ownerId}` : 'probe-channel', messageId,
|
|
30
|
+
...(ownerId ? { userId: ownerId } : {}), isDm: options.dm ?? false };
|
|
31
|
+
let route: ProbeResult['route'] = 'none';
|
|
32
|
+
let seat: string | null = null;
|
|
33
|
+
let body: string | null = null;
|
|
34
|
+
const capture = (path: ProbeResult['route'], seatId: string | null, forwarded: string) => {
|
|
35
|
+
route = path;
|
|
36
|
+
seat = seatId;
|
|
37
|
+
body = Array.from(forwarded).slice(0, 80).join('');
|
|
38
|
+
};
|
|
39
|
+
const dry: DiscordSeatWorkDeps = {
|
|
40
|
+
config,
|
|
41
|
+
// An unknown address must not cause registry I/O or a persona/LLM answer in a dry run.
|
|
42
|
+
personaSource: { list: () => [], get: () => undefined },
|
|
43
|
+
personaAnswer: async () => { throw new Error('dry persona answer unavailable'); },
|
|
44
|
+
answer: async () => null,
|
|
45
|
+
commandDeps: { ownerId, replyTarget: null, runGh: async () => { throw new Error('dry channel delivery unavailable'); },
|
|
46
|
+
append: () => { throw new Error('dry seat delivery unavailable'); } },
|
|
47
|
+
dispatch: async (seatId, forwarded) => {
|
|
48
|
+
capture('dispatch', seatId, forwarded);
|
|
49
|
+
return { channel: 'unknown', reply: `마른 실행 — ${seatId}에 맡길 예정 (실제 맡김 없음).` };
|
|
50
|
+
},
|
|
51
|
+
submit: async (input) => {
|
|
52
|
+
capture('submit', resolveSeat(input.text.split(/\s+/)[0] ?? '')?.id ?? null, input.text);
|
|
53
|
+
return { ok: true, track: 'graph', acceptanceId: 'DRY-RUN (모의 · 실제 접수 없음)' };
|
|
54
|
+
},
|
|
55
|
+
askDeps: {
|
|
56
|
+
open: () => { const db = new Database(':memory:'); return { db, close: () => db.close() } as unknown as MsgStore; },
|
|
57
|
+
dispatch: async (seatId, forwarded) => {
|
|
58
|
+
capture('ask', seatId, forwarded);
|
|
59
|
+
return { channel: 'unknown', reply: '마른 실행 — 질문을 맡기지 않았습니다.' };
|
|
60
|
+
},
|
|
61
|
+
send: async () => { throw new Error('dry Discord delivery unavailable'); },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const live: DiscordSeatWorkDeps = {
|
|
65
|
+
...options.liveDeps,
|
|
66
|
+
config,
|
|
67
|
+
dispatch: async (seatId, forwarded, deps, extra) => {
|
|
68
|
+
capture('dispatch', seatId, forwarded);
|
|
69
|
+
return (options.liveDeps?.dispatch ?? dispatchCeoTask)(seatId, forwarded, deps, extra);
|
|
70
|
+
},
|
|
71
|
+
submit: async (input, deps) => {
|
|
72
|
+
capture('submit', resolveSeat(input.text.split(/\s+/)[0] ?? '')?.id ?? null, input.text);
|
|
73
|
+
return (options.liveDeps?.submit ?? submitIntakeWork)(input, deps);
|
|
74
|
+
},
|
|
75
|
+
askDeps: {
|
|
76
|
+
...options.liveDeps?.askDeps,
|
|
77
|
+
dispatch: async (seatId, forwarded, deps, extra) => {
|
|
78
|
+
capture('ask', seatId, forwarded);
|
|
79
|
+
return (options.liveDeps?.askDeps?.dispatch ?? dispatchCeoTask)(seatId, forwarded, deps, extra);
|
|
80
|
+
},
|
|
81
|
+
// This CLI has no Discord transport: even live mode must not post to Discord.
|
|
82
|
+
send: async () => { throw new Error('probe does not send Discord messages'); },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const start = performance.now();
|
|
86
|
+
const reply = await handleDiscordSeatWork(text, msg, options.live ? live : dry);
|
|
87
|
+
return { text: Array.from(text).slice(0, 80).join(''), ms: Math.round((performance.now() - start) * 100) / 100,
|
|
88
|
+
route, seat, body, reply: reply === null ? null : Array.from(reply).slice(0, 120).join('') };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Extract only the '치는 문장' column of a Markdown runbook table, in row order. */
|
|
92
|
+
export function runbookTexts(markdown: string): string[] {
|
|
93
|
+
const lines = markdown.split(/\r?\n/);
|
|
94
|
+
const header = lines.findIndex((line) => /^\|.*\|\s*치는 문장(?:\s*\([^|]*\))?\s*\|/.test(line));
|
|
95
|
+
if (header < 0) throw new Error('대본의 «치는 문장» 표를 찾지 못했습니다.');
|
|
96
|
+
const columns = lines[header]!.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
97
|
+
const index = columns.findIndex((cell) => cell.startsWith('치는 문장'));
|
|
98
|
+
const texts: string[] = [];
|
|
99
|
+
for (const line of lines.slice(header + 2)) {
|
|
100
|
+
if (!line.startsWith('|')) break;
|
|
101
|
+
const cells = line.split('|').slice(1, -1);
|
|
102
|
+
if (cells.length !== columns.length) throw new Error('대본 표의 열 수가 맞지 않습니다.');
|
|
103
|
+
const cell = cells[index]?.trim() ?? '';
|
|
104
|
+
if (!cell.startsWith('`') || !cell.endsWith('`')) throw new Error('치는 문장 칸이 백틱으로 둘러싸여 있지 않습니다.');
|
|
105
|
+
texts.push(cell.slice(1, -1));
|
|
106
|
+
}
|
|
107
|
+
if (!texts.length) throw new Error('대본에 치는 문장이 없습니다.');
|
|
108
|
+
return texts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runProbeCli(argv: string[], config: UserConfig = getUserConfig()): Promise<ProbeResult[]> {
|
|
112
|
+
let text: string | undefined;
|
|
113
|
+
let runbook: string | undefined;
|
|
114
|
+
let dm = false;
|
|
115
|
+
let live = false;
|
|
116
|
+
let json = false;
|
|
117
|
+
for (let i = 0; i < argv.length; i++) {
|
|
118
|
+
const flag = argv[i];
|
|
119
|
+
if (flag === '--text' || flag === '--runbook') {
|
|
120
|
+
const value = argv[++i];
|
|
121
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} 값이 필요합니다.`);
|
|
122
|
+
if (flag === '--text') text = value;
|
|
123
|
+
else runbook = value;
|
|
124
|
+
} else if (flag === '--dm') dm = true;
|
|
125
|
+
else if (flag === '--live') live = true;
|
|
126
|
+
else if (flag === '--json') json = true;
|
|
127
|
+
else throw new Error(`알 수 없는 옵션: ${flag}`);
|
|
128
|
+
}
|
|
129
|
+
if (Boolean(text) === Boolean(runbook)) throw new Error('--text 또는 --runbook 중 하나만 지정하세요.');
|
|
130
|
+
const texts = runbook ? runbookTexts(readFileSync(resolve(runbook), 'utf8')) : [text!];
|
|
131
|
+
const results: ProbeResult[] = [];
|
|
132
|
+
for (const [index, line] of texts.entries()) {
|
|
133
|
+
const result = await probeDiscordSeatWork(line, { config, dm, live, messageId: `probe-${index + 1}` });
|
|
134
|
+
results.push(result);
|
|
135
|
+
if (!json) console.log(`글: ${result.text} | ${result.ms}ms | 길: ${result.route} | 자리: ${result.seat ?? '-'} | 본문: ${result.body ?? '-'} | 봇 답: ${result.reply ?? '-'}`);
|
|
136
|
+
}
|
|
137
|
+
if (json) console.log(JSON.stringify(results));
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (import.meta.main) {
|
|
142
|
+
try { await runProbeCli(process.argv.slice(2)); }
|
|
143
|
+
catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
|
|
144
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# HQ-REP replication body — the same file runs on both HQ hosts (OP 10-04 10:09 · «되돌리기 방향»).
|
|
3
|
+
# The lease holder is the source, the other host is the standby: mbp → node-b today, node-b → mbp once node-b holds the lease.
|
|
4
|
+
# Called by each host's short cron shim (~/.elanous/bin/hq-standby.sh: lock ⊕ fetch ⊕ detach origin/main) with:
|
|
5
|
+
# HQ_REP_PEER=<the other host's ssh name> HQ_REP_DEFAULT_SOURCE=<lease names of the pre-lease source, comma list>
|
|
6
|
+
# [HQ_REP_SURVIVAL=<host>,s3://bucket/prefix] [HQ_REP_STATE_DIR=<canonical ledgers · default ~/.elanous>]
|
|
7
|
+
# sh scripts/hq/hq-standby.sh core|big|obs|vault
|
|
8
|
+
# HQ_REP_STATE_DIR (OP 10-04 11:27): on node-b the promoted HQ state lives in ~/.elanous-hqstate — its live ~/.elanous is the
|
|
9
|
+
# Pod host's own state and must never be copied as HQ ledgers. The copy refuses a state dir without release/features.sqlite
|
|
10
|
+
# (an empty or wrong root would otherwise become the standby's newest generation).
|
|
11
|
+
# Direction comes from scripts/hq/standby-role.ts: `fenced` → the copy runs under `elanous hq fence --role cron`
|
|
12
|
+
# (TC HQ-FENCE decides · skip exits 0), `direct` → no lease yet and this is the default source, `skip` → no lease, not it.
|
|
13
|
+
MODE="$1"
|
|
14
|
+
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
15
|
+
# Production ledgers on every host — a source-tree command resolves the test universe otherwise.
|
|
16
|
+
# HQ_REP_CONFIG_DIR (TC drill step 0 · 10-04 11:5x): node-b keeps its hq settings (hq.hostName=node-b) in ~/.elanous-hqcfg
|
|
17
|
+
# because its live ~/.elanous is the Pod host's; the role check and `hq fence` must read the same lease name as HQ-HB.
|
|
18
|
+
# ⚠️ The config dir moves only by the `--config-dir` flag (src/elanous-config-dir.ts) — an ELANOUS_CONFIG_DIR env is ignored.
|
|
19
|
+
export ELANOUS_STATE_DIR="$HOME/.elanous"
|
|
20
|
+
CFG="${HQ_REP_CONFIG_DIR:-$HOME/.elanous}"
|
|
21
|
+
STATE="${HQ_REP_STATE_DIR:-$HOME/.elanous}"
|
|
22
|
+
case "$MODE" in core|big|obs|vault) ;; *) echo "$(ts) hq-standby skip: unknown mode $MODE"; exit 2 ;; esac
|
|
23
|
+
[ -n "$HQ_REP_PEER" ] || { echo "$(ts) hq-standby skip($MODE): HQ_REP_PEER not set"; exit 2; }
|
|
24
|
+
[ -n "$HQ_REP_DEFAULT_SOURCE" ] || { echo "$(ts) hq-standby skip($MODE): HQ_REP_DEFAULT_SOURCE not set"; exit 2; }
|
|
25
|
+
verify_received() {
|
|
26
|
+
# Verification is observational: a broken or not-yet-received copy must not fail replication's cron exit status.
|
|
27
|
+
bun scripts/hq/standby-verify.ts --root "$HOME/.elanous-standby" --tier core,big --max-age-min core=30,big=400 || :
|
|
28
|
+
}
|
|
29
|
+
ROLE=$(bun scripts/hq/standby-role.ts --config-dir "$CFG" --default-source "$HQ_REP_DEFAULT_SOURCE" 2>/dev/null) || ROLE=fenced
|
|
30
|
+
case "$ROLE" in
|
|
31
|
+
skip) if [ "$MODE" = core ]; then verify_received; else echo "$(ts) hq-standby skip($MODE): standby — no lease yet and this host is not the default source"; fi; exit 0 ;;
|
|
32
|
+
direct) set -- ;;
|
|
33
|
+
*) ROLE=fenced; set -- bun bin/elanous.mjs --config-dir "$CFG" hq fence --role cron -- ;;
|
|
34
|
+
esac
|
|
35
|
+
echo "$(ts) hq-standby run($MODE) role=$ROLE state=$STATE → $HQ_REP_PEER at $(git rev-parse --short HEAD 2>/dev/null)"
|
|
36
|
+
if [ "$MODE" = vault ]; then
|
|
37
|
+
# Obsidian vault: one-way copy to the standby (no --delete; workspace state and trash stay local; --update keeps newer files there).
|
|
38
|
+
V="$HOME/Obsidian/ElanvitalAI/"
|
|
39
|
+
"$@" rsync -a --update --exclude '.obsidian/workspace*' --exclude '.trash/' "$V" "$HQ_REP_PEER:$V" 2>&1 \
|
|
40
|
+
&& echo "$(ts) hq-standby vault done rc=0 (a fence skip line above means nothing was copied)" || { echo "$(ts) hq-standby vault FAILED"; exit 1; }
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
# In core fenced mode the guard runs inside the fence: a non-holder need not have HQ source ledgers.
|
|
44
|
+
if [ "$MODE" != core ] || [ "$ROLE" = direct ]; then
|
|
45
|
+
[ -f "$STATE/release/features.sqlite" ] || { echo "$(ts) hq-standby skip($MODE): $STATE has no release/features.sqlite — not an HQ state dir, nothing copied"; exit 1; }
|
|
46
|
+
fi
|
|
47
|
+
SURV=""
|
|
48
|
+
[ "$MODE" = core ] && [ -n "$HQ_REP_SURVIVAL" ] && SURV="--survival $HQ_REP_SURVIVAL"
|
|
49
|
+
# The fence exits 0 both after copying and after skipping. Mark execution inside the fence,
|
|
50
|
+
# not by parsing its diagnostic text: only the skipped host owns a received copy to verify.
|
|
51
|
+
if [ "$MODE" = core ] && [ "$ROLE" = fenced ]; then
|
|
52
|
+
FENCE_RAN=$(mktemp) || { echo "$(ts) hq-standby FAILED: cannot track fence execution"; exit 1; }
|
|
53
|
+
# shellcheck disable=SC2086 — SURV is either empty or two words.
|
|
54
|
+
COPY_OUT=$("$@" sh -c '
|
|
55
|
+
[ -f "$1/release/features.sqlite" ] || { echo "hq-standby skip(core): $1 has no release/features.sqlite — not an HQ state dir, nothing copied"; exit 1; }
|
|
56
|
+
printf "ran\n" > "$2"; shift 2; exec "$@"
|
|
57
|
+
' sh "$STATE" "$FENCE_RAN" env ELANOUS_STATE_DIR="$STATE" bun scripts/hq/standby-snapshot.ts --tier "$MODE" --push "$HQ_REP_PEER" $SURV 2>&1)
|
|
58
|
+
COPY_RC=$?
|
|
59
|
+
[ -z "$COPY_OUT" ] || printf '%s\n' "$COPY_OUT"
|
|
60
|
+
if [ "$COPY_RC" -eq 0 ] && [ ! -s "$FENCE_RAN" ]; then verify_received; fi
|
|
61
|
+
rm -f "$FENCE_RAN"
|
|
62
|
+
exit "$COPY_RC"
|
|
63
|
+
fi
|
|
64
|
+
# shellcheck disable=SC2086 — SURV is either empty or two words.
|
|
65
|
+
"$@" env ELANOUS_STATE_DIR="$STATE" bun scripts/hq/standby-snapshot.ts --tier "$MODE" --push "$HQ_REP_PEER" $SURV 2>&1
|
|
@@ -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
|
+
}
|