enigma-cli 1.46.3 → 1.47.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.
@@ -7,6 +7,6 @@
7
7
  "minimalCode"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.46.3",
10
+ "cliVersion": "1.47.0",
11
11
  "sha": "3f0dcc28341bb0407860534f7ce9314cfc91b5e673f8f3d13b89d61851ed75f6"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
6
6
  "updated": "2026-06-16T11:24:30+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, modern TypeScript project configuration (module resolution, strict flags, @/* path alias), API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "652637f818b4ce4f4d5d83fa68fa57fe7842e32fb179523958c5496af54bb938"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports incl. namespace imports for wide module surfaces, path-alias specifiers and no file extensions, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "3beb0383a9cb0331ef8a579308a2517d24d57a8ba33751ad17d16d0a4f66e344"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
6
  "updated": "2026-08-19T12:37:59+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "6b8a924cc11b61d4b45b73384aeb095ce622229359b7cc2c6ce4bb0cf3015ed7"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "codeGraph"
8
8
  ],
9
9
  "updated": "2026-08-15T20:49:24+02:00",
10
- "cliVersion": "1.46.3",
10
+ "cliVersion": "1.47.0",
11
11
  "sha": "95b7202e5659f8d9d6f11ce29e7dc5b01575ba86c78ef948bec4753ccf430155"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: engine selection (PostgreSQL by default, SQLite only for local-first/embedded stores), ORM selection (Prisma in TypeScript/JavaScript), query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
6
  "updated": "2026-08-04T18:33:27+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "26fddc14caf697e830448a2cc453ddc470fe83d098d50bd67b278effba63274e"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "968876bcac9ce6e5a20be05c89ac97191b112c9e08679a8dc506d150c375fec4"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
6
  "updated": "2026-06-01T00:45:28+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Transactional email: React Email templates instead of hand-written HTML tables, server-side rendering, one send module behind the provider SDK, plain-text alternatives, idempotent background sending, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).",
6
6
  "updated": "2026-07-30T19:29:19+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "c9724fdbcdbeab99573be3fd44d4cdd97c2a394d99f3c4395f118f17356b00ed"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
6
6
  "updated": "2026-07-29T01:18:36+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, real-time form validation where an emptied or not-yet-filled field is incomplete rather than invalid (a `*` on the label and an aria-disabled submit, never \"email is not valid\" over an empty input), no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll is the preferred default, pagination the deliberate exception when the design or the user calls for it, virtualization, skeletons, progressive loading), data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render: fuse.js search, the filters its column kinds imply, a date range, sort, filter state in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it, never inert text; machine codes get human labels and raw payloads are never dumped into a cell), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode per field kind, set once in the shared Input, normalized on blur), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), auth screens (breached-password feedback, strength meter, cookie consent before login/register), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
6
6
  "updated": "2026-09-07T04:01:56+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "8bfd675f3a66dbdcaa7e670bc40246adb0c470b93c1d8677338b4092dfa9ada4"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
6
  "updated": "2026-08-22T09:25:34+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "cb320a6a5fd3279604ad7920b877c1a36e48f4f39aeead1a4c4daa89901d9ce0"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "logoColorPolicy"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.46.3",
10
+ "cliVersion": "1.47.0",
11
11
  "sha": "09cdbefd98625b02a7d03685e5deed128238ff8454a83fe22279610fe3ef8ddf"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), credential flows with breach-checked passwords (Have I Been Pwned), OWASP Top 10, transport/crypto baseline, cookies and consent, secure logging, and agent/MCP/tool-use safety.",
6
6
  "updated": "2026-09-05T02:46:46+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "d64fe3a4a54f27a71e3066875ee949500f170d04da5c548ef605268a12102afb"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
6
6
  "updated": "2026-08-03T00:16:54+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "a49f6166f57d734e0f13487332da37903e925f32257447856af0fbd6a5f1dc84"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "4c2197954135dd3375f32d839238b82a95c0e93e976ddf5a9cbc4a0cf0146e97"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Concise, realistic technical copy - UI microcopy, descriptions, hints, empty/error states, and README/doc prose that informs without over-explaining or restating the obvious, and never uses a typographic dash.",
6
6
  "updated": "2026-08-19T12:37:59+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "a5a228a302f50d975a947b9d84676b4c2a4780f2b222f819388cc98d6a12f207"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
6
6
  "updated": "2026-06-16T17:11:49+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), real-time error timing (an emptied or not-yet-filled field is incomplete, not invalid: a `*` and an aria-disabled submit instead of \"not valid\" over an empty input), schema consistency, and safe client-facing error handling.",
6
6
  "updated": "2026-08-11T17:10:56+02:00",
7
- "cliVersion": "1.46.3",
7
+ "cliVersion": "1.47.0",
8
8
  "sha": "24928a4c9eed28ebede9f23f746b09a9d3d8176f8fcca61a7780a853fa5bed4e"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "167bfd91adeffc9656744baddcac59173bfafc91dfe537c865cc31d93655f283",
