elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.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/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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/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 +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/loops/index.html +1 -1
- package/apps/pwa/out/loops/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/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- 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/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -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/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -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/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -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/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/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -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/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 +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- 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/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- 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/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -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/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- 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 +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- 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/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- 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 +187 -40
- 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 +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- 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-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- 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/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/control/page-e6b84ab07d3567d5.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/loops/page-160089123a40dd91.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/release/page-e7e10cd3386222e5.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/page-341480ed8e3c1ea8.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/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.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/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.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 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -125,12 +125,24 @@ export async function scanStoppedPrs({ runGh }: { runGh: RunGh }): Promise<Scann
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
if (import.meta.main) {
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const args = process.argv.slice(2);
|
|
129
|
+
const json = args.includes('--json');
|
|
130
|
+
const recordShadows = args.includes('--record-shadows');
|
|
131
|
+
if (args.some((arg) => arg !== '--json' && arg !== '--record-shadows') || (json && recordShadows) || args.length > 1) {
|
|
132
|
+
console.error('사용법: bun src/harness/helper-scan.ts [--json | --record-shadows]');
|
|
130
133
|
process.exitCode = 1;
|
|
131
134
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
// 열린 PR 본문 전체는 기본 버퍼(1MB)를 넘는다(10-05 운영 ENOBUFS) — 넉넉히 준다.
|
|
136
|
+
const runGh = (args: readonly string[]) => execFileSync('gh', [...args], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
137
|
+
const rows = await scanStoppedPrs({ runGh });
|
|
138
|
+
if (recordShadows) {
|
|
139
|
+
// 명시적 그림자 기록에서만 추가 PR 조회와 헬퍼 원장 쓰기를 수행한다(발사 0).
|
|
140
|
+
const { recordRepairShadows } = await import('./helper-repair.js');
|
|
141
|
+
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
142
|
+
const shadows = await recordRepairShadows(rows, { runGh, root: effectiveInstanceRoot() });
|
|
143
|
+
if (shadows.length) console.log(`수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
|
|
144
|
+
}
|
|
145
|
+
if (json) console.log(JSON.stringify(rows, null, 2));
|
|
134
146
|
else for (const row of rows) {
|
|
135
147
|
const gate = row.blockerCount ? ` · Gate 차단 ${row.blockerCount}(${row.hasPublicExportLeak ? row.blockers.find((line) => /public-export-leak/i.test(line))?.trim() ?? '✗ public-export-leak (6번째 이후)' : row.blockers[0]!.trim()})` : '';
|
|
136
148
|
console.log(`#${row.pr} ${row.category}${gate} → ${row.firstAction}`);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONEDOOR-1 — every harness run stamps which door it came through,
|
|
3
|
+
* who launched it, and whether it went through the queue.
|
|
4
|
+
*
|
|
5
|
+
* The stamp lives on the run ledger's first row (`run-origin`). A ledger
|
|
6
|
+
* written before this stamp, or a launch that named no door, reads as
|
|
7
|
+
* `unknown` — that is distinct from a count of zero.
|
|
8
|
+
*/
|
|
9
|
+
import { readdirSync } from 'node:fs';
|
|
10
|
+
import { loadRunLedger, runLedgerDir, type RunLedgerEntry } from '../self-implement/run-ledger.js';
|
|
11
|
+
|
|
12
|
+
export const LAUNCH_DOORS = [
|
|
13
|
+
'harness-say',
|
|
14
|
+
'harness-ask',
|
|
15
|
+
'dev-ask',
|
|
16
|
+
'self-implement',
|
|
17
|
+
'self-orchestrate',
|
|
18
|
+
'queue-tick',
|
|
19
|
+
'agent-mission',
|
|
20
|
+
'daemon-tool',
|
|
21
|
+
'telegram',
|
|
22
|
+
'pwa',
|
|
23
|
+
'unknown',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type LaunchDoor = (typeof LAUNCH_DOORS)[number];
|
|
27
|
+
|
|
28
|
+
export const LAUNCH_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
29
|
+
export type LaunchSeat = (typeof LAUNCH_SEATS)[number];
|
|
30
|
+
|
|
31
|
+
export interface LaunchStamp {
|
|
32
|
+
readonly entrance: LaunchDoor;
|
|
33
|
+
/** Seat, human session, or loop name — whichever the launch actually named. */
|
|
34
|
+
readonly actor: string;
|
|
35
|
+
readonly viaQueue: boolean;
|
|
36
|
+
readonly queueId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ENTRANCE_TO_DOOR: Readonly<Record<string, LaunchDoor>> = {
|
|
40
|
+
'cli-harness-say': 'harness-say',
|
|
41
|
+
'cli-harness-ask': 'harness-ask',
|
|
42
|
+
'cli-dev-ask': 'dev-ask',
|
|
43
|
+
'cli-drive': 'dev-ask',
|
|
44
|
+
'tui-slash-ask': 'dev-ask',
|
|
45
|
+
'tui-slash-dev': 'dev-ask',
|
|
46
|
+
'cli-self-implement': 'self-implement',
|
|
47
|
+
'nl-self-implement': 'self-implement',
|
|
48
|
+
'daemon-self-implement': 'daemon-tool',
|
|
49
|
+
'cli-self-orchestrate': 'self-orchestrate',
|
|
50
|
+
'nl-self-orchestrate': 'self-orchestrate',
|
|
51
|
+
'cli-harness-orchestrate': 'self-orchestrate',
|
|
52
|
+
'cli-harness-mission': 'agent-mission',
|
|
53
|
+
'agent-mission': 'agent-mission',
|
|
54
|
+
'daemon-harness-ask': 'daemon-tool',
|
|
55
|
+
'daemon-tool': 'daemon-tool',
|
|
56
|
+
telegram: 'telegram',
|
|
57
|
+
pwa: 'pwa',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const DOOR_SET = new Set<string>(LAUNCH_DOORS);
|
|
61
|
+
|
|
62
|
+
export function isLaunchDoor(value: string): value is LaunchDoor {
|
|
63
|
+
return DOOR_SET.has(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Reuse an existing entrance tag. Anything unnamed or unrecognized is `unknown`, never a zero. */
|
|
67
|
+
export function doorFromEntrance(entrance: string | undefined | null): LaunchDoor {
|
|
68
|
+
if (!entrance) return 'unknown';
|
|
69
|
+
const trimmed = entrance.trim();
|
|
70
|
+
if (!trimmed) return 'unknown';
|
|
71
|
+
if (ENTRANCE_TO_DOOR[trimmed]) return ENTRANCE_TO_DOOR[trimmed];
|
|
72
|
+
if (isLaunchDoor(trimmed)) return trimmed;
|
|
73
|
+
return 'unknown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface LaunchStampInput {
|
|
77
|
+
readonly entrance?: string | null;
|
|
78
|
+
readonly seat?: string | null;
|
|
79
|
+
readonly session?: string | null;
|
|
80
|
+
readonly loopName?: string | null;
|
|
81
|
+
readonly viaQueue?: boolean;
|
|
82
|
+
readonly queueId?: string | null;
|
|
83
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function named(value: string | null | undefined): string | undefined {
|
|
87
|
+
const trimmed = value?.trim();
|
|
88
|
+
return trimmed ? trimmed : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Seat wins, then the human session, then the loop name. A blank actor stays blank. */
|
|
92
|
+
export function resolveLaunchActor(input: LaunchStampInput, env: NodeJS.ProcessEnv = process.env): string {
|
|
93
|
+
const seat = named(input.seat) ?? named(env.ELANOUS_HARNESS_SEAT);
|
|
94
|
+
if (seat && (LAUNCH_SEATS as readonly string[]).includes(seat)) return seat;
|
|
95
|
+
return named(input.session)
|
|
96
|
+
?? named(env.ELANOUS_ORIGIN_SESSION)
|
|
97
|
+
?? named(input.loopName)
|
|
98
|
+
?? named(env.ELANOUS_LOOP_NAME)
|
|
99
|
+
?? '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveLaunchStamp(input: LaunchStampInput = {}, env: NodeJS.ProcessEnv = input.env ?? process.env): LaunchStamp {
|
|
103
|
+
const queueId = named(input.queueId) ?? named(env.ELANOUS_HARNESS_QUEUE_LAUNCH);
|
|
104
|
+
const viaQueue = input.viaQueue === true || queueId !== undefined;
|
|
105
|
+
const door = viaQueue ? 'queue-tick' : doorFromEntrance(input.entrance ?? env.ELANOUS_HARNESS_ENTRANCE);
|
|
106
|
+
return {
|
|
107
|
+
entrance: door,
|
|
108
|
+
actor: resolveLaunchActor(input, env),
|
|
109
|
+
viaQueue,
|
|
110
|
+
...(queueId ? { queueId } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Read the stamp off a ledger's first row. Missing or unreadable stamps are `unknown`. */
|
|
115
|
+
export function launchStampFromLedger(entries: readonly RunLedgerEntry[] | null | undefined): LaunchStamp {
|
|
116
|
+
const first = entries?.[0];
|
|
117
|
+
const raw = first?.data?.launch;
|
|
118
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
119
|
+
return { entrance: 'unknown', actor: '', viaQueue: false };
|
|
120
|
+
}
|
|
121
|
+
const launch = raw as Record<string, unknown>;
|
|
122
|
+
const entrance = typeof launch.entrance === 'string' && isLaunchDoor(launch.entrance) ? launch.entrance : 'unknown';
|
|
123
|
+
const queueId = typeof launch.queueId === 'string' && launch.queueId.trim() ? launch.queueId.trim() : undefined;
|
|
124
|
+
return {
|
|
125
|
+
entrance,
|
|
126
|
+
actor: typeof launch.actor === 'string' ? launch.actor : '',
|
|
127
|
+
viaQueue: launch.viaQueue === true || queueId !== undefined,
|
|
128
|
+
...(queueId ? { queueId } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface DoorRow {
|
|
133
|
+
readonly entrance: LaunchDoor;
|
|
134
|
+
readonly runs: number;
|
|
135
|
+
readonly viaQueue: number;
|
|
136
|
+
readonly outsideQueue: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface DoorTable {
|
|
140
|
+
readonly sinceMs: number;
|
|
141
|
+
readonly runs: number;
|
|
142
|
+
readonly unknown: number;
|
|
143
|
+
readonly outsideQueue: number;
|
|
144
|
+
readonly viaQueue: number;
|
|
145
|
+
readonly rows: readonly DoorRow[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface DoorTableOptions {
|
|
149
|
+
readonly dir?: string;
|
|
150
|
+
readonly sinceMs?: number;
|
|
151
|
+
readonly now?: number;
|
|
152
|
+
readonly list?: (dir: string) => string[];
|
|
153
|
+
readonly load?: (runId: string, dir: string) => RunLedgerEntry[] | null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
157
|
+
|
|
158
|
+
function entryTime(entry: RunLedgerEntry | undefined): number | undefined {
|
|
159
|
+
if (!entry?.timestamp) return undefined;
|
|
160
|
+
const parsed = Date.parse(entry.timestamp);
|
|
161
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One row per door that actually fired, plus the period totals. Untagged ledgers count as `unknown`. */
|
|
165
|
+
export function queryLaunchDoors(options: DoorTableOptions = {}): DoorTable {
|
|
166
|
+
const dir = options.dir ?? runLedgerDir();
|
|
167
|
+
const now = options.now ?? Date.now();
|
|
168
|
+
const sinceMs = options.sinceMs ?? DAY_MS;
|
|
169
|
+
const cutoff = now - sinceMs;
|
|
170
|
+
const list = options.list ?? ((path: string) => {
|
|
171
|
+
try { return readdirSync(path); } catch { return []; }
|
|
172
|
+
});
|
|
173
|
+
const load = options.load ?? ((runId: string, ledgerDir: string) => {
|
|
174
|
+
try { return loadRunLedger(runId, ledgerDir); } catch { return null; }
|
|
175
|
+
});
|
|
176
|
+
const counts = new Map<LaunchDoor, { runs: number; viaQueue: number }>();
|
|
177
|
+
let runs = 0;
|
|
178
|
+
let unknown = 0;
|
|
179
|
+
let outsideQueue = 0;
|
|
180
|
+
let viaQueue = 0;
|
|
181
|
+
for (const file of list(dir)) {
|
|
182
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
183
|
+
const runId = file.slice(0, -'.jsonl'.length);
|
|
184
|
+
const entries = load(runId, dir);
|
|
185
|
+
if (!entries || entries.length === 0) continue;
|
|
186
|
+
const started = entryTime(entries[0]);
|
|
187
|
+
if (started !== undefined && started < cutoff) continue;
|
|
188
|
+
const stamp = launchStampFromLedger(entries);
|
|
189
|
+
runs += 1;
|
|
190
|
+
if (stamp.entrance === 'unknown') unknown += 1;
|
|
191
|
+
if (stamp.viaQueue) viaQueue += 1;
|
|
192
|
+
else outsideQueue += 1;
|
|
193
|
+
const row = counts.get(stamp.entrance) ?? { runs: 0, viaQueue: 0 };
|
|
194
|
+
row.runs += 1;
|
|
195
|
+
if (stamp.viaQueue) row.viaQueue += 1;
|
|
196
|
+
counts.set(stamp.entrance, row);
|
|
197
|
+
}
|
|
198
|
+
const rows = [...counts.entries()]
|
|
199
|
+
.map(([entrance, row]) => ({
|
|
200
|
+
entrance,
|
|
201
|
+
runs: row.runs,
|
|
202
|
+
viaQueue: row.viaQueue,
|
|
203
|
+
outsideQueue: row.runs - row.viaQueue,
|
|
204
|
+
}))
|
|
205
|
+
.sort((left, right) => right.runs - left.runs || left.entrance.localeCompare(right.entrance));
|
|
206
|
+
return { sinceMs, runs, unknown, outsideQueue, viaQueue, rows };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderLaunchDoors(table: DoorTable): string {
|
|
210
|
+
const lines = [
|
|
211
|
+
`doors runs=${table.runs} outside-queue=${table.outsideQueue} via-queue=${table.viaQueue} unknown=${table.unknown}`,
|
|
212
|
+
'entrance runs via-queue outside-queue',
|
|
213
|
+
];
|
|
214
|
+
for (const row of table.rows) {
|
|
215
|
+
lines.push(`${row.entrance.padEnd(16)} ${String(row.runs).padStart(4)} ${String(row.viaQueue).padStart(9)} ${String(row.outsideQueue).padStart(13)}`);
|
|
216
|
+
}
|
|
217
|
+
if (table.rows.length === 0) lines.push('(no runs)');
|
|
218
|
+
const summed = table.rows.reduce((sum, row) => sum + row.runs, 0);
|
|
219
|
+
lines.push(`total ${String(summed).padStart(4)}`);
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const SINCE_PATTERN = /^(\d+)(m|h|d)$/;
|
|
224
|
+
|
|
225
|
+
/** `--since 1d` (also `30m`, `12h`). Anything else is refused rather than guessed. */
|
|
226
|
+
export function parseDoorSince(value: string | undefined, now = Date.now()): { sinceMs: number } | { error: string } {
|
|
227
|
+
if (value === undefined || value === '') return { sinceMs: DAY_MS };
|
|
228
|
+
const match = SINCE_PATTERN.exec(value.trim());
|
|
229
|
+
if (!match) return { error: `--since 는 30m · 12h · 1d 형태여야 합니다: ${value}` };
|
|
230
|
+
const amount = Number(match[1]);
|
|
231
|
+
const unit = match[2] === 'm' ? 60_000 : match[2] === 'h' ? 3_600_000 : DAY_MS;
|
|
232
|
+
if (!Number.isSafeInteger(amount) || amount < 1) return { error: `--since 는 1 이상이어야 합니다: ${value}` };
|
|
233
|
+
const sinceMs = amount * unit;
|
|
234
|
+
if (sinceMs > now) return { sinceMs };
|
|
235
|
+
return { sinceMs };
|
|
236
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
3
|
+
import { recordRunExit } from './harness-incidents.js';
|
|
4
|
+
import { appendRunLedgerEntry } from '../self-implement/run-ledger.js';
|
|
5
|
+
|
|
6
|
+
const root = prodInstanceRoot();
|
|
7
|
+
if (effectiveInstanceRoot() !== root) throw new Error('not the effective production root');
|
|
8
|
+
const before = existsSync(root) ? readdirSync(root) : null;
|
|
9
|
+
recordRunExit({ runId: 'run-test', reason: 'signal', status: 1, signal: null, at: new Date().toISOString() }, process.argv[2] ?? root);
|
|
10
|
+
appendRunLedgerEntry({ runId: 'run-test', event: 'start', data: {} }, process.argv[3] ?? `${root}/run-ledger`);
|
|
11
|
+
const after = existsSync(root) ? readdirSync(root) : null;
|
|
12
|
+
console.log(JSON.stringify({ env: { NODE_ENV: process.env.NODE_ENV, ELANOUS_TEST_HOME: process.env.ELANOUS_TEST_HOME, HOME: process.env.HOME, ELANOUS_STATE_DIR: process.env.ELANOUS_STATE_DIR }, root, effectiveInstanceRoot: effectiveInstanceRoot(), before, after }));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
|
|
6
|
+
function physicalPath(path: string): string {
|
|
7
|
+
const absolute = resolve(path);
|
|
8
|
+
try {
|
|
9
|
+
return realpathSync(absolute);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
12
|
+
const parent = dirname(absolute);
|
|
13
|
+
if (parent === absolute) throw error;
|
|
14
|
+
return resolve(physicalPath(parent), relative(parent, absolute));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Refuse a test's write to the production instance, including an explicitly supplied ledger directory. */
|
|
19
|
+
export function refuseProductionLedgerWriteInTest(
|
|
20
|
+
root: string,
|
|
21
|
+
store: string,
|
|
22
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
23
|
+
productionRoot = prodInstanceRoot(),
|
|
24
|
+
instanceRoot = effectiveInstanceRoot(),
|
|
25
|
+
): boolean {
|
|
26
|
+
if (env.NODE_ENV !== 'test' && !env.ELANOUS_TEST_HOME) return false;
|
|
27
|
+
const target = physicalPath(root);
|
|
28
|
+
const production = physicalPath(productionRoot);
|
|
29
|
+
const withinProduction = relative(production, target);
|
|
30
|
+
if (withinProduction === '..' || withinProduction.startsWith(`..${sep}`) || isAbsolute(withinProduction)) return false;
|
|
31
|
+
try {
|
|
32
|
+
debug.log('harness.incidents', 'write-refused', { store, root: target, effectiveInstanceRoot: instanceRoot });
|
|
33
|
+
} catch { /* Observation cannot reopen a closed write boundary. */ }
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
@@ -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 }
|
|
@@ -247,19 +269,55 @@ export function hqHeartbeat(deps: HqDeps = {}) {
|
|
|
247
269
|
|
|
248
270
|
/**
|
|
249
271
|
* 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)
|
|
272
|
+
* (`tailscale ping` ⊕ nexus health · no ssh into home machines); propose to OP by default.
|
|
251
273
|
*/
|
|
252
274
|
export function hqArbiterCheck(deps: HqDeps & { leasePath?: string } = {}) {
|
|
253
275
|
const r0 = resolve(deps);
|
|
254
276
|
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 };
|
|
277
|
+
let outcome: { promoted: boolean; streak: number; holder?: string; generation?: number; ping?: boolean; health?: boolean; runbookFailed?: string } = { promoted: false, streak: 0 };
|
|
278
|
+
let proposal: { holder: string; generation: number; streak: number } | undefined;
|
|
279
|
+
const config = deps.config ?? getUserConfig().hq ?? {};
|
|
280
|
+
const threshold = config.autoPromote?.streak ?? 3;
|
|
281
|
+
const autoPromote = config.autoPromote?.enabled === true;
|
|
282
|
+
let leaseMoved: { from: string; to: string; generation: number } | undefined;
|
|
256
283
|
const result = mutate(r, (record, now) => {
|
|
257
284
|
if (!record) return { ok: false, reason: 'no lease' };
|
|
258
285
|
const seen = probeReachable(record.holder, r.probe);
|
|
259
|
-
const checked = decideArbiterCheck(record, { arbiter: r.me, standby: r.standby, arbiterReachesHolder: seen.reachable, now
|
|
260
|
-
|
|
286
|
+
const checked = decideArbiterCheck(record, { arbiter: r.me, standby: r.standby, arbiterReachesHolder: seen.reachable, now,
|
|
287
|
+
promote: autoPromote, promoteAfterChecks: threshold });
|
|
288
|
+
proposal = !autoPromote && !seen.reachable && record.holder !== r.standby && checked.streak >= threshold
|
|
289
|
+
? { holder: record.holder, generation: record.generation, streak: checked.streak } : undefined;
|
|
290
|
+
// promoteHq's own precondition is «lease holder = target», so the lease moves first and the runbook follows.
|
|
291
|
+
if (checked.promoted) leaseMoved = { from: record.holder, to: checked.next.holder, generation: checked.next.generation };
|
|
292
|
+
outcome = { promoted: false, streak: checked.streak, holder: checked.next.holder, generation: checked.next.generation, ping: seen.ping, health: seen.health };
|
|
261
293
|
return { ok: true, next: checked.next };
|
|
262
294
|
});
|
|
295
|
+
const moved = leaseMoved as { from: string; to: string; generation: number } | undefined; // assigned inside the mutate callback
|
|
296
|
+
if (result.ok && moved) {
|
|
297
|
+
let promotion: PromoteResult | undefined;
|
|
298
|
+
let failure: string | undefined;
|
|
299
|
+
try {
|
|
300
|
+
promotion = (deps.promote ?? ((host: string, apply: boolean) => promoteHq(host, apply)))(moved.to, true);
|
|
301
|
+
if (!promotion.ok) failure = promotion.lines.filter((line) => line.status === 'failed').map((line) => `${line.step}: ${line.measurement}`).join(' · ') || 'runbook returned ok=false';
|
|
302
|
+
} catch (error) { failure = String(error); }
|
|
303
|
+
if (failure === undefined) outcome = { ...outcome, promoted: true };
|
|
304
|
+
else {
|
|
305
|
+
// The lease already names the standby but the services did not move — never report this as a promotion.
|
|
306
|
+
outcome = { ...outcome, promoted: false, runbookFailed: failure };
|
|
307
|
+
try { r.log('hq.arbiter', 'promote-runbook-failed', { ...moved, error: failure }); } catch { /* observation is fail-soft */ }
|
|
308
|
+
try {
|
|
309
|
+
(deps.opRequest ?? ((request) => writeOpSeatRequest(request)))({
|
|
310
|
+
key: `hq:promote-failed:${moved.to}:${moved.generation}`,
|
|
311
|
+
text: `자동 승격 런북 실패 — 임대는 ${moved.to}(세대 ${moved.generation})로 옮겨졌지만 서비스 이전이 끝나지 않았다: ${failure}. OP 가 \`eln hq promote --to ${moved.to}\` 로 확인한다.`,
|
|
312
|
+
});
|
|
313
|
+
} catch (error) { try { r.log('hq.arbiter', 'promote-request-failed', { ...moved, error: String(error) }); } catch { /* fail-soft */ } }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const proposed = proposal as { holder: string; generation: number; streak: number } | undefined;
|
|
317
|
+
if (result.ok && proposed) {
|
|
318
|
+
try { proposeHqPromotion({ ...proposed, standby: r.standby }, { request: deps.opRequest, log: r.log }); }
|
|
319
|
+
catch (error) { try { r.log('hq.arbiter', 'promote-proposal-failed', { ...proposed, error: String(error) }); } catch { /* observation is fail-soft */ } }
|
|
320
|
+
}
|
|
263
321
|
observe(r, 'hq.lease', outcome.promoted ? 'promoted' : 'arbiter-check', { ...outcome, ok: result.ok });
|
|
264
322
|
return { ok: result.ok, ...outcome };
|
|
265
323
|
}
|
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';
|
|
@@ -64,9 +64,9 @@ export function recordView(record: LeaseRecord, observer: string, reachable: boo
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* 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;
|
|
67
|
+
* fresh view of the same holder also says unreachable; promotion is optional and its threshold is configurable.
|
|
68
68
|
*/
|
|
69
|
-
export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string; standby: string; arbiterReachesHolder: boolean; now: number }):
|
|
69
|
+
export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string; standby: string; arbiterReachesHolder: boolean; now: number; promote?: boolean; promoteAfterChecks?: number }):
|
|
70
70
|
{ next: LeaseRecord; promoted: boolean; streak: number } {
|
|
71
71
|
let next = recordView(record, input.arbiter, input.arbiterReachesHolder, input.now);
|
|
72
72
|
const view = next.views?.[input.standby];
|
|
@@ -74,7 +74,7 @@ export function decideArbiterCheck(record: LeaseRecord, input: { arbiter: string
|
|
|
74
74
|
const dual = !input.arbiterReachesHolder && standbyDown && record.holder !== input.standby;
|
|
75
75
|
const streak = dual ? (record.dualUnreachableStreak ?? 0) + 1 : 0;
|
|
76
76
|
next = { ...next, dualUnreachableStreak: streak, lastCheckAt: input.now };
|
|
77
|
-
if (streak < PROMOTE_AFTER_CHECKS) return { next, promoted: false, streak };
|
|
77
|
+
if (input.promote === false || streak < (input.promoteAfterChecks ?? PROMOTE_AFTER_CHECKS)) return { next, promoted: false, streak };
|
|
78
78
|
return {
|
|
79
79
|
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
80
|
promoted: true, streak,
|