@oneie/claude 0.7.0 → 0.9.0
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/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// govern.ts — the machine-wide concurrency governor, as ONE Bun program.
|
|
3
|
+
//
|
|
4
|
+
// manifest: portable
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS AS TYPESCRIPT (§ GATES item 5, text/factory-do.md)
|
|
7
|
+
// The DESIGN of the governor is proven — slots, bound, claims, the two memo
|
|
8
|
+
// keys, four-state gates. The RUNTIME was the recurring defect: bash 3.2 on
|
|
9
|
+
// macOS has no flock(1), no timeout(1) and no BASHPID; `set -m` needs job
|
|
10
|
+
// control; a watchdog subshell cannot own the sleep it forks; and the
|
|
11
|
+
// `find | sort -z | xargs` pipeline plus zsh-vs-bash word splitting produced
|
|
12
|
+
// four separate measured bugs on 2026-09-04 alone. Every one of those lives in
|
|
13
|
+
// a MECHANISM, not in a decision. So the mechanisms moved here and the
|
|
14
|
+
// decisions did not change.
|
|
15
|
+
//
|
|
16
|
+
// lib/govern.sh is now a thin shim that keeps the shell function NAMES and EXIT
|
|
17
|
+
// CODES every caller sources (gate-run.sh, do-fleet.sh, session-start.sh,
|
|
18
|
+
// tsc-cached.sh, do-reconcile.sh, machine-check.sh, the hooks …), so no caller
|
|
19
|
+
// changed. Two seams deliberately STAY in bash, because three of the six
|
|
20
|
+
// governor proofs stub them as bash functions and the acceptance is that those
|
|
21
|
+
// proofs are unchanged:
|
|
22
|
+
//
|
|
23
|
+
// _gv_alive (kill -0) — govern-claims-check.sh R1/R2 stubs it true
|
|
24
|
+
// _gv_vm_stat / _gv_swapusage / _gv_memsize — govern-mem-check.sh drives a
|
|
25
|
+
// synthetic machine through them
|
|
26
|
+
//
|
|
27
|
+
// Neither carries any of the runtime defects above (one signal, one exec), so
|
|
28
|
+
// leaving them in the shell costs nothing and keeps the red proofs honest. The
|
|
29
|
+
// shim pipes their output in: vm_stat text on stdin, liveness as
|
|
30
|
+
// GOVERN_ALIVE_PIDS when (and only when) the caller has stubbed it.
|
|
31
|
+
//
|
|
32
|
+
// Owner pid: the LOCK/CLAIM owner is the CALLING SHELL ($$ passed as
|
|
33
|
+
// GOVERN_OWNER_PID), never this short-lived bun process. A lock owned by a
|
|
34
|
+
// process that exits the moment it is taken is reaped by the next contender —
|
|
35
|
+
// which would silently break governor-doors-check.sh's re-entrancy RED half.
|
|
36
|
+
|
|
37
|
+
import { spawn } from "node:child_process";
|
|
38
|
+
import * as fs from "node:fs";
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
|
|
41
|
+
const E = process.env;
|
|
42
|
+
|
|
43
|
+
function envInt(name: string, dflt: number): number {
|
|
44
|
+
const v = E[name];
|
|
45
|
+
if (v === undefined || v === "" || !/^[0-9]+$/.test(v)) return dflt;
|
|
46
|
+
return parseInt(v, 10);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const GOVERN_DIR = E.GOVERN_DIR || `${E.TMPDIR || "/tmp"}/one-govern`;
|
|
50
|
+
const CLAIMS_DIR = E.GOVERN_CLAIMS_DIR || `${GOVERN_DIR}/claims`;
|
|
51
|
+
const CLAIM_TTL = envInt("GOVERN_CLAIM_TTL_SECS", 5400);
|
|
52
|
+
const MAX_GATES = envInt("GOVERN_MAX_GATES", 2);
|
|
53
|
+
// The shell whose LIFETIME a lock/claim tracks. Falls back to our own pid so a
|
|
54
|
+
// direct `bun govern.ts …` invocation still behaves, but every shim call sets it.
|
|
55
|
+
const OWNER_PID = envInt("GOVERN_OWNER_PID", process.pid);
|
|
56
|
+
// claim_take/claim_release key on GOVERN_CLAIM_PID when set (a short-lived
|
|
57
|
+
// emitter claiming ground FOR a worker); claim_release_all keys on the plain
|
|
58
|
+
// shell pid. That asymmetry exists in the bash original and is preserved
|
|
59
|
+
// verbatim — normalising it while porting would change behaviour no proof covers.
|
|
60
|
+
const CLAIM_PID = envInt("GOVERN_CLAIM_PID", OWNER_PID);
|
|
61
|
+
const SHELL_PID = OWNER_PID;
|
|
62
|
+
|
|
63
|
+
function log(msg: string) {
|
|
64
|
+
if (E.GOVERN_DEBUG) process.stderr.write(`[govern] ${msg}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── liveness ────────────────────────────────────────────────────────────────
|
|
68
|
+
// GOVERN_ALIVE_SET=1 means the CALLER has already decided who is alive (its
|
|
69
|
+
// _gv_alive may be stubbed). Otherwise ask the kernel, which is what lets a long
|
|
70
|
+
// gate_lock wait notice an owner dying mid-wait.
|
|
71
|
+
const ALIVE_SET = E.GOVERN_ALIVE_SET === "1";
|
|
72
|
+
const ALIVE_PIDS = new Set(
|
|
73
|
+
(E.GOVERN_ALIVE_PIDS || "").split(",").filter((s) => s !== ""),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
function alive(pid: string | undefined): boolean {
|
|
77
|
+
if (!pid) return false;
|
|
78
|
+
if (ALIVE_SET) return ALIVE_PIDS.has(pid);
|
|
79
|
+
const n = parseInt(pid, 10);
|
|
80
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
81
|
+
try {
|
|
82
|
+
process.kill(n, 0);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (e: any) {
|
|
85
|
+
// EPERM means it exists and is not ours — `kill -0` reports the same.
|
|
86
|
+
return e && e.code === "EPERM";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rmrf(p: string) {
|
|
91
|
+
try {
|
|
92
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readTrim(p: string): string {
|
|
97
|
+
try {
|
|
98
|
+
return fs.readFileSync(p, "utf8").trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function now(): number {
|
|
105
|
+
return Math.floor(Date.now() / 1000);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── locks · slots ───────────────────────────────────────────────────────────
|
|
109
|
+
// mkdir(2) is the arbiter, exactly as in the bash original: atomic on every
|
|
110
|
+
// POSIX fs, so N racers produce exactly one winner and no coordinator exists.
|
|
111
|
+
|
|
112
|
+
function reapLock(d: string): boolean {
|
|
113
|
+
if (!fs.existsSync(d)) return false;
|
|
114
|
+
const owner = readTrim(`${d}/pid`);
|
|
115
|
+
if (!alive(owner)) {
|
|
116
|
+
log(`reaping stale lock ${d} (owner ${owner} gone)`);
|
|
117
|
+
rmrf(d);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tryLock(name: string): boolean {
|
|
124
|
+
const d = `${GOVERN_DIR}/lock-${name}`;
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(d);
|
|
127
|
+
fs.writeFileSync(`${d}/pid`, `${OWNER_PID}\n`);
|
|
128
|
+
log(`acquired ${name}`);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function sleep(ms: number) {
|
|
136
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// One non-blocking attempt, INCLUDING the reap-and-retake a dead owner earns.
|
|
140
|
+
// `gate_lock <name> 0` did exactly this in the bash original and gate_slot leans
|
|
141
|
+
// on it: a slot whose holder died must be takeable on this pass, not 2s later.
|
|
142
|
+
function acquireNoWait(name: string): boolean {
|
|
143
|
+
if (tryLock(name)) return true;
|
|
144
|
+
if (reapLock(`${GOVERN_DIR}/lock-${name}`)) return tryLock(name);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function cmdLock(name: string, waitS: number): Promise<number> {
|
|
149
|
+
let waited = 0;
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (tryLock(name)) return 0;
|
|
152
|
+
const d = `${GOVERN_DIR}/lock-${name}`;
|
|
153
|
+
if (reapLock(d)) continue; // dead owner -> retry immediately
|
|
154
|
+
if (waited >= waitS) {
|
|
155
|
+
log(`busy ${name}`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
await sleep(1000);
|
|
159
|
+
waited += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cmdUnlock(name: string): number {
|
|
164
|
+
const d = `${GOVERN_DIR}/lock-${name}`;
|
|
165
|
+
if (readTrim(`${d}/pid`) === String(OWNER_PID)) rmrf(d);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// gate_slot — take one of the effective slots. The effective cap is the SMALLER
|
|
170
|
+
// of the configured GOVERN_MAX_GATES and what memory can actually fund. A static
|
|
171
|
+
// config must never authorise more than the box can pay for; a broken sensor
|
|
172
|
+
// (headroom 99) must never silently serialise the fleet. Floored at 1: work
|
|
173
|
+
// slows down, it never stops.
|
|
174
|
+
async function cmdSlot(waitS: number): Promise<number> {
|
|
175
|
+
let waited = 0;
|
|
176
|
+
for (;;) {
|
|
177
|
+
let cap = MAX_GATES;
|
|
178
|
+
const head = headroom(readVmStat());
|
|
179
|
+
if (head < cap) cap = head;
|
|
180
|
+
if (cap < 1) cap = 1;
|
|
181
|
+
for (let i = 1; i <= cap; i++) {
|
|
182
|
+
if (acquireNoWait(`slot-${i}`)) {
|
|
183
|
+
process.stdout.write(`slot-${i}\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (waited >= waitS) return 1;
|
|
188
|
+
await sleep(2000);
|
|
189
|
+
waited += 2;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// gate_sweep_stale — _gv_reap only fires when someone CONTENDS for a lock, so a
|
|
194
|
+
// session that died holding a slot would shrink the effective cap until the next
|
|
195
|
+
// contender happened along. Run at session start. Echoes the number swept.
|
|
196
|
+
function cmdSweepStale(): number {
|
|
197
|
+
let n = 0;
|
|
198
|
+
let ents: string[] = [];
|
|
199
|
+
try {
|
|
200
|
+
ents = fs.readdirSync(GOVERN_DIR);
|
|
201
|
+
} catch {
|
|
202
|
+
process.stdout.write("0\n");
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
for (const e of ents) {
|
|
206
|
+
if (!e.startsWith("lock-")) continue;
|
|
207
|
+
const d = `${GOVERN_DIR}/${e}`;
|
|
208
|
+
try {
|
|
209
|
+
if (!fs.statSync(d).isDirectory()) continue;
|
|
210
|
+
} catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!alive(readTrim(`${d}/pid`))) {
|
|
214
|
+
rmrf(d);
|
|
215
|
+
n++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(`${n}\n`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── run_bounded ─────────────────────────────────────────────────────────────
|
|
223
|
+
// A hard wall-clock cap that kills the whole PROCESS GROUP. Replaces the missing
|
|
224
|
+
// timeout(1), and replaces the bash watchdog whose two measured failures were:
|
|
225
|
+
// 1. it inherited the caller's stdout (then stderr), so `$(run_bounded …)` and
|
|
226
|
+
// `run_bounded … 2>&1 | tail` blocked for the FULL timeout after the
|
|
227
|
+
// command had already answered — read as a hang, killed at 144 = UNRUN;
|
|
228
|
+
// 2. `kill $watch_pid` reaped the subshell but not the `sleep` it forked — 21
|
|
229
|
+
// orphaned `sleep 1800` (ppid 1) were on the box at once, each holding
|
|
230
|
+
// whatever pipe its dead parent held.
|
|
231
|
+
// Neither can happen here: the timer is an in-process setTimeout (it forks
|
|
232
|
+
// nothing and holds no fd) and this process holds no pipe of its own — stdio is
|
|
233
|
+
// inherited straight through to the child.
|
|
234
|
+
//
|
|
235
|
+
// `detached: true` is load-bearing: it is the only way to give the child its own
|
|
236
|
+
// process group, and the group is the point. `bun run verify` is
|
|
237
|
+
// bash -> bun -> node -> N vitest forks; killing the shell alone reparents all
|
|
238
|
+
// of them to launchd, which is exactly how orphaned verify trees leaked here.
|
|
239
|
+
function cmdRunBounded(secs: number, argv: string[]): Promise<number> {
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
if (argv.length === 0) return resolve(0);
|
|
242
|
+
const child = spawn(argv[0]!, argv.slice(1), {
|
|
243
|
+
stdio: "inherit",
|
|
244
|
+
detached: true,
|
|
245
|
+
});
|
|
246
|
+
let timedOut = false;
|
|
247
|
+
let done = false;
|
|
248
|
+
let killer: ReturnType<typeof setTimeout> | undefined;
|
|
249
|
+
|
|
250
|
+
const killGroup = (sig: NodeJS.Signals) => {
|
|
251
|
+
try {
|
|
252
|
+
process.kill(-child.pid!, sig);
|
|
253
|
+
} catch {}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
if (done) return; // the command answered first — nothing to kill
|
|
258
|
+
timedOut = true;
|
|
259
|
+
killGroup("SIGTERM");
|
|
260
|
+
killer = setTimeout(() => killGroup("SIGKILL"), 5000);
|
|
261
|
+
}, secs * 1000);
|
|
262
|
+
|
|
263
|
+
// Forward a signal aimed at us to the group, so a killed gate does not
|
|
264
|
+
// reparent its whole tree to launchd.
|
|
265
|
+
const fwd = (sig: NodeJS.Signals) => {
|
|
266
|
+
if (!done) killGroup(sig);
|
|
267
|
+
};
|
|
268
|
+
process.on("SIGTERM", () => fwd("SIGTERM"));
|
|
269
|
+
process.on("SIGINT", () => fwd("SIGINT"));
|
|
270
|
+
process.on("SIGHUP", () => fwd("SIGHUP"));
|
|
271
|
+
|
|
272
|
+
child.on("error", () => {
|
|
273
|
+
done = true;
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
if (killer) clearTimeout(killer);
|
|
276
|
+
resolve(127);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
child.on("exit", (code, signal) => {
|
|
280
|
+
done = true;
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
if (killer) clearTimeout(killer);
|
|
283
|
+
if (timedOut) {
|
|
284
|
+
// The PARENT prints, from a shell that is allowed to — after the wait,
|
|
285
|
+
// never from inside the watchdog.
|
|
286
|
+
process.stderr.write(
|
|
287
|
+
`[govern] TIMEOUT ${secs}s — killed process group ${child.pid}\n`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (code !== null && code !== undefined) return resolve(code);
|
|
291
|
+
// bash `wait` reports 128+signum; keep the numbers callers already compare.
|
|
292
|
+
const SIGNUM: Record<string, number> = {
|
|
293
|
+
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGABRT: 6, SIGFPE: 8,
|
|
294
|
+
SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15,
|
|
295
|
+
};
|
|
296
|
+
resolve(128 + (SIGNUM[signal as string] ?? 0));
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── memory probe ────────────────────────────────────────────────────────────
|
|
302
|
+
// The honest quantities are in vm_stat, never memory_pressure's "free
|
|
303
|
+
// percentage" (which counts file cache, purgeable and already-compressed pages
|
|
304
|
+
// as free — measured 74% on a box with 1.3% of its RAM actually free and 9.4 GB
|
|
305
|
+
// in swap). Available = free + purgeable + min(file-backed, inactive). No swap
|
|
306
|
+
// term, deliberately: swap in use is a scar, not a signal.
|
|
307
|
+
//
|
|
308
|
+
// The vm_stat TEXT arrives on stdin, produced by the shim's _gv_vm_stat — the
|
|
309
|
+
// stub seam govern-mem-check.sh drives.
|
|
310
|
+
|
|
311
|
+
function readStdin(): string {
|
|
312
|
+
try {
|
|
313
|
+
return fs.readFileSync(0, "utf8");
|
|
314
|
+
} catch {
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let _vmCache: string | null = null;
|
|
320
|
+
function readVmStat(): string {
|
|
321
|
+
if (_vmCache !== null) return _vmCache;
|
|
322
|
+
// Only used by paths the shim does NOT feed (gate_slot's internal headroom).
|
|
323
|
+
try {
|
|
324
|
+
const out = Bun.spawnSync(["vm_stat"]);
|
|
325
|
+
_vmCache = out.success ? out.stdout.toString() : "";
|
|
326
|
+
} catch {
|
|
327
|
+
_vmCache = "";
|
|
328
|
+
}
|
|
329
|
+
return _vmCache;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function fieldPages(vs: string, prefix: string): string | null {
|
|
333
|
+
for (const line of vs.split("\n")) {
|
|
334
|
+
if (!line.startsWith(prefix)) continue;
|
|
335
|
+
const colon = line.indexOf(":");
|
|
336
|
+
if (colon < 0) return "";
|
|
337
|
+
return line.slice(colon + 1).replace(/[^0-9]/g, "");
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isNum(s: string | null | undefined): s is string {
|
|
343
|
+
return typeof s === "string" && s !== "" && /^[0-9]+$/.test(s);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Available RAM in MB, or null when ANY field is unreadable. Every caller treats
|
|
347
|
+
// null as "do not constrain": a broken sensor must never serialise the fleet.
|
|
348
|
+
function memAvailMb(vs: string): number | null {
|
|
349
|
+
if (vs.replace(/\n+$/, "") === "") return null;
|
|
350
|
+
const m = vs.match(/page size of ([0-9]+) bytes/);
|
|
351
|
+
const psz = m ? m[1]! : "";
|
|
352
|
+
const free = fieldPages(vs, "Pages free");
|
|
353
|
+
let purge = fieldPages(vs, "Pages purgeable");
|
|
354
|
+
const inact = fieldPages(vs, "Pages inactive");
|
|
355
|
+
const filebk = fieldPages(vs, "File-backed pages");
|
|
356
|
+
if (purge === null || purge === "") purge = "0";
|
|
357
|
+
if (!isNum(psz)) return null;
|
|
358
|
+
if (!isNum(free)) return null;
|
|
359
|
+
if (!isNum(purge)) return null;
|
|
360
|
+
if (!isNum(inact)) return null;
|
|
361
|
+
if (!isNum(filebk)) return null;
|
|
362
|
+
const p = parseInt(psz, 10);
|
|
363
|
+
if (p <= 0) return null;
|
|
364
|
+
const cold = Math.min(parseInt(filebk, 10), parseInt(inact, 10));
|
|
365
|
+
return Math.floor(
|
|
366
|
+
((parseInt(free, 10) + parseInt(purge, 10) + cold) * p) / 1048576,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// What ONE gate slot actually costs in RAM. Derived from the LIVE fork count,
|
|
371
|
+
// never a stale constant — that drift is the defect: vitest maxWorkers moved
|
|
372
|
+
// 4 -> 8 and test-lanes.sh runs two drivers in one slot, so the same gate had
|
|
373
|
+
// two different costs depending on who launched it. Constants measured
|
|
374
|
+
// 2026-09-03 (125 fork samples, 65 driver samples). All overridable; an explicit
|
|
375
|
+
// GOVERN_GB_PER_CYCLE is still the operator's override and wins.
|
|
376
|
+
function priceMb(): number {
|
|
377
|
+
const ovr = E.GOVERN_GB_PER_CYCLE;
|
|
378
|
+
if (ovr !== undefined && ovr !== "") {
|
|
379
|
+
const n = parseInt(ovr, 10);
|
|
380
|
+
return (Number.isFinite(n) ? n : 0) * 1024;
|
|
381
|
+
}
|
|
382
|
+
let forks = envInt("VITEST_MAX_FORKS", 8);
|
|
383
|
+
let lanes = envInt("GOVERN_TEST_LANES", 2);
|
|
384
|
+
if (lanes < 1) lanes = 1;
|
|
385
|
+
if (forks < 1) forks = 1;
|
|
386
|
+
return (
|
|
387
|
+
lanes * envInt("GOVERN_MB_PER_DRIVER", 300) +
|
|
388
|
+
(forks + lanes - 1) * envInt("GOVERN_MB_PER_FORK", 200) +
|
|
389
|
+
envInt("GOVERN_MB_TSC", 400) +
|
|
390
|
+
envInt("GOVERN_MB_OVERHEAD", 300)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// How many concurrent heavy CYCLES this box can afford right now. >= 1 always;
|
|
395
|
+
// 99 ("don't constrain") when memory cannot be read. The floor is load-bearing:
|
|
396
|
+
// do-fleet.sh clamps its slot count to this with no floor of its own, so a 0
|
|
397
|
+
// here would launch nothing at all.
|
|
398
|
+
function headroom(vs: string): number {
|
|
399
|
+
const avail = memAvailMb(vs);
|
|
400
|
+
if (avail === null) return 99;
|
|
401
|
+
let price = priceMb();
|
|
402
|
+
if (price < 1) price = 1024;
|
|
403
|
+
const reserve = envInt("GOVERN_RESERVE_GB", 2) * 1024;
|
|
404
|
+
let usable = avail - reserve;
|
|
405
|
+
if (usable < 0) usable = 0;
|
|
406
|
+
const slots = Math.floor(usable / price);
|
|
407
|
+
return slots < 1 ? 1 : slots;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 0 = the machine has headroom, 1 = it is already thrashing. Two conditions,
|
|
411
|
+
// either fires: what is available cannot fund one more gate at its measured
|
|
412
|
+
// price, or the free percentage is under the floor. Unreadable sensor -> 0.
|
|
413
|
+
function pressure(vs: string, memsize: string): number {
|
|
414
|
+
const avail = memAvailMb(vs);
|
|
415
|
+
if (avail === null) return 0;
|
|
416
|
+
if (avail < envInt("GOVERN_RESERVE_GB", 2) * 1024 + priceMb()) return 1;
|
|
417
|
+
if (!isNum(memsize)) return 0;
|
|
418
|
+
const total = Math.floor(parseInt(memsize, 10) / 1048576);
|
|
419
|
+
if (total < 1) return 0;
|
|
420
|
+
const pct = Math.floor((avail * 100) / total);
|
|
421
|
+
if (pct < envInt("GOVERN_MIN_FREE_PCT", 12)) return 1;
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function memReport(vs: string, memsize: string, swapusage: string): string {
|
|
426
|
+
const avail = memAvailMb(vs);
|
|
427
|
+
let total = 0;
|
|
428
|
+
if (isNum(memsize)) total = Math.floor(parseInt(memsize, 10) / 1048576);
|
|
429
|
+
const sm = swapusage.match(/used = ([0-9]+)\.?[0-9]*M/);
|
|
430
|
+
const swap = sm ? sm[1]! : "0";
|
|
431
|
+
const tail = `swap_used_mb=${swap} price_mb=${priceMb()} slots=${headroom(vs)}`;
|
|
432
|
+
if (avail === null) return `avail_mb=? total_mb=${total} avail_pct=? ${tail}`;
|
|
433
|
+
const pct = total > 0 ? Math.floor((avail * 100) / total) : 0;
|
|
434
|
+
return `avail_mb=${avail} total_mb=${total} avail_pct=${pct} ${tail}`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─── claims ──────────────────────────────────────────────────────────────────
|
|
438
|
+
// A machine-wide registry of who is working on WHICH REGION right now. NOT a
|
|
439
|
+
// queue, no coordinator: a worker that finds its region claimed takes the next
|
|
440
|
+
// candidate, so contention costs a re-rank and never a wait.
|
|
441
|
+
//
|
|
442
|
+
// EVAPORATION is the whole contract — a claim expires with no human in the loop,
|
|
443
|
+
// two ways: the owner pid is dead, or the lease expired. Both fire on the READ
|
|
444
|
+
// path, so the next contender reclaims dead ground by itself.
|
|
445
|
+
//
|
|
446
|
+
// The KEY is computed by the shim (`cksum` is the POSIX CRC, not zlib's, and
|
|
447
|
+
// re-deriving it here would be a second definition of an injective mapping the
|
|
448
|
+
// checkers read directly off disk). It arrives as an argument.
|
|
449
|
+
|
|
450
|
+
function claimGet(d: string, field: string): string {
|
|
451
|
+
let txt = "";
|
|
452
|
+
try {
|
|
453
|
+
txt = fs.readFileSync(`${d}/meta`, "utf8");
|
|
454
|
+
} catch {
|
|
455
|
+
return "";
|
|
456
|
+
}
|
|
457
|
+
for (const line of txt.split("\n")) {
|
|
458
|
+
if (line.startsWith(`${field}=`)) return line.slice(field.length + 1);
|
|
459
|
+
}
|
|
460
|
+
return "";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// true = reaped (the ground is now free), false = still held by a live, in-lease
|
|
464
|
+
// owner.
|
|
465
|
+
function claimReap(d: string): boolean {
|
|
466
|
+
if (!fs.existsSync(d)) return true;
|
|
467
|
+
const owner = claimGet(d, "pid");
|
|
468
|
+
const started = claimGet(d, "started");
|
|
469
|
+
if (!alive(owner)) {
|
|
470
|
+
// `minhold` is the grace window for a claim taken FOR a worker by a
|
|
471
|
+
// short-lived emitter: without it the very next racer reaps the claim in the
|
|
472
|
+
// moment between it landing and the worker adopting it. Opt-in — a claim
|
|
473
|
+
// written without one reaps on a dead pid exactly as it always did.
|
|
474
|
+
const minhold = claimGet(d, "minhold");
|
|
475
|
+
if (minhold !== "" && started !== "" && now() - parseInt(started, 10) < parseInt(minhold, 10)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
log(`reaping claim ${d} (owner ${owner} gone)`);
|
|
479
|
+
rmrf(d);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
let ttl = claimGet(d, "ttl");
|
|
483
|
+
if (started === "") return false;
|
|
484
|
+
if (ttl === "") ttl = String(CLAIM_TTL);
|
|
485
|
+
const age = now() - parseInt(started, 10);
|
|
486
|
+
if (age >= parseInt(ttl, 10)) {
|
|
487
|
+
log(`reaping claim ${d} (lease expired: ${age}s >= ${ttl}s)`);
|
|
488
|
+
rmrf(d);
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function claimTake(key: string, region: string, slug: string): number {
|
|
495
|
+
try {
|
|
496
|
+
fs.mkdirSync(CLAIMS_DIR, { recursive: true });
|
|
497
|
+
} catch {}
|
|
498
|
+
const d = `${CLAIMS_DIR}/${key}`;
|
|
499
|
+
try {
|
|
500
|
+
fs.mkdirSync(d);
|
|
501
|
+
let meta =
|
|
502
|
+
`pid=${CLAIM_PID}\n` +
|
|
503
|
+
`slug=${slug}\n` +
|
|
504
|
+
`region=${region}\n` +
|
|
505
|
+
`started=${now()}\n` +
|
|
506
|
+
`ttl=${CLAIM_TTL}\n`;
|
|
507
|
+
const mh = E.GOVERN_CLAIM_MIN_HOLD_SECS;
|
|
508
|
+
if (mh) meta += `minhold=${mh}\n`;
|
|
509
|
+
fs.writeFileSync(`${d}/meta`, meta);
|
|
510
|
+
log(`claimed ${region} (${slug})`);
|
|
511
|
+
return 0;
|
|
512
|
+
} catch {}
|
|
513
|
+
// Held. Re-claiming your own region is idempotent — do-fleet re-claims on
|
|
514
|
+
// every launch, including relaunches into an existing worktree.
|
|
515
|
+
if (claimGet(d, "pid") === String(CLAIM_PID)) return 0;
|
|
516
|
+
if (!claimReap(d)) return 1;
|
|
517
|
+
return claimTake(key, region, slug); // ground reaped — take it
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function claimDirs(): string[] {
|
|
521
|
+
try {
|
|
522
|
+
return fs
|
|
523
|
+
.readdirSync(CLAIMS_DIR)
|
|
524
|
+
.map((e) => `${CLAIMS_DIR}/${e}`)
|
|
525
|
+
.filter((p) => {
|
|
526
|
+
try {
|
|
527
|
+
return fs.statSync(p).isDirectory();
|
|
528
|
+
} catch {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
} catch {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ─── tsc_tree_fingerprint ────────────────────────────────────────────────────
|
|
538
|
+
// ONE hash of everything tsc READS for <folder>, keyed BY CONTENT, never by
|
|
539
|
+
// mtime: two checkouts of one commit have identical bytes and different mtimes,
|
|
540
|
+
// so N worktrees each paid a full tsc and wrote their own stamp (28 unshared
|
|
541
|
+
// stamps, measured 2026-09-03). Same bytes ⇒ same key, in any checkout; a doc
|
|
542
|
+
// edit moves nothing.
|
|
543
|
+
//
|
|
544
|
+
// node_modules / .git / dist / .wrangler are pruned (dependency drift is covered
|
|
545
|
+
// by the lockfile), and dot-entries are pruned EXCEPT .astro — without that the
|
|
546
|
+
// key carried .w2-spec.json and friends, cycle receipts that move every wave and
|
|
547
|
+
// are not tsc inputs, so the memo missed on every wave.
|
|
548
|
+
//
|
|
549
|
+
// The BYTES hashed are identical to the shell pipeline this replaces
|
|
550
|
+
// (`find -print0 | LC_ALL=C sort -z | xargs -0 shasum -a 256` folded into one
|
|
551
|
+
// shasum), so the shared `tsc-<folder>.<fp>` namespace is unchanged and
|
|
552
|
+
// do-reconcile.sh `types` — the other caller of this one derivation — still
|
|
553
|
+
// shares the memo.
|
|
554
|
+
|
|
555
|
+
const FP_PRUNE = new Set(["node_modules", ".git", "dist", ".wrangler"]);
|
|
556
|
+
const FP_EXT = [".ts", ".tsx", ".mts", ".cts", ".json"];
|
|
557
|
+
const FP_NAME = new Set(["bun.lock", "package-lock.json"]);
|
|
558
|
+
|
|
559
|
+
function fpMatches(name: string): boolean {
|
|
560
|
+
if (FP_NAME.has(name)) return true;
|
|
561
|
+
return FP_EXT.some((x) => name.endsWith(x));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function fpWalk(start: string, out: string[]) {
|
|
565
|
+
const stack = [start];
|
|
566
|
+
while (stack.length) {
|
|
567
|
+
const dir = stack.pop()!;
|
|
568
|
+
let ents: fs.Dirent[];
|
|
569
|
+
try {
|
|
570
|
+
ents = fs.readdirSync(dir, { withFileTypes: true });
|
|
571
|
+
} catch {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
for (const e of ents) {
|
|
575
|
+
const name = e.name;
|
|
576
|
+
// The find(1) prune expression, verbatim: the four heavy dirs plus every
|
|
577
|
+
// dot-entry that is not .astro. It prunes FILES too — that is what keeps
|
|
578
|
+
// .w2-spec.json out of the key.
|
|
579
|
+
if (FP_PRUNE.has(name) || (name.startsWith(".") && name !== ".astro")) continue;
|
|
580
|
+
const full = `${dir}/${name}`;
|
|
581
|
+
if (e.isDirectory()) stack.push(full);
|
|
582
|
+
else if (e.isFile() && fpMatches(name)) out.push(full);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function fingerprint(inputs: string[]): string {
|
|
588
|
+
const chunks: Buffer[] = [];
|
|
589
|
+
for (const d of inputs) {
|
|
590
|
+
let isDir = false;
|
|
591
|
+
try {
|
|
592
|
+
isDir = fs.statSync(d).isDirectory();
|
|
593
|
+
} catch {}
|
|
594
|
+
if (!isDir) {
|
|
595
|
+
// The shell emitted `ABSENT <dir>` into the same NUL-delimited stream.
|
|
596
|
+
chunks.push(Buffer.from(`ABSENT ${d}\n`));
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const files: string[] = [];
|
|
600
|
+
fpWalk(d, files);
|
|
601
|
+
for (const f of files) chunks.push(Buffer.concat([Buffer.from(f), Buffer.from([0])]));
|
|
602
|
+
}
|
|
603
|
+
const raw = Buffer.concat(chunks);
|
|
604
|
+
const recs: Buffer[] = [];
|
|
605
|
+
let start = 0;
|
|
606
|
+
for (let i = 0; i < raw.length; i++) {
|
|
607
|
+
if (raw[i] === 0) {
|
|
608
|
+
recs.push(raw.subarray(start, i));
|
|
609
|
+
start = i + 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (start < raw.length) recs.push(raw.subarray(start));
|
|
613
|
+
recs.sort(Buffer.compare); // LC_ALL=C sort -z — byte-wise
|
|
614
|
+
const outer = createHash("sha256");
|
|
615
|
+
for (const r of recs) {
|
|
616
|
+
const p = r.toString("utf8");
|
|
617
|
+
let st: fs.Stats;
|
|
618
|
+
try {
|
|
619
|
+
st = fs.statSync(p);
|
|
620
|
+
} catch {
|
|
621
|
+
continue; // an ABSENT record is not a file; shasum printed nothing for it
|
|
622
|
+
}
|
|
623
|
+
if (!st.isFile()) continue;
|
|
624
|
+
let buf: Buffer;
|
|
625
|
+
try {
|
|
626
|
+
buf = fs.readFileSync(p);
|
|
627
|
+
} catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
outer.update(`${createHash("sha256").update(buf).digest("hex")} ${p}\n`);
|
|
631
|
+
}
|
|
632
|
+
return outer.digest("hex");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ─── the second memo key: test-cached.sh's per-file hash ──────────────────────
|
|
636
|
+
// `<hash> <repo-relative path>` — the shape `shasum <file>` prints, minus
|
|
637
|
+
// $ROOT. Repo-relative is load-bearing: an absolute path puts the WORKTREE into
|
|
638
|
+
// the key, and the same bytes hashed from two checkouts then produced two keys.
|
|
639
|
+
// A missing file hashes to the empty string, exactly as
|
|
640
|
+
// `$(shasum -a 256 < missing | cut -d' ' -f1)` did.
|
|
641
|
+
function hashRel(root: string, rels: string[]): number {
|
|
642
|
+
let out = "";
|
|
643
|
+
for (const rel of rels) {
|
|
644
|
+
let h = "";
|
|
645
|
+
try {
|
|
646
|
+
h = createHash("sha256").update(fs.readFileSync(`${root}/${rel}`)).digest("hex");
|
|
647
|
+
} catch {}
|
|
648
|
+
out += `${h} ${rel}\n`;
|
|
649
|
+
}
|
|
650
|
+
process.stdout.write(out);
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ─── dispatch ────────────────────────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
async function main(): Promise<number> {
|
|
657
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
658
|
+
switch (cmd) {
|
|
659
|
+
case "lock":
|
|
660
|
+
return cmdLock(rest[0]!, parseInt(rest[1] || "0", 10) || 0);
|
|
661
|
+
case "unlock":
|
|
662
|
+
return cmdUnlock(rest[0]!);
|
|
663
|
+
case "slot":
|
|
664
|
+
return cmdSlot(parseInt(rest[0] || "900", 10) || 0);
|
|
665
|
+
case "sweep-stale":
|
|
666
|
+
return cmdSweepStale();
|
|
667
|
+
case "run-bounded": {
|
|
668
|
+
const secs = parseInt(rest[0] || "0", 10) || 0;
|
|
669
|
+
return cmdRunBounded(secs, rest.slice(1));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
case "mem-avail-mb": {
|
|
673
|
+
const v = memAvailMb(readStdin());
|
|
674
|
+
if (v === null) return 1;
|
|
675
|
+
process.stdout.write(`${v}\n`);
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
case "price-mb":
|
|
679
|
+
process.stdout.write(`${priceMb()}\n`);
|
|
680
|
+
return 0;
|
|
681
|
+
case "headroom":
|
|
682
|
+
process.stdout.write(`${headroom(readStdin())}\n`);
|
|
683
|
+
return 0;
|
|
684
|
+
case "pressure":
|
|
685
|
+
return pressure(readStdin(), E.GOVERN_MEMSIZE || "");
|
|
686
|
+
case "mem-report":
|
|
687
|
+
process.stdout.write(
|
|
688
|
+
`${memReport(readStdin(), E.GOVERN_MEMSIZE || "", E.GOVERN_SWAPUSAGE || "")}\n`,
|
|
689
|
+
);
|
|
690
|
+
return 0;
|
|
691
|
+
|
|
692
|
+
case "claim-take":
|
|
693
|
+
return claimTake(rest[0]!, rest[1]!, rest[2] || "");
|
|
694
|
+
case "claim-owner": {
|
|
695
|
+
const d = `${CLAIMS_DIR}/${rest[0]!}`;
|
|
696
|
+
if (!fs.existsSync(d)) return 0;
|
|
697
|
+
if (claimReap(d)) return 0;
|
|
698
|
+
process.stdout.write(`${claimGet(d, "slug")} ${claimGet(d, "pid")}\n`);
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
case "claim-release": {
|
|
702
|
+
const d = `${CLAIMS_DIR}/${rest[0]!}`;
|
|
703
|
+
if (claimGet(d, "pid") === String(CLAIM_PID)) rmrf(d);
|
|
704
|
+
return 0;
|
|
705
|
+
}
|
|
706
|
+
case "claim-release-all": {
|
|
707
|
+
// keys on the plain shell pid, not GOVERN_CLAIM_PID — see the note at
|
|
708
|
+
// CLAIM_PID above. Preserved verbatim from the bash original.
|
|
709
|
+
for (const d of claimDirs()) {
|
|
710
|
+
if (claimGet(d, "pid") === String(SHELL_PID)) rmrf(d);
|
|
711
|
+
}
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
case "claim-list": {
|
|
715
|
+
let out = "";
|
|
716
|
+
for (const d of claimDirs()) {
|
|
717
|
+
if (claimReap(d)) continue;
|
|
718
|
+
out += `${claimGet(d, "region")}\t${claimGet(d, "slug")}\t${claimGet(d, "pid")}\n`;
|
|
719
|
+
}
|
|
720
|
+
process.stdout.write(out);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
case "claim-sweep": {
|
|
724
|
+
let n = 0;
|
|
725
|
+
if (fs.existsSync(CLAIMS_DIR)) {
|
|
726
|
+
for (const d of claimDirs()) if (claimReap(d)) n++;
|
|
727
|
+
}
|
|
728
|
+
process.stdout.write(`${n}\n`);
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
case "claim-pids": {
|
|
732
|
+
// every pid the registry currently names — the shim filters these through
|
|
733
|
+
// its own (stubbable) _gv_alive before handing back an alive set.
|
|
734
|
+
const seen = new Set<string>();
|
|
735
|
+
for (const d of claimDirs()) {
|
|
736
|
+
const p = claimGet(d, "pid");
|
|
737
|
+
if (p) seen.add(p);
|
|
738
|
+
}
|
|
739
|
+
process.stdout.write([...seen].join("\n") + (seen.size ? "\n" : ""));
|
|
740
|
+
return 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
case "fingerprint":
|
|
744
|
+
if (rest.length === 0 || !rest[0]) return 1;
|
|
745
|
+
process.stdout.write(`${fingerprint(rest)}\n`);
|
|
746
|
+
return 0;
|
|
747
|
+
case "hash-rel":
|
|
748
|
+
return hashRel(rest[0]!, rest.slice(1));
|
|
749
|
+
|
|
750
|
+
default:
|
|
751
|
+
process.stderr.write(`govern.ts: unknown command '${cmd ?? ""}'\n`);
|
|
752
|
+
return 2;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
process.exitCode = await main();
|