@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.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. 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();