3
- "enigma-linux-arm64": "a5764ad80cd3b9c678656d950723f2f76282b7f2b698aaa2af6b497fb2df8725",
4
- "enigma-linux-x64": "59778479f1166e1ac62eeafedca1c3216dce26a725d2109dd4f0fbe0dc00d8a6",
5
- "enigma-win32-x64.exe": "e4a5dde783e47963abda6b09db0475bc2d6a103e9a2ebfaf7b2cd5fc6fe93a39"
2
+ "enigma-darwin-arm64": "a7fb66c4f1d421d71822eb1179118e59b35fb77062c53d4453b880b7fe78b2da",
3
+ "enigma-linux-arm64": "3b0fd51bf5f6160215d0a00f4fb9225b4b6551b5a046250ed457310fc10775c1",
4
+ "enigma-linux-x64": "b4ca6b52c7a752e45fbc3be2fe3a8cf066c5d214dd184d3e7d9a752781604ab3",
5
+ "enigma-win32-x64.exe": "c95d6ffbe7985e30f5b0ce761eb1a8e5f230a9c2f6f377324275d114bd7da041"
6
6
  }
package/bin/download.mjs CHANGED
@@ -11,9 +11,9 @@
11
11
  * Used by bin/postinstall.mjs (at install) and bin/enigma.mjs (lazy, first run when
12
12
  * install scripts were skipped). Node builtins only.
13
13
  */
14
- import { dirname } from "node:path";
15
14
  import { createHash } from "node:crypto";
16
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
17
17
 
