elanous 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +82 -31
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +437 -0
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +82 -14
- package/scripts/release-loop/vault-note-node.ts +257 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/monthly.ts +97 -0
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +276 -0
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +66 -29
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/discord-self-message.ts +9 -0
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +147 -17
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +67 -8
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +218 -0
- package/src/harness/helper-scan.ts +27 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +70 -6
- package/src/hq/lease.ts +9 -7
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +189 -13
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +65 -21
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +107 -29
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +4 -3
- package/src/maturity/tui-slash-availability.ts +40 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +19 -5
- package/src/release-loop/feature-store.ts +59 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +98 -29
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +215 -12
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +14 -9
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +50 -6
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +325 -47
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +6 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +45 -16
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +20 -2
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +72 -4
- package/src/user-config.ts +64 -7
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GoalType } from '../self-implement/goal-author.js';
|
|
2
|
+
import type { PodMemoryTier } from '../task-orchestrator/surfaces/self-implement-pod.js';
|
|
3
|
+
|
|
4
|
+
export type PodMemorySelectionReason = 'explicit-option' | 'goal-type-default' | 'existing-tier';
|
|
5
|
+
|
|
6
|
+
/** Select the harness Pod tier without discarding the existing implementation/operate decision. */
|
|
7
|
+
export function selectPodMemoryTier(
|
|
8
|
+
goalType: GoalType,
|
|
9
|
+
explicitPodMemory: PodMemoryTier | undefined,
|
|
10
|
+
existingTier: PodMemoryTier = 'standard',
|
|
11
|
+
): { tier: PodMemoryTier; reason: PodMemorySelectionReason } {
|
|
12
|
+
if (explicitPodMemory !== undefined) return { tier: explicitPodMemory, reason: 'explicit-option' };
|
|
13
|
+
if (goalType === 'research' || goalType === 'document') return { tier: 'lite', reason: 'goal-type-default' };
|
|
14
|
+
return { tier: existingTier, reason: 'existing-tier' };
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
3
|
+
|
|
4
|
+
const REPOSITORY_NAME = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
5
|
+
|
|
6
|
+
export interface RepositoryNameOptions {
|
|
7
|
+
/** Explicit --repo value. */
|
|
8
|
+
repo?: string;
|
|
9
|
+
config?: Pick<UserConfig, 'harness'>;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
executeGh?: (args: string[]) => string;
|
|
13
|
+
/** Test seam. Default asks git whether cwd is a work tree. */
|
|
14
|
+
insideGitWorkTree?: (cwd: string) => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolve the GitHub owner/name without asking gh outside a git checkout. */
|
|
18
|
+
export function resolveRepositoryName(options: RepositoryNameOptions = {}): string {
|
|
19
|
+
const env = options.env ?? process.env;
|
|
20
|
+
const cwd = options.cwd ?? process.cwd();
|
|
21
|
+
const valid = (source: string, candidate: string | undefined): string | undefined => {
|
|
22
|
+
if (!candidate?.trim()) return undefined;
|
|
23
|
+
const name = candidate.trim();
|
|
24
|
+
if (!REPOSITORY_NAME.test(name) || name.includes('..')) {
|
|
25
|
+
throw new Error(`invalid ${source} (expected owner/name): ${candidate}`);
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
};
|
|
29
|
+
const flag = valid('--repo', options.repo);
|
|
30
|
+
if (flag) return flag;
|
|
31
|
+
const configured = valid('harness.repo', (options.config ?? getUserConfig()).harness?.repo);
|
|
32
|
+
if (configured) return configured;
|
|
33
|
+
const inherited = valid('GH_REPO', env.GH_REPO);
|
|
34
|
+
if (inherited) return inherited;
|
|
35
|
+
const insideGit = options.insideGitWorkTree ?? ((dir) => {
|
|
36
|
+
try {
|
|
37
|
+
return execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
38
|
+
cwd: dir, encoding: 'utf8', timeout: 5_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
}).trim() === 'true';
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
});
|
|
42
|
+
if (insideGit(cwd)) {
|
|
43
|
+
try {
|
|
44
|
+
const name = (options.executeGh ?? ((args) => execFileSync('gh', args, {
|
|
45
|
+
cwd, encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'], env,
|
|
46
|
+
})))(['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner']).trim();
|
|
47
|
+
if (REPOSITORY_NAME.test(name) && !name.includes('..')) return name;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
const line = message.split(/\r?\n/).map((row) => row.trim()).find((row) => row && !/^at\s/.test(row));
|
|
51
|
+
throw new Error(line || '저장소 이름 없음 — --repo 또는 harness.repo 설정');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error('저장소 이름 없음 — --repo 또는 harness.repo 설정');
|
|
55
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Read-only HQ fence inventory: use the installed crontab, never install a proposed replacement.
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import { FENCE_ROLES } from './hq.js';
|
|
4
|
+
import { inventoryCrontab, listSchedules, openSchedulesDb, parseCronLine, readCrontab } from '../domains/schedule-registry.js';
|
|
5
|
+
import { cronLaunchedActionsWithFence, listAllLoops, shellSegments, type LoopRegistryOptions } from '../loops/registry.js';
|
|
6
|
+
|
|
7
|
+
type SuggestedRole = 'cron' | 'seat-loop' | 'release-run' | 'conatus' | 'git-push' | 'ledger-cli' | 'unknown';
|
|
8
|
+
export interface UnfencedCron {
|
|
9
|
+
raw: string;
|
|
10
|
+
cron: string;
|
|
11
|
+
command: string;
|
|
12
|
+
recommendedRole: SuggestedRole;
|
|
13
|
+
suggestedLine: string;
|
|
14
|
+
/** Unknown roles are not executable fence arguments; the operator must choose a role. */
|
|
15
|
+
manualReview?: true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only infer a specific role where the launched command names that activity. */
|
|
19
|
+
function roleFor(command: string): SuggestedRole {
|
|
20
|
+
const launched = shellSegments(command).map(segment => segment.replace(/^cd\s+\S+\s*&&\s*/, '').replace(/(?:^|\s)(?:--message|--label|--note)\s+(?:'[^']*'|"[^"]*")/g, '')).join(' ');
|
|
21
|
+
if (/(?:^|[\s;&|])(?:\S*\/)?git\s+push\b|(?:^|[\s/])git-push\b|tree-sync\.sh\b/i.test(launched)) return 'git-push';
|
|
22
|
+
if (/(?:^|[\s/])(?:seat-loop|(?:op|tc|mk|ux)-seat|coord-bridge)(?:[.\s/-]|$)/i.test(launched)) return 'seat-loop';
|
|
23
|
+
if (/(?:^|[\s/])(?:release-run|landing-heal)(?:[.\s/-]|$)|\brelease\s+(?:run|publish|place|rebalance)\b/i.test(launched)) return 'release-run';
|
|
24
|
+
if (/(?:^|[\s/])conatus(?:[.\s/-]|$)/i.test(launched)) return 'conatus';
|
|
25
|
+
if (/\b(?:ledger|checklist)\s+(?:set|write|update|add|create|delete)\b|(?:^|[\s/])ledger-cli(?:[.\s/-]|$)/i.test(launched)) return 'ledger-cli';
|
|
26
|
+
if (/(?:^|[\s/])(?:steward|draft-cleanup|test-diet|contract-coordinator|market-posture|mission-request-judge)(?:[.\s/-]|$)/i.test(launched)) return 'cron';
|
|
27
|
+
if (/\b(?:graph\s+run|harness\s+queue\s+tick|card\s+intake-scan|flow\s+tick)\b/i.test(launched)) return 'cron';
|
|
28
|
+
return 'unknown';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const READ_ONLY = /^(?:hq (?:heartbeat|arbiter-check|seen|lease status|fence-audit)|(?:logs|config) (?:query|get|list)|loop (?:status|list)|card list|harness queue list)(?: |$)/i;
|
|
32
|
+
/** `hq fence --role <x>` with a role the CLI rejects never runs the payload fenced. */
|
|
33
|
+
function isFenceRole(role: string | undefined): boolean {
|
|
34
|
+
return !!role && (FENCE_ROLES as readonly string[]).includes(role);
|
|
35
|
+
}
|
|
36
|
+
function isReadOnly(script: string, args: string[]): boolean {
|
|
37
|
+
if (/^(?:cd|echo|printf|date|true)$/.test(script.split('/').pop() ?? '')) return true;
|
|
38
|
+
if (!/^(?:elanous|elanous\.mjs|eln)$/.test(script.split('/').pop() ?? '')) return false;
|
|
39
|
+
return READ_ONLY.test(args.slice(0, 3).join(' '));
|
|
40
|
+
}
|
|
41
|
+
const quote = (command: string) => `'${command.replaceAll("'", "'\"'\"'")}'`;
|
|
42
|
+
|
|
43
|
+
/** Reuse the installed loop inventory without touching the production schedule DB. A registry fenceRole
|
|
44
|
+
* may describe only one job or segment, so each launched action is checked independently below. */
|
|
45
|
+
export function hqFenceAudit(deps: {
|
|
46
|
+
read?: () => string;
|
|
47
|
+
loops?: typeof listAllLoops;
|
|
48
|
+
loopOptions?: LoopRegistryOptions;
|
|
49
|
+
log?: typeof debug.log;
|
|
50
|
+
} = {}): UnfencedCron[] {
|
|
51
|
+
const text = (deps.read ?? readCrontab)();
|
|
52
|
+
const parsed = text.split('\n').map(raw => ({ raw, job: parseCronLine(raw) })).filter(
|
|
53
|
+
(entry): entry is { raw: string; job: { cron: string; command: string } } => entry.job !== null,
|
|
54
|
+
);
|
|
55
|
+
// Inventory into an in-memory registry so the production schedule DB is never written by an audit.
|
|
56
|
+
let schedules: ReturnType<typeof listSchedules> = [];
|
|
57
|
+
try {
|
|
58
|
+
const db = openSchedulesDb(':memory:');
|
|
59
|
+
try {
|
|
60
|
+
inventoryCrontab(db, { crontab: text });
|
|
61
|
+
schedules = listSchedules(db);
|
|
62
|
+
} finally { db.close(); }
|
|
63
|
+
} catch { /* schedule metadata is advisory; the installed crontab is the source */ }
|
|
64
|
+
// Registry rows are hints only: a fenceRole on one segment must not hide an unfenced sibling.
|
|
65
|
+
// A broken graph or schedule inventory must not prevent auditing the installed crontab.
|
|
66
|
+
let loops: ReturnType<typeof listAllLoops> = [];
|
|
67
|
+
try { loops = (deps.loops ?? listAllLoops)({ ...deps.loopOptions, schedules }); } catch { /* cron inspection remains available */ }
|
|
68
|
+
const results: UnfencedCron[] = [];
|
|
69
|
+
for (const { raw, job: { cron, command } } of parsed) {
|
|
70
|
+
const launches = cronLaunchedActionsWithFence(command);
|
|
71
|
+
const unfenced = launches.filter(({ script, args, fenceRole }) => script && !isFenceRole(fenceRole) && !isReadOnly(script, args));
|
|
72
|
+
if (!unfenced.length) continue;
|
|
73
|
+
const unfencedCommand = unfenced.map(({ script, args }) => [script, ...args].join(' ')).join(' ; ');
|
|
74
|
+
const registered = loops.find(loop => loop.cron === cron && loop.command === command);
|
|
75
|
+
// An unfenced registered orchestrator can supply a role for an opaque launch.
|
|
76
|
+
// If the aggregate inventory carries a fenceRole from another segment, do not
|
|
77
|
+
// borrow that segment's classification for the unfenced sibling.
|
|
78
|
+
const inferred = roleFor(unfencedCommand);
|
|
79
|
+
const recommendedRole = inferred === 'unknown' && registered?.kind === 'orchestrator' && !registered.fenceRole ? 'cron' : inferred;
|
|
80
|
+
results.push({ raw, cron, command, recommendedRole,
|
|
81
|
+
...(recommendedRole === 'unknown' ? { manualReview: true as const } : {}),
|
|
82
|
+
suggestedLine: recommendedRole === 'unknown'
|
|
83
|
+
? `# manual review (unknown role): ${cron} ${command}`
|
|
84
|
+
: `${cron} hq-fence ${recommendedRole} ${quote(command)}` });
|
|
85
|
+
}
|
|
86
|
+
try { (deps.log ?? debug.log.bind(debug))('hq.fence-audit', 'listed', {
|
|
87
|
+
unfenced: results.length, unknownRole: results.filter(row => row.recommendedRole === 'unknown').length,
|
|
88
|
+
}); } catch { /* observation is fail-soft */ }
|
|
89
|
+
return results;
|
|
90
|
+
}
|
package/src/hq/hq.ts
CHANGED
|
@@ -4,10 +4,14 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync,
|
|
|
4
4
|
import { homedir, hostname } from 'node:os';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
import { writeStdoutJson } from '../cli/stdout-json.js';
|
|
7
9
|
import { debug } from '../debug/log.js';
|
|
8
10
|
import { DEFAULT_NEXUS_HTTP_PORT } from '../nexus/default-port.js';
|
|
9
11
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
10
12
|
import { getUserConfig, type HqConfig, type HqFenceRole } from '../user-config.js';
|
|
13
|
+
import { promoteHq, proposeHqPromotion, writeOpSeatRequest } from './promote.js';
|
|
14
|
+
import type { HqOpRequestWriter, PromoteResult } from './promote.js';
|
|
11
15
|
import {
|
|
12
16
|
DEFAULT_TTL_SECONDS, decideAcquire, decideArbiterCheck, decideFence, decideRelease, decideRenew, defaultSshRunner,
|
|
13
17
|
fileLeaseStore, leaseExpired, parseLease, probeReachable, recordView, serializeLease, sshLeaseStore, sshReachable, localShellRunner,
|
|
@@ -16,6 +20,21 @@ import {
|
|
|
16
20
|
|
|
17
21
|
export const FENCE_ROLES: readonly HqFenceRole[] = ['telegram-poller', 'cron', 'seat-loop', 'release-run', 'conatus', 'git-push', 'ledger-cli'];
|
|
18
22
|
|
|
23
|
+
/** Register only the read-only move inventory; keep it outside lease/fence mutation flows. */
|
|
24
|
+
export function registerHqMovePlanCommand(hqCmd: Command): void {
|
|
25
|
+
hqCmd.command('move-plan').description('현재 호스트의 작업·launchd 및 대상 호스트 파일을 조회(읽기 전용)')
|
|
26
|
+
.requiredOption('--to <host>', '이동 대상 호스트')
|
|
27
|
+
.option('--json', 'JSON 출력')
|
|
28
|
+
.action(async (opts: { to: string; json?: boolean }) => {
|
|
29
|
+
try {
|
|
30
|
+
const { hqMovePlan, formatMovePlan } = await import('./move-plan.js');
|
|
31
|
+
const result = hqMovePlan(opts.to);
|
|
32
|
+
if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
|
|
33
|
+
else console.log(formatMovePlan(result));
|
|
34
|
+
} catch (error) { console.error(`hq move-plan: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export interface HqDeps {
|
|
20
39
|
config?: HqConfig;
|
|
21
40
|
store?: LeaseStore;
|
|
@@ -29,6 +48,9 @@ export interface HqDeps {
|
|
|
29
48
|
seenPath?: string;
|
|
30
49
|
now?: () => number;
|
|
31
50
|
log?: typeof debug.log;
|
|
51
|
+
/** Injection points for the OP seat request and the promotion runbook; production uses durable defaults. */
|
|
52
|
+
opRequest?: HqOpRequestWriter;
|
|
53
|
+
promote?: (host: string, apply: boolean) => PromoteResult;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
interface Resolved { me: string; arbiter: string; standby: string; ttl: number; failOpen: Set<HqFenceRole>; store: LeaseStore; ssh: SshRunner; probe: HostProbe; localPath: string; seenPath: string; now: () => number; log: typeof debug.log }
|
|
@@ -184,7 +206,7 @@ function mutate(r: Resolved, decide: (record: LeaseRecord | null, now: number) =
|
|
|
184
206
|
}
|
|
185
207
|
|
|
186
208
|
export type LeaseAction = 'acquire' | 'renew' | 'status' | 'release';
|
|
187
|
-
export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: string } = {}) {
|
|
209
|
+
export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: string; expectedHolder?: string; expectedGeneration?: number } = {}) {
|
|
188
210
|
const r0 = resolve(deps);
|
|
189
211
|
const r = opts.host ? { ...r0, me: opts.host } : r0;
|
|
190
212
|
if (action === 'status') {
|
|
@@ -193,9 +215,15 @@ export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: s
|
|
|
193
215
|
noteSeen(r, record);
|
|
194
216
|
return { ok: true as const, action, record, ageSeconds: record ? now - record.renewedAt : null, expired: record ? leaseExpired(record, now) : null };
|
|
195
217
|
}
|
|
218
|
+
if (action === 'release' && ((opts.expectedHolder === undefined) !== (opts.expectedGeneration === undefined)
|
|
219
|
+
|| (opts.expectedGeneration !== undefined && (!Number.isSafeInteger(opts.expectedGeneration) || opts.expectedGeneration < 1))
|
|
220
|
+
|| (opts.expectedHolder !== undefined && !opts.expectedHolder)))
|
|
221
|
+
throw new Error('hq lease release: expected holder and positive generation must be supplied together');
|
|
196
222
|
const local = readLocal(r.localPath);
|
|
223
|
+
const expected = opts.expectedHolder !== undefined && opts.expectedGeneration !== undefined
|
|
224
|
+
? { holder: opts.expectedHolder, generation: opts.expectedGeneration } : undefined;
|
|
197
225
|
const result = mutate(r, (record, now) => action === 'acquire' ? decideAcquire(record, r.me, now, r.ttl)
|
|
198
|
-
: action === 'renew' ? decideRenew(record, r.me, local.generation, now) : decideRelease(record, r.me));
|
|
226
|
+
: action === 'renew' ? decideRenew(record, r.me, local.generation, now) : decideRelease(record, r.me, expected));
|
|
199
227
|
noteSeen(r, result.record);
|
|
200
228
|
if (result.ok && action !== 'release') writeLocal(r.localPath, { holder: result.record.holder, generation: result.record.generation, confirmedAt: r.now(), ttlSeconds: result.record.ttlSeconds });
|
|
201
229
|
if (result.ok && action === 'release') writeLocal(r.localPath, { ...local, holder: undefined, confirmedAt: undefined });
|
|
@@ -247,19 +275,55 @@ export function hqHeartbeat(deps: HqDeps = {}) {
|
|
|
247
275
|
|
|
248
276
|
/**
|
|
249
277
|
* Arbiter check (cron on cloud-vm every 10 min, next to the file): probe the holder over the tailnet
|
|
250
|
-
* (`tailscale ping` ⊕ nexus health · no ssh into home machines)
|
|
278
|
+
* (`tailscale ping` ⊕ nexus health · no ssh into home machines); propose to OP by default.
|
|
251
279
|
*/
|
|
252
280
|
export function hqArbiterCheck(deps: HqDeps & { leasePath?: string } = {}) {
|
|
253
281
|
const r0 = resolve(deps);
|
|
254
282
|
const r = deps.store ? r0 : { ...r0, store: fileLeaseStore(deps.leasePath ?? join(process.env.HOME ?? '', '.elanous-hq', 'lease.json')) };
|
|
255
|
-
let outcome: { promoted: boolean; streak: number; holder?: string; generation?: number; ping?: boolean; health?: boolean } = { promoted: false, streak: 0 };
|
|
283
|
+
let outcome: { promoted: boolean; streak: number; holder?: string; generation?: number; ping?: boolean; health?: boolean; runbookFailed?: string } = { promoted: false, streak: 0 };
|
|
284
|
+
let proposal: { holder: string; generation: number; streak: number } | undefined;
|
|
285
|
+
const config = deps.config ?? getUserConfig().hq ?? {};
|
|
286
|
+
const threshold = config.autoPromote?.streak ?? 3;
|
|
287
|
+
const autoPromote = config.autoPromote?.enabled === true;
|
|
288
|
+
let leaseMoved: { from: string; to: string; generation: number } | undefined;
|
|
256
289
|
const result = mutate(r, (record, now) => {
|
|
257
290
|
if (!record) return { ok: false, reason: 'no lease' };
|
|
258
291
|
const seen = probeReachable(record.holder, r.probe);
|
|
259
|
-
const checked = decideArbiterCheck(record, { arbiter: r.me, standby: r.standby, arbiterReachesHolder: seen.reachable, now
|
|
260
|
-
|
|
292
|
+
const checked = decideArbiterCheck(record, { arbiter: r.me, standby: r.standby, arbiterReachesHolder: seen.reachable, now,
|
|
293
|
+
promote: autoPromote, promoteAfterChecks: threshold });
|
|
294
|
+
proposal = !autoPromote && !seen.reachable && record.holder !== r.standby && checked.streak >= threshold
|
|
295
|
+
? { holder: record.holder, generation: record.generation, streak: checked.streak } : undefined;
|
|
296
|
+
// promoteHq's own precondition is «lease holder = target», so the lease moves first and the runbook follows.
|
|
297
|
+
if (checked.promoted) leaseMoved = { from: record.holder, to: checked.next.holder, generation: checked.next.generation };
|
|
298
|
+
outcome = { promoted: false, streak: checked.streak, holder: checked.next.holder, generation: checked.next.generation, ping: seen.ping, health: seen.health };
|
|
261
299
|
return { ok: true, next: checked.next };
|
|
262
300
|
});
|
|
301
|
+
const moved = leaseMoved as { from: string; to: string; generation: number } | undefined; // assigned inside the mutate callback
|
|
302
|
+
if (result.ok && moved) {
|
|
303
|
+
let promotion: PromoteResult | undefined;
|
|
304
|
+
let failure: string | undefined;
|
|
305
|
+
try {
|
|
306
|
+
promotion = (deps.promote ?? ((host: string, apply: boolean) => promoteHq(host, apply)))(moved.to, true);
|
|
307
|
+
if (!promotion.ok) failure = promotion.lines.filter((line) => line.status === 'failed').map((line) => `${line.step}: ${line.measurement}`).join(' · ') || 'runbook returned ok=false';
|
|
308
|
+
} catch (error) { failure = String(error); }
|
|
309
|
+
if (failure === undefined) outcome = { ...outcome, promoted: true };
|
|
310
|
+
else {
|
|
311
|
+
// The lease already names the standby but the services did not move — never report this as a promotion.
|
|
312
|
+
outcome = { ...outcome, promoted: false, runbookFailed: failure };
|
|
313
|
+
try { r.log('hq.arbiter', 'promote-runbook-failed', { ...moved, error: failure }); } catch { /* observation is fail-soft */ }
|
|
314
|
+
try {
|
|
315
|
+
(deps.opRequest ?? ((request) => writeOpSeatRequest(request)))({
|
|
316
|
+
key: `hq:promote-failed:${moved.to}:${moved.generation}`,
|
|
317
|
+
text: `자동 승격 런북 실패 — 임대는 ${moved.to}(세대 ${moved.generation})로 옮겨졌지만 서비스 이전이 끝나지 않았다: ${failure}. OP 가 \`eln hq promote --to ${moved.to}\` 로 확인한다.`,
|
|
318
|
+
});
|
|
319
|
+
} catch (error) { try { r.log('hq.arbiter', 'promote-request-failed', { ...moved, error: String(error) }); } catch { /* fail-soft */ } }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const proposed = proposal as { holder: string; generation: number; streak: number } | undefined;
|
|
323
|
+
if (result.ok && proposed) {
|
|
324
|
+
try { proposeHqPromotion({ ...proposed, standby: r.standby }, { request: deps.opRequest, log: r.log }); }
|
|
325
|
+
catch (error) { try { r.log('hq.arbiter', 'promote-proposal-failed', { ...proposed, error: String(error) }); } catch { /* observation is fail-soft */ } }
|
|
326
|
+
}
|
|
263
327
|
observe(r, 'hq.lease', outcome.promoted ? 'promoted' : 'arbiter-check', { ...outcome, ok: result.ok });
|
|
264
328
|
return { ok: result.ok, ...outcome };
|
|
265
329
|
}
|
package/src/hq/lease.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// HQ lease (본부 임대) — single writer across mbp · node-b · cloud-vm with a 2-of-3 quorum (OP 10-04 08:33 · 08:40).
|
|
2
|
-
// The arbiter (cloud-vm) keeps one record;
|
|
2
|
+
// The arbiter (cloud-vm) keeps one record; promotion requires an explicit opt-in and consecutive
|
|
3
3
|
// checks in which neither the arbiter nor the standby can reach the holder. Fenced work carries the generation.
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -39,8 +39,8 @@ export const leaseExpired = (record: LeaseRecord, now: number): boolean => recor
|
|
|
39
39
|
/** acquire: only when there is no lease or it has expired; a new holder gets the next generation. */
|
|
40
40
|
export function decideAcquire(record: LeaseRecord | null, me: string, now: number, ttlSeconds = DEFAULT_TTL_SECONDS):
|
|
41
41
|
{ ok: true; next: LeaseRecord } | { ok: false; reason: string } {
|
|
42
|
-
if (record && record.holder === me && !leaseExpired(record, now)) return { ok: true, next: { ...record, renewedAt: now, ttlSeconds } };
|
|
43
|
-
if (record && !leaseExpired(record, now)) return { ok: false, reason: `held by ${record.holder} (generation ${record.generation})` };
|
|
42
|
+
if (record && record.renewedAt !== 0 && record.holder === me && !leaseExpired(record, now)) return { ok: true, next: { ...record, renewedAt: now, ttlSeconds } };
|
|
43
|
+
if (record && record.renewedAt !== 0 && !leaseExpired(record, now)) return { ok: false, reason: `held by ${record.holder} (generation ${record.generation})` };
|
|
44
44
|
return { ok: true, next: { holder: me, generation: (record?.generation ?? 0) + 1, acquiredAt: now, renewedAt: now, ttlSeconds, views: {}, dualUnreachableStreak: 0 } };
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -52,8 +52,10 @@ export function decideRenew(record: LeaseRecord | null, me: string, generation:
|
|
|
52
52
|
return { ok: true, next: { ...record, renewedAt: now, dualUnreachableStreak: 0 } };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function decideRelease(record: LeaseRecord | null, me: string): { ok: true; next: LeaseRecord } | { ok: false; reason: string } {
|
|
55
|
+
export function decideRelease(record: LeaseRecord | null, me: string, expected?: { holder: string; generation: number }): { ok: true; next: LeaseRecord } | { ok: false; reason: string } {
|
|
56
56
|
if (!record || record.holder !== me) return { ok: false, reason: record ? `held by ${record.holder}` : 'no lease' };
|
|
57
|
+
if (expected && (record.holder !== expected.holder || record.generation !== expected.generation))
|
|
58
|
+
return { ok: false, reason: `lease changed: expected ${expected.holder} generation ${expected.generation}, observed ${record.holder} generation ${record.generation}` };
|
|
57
59
|
return { ok: true, next: { ...record, renewedAt: 0 } };
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -64,9 +66,9 @@ export function recordView(record: LeaseRecord, observer: string, reachable: boo
|
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
68
|
* Arbiter check (rule ②). Records the arbiter's own view; counts a dual-unreachable check only when the standby's
|
|
67
|
-
* fresh view of the same holder also says unreachable;
|
|
69
|
+
* fresh view of the same holder also says unreachable; promotion is optional and its threshold is configurable.
|
|
68
70
|
*/
|
|
69
|
-
export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string; standby: string; arbiterReachesHolder: boolean; now: number }):
|
|
71
|
+
export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string; standby: string; arbiterReachesHolder: boolean; now: number; promote?: boolean; promoteAfterChecks?: number }):
|
|
70
72
|
{ next: LeaseRecord; promoted: boolean; streak: number } {
|
|
71
73
|
let next = recordView(record, input.arbiter, input.arbiterReachesHolder, input.now);
|
|
72
74
|
const view = next.views?.[input.standby];
|
|
@@ -74,7 +76,7 @@ export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string
|
|
|
74
76
|
const dual = !input.arbiterReachesHolder && standbyDown && record.holder !== input.standby;
|
|
75
77
|
const streak = dual ? (record.dualUnreachableStreak ?? 0) + 1 : 0;
|
|
76
78
|
next = { ...next, dualUnreachableStreak: streak, lastCheckAt: input.now };
|
|
77
|
-
if (streak < PROMOTE_AFTER_CHECKS) return { next, promoted: false, streak };
|
|
79
|
+
if (input.promote === false || streak < (input.promoteAfterChecks ?? PROMOTE_AFTER_CHECKS)) return { next, promoted: false, streak };
|
|
78
80
|
return {
|
|
79
81
|
next: { holder: input.standby, generation: record.generation + 1, acquiredAt: input.now, renewedAt: input.now, ttlSeconds: record.ttlSeconds, views: {}, dualUnreachableStreak: 0, lastCheckAt: input.now, promotedFrom: record.holder },
|
|
80
82
|
promoted: true, streak,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Read-only inventory of work tied to the current HQ host before moving it.
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir, hostname } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { inventoryCrontab, listSchedules, openSchedulesDb, parseCronLine } from '../domains/schedule-registry.js';
|
|
7
|
+
import { cronLaunchedActionsWithFence, listAllLoops, type AllLoopEntry, type LoopRegistryOptions } from '../loops/registry.js';
|
|
8
|
+
import { defaultSshRunner, type SshRunner } from './lease.js';
|
|
9
|
+
import { FENCE_ROLES } from './hq.js';
|
|
10
|
+
|
|
11
|
+
type Measurement<T> = { status: 'measured'; value: T } | { status: '못 잼'; reason: string };
|
|
12
|
+
interface MovePlan {
|
|
13
|
+
to: string;
|
|
14
|
+
from: string | null;
|
|
15
|
+
unfencedCron: Measurement<Array<{ raw: string; cron: string; command: string }>>;
|
|
16
|
+
hostLoops: Measurement<AllLoopEntry[]>;
|
|
17
|
+
targetHost: Measurement<{ exists: boolean; name: string | null }>;
|
|
18
|
+
nexusLaunchd: Measurement<{ present: boolean; measuredOn: 'local' }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type CommandResult = { status: number | null; stdout: string; stderr: string };
|
|
22
|
+
interface MovePlanDeps {
|
|
23
|
+
crontab?: () => CommandResult;
|
|
24
|
+
loops?: typeof listAllLoops;
|
|
25
|
+
loopOptions?: LoopRegistryOptions;
|
|
26
|
+
ssh?: SshRunner;
|
|
27
|
+
launchctl?: () => CommandResult;
|
|
28
|
+
localHost?: () => string;
|
|
29
|
+
hostFile?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const measured = <T>(value: T): Measurement<T> => ({ status: 'measured', value });
|
|
33
|
+
const unknown = <T>(reason: string): Measurement<T> => ({ status: '못 잼', reason });
|
|
34
|
+
const run = (command: string, args: string[]): CommandResult => {
|
|
35
|
+
const r = spawnSync(command, args, { encoding: 'utf8', timeout: 15_000, env: process.env });
|
|
36
|
+
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? String(r.error ?? '') };
|
|
37
|
+
};
|
|
38
|
+
const message = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
39
|
+
|
|
40
|
+
// Only known HQ jobs qualify by script name; a random scripts/*.ts is not HQ work.
|
|
41
|
+
const HQ_SCRIPTS = new Set([
|
|
42
|
+
'steward.ts', 'draft-cleanup.ts', 'test-diet.ts', 'contract-coordinator.ts',
|
|
43
|
+
'market-posture.ts', 'mission-request-judge.ts',
|
|
44
|
+
]);
|
|
45
|
+
function isHqAction(script: string): boolean {
|
|
46
|
+
if (/(?:^|\/)(?:eln|elanous|elanous\.mjs)$/i.test(script)) return true;
|
|
47
|
+
if (/(?:^|\/)\.elanous(?:\/|$)/i.test(script)) return true;
|
|
48
|
+
const match = script.match(/(?:^|\/)scripts\/([^/\s]+\.ts)$/);
|
|
49
|
+
return !!match && HQ_SCRIPTS.has(match[1]!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function hqMovePlan(to: string, deps: MovePlanDeps = {}): MovePlan {
|
|
53
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(to) || to.includes('..')) throw new Error('hq move-plan: invalid host');
|
|
54
|
+
let unfencedCron: MovePlan['unfencedCron'];
|
|
55
|
+
let hostLoops: MovePlan['hostLoops'];
|
|
56
|
+
let localName: string | undefined;
|
|
57
|
+
let localHostError: string | undefined;
|
|
58
|
+
try {
|
|
59
|
+
const value = readFileSync(deps.hostFile ?? join(homedir(), '.elanous-hq', 'host'), 'utf8');
|
|
60
|
+
const name = value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
61
|
+
if (!name || name !== name.trim() || /[\r\n]/.test(name)) throw new Error('invalid local HQ host file');
|
|
62
|
+
localName = name;
|
|
63
|
+
} catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') localHostError = message(error); }
|
|
64
|
+
const from = deps.localHost?.() ?? localName ?? null;
|
|
65
|
+
let text = '';
|
|
66
|
+
try {
|
|
67
|
+
const r = (deps.crontab ?? (() => run('crontab', ['-l'])))();
|
|
68
|
+
if (r.status !== 0 && !(r.status === 1 && /no crontab for /i.test(r.stderr))) {
|
|
69
|
+
throw new Error(r.stderr.trim() || `crontab -l exit=${r.status}`);
|
|
70
|
+
}
|
|
71
|
+
text = r.status === 0 ? r.stdout : '';
|
|
72
|
+
const rows = text.split('\n').map(raw => ({ raw, parsed: parseCronLine(raw) }))
|
|
73
|
+
.filter((row): row is { raw: string; parsed: { cron: string; command: string } } => row.parsed !== null);
|
|
74
|
+
unfencedCron = measured(rows.filter(({ parsed }) =>
|
|
75
|
+
cronLaunchedActionsWithFence(parsed.command).some(action =>
|
|
76
|
+
!!action.script && !(FENCE_ROLES as readonly string[]).includes(action.fenceRole ?? '')
|
|
77
|
+
&& isHqAction(action.script)))
|
|
78
|
+
.map(({ raw, parsed }) => ({ raw, cron: parsed.cron, command: parsed.command })));
|
|
79
|
+
} catch (error) { unfencedCron = unknown(message(error)); }
|
|
80
|
+
try {
|
|
81
|
+
if (unfencedCron.status !== 'measured') throw new Error(unfencedCron.reason);
|
|
82
|
+
// The registry accepts an already-inventoried schedule set. Never open the production DB.
|
|
83
|
+
const db = openSchedulesDb(':memory:');
|
|
84
|
+
let schedules: ReturnType<typeof listSchedules>;
|
|
85
|
+
try { inventoryCrontab(db, { crontab: text }); schedules = listSchedules(db); }
|
|
86
|
+
finally { db.close(); }
|
|
87
|
+
if (localHostError) throw new Error(localHostError);
|
|
88
|
+
if (!from) throw new Error('local HQ host identity not confirmed (~/.elanous-hq/host missing)');
|
|
89
|
+
// Cron rows use os.hostname(); match that label only after confirming this machine's HQ identity.
|
|
90
|
+
const names = new Set([from, hostname()]);
|
|
91
|
+
hostLoops = measured((deps.loops ?? listAllLoops)({ ...deps.loopOptions, schedules })
|
|
92
|
+
.filter(loop => !!loop.host && names.has(loop.host)));
|
|
93
|
+
} catch (error) { hostLoops = unknown(message(error)); }
|
|
94
|
+
let targetHost: MovePlan['targetHost'];
|
|
95
|
+
try {
|
|
96
|
+
const r = (deps.ssh ?? defaultSshRunner)(to,
|
|
97
|
+
'if [ -f "$HOME/.elanous-hq/host" ]; then printf "present\\n"; cat "$HOME/.elanous-hq/host"; elif [ ! -e "$HOME/.elanous-hq/host" ]; then printf "absent\\n"; else exit 2; fi');
|
|
98
|
+
if (r.status !== 0) throw new Error(r.stderr.trim() || `ssh exit=${r.status}`);
|
|
99
|
+
const [presence, ...lines] = r.stdout.split('\n');
|
|
100
|
+
if (presence === 'absent') targetHost = measured({ exists: false, name: null });
|
|
101
|
+
else if (presence === 'present' && lines.join('\n').trim() && !/[\r\n]/.test(lines.join('\n').trim()))
|
|
102
|
+
targetHost = measured({ exists: true, name: lines.join('\n').trim() });
|
|
103
|
+
else throw new Error('target host file unreadable or empty');
|
|
104
|
+
} catch (error) { targetHost = unknown(message(error)); }
|
|
105
|
+
let nexusLaunchd: MovePlan['nexusLaunchd'];
|
|
106
|
+
try {
|
|
107
|
+
const r = (deps.launchctl ?? (() => run('launchctl', ['list', 'com.elanous.nexus'])))();
|
|
108
|
+
if (r.status === 0) nexusLaunchd = measured({ present: true, measuredOn: 'local' });
|
|
109
|
+
else if (r.status !== null && /Could not find service|service not found|not found/i.test(r.stderr)) nexusLaunchd = measured({ present: false, measuredOn: 'local' });
|
|
110
|
+
else throw new Error(r.stderr.trim() || `launchctl exit=${r.status}`);
|
|
111
|
+
} catch (error) { nexusLaunchd = unknown(message(error)); }
|
|
112
|
+
return { to, from, unfencedCron, hostLoops, targetHost, nexusLaunchd };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatMovePlan(plan: MovePlan): string {
|
|
116
|
+
const lines = [`hq move-plan: ${plan.from ?? '못 잼'} → ${plan.to} (read-only)`];
|
|
117
|
+
const section = <T>(name: string, result: Measurement<T>, render: (value: T) => string[]) => {
|
|
118
|
+
lines.push(`${name}: ${result.status === 'measured' ? '' : `못 잼 (${result.reason})`}`.trimEnd());
|
|
119
|
+
if (result.status === 'measured') lines.push(...render(result.value));
|
|
120
|
+
};
|
|
121
|
+
section('① unfenced elanous crontab', plan.unfencedCron, rows => rows.length ? rows.map(row => ` ${row.raw}`) : [' none']);
|
|
122
|
+
section('② local host loops (loop status --all)', plan.hostLoops, rows => rows.length ? rows.map(row => ` ${row.id} · ${row.host} · ${row.title}`) : [' none']);
|
|
123
|
+
section('③ target ~/.elanous-hq/host', plan.targetHost, host => [` ${host.exists ? host.name : 'absent'}`]);
|
|
124
|
+
section('④ local launchd com.elanous.nexus', plan.nexusLaunchd, service => [` ${service.present ? 'present' : 'absent'} (measured on local machine)`]);
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
package/src/hq/promote.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// HQ promotion runbook: read-only preview by default; all remote mutations are behind --apply.
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { getUserConfig } from '../user-config.js';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
|
|
4
8
|
import { parseLease, sshLeaseStore, defaultSshRunner, localShellRunner } from './lease.js';
|
|
5
9
|
|
|
6
10
|
export interface PromoteRunner {
|
|
@@ -295,6 +299,33 @@ export function promoteHq(host: string, apply = false, runner: PromoteRunner = d
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
/** One OP seat request (never a CEO decision card · 대표 10-05 10:03); idempotent by key. */
|
|
303
|
+
export type HqOpRequest = { key: string; text: string; [field: string]: unknown };
|
|
304
|
+
export type HqOpRequestWriter = (request: HqOpRequest) => { key: string; created: boolean };
|
|
305
|
+
|
|
306
|
+
export function writeOpSeatRequest(request: HqOpRequest, root = effectiveInstanceRoot()): { key: string; created: boolean } {
|
|
307
|
+
const path = join(root, 'seat-requests', 'requests.jsonl');
|
|
308
|
+
return withSeatRequestLedgerLock(path, () => {
|
|
309
|
+
if (listSeatRequests(root).some((row) => row.key === request.key)) return { key: request.key, created: false };
|
|
310
|
+
appendSeatRequestRows(path, [{ ...request, seat: 'OP', status: 'pending' as const, queuedAt: new Date().toISOString(), source: 'hq-arbiter' }]);
|
|
311
|
+
return { key: request.key, created: true };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Propose promotion to the OP seat for a silent lease generation; the runbook runs only under explicit opt-in (see hqArbiterCheck). */
|
|
316
|
+
export function proposeHqPromotion(
|
|
317
|
+
input: { holder: string; standby: string; generation: number; streak: number },
|
|
318
|
+
deps: { request?: HqOpRequestWriter; log?: typeof debug.log } = {},
|
|
319
|
+
): { promoted: false; requestKey: string; created: boolean } {
|
|
320
|
+
const key = `hq:promote:${input.holder}:${input.generation}`;
|
|
321
|
+
const written = (deps.request ?? writeOpSeatRequest)({ key,
|
|
322
|
+
text: `본부 ${input.holder}(임대 세대 ${input.generation}) 연속 무응답 ${input.streak}회 — ${input.standby} 승격을 OP 가 판단한다(자동 승격 꺼짐). 승격하면 \`eln hq promote --to ${input.standby} --apply\`.`,
|
|
323
|
+
holder: input.holder, standby: input.standby, generation: input.generation, streak: input.streak });
|
|
324
|
+
try { (deps.log ?? debug.log.bind(debug))('hq.arbiter', 'promote-proposed', { ...input, ref: key, mode: 'op-request', created: written.created }); }
|
|
325
|
+
catch { /* observation cannot change the proposal */ }
|
|
326
|
+
return { promoted: false, requestKey: key, created: written.created };
|
|
327
|
+
}
|
|
328
|
+
|
|
298
329
|
export function formatPromote(result: PromoteResult): string {
|
|
299
330
|
return [`hq promote --to ${result.host} ${result.apply ? '--apply' : '(dry-run)'}`,
|
|
300
331
|
...result.lines.map(line => `${line.step}: ${line.status} · ${line.measurement}${line.status === 'ready' ? ` · ${line.command}` : ''}`)].join('\n');
|