grepmax 0.26.15 → 0.26.18

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.15",
13
+ "version": "0.26.18",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -98,6 +98,12 @@ Examples:
98
98
  process.exitCode = 1;
99
99
  return;
100
100
  }
101
+ if ((0, blocked_roots_1.isGitWorktreeRoot)(projectRoot)) {
102
+ logBlockedAttempt("git_worktree", projectRoot);
103
+ console.error(`Refusing to add ${projectRoot}: ${blocked_roots_1.WORKTREE_REFUSAL}`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
101
107
  // Check if already registered
102
108
  const existing = (0, project_registry_1.getProject)(projectRoot);
103
109
  if (existing) {
@@ -59,8 +59,10 @@ const server_1 = require("../llm/server");
59
59
  const meta_cache_1 = require("../store/meta-cache");
60
60
  const store_lease_1 = require("../store/store-lease");
61
61
  const vector_db_1 = require("../store/vector-db");
62
+ const blocked_roots_1 = require("../utils/blocked-roots");
62
63
  const daemon_client_1 = require("../utils/daemon-client");
63
64
  const daemon_launcher_1 = require("../utils/daemon-launcher");
65
+ const kernel_zone_1 = require("../utils/kernel-zone");
64
66
  const keyed_mutex_1 = require("../utils/keyed-mutex");
65
67
  const log_rotate_1 = require("../utils/log-rotate");
66
68
  const logger_1 = require("../utils/logger");
@@ -127,6 +129,8 @@ class Daemon {
127
129
  this.heartbeatInterval = null;
128
130
  this.idleInterval = null;
129
131
  this.heartbeatTick = 0;
132
+ /** Rate-limits the kernel-zone warn log — see checkKernelZonePressure. */
133
+ this.lastZoneWarnMs = 0;
130
134
  this.shuttingDown = false;
131
135
  this.recycling = false;
132
136
  // False until LanceDB + MetaCache are open. The socket starts listening early
@@ -406,6 +410,7 @@ class Daemon {
406
410
  void this.mlxServerManager.checkMlxHealth();
407
411
  this.processManager.sweepOrphanWorkers();
408
412
  this.maybeRecycle();
413
+ this.checkKernelZonePressure();
409
414
  }
410
415
  }, HEARTBEAT_INTERVAL_MS);
411
416
  // 10. Idle timeout (skip when disabled via env)
@@ -785,6 +790,10 @@ class Daemon {
785
790
  return;
786
791
  }
787
792
  const project = (0, project_registry_1.getProject)(root);
793
+ if (!project && (0, blocked_roots_1.isGitWorktreeRoot)(root)) {
794
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: blocked_roots_1.WORKTREE_REFUSAL });
795
+ return;
796
+ }
788
797
  if ((project === null || project === void 0 ? void 0 : project.status) === "indexed") {
789
798
  if (!this.activeGeneration ||
790
799
  (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration)
@@ -850,6 +859,10 @@ class Daemon {
850
859
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
851
860
  return;
852
861
  }
862
+ if (!(0, project_registry_1.getProject)(root) && (0, blocked_roots_1.isGitWorktreeRoot)(root)) {
863
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: blocked_roots_1.WORKTREE_REFUSAL });
864
+ return;
865
+ }
853
866
  if (!(0, project_registry_1.getProject)(root)) {
854
867
  (0, project_registry_1.registerProject)({
855
868
  root,
@@ -1698,6 +1711,50 @@ class Daemon {
1698
1711
  this.recycling = true;
1699
1712
  void this.shutdown({ relaunch: true }).finally(() => process.exit(0));
1700
1713
  }
1714
+ /**
1715
+ * Stand down before our own write volume exhausts a leaking kernel zone.
1716
+ *
1717
+ * On macOS 26.5.2 sustained filesystem writes leak `data.kalloc.1024`
1718
+ * irreversibly, and exhausting it panics the host with no warning of any kind —
1719
+ * the daemon does not get to observe the failure, it just dies with the machine.
1720
+ * This host lost three sessions that way before the compactor was identified as
1721
+ * the largest write source on it.
1722
+ *
1723
+ * So the daemon watches the zone it is pressurizing. A warn-level reading is
1724
+ * logged and left alone; a critical reading exits without relaunch, because
1725
+ * relaunching would resume the writes that caused it. Nothing here can reclaim
1726
+ * the leaked memory — only a reboot does that — so the only useful move is to
1727
+ * stop adding to it while the user still has a working machine.
1728
+ *
1729
+ * Deliberately does nothing when the sample is unavailable or unparseable
1730
+ * (non-macOS, no `zprint`, changed output format): an unknown reading must never
1731
+ * shut the daemon down.
1732
+ */
1733
+ checkKernelZonePressure() {
1734
+ if (this.shuttingDown || this.recycling)
1735
+ return;
1736
+ const usage = (0, kernel_zone_1.readKernelZoneUsage)();
1737
+ if (!usage || usage.pressure === "ok")
1738
+ return;
1739
+ if (usage.pressure === "warn") {
1740
+ // Rate-limited to once an hour: at the 5-minute heartbeat this would
1741
+ // otherwise print 12 identical lines an hour for as long as the condition
1742
+ // holds, which on a leaking host is until reboot.
1743
+ const now = Date.now();
1744
+ if (now - this.lastZoneWarnMs < 60 * 60 * 1000)
1745
+ return;
1746
+ this.lastZoneWarnMs = now;
1747
+ console.log(`[daemon] WARNING: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — macOS kernel zone is leaking ` +
1748
+ `under write pressure. The daemon will stop itself at ` +
1749
+ `${(kernel_zone_1.ZONE_THRESHOLDS.criticalBytes / Math.pow(1024, 3)).toFixed(0)}GiB. ` +
1750
+ `Only a reboot reclaims this memory.`);
1751
+ return;
1752
+ }
1753
+ console.log(`[daemon] CRITICAL: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — stopping to avoid a kernel panic. ` +
1754
+ `Reboot to reclaim the zone; the index will catch up on next start.`);
1755
+ this.recycling = true;
1756
+ void this.shutdown().finally(() => process.exit(0));
1757
+ }
1701
1758
  shutdown(opts = {}) {
1702
1759
  if (!this.shutdownPromise) {
1703
1760
  this.shutdownPromise = this.performShutdown(opts);
@@ -157,6 +157,36 @@ function isMissingTableError(err) {
157
157
  const TABLE_NAME = "chunks";
158
158
  const MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
159
159
  const CLEAN_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000;
160
+ /**
161
+ * Floor between opportunistic full-table compactions.
162
+ *
163
+ * A compaction rewrites the whole table, so its cost scales with store size, not
164
+ * with how much changed. Neither opportunistic caller was rate-limited: the
165
+ * 5-minute maintenance tick re-optimizes whenever the write epoch moved, and
166
+ * `compactIfNeeded` fires whenever small fragments cross a threshold. On a store
167
+ * under continuous reindex pressure both are true essentially always, so the
168
+ * table gets rewritten every few minutes forever.
169
+ *
170
+ * That is not hypothetical. On 2026-08-16..17 this store (16 GB) took 43 full
171
+ * compactions in two days — an accelerating 6 → 30 → 63 → 54 per day — which a
172
+ * microstackshot measured as 549.76 GB of file-backed writes in 12.3 hours. On
173
+ * macOS 26.5.2 that write volume exhausted the `data.kalloc.1024` kernel zone and
174
+ * panicked the host. See docs/2026-08-04-macos-kernel-zone-panic-incident.md.
175
+ *
176
+ * The floor caps compaction at twice an hour regardless of how much churn arrives.
177
+ * Fragments accumulating between passes cost read performance, which is recoverable;
178
+ * unbounded rewrite volume was not.
179
+ */
180
+ const COMPACTION_MIN_INTERVAL_MS = 30 * 60 * 1000;
181
+ /**
182
+ * Ceiling for the unproductive-compaction backoff.
183
+ *
184
+ * A compaction that removes no fragments and frees no bytes rewrote the table for
185
+ * nothing, and the next one a few minutes later will almost certainly do the same.
186
+ * The interval doubles on each such pass and resets the moment one does real work,
187
+ * so a store that cannot be improved stops paying for the attempt.
188
+ */
189
+ const COMPACTION_MAX_INTERVAL_MS = 6 * 60 * 60 * 1000;
160
190
  class VectorDB {
161
191
  constructor(lancedbDir, vectorDim, suppliedLease) {
162
192
  this.lancedbDir = lancedbDir;
@@ -174,6 +204,9 @@ class VectorDB {
174
204
  /** Only an index owner may create/rebuild shared indexes — see markIndexOwner. */
175
205
  this.indexOwner = false;
176
206
  this.lastOptimizeDidWork = false;
207
+ /** Compaction rate limiter — see COMPACTION_MIN_INTERVAL_MS. */
208
+ this.lastCompactionMs = 0;
209
+ this.compactionIntervalMs = COMPACTION_MIN_INTERVAL_MS;
177
210
  this.maintenanceRunner = (fn) => fn();
178
211
  /** Latched when a from-scratch FTS rebuild failed to stop an optimize
179
212
  * panic. Stops the (expensive) rebuild from re-running every maintenance
@@ -1152,13 +1185,30 @@ class VectorDB {
1152
1185
  }
1153
1186
  yield this.createFTSIndex();
1154
1187
  yield this.createVectorIndex();
1155
- if (pressure === "low") {
1188
+ // Index creation above is idempotent and self-guarding, so it stays on the
1189
+ // 5-minute cadence. Compaction is the expensive half — it rewrites the whole
1190
+ // table — and gets rate-limited. Without this the tick compacts every 5
1191
+ // minutes for as long as writes keep arriving, which is how this store
1192
+ // reached 43 full rewrites in two days and panicked the host.
1193
+ const throttleMs = options.force
1194
+ ? 0
1195
+ : this.compactionThrottleRemainingMs();
1196
+ if (throttleMs > 0) {
1197
+ (0, logger_1.debug)("vectordb", `Compaction throttled — ${Math.ceil(throttleMs / 60000)}min until next full pass ` +
1198
+ `(interval ${Math.round(this.compactionIntervalMs / 60000)}min)`);
1199
+ }
1200
+ else if (pressure === "low") {
1156
1201
  (0, logger_1.log)("vectordb", `Low disk — single-pass optimize (no bloat retry)`);
1157
1202
  yield this.optimize(1, 0, true);
1203
+ this.noteCompaction(this.lastOptimizeDidWork);
1158
1204
  }
1159
1205
  else {
1160
1206
  // Normal maintenance: full optimize + bloat check
1161
1207
  yield this.optimize(5, 0, true);
1208
+ // Track across both passes: `lastOptimizeDidWork` is reset per optimize()
1209
+ // call, so a productive first pass followed by a barren bloat retry would
1210
+ // otherwise read as unproductive and trigger a spurious backoff.
1211
+ let didWork = this.lastOptimizeDidWork;
1162
1212
  if (this.lastOptimizeDidWork) {
1163
1213
  const table = yield this.openExistingTableUnsafe();
1164
1214
  if (table) {
@@ -1180,9 +1230,11 @@ class VectorDB {
1180
1230
  (0, logger_1.log)("vectordb", `Bloat detected after optimize: ${(diskSize / 1024 / 1024).toFixed(0)}MB disk vs ${(logicalSize / 1024 / 1024).toFixed(0)}MB logical (${bloatRatio.toFixed(1)}x) — retrying`);
1181
1231
  yield new Promise((r) => setTimeout(r, 2000));
1182
1232
  yield this.optimize(5, 0, true);
1233
+ didWork = didWork || this.lastOptimizeDidWork;
1183
1234
  }
1184
1235
  }
1185
1236
  }
1237
+ this.noteCompaction(didWork);
1186
1238
  }
1187
1239
  const maintainedTable = yield this.openExistingTableUnsafe();
1188
1240
  this.maintainedTableVersion =
@@ -1195,6 +1247,47 @@ class VectorDB {
1195
1247
  }
1196
1248
  });
1197
1249
  }
1250
+ /**
1251
+ * Milliseconds until an opportunistic full-table compaction is allowed again,
1252
+ * or 0 if one may run now. Forced callers (`gmax index`, `doctor --fix`) are
1253
+ * expected to bypass this rather than consult it.
1254
+ */
1255
+ compactionThrottleRemainingMs(now = Date.now()) {
1256
+ if (this.lastCompactionMs === 0)
1257
+ return 0;
1258
+ const elapsed = now - this.lastCompactionMs;
1259
+ // A clock that jumped backwards would otherwise wedge compaction shut.
1260
+ if (elapsed < 0)
1261
+ return 0;
1262
+ return Math.max(0, this.compactionIntervalMs - elapsed);
1263
+ }
1264
+ /**
1265
+ * Record a completed compaction and adjust the interval. Productive passes reset
1266
+ * to the floor; unproductive ones double up to the ceiling so a store that cannot
1267
+ * be improved stops being rewritten every few minutes.
1268
+ */
1269
+ noteCompaction(didWork) {
1270
+ this.lastCompactionMs = Date.now();
1271
+ if (didWork) {
1272
+ if (this.compactionIntervalMs !== COMPACTION_MIN_INTERVAL_MS) {
1273
+ (0, logger_1.debug)("vectordb", "Productive compaction — interval reset to floor");
1274
+ }
1275
+ this.compactionIntervalMs = COMPACTION_MIN_INTERVAL_MS;
1276
+ return;
1277
+ }
1278
+ const next = Math.min(this.compactionIntervalMs * 2, COMPACTION_MAX_INTERVAL_MS);
1279
+ if (next !== this.compactionIntervalMs) {
1280
+ (0, logger_1.log)("vectordb", `Compaction reclaimed nothing — backing off to ${Math.round(next / 60000)}min`);
1281
+ }
1282
+ this.compactionIntervalMs = next;
1283
+ }
1284
+ /** Test/diagnostic view of the rate limiter. */
1285
+ getCompactionThrottleState() {
1286
+ return {
1287
+ intervalMs: this.compactionIntervalMs,
1288
+ remainingMs: this.compactionThrottleRemainingMs(),
1289
+ };
1290
+ }
1198
1291
  compactIfNeeded() {
1199
1292
  return __awaiter(this, arguments, void 0, function* (threshold = config_1.FRAGMENT_COMPACT_THRESHOLD) {
1200
1293
  if (this.maintenanceRunning)
@@ -1206,8 +1299,14 @@ class VectorDB {
1206
1299
  const table = yield this.ensureTable();
1207
1300
  const stats = yield table.stats();
1208
1301
  if (stats.fragmentStats.numSmallFragments > threshold) {
1302
+ const throttleMs = this.compactionThrottleRemainingMs();
1303
+ if (throttleMs > 0) {
1304
+ (0, logger_1.debug)("vectordb", `Fragment threshold exceeded (${stats.fragmentStats.numSmallFragments} > ${threshold}) — throttled, ${Math.ceil(throttleMs / 60000)}min remaining`);
1305
+ return false;
1306
+ }
1209
1307
  (0, logger_1.log)("vectordb", `Fragment threshold exceeded (${stats.fragmentStats.numSmallFragments} > ${threshold}) — compacting`);
1210
1308
  yield this.optimize(2, 0, true);
1309
+ this.noteCompaction(this.lastOptimizeDidWork);
1211
1310
  return true;
1212
1311
  }
1213
1312
  }
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.BLOCKED_ROOTS_DESCRIPTION = void 0;
36
+ exports.WORKTREE_REFUSAL = exports.BLOCKED_ROOTS_DESCRIPTION = void 0;
37
37
  exports.getBlockedProjectRoots = getBlockedProjectRoots;
38
38
  exports.isBlockedProjectRoot = isBlockedProjectRoot;
39
+ exports.isGitWorktreeRoot = isGitWorktreeRoot;
40
+ const fs = __importStar(require("node:fs"));
39
41
  const os = __importStar(require("node:os"));
40
42
  const path = __importStar(require("node:path"));
41
43
  exports.BLOCKED_ROOTS_DESCRIPTION = "home, /, /Users, /tmp, /private, /var, /usr, /opt, /etc, /System, /Library, /Applications";
@@ -61,3 +63,27 @@ function getBlockedProjectRoots() {
61
63
  function isBlockedProjectRoot(root) {
62
64
  return getBlockedProjectRoots().has(path.resolve(root));
63
65
  }
66
+ /**
67
+ * A linked git worktree has a `.git` FILE (not a directory) pointing back into
68
+ * the main repo's `.git/worktrees/<name>`. Indexing one duplicates the whole
69
+ * corpus of the repo it was cut from — three platform worktrees once put 680k
70
+ * duplicate chunks in the index and pinned seven embed workers at ~1GB each.
71
+ */
72
+ function isGitWorktreeRoot(root) {
73
+ const gitPath = path.join(path.resolve(root), ".git");
74
+ let contents;
75
+ try {
76
+ if (!fs.statSync(gitPath).isFile())
77
+ return false;
78
+ contents = fs.readFileSync(gitPath, "utf8");
79
+ }
80
+ catch (_a) {
81
+ return false;
82
+ }
83
+ const match = /^gitdir:\s*(.+)$/m.exec(contents);
84
+ if (!(match === null || match === void 0 ? void 0 : match[1]))
85
+ return false;
86
+ const gitdir = match[1].trim().replace(/\\/g, "/");
87
+ return /(^|\/)\.git\/worktrees\//.test(gitdir);
88
+ }
89
+ exports.WORKTREE_REFUSAL = "gmax does not index git worktrees — they duplicate the repository they were cut from. Add the main repository root instead.";
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZONE_THRESHOLDS = void 0;
4
+ exports.classifyZonePressure = classifyZonePressure;
5
+ exports.parseZprintOutput = parseZprintOutput;
6
+ exports.readKernelZoneUsage = readKernelZoneUsage;
7
+ exports.formatZoneUsage = formatZoneUsage;
8
+ const node_child_process_1 = require("node:child_process");
9
+ /**
10
+ * macOS kernel-zone pressure guard.
11
+ *
12
+ * On macOS 26.5.2 (build 25F84) the `data.kalloc.1024` kernel zone leaks under
13
+ * sustained filesystem write pressure and is never reclaimed. Exhausting it panics
14
+ * the host outright — no jetsam, no swap warning, no chance to intervene, because
15
+ * the failing resource is wired kernel memory rather than anything the VM system
16
+ * manages.
17
+ *
18
+ * This host panicked three times that way. The third time the zone sat near 1.4 GB
19
+ * for twelve days, then climbed to 17.9 GB in eight hours while gmax's compactor
20
+ * dirtied 549.76 GB of file-backed memory. See
21
+ * docs/2026-08-04-macos-kernel-zone-panic-incident.md.
22
+ *
23
+ * gmax cannot fix the kernel defect, but it can decline to be the thing that
24
+ * detonates it. Sampling the zone is cheap (~0.2 s), so the daemon watches its own
25
+ * blast radius and stands down while there are still hours of headroom.
26
+ */
27
+ /** The zone that leaks. Named explicitly so `zprint` returns a single line. */
28
+ const ZONE_NAME = "data.kalloc.1024";
29
+ /**
30
+ * Warn threshold.
31
+ *
32
+ * Twelve days of ordinary use held this zone between 0.67 and 1.37 GiB, so 4 GiB is
33
+ * roughly 3x anything a healthy host has been observed to reach. During the
34
+ * August 17 burst the zone crossed this about 90 minutes in — early enough that
35
+ * stopping then would have avoided the panic with hours to spare.
36
+ */
37
+ const WARN_BYTES = 4 * Math.pow(1024, 3);
38
+ /**
39
+ * Critical threshold — the daemon stands down here.
40
+ *
41
+ * The observed zone map cap on this hardware is ~17.6 GiB. Eight GiB is under half
42
+ * of it, and at the burst rate actually measured (~2.0 GiB/hour) it still leaves
43
+ * roughly five hours before exhaustion. The margin is deliberately generous: the
44
+ * cost of standing down early is a stale index, and the cost of standing down late
45
+ * is an unplanned reboot.
46
+ */
47
+ const CRITICAL_BYTES = 8 * Math.pow(1024, 3);
48
+ function classifyZonePressure(bytes) {
49
+ if (bytes >= CRITICAL_BYTES)
50
+ return "critical";
51
+ if (bytes >= WARN_BYTES)
52
+ return "warn";
53
+ return "ok";
54
+ }
55
+ /**
56
+ * Parse one `zprint <zone>` report.
57
+ *
58
+ * Exported for tests; the column layout is a stable but undocumented text format,
59
+ * so this validates rather than trusting it:
60
+ *
61
+ * ```
62
+ * elem cur max cur max cur alloc alloc
63
+ * zone name size size size #elts #elts inuse size count
64
+ * data.kalloc.1024 1024 0K 0K 0 0 3416 0K 0
65
+ * ```
66
+ *
67
+ * The `cur size` column reads `0K` on this build, which is why the byte figure is
68
+ * derived from `cur inuse` x `elem size` rather than read directly.
69
+ */
70
+ function parseZprintOutput(output, zoneName = ZONE_NAME) {
71
+ for (const line of output.split("\n")) {
72
+ const fields = line.trim().split(/\s+/);
73
+ if (fields[0] !== zoneName)
74
+ continue;
75
+ const elementSize = Number(fields[1]);
76
+ const elements = Number(fields[6]);
77
+ if (!Number.isInteger(elementSize) || elementSize <= 0)
78
+ return null;
79
+ if (!Number.isInteger(elements) || elements < 0)
80
+ return null;
81
+ return { elements, elementSize };
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Sample the zone. Returns null off macOS, or whenever the sample cannot be
87
+ * trusted — an unparseable report must read as "unknown", never as "healthy" and
88
+ * never as a reason to stop working.
89
+ */
90
+ function readKernelZoneUsage(zoneName = ZONE_NAME) {
91
+ if (process.platform !== "darwin")
92
+ return null;
93
+ try {
94
+ const output = (0, node_child_process_1.execFileSync)("zprint", [zoneName], {
95
+ encoding: "utf-8",
96
+ timeout: 5000,
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ });
99
+ const parsed = parseZprintOutput(output, zoneName);
100
+ if (!parsed)
101
+ return null;
102
+ const bytes = parsed.elements * parsed.elementSize;
103
+ return Object.assign(Object.assign({}, parsed), { bytes, pressure: classifyZonePressure(bytes) });
104
+ }
105
+ catch (_a) {
106
+ return null;
107
+ }
108
+ }
109
+ function formatZoneUsage(usage) {
110
+ const gib = (usage.bytes / Math.pow(1024, 3)).toFixed(2);
111
+ return `${ZONE_NAME} at ${gib}GiB (${usage.elements.toLocaleString()} elements)`;
112
+ }
113
+ exports.ZONE_THRESHOLDS = {
114
+ warnBytes: WARN_BYTES,
115
+ criticalBytes: CRITICAL_BYTES,
116
+ zoneName: ZONE_NAME,
117
+ };
@@ -63,6 +63,7 @@ const path = __importStar(require("node:path"));
63
63
  const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
64
64
  const config_1 = require("../../config");
65
65
  const embedding_generation_1 = require("../index/embedding-generation");
66
+ const blocked_roots_1 = require("./blocked-roots");
66
67
  const REGISTRY_PATH = path.join(config_1.PATHS.globalRoot, "projects.json");
67
68
  const REBUILD_JOURNAL_PATH = path.join(config_1.PATHS.globalRoot, "rebuild-journal.json");
68
69
  class ProjectRegistryConflictError extends Error {
@@ -207,6 +208,10 @@ function registerProject(entry) {
207
208
  }
208
209
  withRegistryLock(() => {
209
210
  const entries = loadRegistry();
211
+ if (!entries.some((e) => e.root === entry.root) &&
212
+ (0, blocked_roots_1.isGitWorktreeRoot)(entry.root)) {
213
+ throw new Error(`${blocked_roots_1.WORKTREE_REFUSAL} (${entry.root})`);
214
+ }
210
215
  let canonicalRoot;
211
216
  try {
212
217
  canonicalRoot = fs.realpathSync(entry.root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.15",
3
+ "version": "0.26.18",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.15",
3
+ "version": "0.26.18",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -78,7 +78,23 @@ function isProjectRegistered() {
78
78
  }
79
79
  }
80
80
 
81
+ // A kill switch for the auto-start. Create ~/.gmax/autostart-disabled (or export
82
+ // GMAX_NO_AUTOSTART=1) to keep sessions from reviving the daemon — needed when the
83
+ // host is quarantined after a kernel-zone incident and the daemon's write volume is
84
+ // the thing under investigation. Delete the file to restore normal behavior.
85
+ function isAutostartDisabled() {
86
+ if (process.env.GMAX_NO_AUTOSTART === "1") return true;
87
+ try {
88
+ return require("node:fs").existsSync(
89
+ _path.join(require("node:os").homedir(), ".gmax", "autostart-disabled"),
90
+ );
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
81
96
  function startWatcher() {
97
+ if (isAutostartDisabled()) return;
82
98
  if (!isProjectRegistered()) return;
83
99
  try {
84
100
  execFileSync("gmax", ["watch", "--daemon", "-b"], {