18
18
  import {
19
19
  ARCH,
@@ -21,6 +21,7 @@ import {
21
21
  assetName,
22
22
  binTargetPath,
23
23
  isWindows,
24
+ legacyBinTargetPath,
24
25
  loadChecksums,
25
26
  packageVersion,
26
27
  platformKeys
@@ -52,6 +53,54 @@ function sidecarPath() {
52
53
  return `${binTargetPath()}.sha256`;
53
54
  };
54
55
 
56
+ /**
57
+ * Move an existing binary aside instead of deleting it.
58
+ *
59
+ * Windows refuses to delete or overwrite an `.exe` that a running process has mapped as an
60
+ * image, and it DOES allow renaming one: the live processes keep the image they already
61
+ * mapped, and the next launch picks up the new file. Deleting first - what this did - failed
62
+ * precisely when an agent session was open, which is most of the time.
63
+ */
64
+ function retireBinary(target) {
65
+ if (!existsSync(target)) return;
66
+ try {
67
+ renameSync(target, `${target}.old-${Date.now()}`);
68
+ } catch {
69
+ // No rename either (a permission problem rather than a mapped image): fall back to the
70
+ // delete, and let the rename that follows surface whatever is really wrong.
71
+ try { unlinkSync(target); } catch { /* reported by the caller's rename */ }
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Delete what this version has finished with. Best-effort: a file still mapped by a live
77
+ * process refuses to go and is left for the next run.
78
+ *
79
+ * Deliberately NOT the sibling version directories. A machine can have enigma installed under
80
+ * two Node versions at two different versions - which is exactly the setup that made the
81
+ * per-version directory necessary - and an install that deleted its neighbour's binary would
82
+ * have the two of them take turns re-downloading 96 MB forever. Superseded versions are left
83
+ * on disk and reclaimed by `enigma resources`, where the user is already deleting things on
84
+ * purpose.
85
+ */
86
+ function sweepStaleBinaries(target) {
87
+ const versionDir = dirname(target);
88
+ for (const name of safeReaddir(versionDir)) {
89
+ if (!/\.old-\d+$/.test(name)) continue;
90
+ try { unlinkSync(join(versionDir, name)); } catch { /* still mapped by a live process */ }
91
+ }
92
+ // The pre-1.46.4 location, inside the npm package: dead weight now, and the file npm used
93
+ // to trip over on upgrade.
94
+ for (const path of [legacyBinTargetPath(), `${legacyBinTargetPath()}.sha256`]) {
95
+ try { unlinkSync(path); } catch { /* absent, or still mapped by an older launcher */ }
96
+ }
97
+ };
98
+
99
+ /** readdirSync that answers with an empty list instead of throwing on a missing directory. */
100
+ function safeReaddir(dir) {
101
+ try { return readdirSync(dir); } catch { return []; }
102
+ };
103
+
55
104
  /** The sha256 this package version expects for the host's asset, or null if none is known. */
56
105
  function expectedSha() {
57
106
  const choice = selectAsset();
@@ -103,19 +152,19 @@ export async function downloadBinary({ log = () => {} } = {}) {
103
152
 
104
153
  const actual = createHash("sha256").update(buffer).digest("hex");
105
154
  if (actual !== choice.sha256) throw new Error(`Checksum mismatch for ${choice.asset}: expected ${choice.sha256}, got ${actual}.`);
106
-
155
+
107
156
  const target = binTargetPath();
108
157
  mkdirSync(dirname(target), { recursive: true });
109
158
  const tmp = `${target}.download-${process.pid}`;
110
159
  writeFileSync(tmp, buffer);
111
160
  if (!isWindows) chmodSync(tmp, 0o755);
112
- // rename is atomic on POSIX; on Windows it fails over an existing file, so drop it first.
113
- if (existsSync(target)) try { unlinkSync(target); } catch { /* in use; rename will surface the error */ }
161
+ retireBinary(target);
114
162
  renameSync(tmp, target);
163
+ sweepStaleBinaries(target);
115
164
  // Record the installed sha so installedBinary() can detect a stale binary after an
116
165
  // update without re-hashing the whole file on every launch. Best-effort: a missing
117
166
  // sidecar just triggers one extra re-download next time.
118
167
  try { writeFileSync(sidecarPath(), choice.sha256); } catch { /* sidecar is best-effort */ }
119
168
  log(`Installed ${choice.asset} -> ${target}`);
120
169
  return target;
121
- };
170
+ };
package/bin/platform.mjs CHANGED
@@ -59,9 +59,45 @@ export function assetName(key) {
59
59
  return `enigma-${key}${isWindows ? ".exe" : ""}`;
60
60
  };
61
61
 
62
- /** On-disk path of the installed binary inside this package's bin/ directory. */
62
+ /** File name of the compiled binary for this host. */
63
+ const binName = () => (isWindows ? "enigma-bin.exe" : "enigma-bin");
64
+
65
+ /**
66
+ * Directory the compiled binaries live in, one subdirectory per package version.
67
+ *
68
+ * Deliberately OUTSIDE the npm package, and this is the whole point of it. npm copies a
69
+ * package's directory aside before replacing it, and Windows refuses to copy or delete an
70
+ * `.exe` that a running process has mapped as an image - so keeping a 96 MB binary in
71
+ * `<pkg>/bin` made `npm i -g enigma-cli@latest` fail with EBUSY for anyone holding an agent
72
+ * session open, which on a machine running several is always. Nothing npm touches can hold
73
+ * the binary any more.
74
+ *
75
+ * Per VERSION because a machine can have enigma installed under more than one Node version
76
+ * (nvm), and a single shared path would have the two installs overwrite each other's binary
77
+ * and re-download on every launch, each seeing the other's checksum as stale.
78
+ */
79
+ export function binDir() {
80
+ return join(process.env.ENIGMA_CONFIG_HOME || os.homedir(), ".enigma", "bin");
81
+ };
82
+
83
+ /**
84
+ * On-disk path of the installed binary for THIS package version.
85
+ *
86
+ * The version is read defensively: this path is resolved on every launch, and an unreadable
87
+ * package.json must degrade to one shared directory rather than throw and take the CLI with it.
88
+ */
63
89
  export function binTargetPath() {
64
- return join(pkgRoot, "bin", isWindows ? "enigma-bin.exe" : "enigma-bin");
90
+ let version = "unknown";
91
+ try { version = packageVersion(); } catch { /* fall back to the shared directory */ }
92
+ return join(binDir(), version, binName());
93
+ };
94
+
95
+ /**
96
+ * Where releases up to 1.46.3 put the binary: inside the package. Kept so a stale copy can be
97
+ * swept after an upgrade - it is dead weight, and the thing npm used to trip over.
98
+ */
99
+ export function legacyBinTargetPath() {
100
+ return join(pkgRoot, "bin", binName());
65
101
  };
66
102
 
67
103
  /** This package's declared version (release tag is `v<version>`). */
@@ -78,4 +114,4 @@ export function loadChecksums() {
78
114
  } catch {
79
115
  return {};
80
116
  }
81
- };
117
+ };
@@ -538,7 +538,7 @@ var init_codegraph_rank = __esm({
538
538
  });
539
539
 
540
540
  // src/codegraph-extract.ts
541
- import { execFileSync } from "child_process";
541
+ import { execFileSync as execFileSync2 } from "child_process";
542
542
  import { extname, join as join4, relative as relative2, resolve as resolve2 } from "path";
543
543
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
544
544
  function symRules(lang) {
@@ -830,7 +830,7 @@ function extractFile(lang, content) {
830
830
  }
831
831
  function gitList(root, args) {
832
832
  try {
833
- return execFileSync("git", ["-C", root, "ls-files", ...args, "-z"], {
833
+ return execFileSync2("git", ["-C", root, "ls-files", ...args, "-z"], {
834
834
  encoding: "utf8",
835
835
  maxBuffer: 64 * 1024 * 1024,
836
836
  stdio: ["ignore", "pipe", "ignore"],
@@ -2246,6 +2246,24 @@ var init_codegraph_format = __esm({
2246
2246
 
2247
2247
  // src/codegraph-hook.ts
2248
2248
  import { homedir as homedir3 } from "os";
2249
+
2250
+ // src/governor.ts
2251
+ import { execFileSync } from "child_process";
2252
+ import { cpus, totalmem, freemem, setPriority } from "os";
2253
+ function priorityValue(priority) {
2254
+ return priority === "idle" ? 19 : 10;
2255
+ }
2256
+ function deprioritize(child) {
2257
+ if (child.pid === void 0) return false;
2258
+ try {
2259
+ setPriority(child.pid, priorityValue("below-normal"));
2260
+ return true;
2261
+ } catch {
2262
+ return false;
2263
+ }
2264
+ }
2265
+
2266
+ // src/codegraph-hook.ts
2249
2267
  import { spawn } from "child_process";
2250
2268
  import { basename as basename2, join as join7, relative as relative3, resolve as resolve4 } from "path";
2251
2269
 
@@ -2400,6 +2418,7 @@ function backgroundIndex(cg, dir) {
2400
2418
  if (cg.isManagedPath(dir) || !claimIndex(dir)) return;
2401
2419
  try {
2402
2420
  const child = spawn(process.execPath, [process.argv[1], "codegraph", "index", dir], { detached: true, stdio: "ignore", windowsHide: true });
2421
+ deprioritize(child);
2403
2422
  child.unref();
2404
2423
  } catch {
2405
2424
  }
@@ -777,6 +777,55 @@ var BUILTIN_RULES = [
777
777
  severity: "block",
778
778
  skill: "frontend-policy"
779
779
  },
780
+ {
781
+ id: "fe-mutation-without-optimistic",
782
+ label: "A mutation that leaves the UI waiting",
783
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte"],
784
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
785
+ scope: "file",
786
+ // DIFF stage for the same reason as the rule above, and more urgently: the measured
787
+ // backlog is 116 of 140 mutating files with no optimistic update at all, so an
788
+ // edit-stage version of this would fire on almost every UI file forever. Against the
789
+ // lines a turn ADDED there is no backlog - it fires on a handler just written.
790
+ stage: "diff",
791
+ fileCheck: "fe-mutation-without-optimistic",
792
+ message: "This handler sends the mutation and then updates nothing: whatever the user sees has to come back from the server, so the interface sits still for the whole round trip on an action that usually cannot fail in an interesting way. Apply the change to local state FIRST, then send the request, and on failure restore the value you saved and say what failed - a silent revert is worse than the wait. A refetch, a revalidation or a router refresh after the await is the same wait wearing a different name. Mark the line `enigma:allow-server-first` when the response is genuinely required before the UI may change (a payment, a server-assigned identifier, an irreversible action) (frontend-policy).",
793
+ // Warn, not block: waiting is a defect for a save, a toggle or a delete and correct for
794
+ // a payment, and nothing in the file separates them. The sibling rule blocks because it
795
+ // has evidence; this one only has an absence, so it says its piece and gets out of the way.
796
+ severity: "warn",
797
+ skill: "frontend-policy"
798
+ },
799
+ {
800
+ id: "perf-unbounded-fanout",
801
+ label: "Parallel work over a list nobody counted",
802
+ files: ["*.ts", "*.tsx", "*.mts", "*.cts", "*.js", "*.jsx", "*.mjs", "*.cjs"],
803
+ excludeFiles: [
804
+ "*.test.*",
805
+ "*.spec.*",
806
+ "**/tests/**",
807
+ "**/__tests__/**",
808
+ "**/fixtures/**",
809
+ "*.min.js",
810
+ "*.d.ts",
811
+ "**/dist/**",
812
+ "**/build/**",
813
+ "**/node_modules/**",
814
+ "**/vendor/**",
815
+ "dist/**",
816
+ "build/**",
817
+ "node_modules/**",
818
+ "vendor/**"
819
+ ],
820
+ scope: "file",
821
+ // DIFF stage: an existing fan-out has been running in production and is somebody's
822
+ // decision; the one a turn just wrote is the one nobody has counted yet.
823
+ stage: "diff",
824
+ fileCheck: "perf-unbounded-fanout",
825
+ message: "This starts one expensive operation per element of a list whose length nothing here bounds - a process, a socket, a file handle or a database round trip, times however many the glob, the query or the response happened to return. It is fine for the ten items you have in mind and takes the machine down for the fifty thousand it may actually get. Cap the concurrency (a limiter, a `concurrency` option, or process the list in chunks) so the number is a decision instead of an accident, or say why the list is small enough not to need one.",
826
+ severity: "warn",
827
+ skill: "core-engineering-policy"
828
+ },
780
829
  {
781
830
  id: "fe-textarea-size-bounds",
782
831
  label: "Textarea declares a minimum and a maximum size",
@@ -2048,6 +2097,8 @@ function newPasswordAffordanceOnSignIn(content) {
2048
2097
  var FILE_CHECKS = {
2049
2098
  "proc-windows-hide": (content) => missingWindowsHide(content),
2050
2099
  "fe-server-first-mutation": (content) => serverFirstMutation(content),
2100
+ "fe-mutation-without-optimistic": (content) => mutationWithoutOptimisticUpdate(content),
2101
+ "perf-unbounded-fanout": (content) => unboundedFanout(content),
2051
2102
  "fe-textarea-size-bounds": (content) => textareaSizeBounds(content),
2052
2103
  "fe-view-blanked-while-loading": (content) => viewBlankedWhileLoading(content),
2053
2104
  "fe-truncated-value-unreachable": (content) => truncatedValueUnreachable(content),
@@ -2219,6 +2270,45 @@ function serverFirstMutation(content) {
2219
2270
  }
2220
2271
  return out;
2221
2272
  }
2273
+ var REVALIDATE_AFTER = /\brouter\.refresh\s*\(|\brevalidatePath\s*\(|\brevalidateTag\s*\(|\binvalidateQueries\s*\(|\binvalidateAll\s*\(|\brefetch\s*\(|\bmutate\s*\(\s*[)"'`]/;
2274
+ function mutationWithoutOptimisticUpdate(content) {
2275
+ if (OPTIMISTIC_SIGNAL.test(content)) return [];
2276
+ const lines = content.split("\n");
2277
+ const out = [];
2278
+ for (let i = 0; i < lines.length; i++) {
2279
+ const line = lines[i];
2280
+ if (COMMENT_LINE.test(line) || !MUTATING_REQUEST.test(line)) continue;
2281
+ const { start, end } = enclosingBlock(lines, i);
2282
+ const body = lines.slice(start, end + 1);
2283
+ if (body.some((l) => ALLOW_SERVER_FIRST.test(l))) continue;
2284
+ if (body.some((l) => !COMMENT_LINE.test(l) && ENTITY_WRITE.test(l))) continue;
2285
+ const reload = lines.slice(i + 1, end + 1).find((l) => !COMMENT_LINE.test(l) && REVALIDATE_AFTER.test(l));
2286
+ if (reload) out.push({ line: i + 1, detail: `the screen only changes once the server answers: ${reload.trim().slice(0, 80)}` });
2287
+ }
2288
+ return out;
2289
+ }
2290
+ var PROMISE_FANOUT = /\bPromise\.(all|allSettled)\s*\(/;
2291
+ var FANOUT_LITERAL = /\bPromise\.(all|allSettled)\s*\(\s*\[/;
2292
+ var FANOUT_MAPPED = /\.\s*map\s*\(/;
2293
+ var FANOUT_OVER_OBJECT = /\bObject\.(values|keys|entries)\s*\(/;
2294
+ var EXPENSIVE_PER_ITEM = /\b(?:fetch|spawn|spawnSync|exec|execSync|execFile|execFileSync|request)\s*\(|\b(?:readFile|writeFile|readFileSync|writeFileSync|copyFile|rm|mkdir)\s*\(|\.\s*(?:query|findMany|aggregate|createMany|send|upload|download)\s*\(/;
2295
+ var CONCURRENCY_BOUND = /\bp-?[Ll]imit\b|\bpMap\b|\bBottleneck\b|\bSemaphore\b|\bconcurrency\b|\bchunk(?:ed|s)?\s*\(|\bbatch(?:ed|es)?\s*\(/;
2296
+ var FANOUT_WINDOW = 6;
2297
+ function unboundedFanout(content) {
2298
+ if (CONCURRENCY_BOUND.test(content)) return [];
2299
+ const lines = content.split("\n");
2300
+ const out = [];
2301
+ for (let i = 0; i < lines.length; i++) {
2302
+ const line = lines[i];
2303
+ if (COMMENT_LINE.test(line) || !PROMISE_FANOUT.test(line) || FANOUT_LITERAL.test(line)) continue;
2304
+ const window = lines.slice(i, Math.min(lines.length, i + FANOUT_WINDOW)).filter((l) => !COMMENT_LINE.test(l));
2305
+ const text = window.join("\n");
2306
+ if (!FANOUT_MAPPED.test(text) || FANOUT_OVER_OBJECT.test(text)) continue;
2307
+ const heavy = EXPENSIVE_PER_ITEM.exec(text);
2308
+ if (heavy) out.push({ line: i + 1, detail: `one ${heavy[0].replace(/[\s(.]/g, "")} per element, and nothing says how many elements there are` });
2309
+ }
2310
+ return out;
2311
+ }
2222
2312
  var TEXTAREA = /<textarea\b/;
2223
2313
  var TEXTAREA_LOWER = /\brows\s*=|\brows:\s*\d|min-h-|min-height|minHeight|\bh-\[|\bh-\d|height\s*:\s*\d/;
2224
2314
  var TEXTAREA_UPPER = /max-h-|max-height|maxHeight/;
@@ -3067,6 +3157,7 @@ export {
3067
3157
  loadRules,
3068
3158
  missingPathAlias,
3069
3159
  missingWindowsHide,
3160
+ mutationWithoutOptimisticUpdate,
3070
3161
  newPasswordAffordanceOnSignIn,
3071
3162
  operatorHomePathLeak,
3072
3163
  pageAwaitWithoutBoundary,
@@ -3082,6 +3173,7 @@ export {
3082
3173
  textareaSizeBounds,
3083
3174
  truncatedValueUnreachable,
3084
3175
  twoFactorPasswordPrompt,
3176
+ unboundedFanout,
3085
3177
  unboundedRemoteList,
3086
3178
  viewBlankedWhileLoading,
3087
3179
  wideNamedImports
package/dist/post-edit.js CHANGED
@@ -578,6 +578,7 @@ __export(guardrails_exports, {
578
578
  loadRules: () => loadRules,
579
579
  missingPathAlias: () => missingPathAlias,
580
580
  missingWindowsHide: () => missingWindowsHide,
581
+ mutationWithoutOptimisticUpdate: () => mutationWithoutOptimisticUpdate,
581
582
  newPasswordAffordanceOnSignIn: () => newPasswordAffordanceOnSignIn,
582
583
  operatorHomePathLeak: () => operatorHomePathLeak,
583
584
  pageAwaitWithoutBoundary: () => pageAwaitWithoutBoundary,
@@ -593,6 +594,7 @@ __export(guardrails_exports, {
593
594
  textareaSizeBounds: () => textareaSizeBounds,
594
595
  truncatedValueUnreachable: () => truncatedValueUnreachable,
595
596
  twoFactorPasswordPrompt: () => twoFactorPasswordPrompt,
597
+ unboundedFanout: () => unboundedFanout,
596
598
  unboundedRemoteList: () => unboundedRemoteList,
597
599
  viewBlankedWhileLoading: () => viewBlankedWhileLoading,
598
600
  wideNamedImports: () => wideNamedImports
@@ -736,6 +738,37 @@ function serverFirstMutation(content) {
736
738
  }
737
739
  return out;
738
740
  }
741
+ function mutationWithoutOptimisticUpdate(content) {
742
+ if (OPTIMISTIC_SIGNAL.test(content)) return [];
743
+ const lines = content.split("\n");
744
+ const out = [];
745
+ for (let i = 0; i < lines.length; i++) {
746
+ const line = lines[i];
747
+ if (COMMENT_LINE.test(line) || !MUTATING_REQUEST.test(line)) continue;
748
+ const { start, end } = enclosingBlock(lines, i);
749
+ const body = lines.slice(start, end + 1);
750
+ if (body.some((l) => ALLOW_SERVER_FIRST.test(l))) continue;
751
+ if (body.some((l) => !COMMENT_LINE.test(l) && ENTITY_WRITE.test(l))) continue;
752
+ const reload = lines.slice(i + 1, end + 1).find((l) => !COMMENT_LINE.test(l) && REVALIDATE_AFTER.test(l));
753
+ if (reload) out.push({ line: i + 1, detail: `the screen only changes once the server answers: ${reload.trim().slice(0, 80)}` });
754
+ }
755
+ return out;
756
+ }
757
+ function unboundedFanout(content) {
758
+ if (CONCURRENCY_BOUND.test(content)) return [];
759
+ const lines = content.split("\n");
760
+ const out = [];
761
+ for (let i = 0; i < lines.length; i++) {
762
+ const line = lines[i];
763
+ if (COMMENT_LINE.test(line) || !PROMISE_FANOUT.test(line) || FANOUT_LITERAL.test(line)) continue;
764
+ const window = lines.slice(i, Math.min(lines.length, i + FANOUT_WINDOW)).filter((l) => !COMMENT_LINE.test(l));
765
+ const text = window.join("\n");
766
+ if (!FANOUT_MAPPED.test(text) || FANOUT_OVER_OBJECT.test(text)) continue;
767
+ const heavy = EXPENSIVE_PER_ITEM.exec(text);
768
+ if (heavy) out.push({ line: i + 1, detail: `one ${heavy[0].replace(/[\s(.]/g, "")} per element, and nothing says how many elements there are` });
769
+ }
770
+ return out;
771
+ }
739
772
  function textareaSizeBounds(content) {
740
773
  const lower = TEXTAREA_LOWER.test(content);
741
774
  const upper = TEXTAREA_UPPER.test(content);
@@ -1462,7 +1495,7 @@ enigma-guardrails: BLOCKED - ${r.blocks.length} convention violation(s):`);
1462
1495
  console.log(`enigma-guardrails: ${r.count} ${all ? "tracked" : "staged"} file(s) checked, no blocking violations.`);
1463
1496
  return 0;
1464
1497
  }
1465
- var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, LIGHTBOX_IMAGE, LIGHTBOX_WHEEL, LIGHTBOX_ZOOM, LIGHTBOX_DIALOG, LIGHTBOX_MITIGATED, VIDEO_ELEMENT, VIDEO_DRIVEN, VIDEO_TIMELINE, VIDEO_CONTROLS, VIDEO_MITIGATED, TWOFA_SUBJECT, TWOFA_ENROLL, TWOFA_PASSWORD, TWOFA_MITIGATED, TWOFA_WINDOW, LISTBOX_ROLE, LISTBOX_OPEN, LISTBOX_OPTIONS, LISTBOX_MITIGATED, TOAST_QUEUE, TOAST_TIMER, TOAST_MITIGATED, PALETTE_SHORTCUT, PALETTE_MODIFIER, PALETTE_OPEN, PALETTE_FILTER, PALETTE_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1498
+ var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, REVALIDATE_AFTER, PROMISE_FANOUT, FANOUT_LITERAL, FANOUT_MAPPED, FANOUT_OVER_OBJECT, EXPENSIVE_PER_ITEM, CONCURRENCY_BOUND, FANOUT_WINDOW, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, LIGHTBOX_IMAGE, LIGHTBOX_WHEEL, LIGHTBOX_ZOOM, LIGHTBOX_DIALOG, LIGHTBOX_MITIGATED, VIDEO_ELEMENT, VIDEO_DRIVEN, VIDEO_TIMELINE, VIDEO_CONTROLS, VIDEO_MITIGATED, TWOFA_SUBJECT, TWOFA_ENROLL, TWOFA_PASSWORD, TWOFA_MITIGATED, TWOFA_WINDOW, LISTBOX_ROLE, LISTBOX_OPEN, LISTBOX_OPTIONS, LISTBOX_MITIGATED, TOAST_QUEUE, TOAST_TIMER, TOAST_MITIGATED, PALETTE_SHORTCUT, PALETTE_MODIFIER, PALETTE_OPEN, PALETTE_FILTER, PALETTE_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1466
1499
  var init_guardrails = __esm({
1467
1500
  "src/guardrails.ts"() {
1468
1501
  "use strict";
@@ -2237,6 +2270,55 @@ var init_guardrails = __esm({
2237
2270
  severity: "block",
2238
2271
  skill: "frontend-policy"
2239
2272
  },
2273
+ {
2274
+ id: "fe-mutation-without-optimistic",
2275
+ label: "A mutation that leaves the UI waiting",
2276
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte"],
2277
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
2278
+ scope: "file",
2279
+ // DIFF stage for the same reason as the rule above, and more urgently: the measured
2280
+ // backlog is 116 of 140 mutating files with no optimistic update at all, so an
2281
+ // edit-stage version of this would fire on almost every UI file forever. Against the
2282
+ // lines a turn ADDED there is no backlog - it fires on a handler just written.
2283
+ stage: "diff",
2284
+ fileCheck: "fe-mutation-without-optimistic",
2285
+ message: "This handler sends the mutation and then updates nothing: whatever the user sees has to come back from the server, so the interface sits still for the whole round trip on an action that usually cannot fail in an interesting way. Apply the change to local state FIRST, then send the request, and on failure restore the value you saved and say what failed - a silent revert is worse than the wait. A refetch, a revalidation or a router refresh after the await is the same wait wearing a different name. Mark the line `enigma:allow-server-first` when the response is genuinely required before the UI may change (a payment, a server-assigned identifier, an irreversible action) (frontend-policy).",
2286
+ // Warn, not block: waiting is a defect for a save, a toggle or a delete and correct for
2287
+ // a payment, and nothing in the file separates them. The sibling rule blocks because it
2288
+ // has evidence; this one only has an absence, so it says its piece and gets out of the way.
2289
+ severity: "warn",
2290
+ skill: "frontend-policy"
2291
+ },
2292
+ {
2293
+ id: "perf-unbounded-fanout",
2294
+ label: "Parallel work over a list nobody counted",
2295
+ files: ["*.ts", "*.tsx", "*.mts", "*.cts", "*.js", "*.jsx", "*.mjs", "*.cjs"],
2296
+ excludeFiles: [
2297
+ "*.test.*",
2298
+ "*.spec.*",
2299
+ "**/tests/**",
2300
+ "**/__tests__/**",
2301
+ "**/fixtures/**",
2302
+ "*.min.js",
2303
+ "*.d.ts",
2304
+ "**/dist/**",
2305
+ "**/build/**",
2306
+ "**/node_modules/**",
2307
+ "**/vendor/**",
2308
+ "dist/**",
2309
+ "build/**",
2310
+ "node_modules/**",
2311
+ "vendor/**"
2312
+ ],
2313
+ scope: "file",
2314
+ // DIFF stage: an existing fan-out has been running in production and is somebody's
2315
+ // decision; the one a turn just wrote is the one nobody has counted yet.
2316
+ stage: "diff",
2317
+ fileCheck: "perf-unbounded-fanout",
2318
+ message: "This starts one expensive operation per element of a list whose length nothing here bounds - a process, a socket, a file handle or a database round trip, times however many the glob, the query or the response happened to return. It is fine for the ten items you have in mind and takes the machine down for the fifty thousand it may actually get. Cap the concurrency (a limiter, a `concurrency` option, or process the list in chunks) so the number is a decision instead of an accident, or say why the list is small enough not to need one.",
2319
+ severity: "warn",
2320
+ skill: "core-engineering-policy"
2321
+ },
2240
2322
  {
2241
2323
  id: "fe-textarea-size-bounds",
2242
2324
  label: "Textarea declares a minimum and a maximum size",
@@ -3496,6 +3578,8 @@ var init_guardrails = __esm({
3496
3578
  FILE_CHECKS = {
3497
3579
  "proc-windows-hide": (content) => missingWindowsHide(content),
3498
3580
  "fe-server-first-mutation": (content) => serverFirstMutation(content),
3581
+ "fe-mutation-without-optimistic": (content) => mutationWithoutOptimisticUpdate(content),
3582
+ "perf-unbounded-fanout": (content) => unboundedFanout(content),
3499
3583
  "fe-textarea-size-bounds": (content) => textareaSizeBounds(content),
3500
3584
  "fe-view-blanked-while-loading": (content) => viewBlankedWhileLoading(content),
3501
3585
  "fe-truncated-value-unreachable": (content) => truncatedValueUnreachable(content),
@@ -3545,6 +3629,14 @@ var init_guardrails = __esm({
3545
3629
  RESULT_BINDING = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?/;
3546
3630
  BLOCK_LOOKBACK = 120;
3547
3631
  BLOCK_LOOKAHEAD = 200;
3632
+ REVALIDATE_AFTER = /\brouter\.refresh\s*\(|\brevalidatePath\s*\(|\brevalidateTag\s*\(|\binvalidateQueries\s*\(|\binvalidateAll\s*\(|\brefetch\s*\(|\bmutate\s*\(\s*[)"'`]/;
3633
+ PROMISE_FANOUT = /\bPromise\.(all|allSettled)\s*\(/;
3634
+ FANOUT_LITERAL = /\bPromise\.(all|allSettled)\s*\(\s*\[/;
3635
+ FANOUT_MAPPED = /\.\s*map\s*\(/;
3636
+ FANOUT_OVER_OBJECT = /\bObject\.(values|keys|entries)\s*\(/;
3637
+ EXPENSIVE_PER_ITEM = /\b(?:fetch|spawn|spawnSync|exec|execSync|execFile|execFileSync|request)\s*\(|\b(?:readFile|writeFile|readFileSync|writeFileSync|copyFile|rm|mkdir)\s*\(|\.\s*(?:query|findMany|aggregate|createMany|send|upload|download)\s*\(/;
3638
+ CONCURRENCY_BOUND = /\bp-?[Ll]imit\b|\bpMap\b|\bBottleneck\b|\bSemaphore\b|\bconcurrency\b|\bchunk(?:ed|s)?\s*\(|\bbatch(?:ed|es)?\s*\(/;
3639
+ FANOUT_WINDOW = 6;
3548
3640
  TEXTAREA = /<textarea\b/;
3549
3641
  TEXTAREA_LOWER = /\brows\s*=|\brows:\s*\d|min-h-|min-height|minHeight|\bh-\[|\bh-\d|height\s*:\s*\d/;
3550
3642
  TEXTAREA_UPPER = /max-h-|max-height|maxHeight/;
@@ -3646,6 +3738,27 @@ var init_guardrails = __esm({
3646
3738
  }
3647
3739
  });
3648
3740
 
3741
+ // src/governor.ts
3742
+ import { execFileSync as execFileSync3 } from "child_process";
3743
+ import { cpus, totalmem, freemem, setPriority } from "os";
3744
+ function priorityValue(priority) {
3745
+ return priority === "idle" ? 19 : 10;
3746
+ }
3747
+ function deprioritize(child) {
3748
+ if (child.pid === void 0) return false;
3749
+ try {
3750
+ setPriority(child.pid, priorityValue("below-normal"));
3751
+ return true;
3752
+ } catch {
3753
+ return false;
3754
+ }
3755
+ }
3756
+ var init_governor = __esm({
3757
+ "src/governor.ts"() {
3758
+ "use strict";
3759
+ }
3760
+ });
3761
+
3649
3762
  // src/repo-keyed-file.ts
3650
3763
  import { dirname as dirname3 } from "path";
3651
3764
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
@@ -3891,7 +4004,7 @@ var init_codegraph_rank = __esm({
3891
4004
  });
3892
4005
 
3893
4006
  // src/codegraph-extract.ts
3894
- import { execFileSync as execFileSync3 } from "child_process";
4007
+ import { execFileSync as execFileSync4 } from "child_process";
3895
4008
  import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
3896
4009
  import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
3897
4010
  function symRules(lang) {
@@ -4183,7 +4296,7 @@ function extractFile(lang, content) {
4183
4296
  }
4184
4297
  function gitList(root, args) {
4185
4298
  try {
4186
- return execFileSync3("git", ["-C", root, "ls-files", ...args, "-z"], {
4299
+ return execFileSync4("git", ["-C", root, "ls-files", ...args, "-z"], {
4187
4300
  encoding: "utf8",
4188
4301
  maxBuffer: 64 * 1024 * 1024,
4189
4302
  stdio: ["ignore", "pipe", "ignore"],
@@ -5712,6 +5825,7 @@ function backgroundIndex(cg, dir) {
5712
5825
  if (cg.isManagedPath(dir) || !claimIndex(dir)) return;
5713
5826
  try {
5714
5827
  const child = spawn(process.execPath, [process.argv[1], "codegraph", "index", dir], { detached: true, stdio: "ignore", windowsHide: true });
5828
+ deprioritize(child);
5715
5829
  child.unref();
5716
5830
  } catch {
5717
5831
  }
@@ -5794,6 +5908,7 @@ var MIN_PROMPT_CHARS, PROMPT_HITS, STRONG_FLOOR, BROAD_FLOOR, INJECTED_CAP, BLAS
5794
5908
  var init_codegraph_hook = __esm({
5795
5909
  "src/codegraph-hook.ts"() {
5796
5910
  "use strict";
5911
+ init_governor();
5797
5912
  init_repo_keyed_file();
5798
5913
  MIN_PROMPT_CHARS = 12;
5799
5914
  PROMPT_HITS = 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.46.3",
3
+ "version": "1.47.0",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {