grepmax 0.26.56 → 0.26.58

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.56",
13
+ "version": "0.26.58",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -57,7 +57,7 @@ gmax doctor # Health check
57
57
  gmax doctor --fix # Repair checks; full-table maintenance is disabled
58
58
  ```
59
59
 
60
- **Storage containment:** full-table compaction is disabled, including forced maintenance and doctor repair. Existing retained index copies are not reclaimed by this release. The verified prune-only recovery path is separate work; normal incremental indexing remains available when the host is healthy and not quarantined. A persistent host safety stop blocks mutation and daemon restart. Installing an update preserves existing quarantine. Speculative embedding warmup and maintenance timers are disabled. On macOS, startup, heavy queries, worker forks and MLX launches require known normal OS memory pressure and a healthy kernel-zone sample; unavailable probes report bounded diagnostic details and refuse work.
60
+ **Storage containment:** full-table compaction is disabled, including forced maintenance and doctor repair. Existing retained index copies are not reclaimed by this release. The verified prune-only recovery path is separate work; normal incremental indexing remains available when the host is healthy and not quarantined. A persistent host safety stop blocks mutation and normal daemon restart; an explicit read-only start can serve the retained index. Installing an update preserves existing quarantine. Speculative embedding warmup and maintenance timers are disabled. On macOS, startup, heavy queries, worker forks and MLX launches require known normal OS memory pressure and a healthy kernel-zone sample; warning or unavailable probes pause heavy work while the daemon serves bounded reads. Known critical pressure still stops the daemon.
61
61
 
62
62
  ### Core Commands
63
63
 
@@ -472,6 +472,12 @@ gmax watch restart # Restart daemon
472
472
 
473
473
  `gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
474
474
 
475
+ `gmax doctor` and `gmax status` report persistent safety stops and daemon startup quarantine. `doctor` exits with code 2 when startup is blocked; `status --json` includes `daemon.startupBlockedReason`. Installing an update preserves these stops. macOS kernel-zone probes use `zprint -L` to omit the wired-memory report and kernel symbolication while retaining the same zone counters and pressure thresholds.
476
+
477
+ When heavy work is paused, the daemon keeps metadata, bounded symbol/file lookups, stored skeletons by exact file path, and keyword search available. Search uses the existing FTS index and excludes unindexed rows, loads no embedding model, returns at most 20 results, and reports keyword-only scores and retained-index freshness. Native queries have a two-second deadline and run one at a time with 48 MiB of combined Lance cache capacity. Indexing, watchers, vector/graph scans, and model launches remain paused. Missing FTS indexes return an error; paused search never rebuilds an index or falls back to a corpus scan.
478
+
479
+ To serve bounded reads under an existing startup quarantine, run `gmax watch --daemon --read-only -b`. This preserves the safety markers. `status --json` reports `daemon.service.mode` and its reason; `doctor` reports paused availability and skips native corpus diagnostics. Pausing is sticky for that daemon process. Full service requires a deliberate restart after host recovery and quarantine review. Known critical OS or kernel pressure still stops even the read-only service.
480
+
475
481
  Full-table compaction and new FTS/ANN index builds are disabled in the containment release, including forced maintenance and doctor repair. Existing indexes remain readable; missing lexical indexes fall back to available retrieval. Retained copies are not recovered by this release. A separate exclusive prune-only path requires validation before deployment. Do not repeatedly force repair, use future cutoffs or manually remove index fragments.
476
482
 
477
483
  `~/.gmax/logs/daemon.log` records containment skips and retained historical compaction outcomes, including duration, logical size, disk size before/after, free space, and bytes reclaimed. `gmax status --json` exposes the last outcome under `daemon.compaction`; its `at` timestamp identifies when it was recorded. Five-minute resource snapshots track footprint, RSS, heap, external/ArrayBuffer memory, Lance cache, workers and pending work for memory investigations. Reading status uses retained snapshots and does not scan the index directory. Logs rotate to `daemon.log.prev`.
@@ -52,6 +52,7 @@ const config_1 = require("../config");
52
52
  const embedding_status_1 = require("../lib/index/embedding-status");
53
53
  const index_config_1 = require("../lib/index/index-config");
54
54
  const maintenance_policy_1 = require("../lib/store/maintenance-policy");
55
+ const autostart_1 = require("../lib/utils/autostart");
55
56
  const doctor_optimize_1 = require("../lib/utils/doctor-optimize");
56
57
  const doctor_status_1 = require("../lib/utils/doctor-status");
57
58
  const exit_1 = require("../lib/utils/exit");
@@ -346,7 +347,7 @@ exports.doctor = new commander_1.Command("doctor")
346
347
  .option("--agent", "Compact output for AI agents", false)
347
348
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
348
349
  var _a, _b;
349
- var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
350
+ var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
350
351
  if (opts.fix) {
351
352
  const denied = (0, maintenance_policy_1.storeMutationDeniedReason)();
352
353
  if (denied !== null) {
@@ -357,6 +358,32 @@ exports.doctor = new commander_1.Command("doctor")
357
358
  }
358
359
  if (!opts.agent)
359
360
  console.log("gmax Doctor\n");
361
+ const startupBlockedReason = (0, autostart_1.daemonStartDeniedReason)();
362
+ if (startupBlockedReason !== null) {
363
+ const reason = startupBlockedReason.replace(/[\r\n\t]/g, " ");
364
+ console.log(opts.agent
365
+ ? `daemon_startup\tblocked=true\treason=${reason}`
366
+ : `FAIL Daemon startup blocked: ${reason}`);
367
+ // Inspect the retained index, but do not report a healthy installation
368
+ // while the persistent host stop or autostart quarantine prevents use.
369
+ process.exitCode = 2;
370
+ }
371
+ // Metadata-only diagnostics while paused: never open a second native
372
+ // reader or scan the retained corpus on a pressured host.
373
+ const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
374
+ const daemonStatus = yield sendDaemonCommand({ cmd: "status" });
375
+ const service = daemonStatus.service;
376
+ if (daemonStatus.ok && (service === null || service === void 0 ? void 0 : service.mode) === "paused") {
377
+ const reason = String((_c = service.reason) !== null && _c !== void 0 ? _c : "read-only service requested").replace(/[\r\n\t]/g, " ");
378
+ console.log(opts.agent
379
+ ? `daemon_service\tmode=paused\treads=bounded\treason=${reason}`
380
+ : `WARN Daemon available for bounded reads; indexing and embeddings paused: ${reason}`);
381
+ console.log(opts.agent
382
+ ? "index_diagnostics\tskipped=paused_service"
383
+ : "INFO Native index diagnostics skipped while heavy work is paused");
384
+ process.exitCode = 2;
385
+ return;
386
+ }
360
387
  const root = config_1.PATHS.globalRoot;
361
388
  const models = config_1.PATHS.models;
362
389
  const grammars = config_1.PATHS.grammars;
@@ -371,12 +398,12 @@ exports.doctor = new commander_1.Command("doctor")
371
398
  checkDir("Grammars", grammars);
372
399
  }
373
400
  const globalConfig = (0, index_config_1.readGlobalConfig)();
374
- const tier = (_c = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _c !== void 0 ? _c : config_1.MODEL_TIERS.small;
401
+ const tier = (_d = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _d !== void 0 ? _d : config_1.MODEL_TIERS.small;
375
402
  if (!config_1.MODEL_TIERS[globalConfig.modelTier]) {
376
403
  console.log(`WARN Unknown model tier '${globalConfig.modelTier}', falling back to 'small'`);
377
404
  }
378
405
  const embedModel = globalConfig.embedMode === "gpu"
379
- ? ((_d = globalConfig.mlxModel) !== null && _d !== void 0 ? _d : tier.mlxModel)
406
+ ? ((_e = globalConfig.mlxModel) !== null && _e !== void 0 ? _e : tier.mlxModel)
380
407
  : tier.onnxModel;
381
408
  if (!opts.agent) {
382
409
  console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
@@ -437,7 +464,7 @@ exports.doctor = new commander_1.Command("doctor")
437
464
  body: JSON.stringify({ texts: ["gmax health check"] }),
438
465
  });
439
466
  const embedData = yield embedRes.json();
440
- const dim = (_e = (_b = (_a = embedData === null || embedData === void 0 ? void 0 : embedData.vectors) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.length) !== null && _e !== void 0 ? _e : 0;
467
+ const dim = (_f = (_b = (_a = embedData === null || embedData === void 0 ? void 0 : embedData.vectors) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.length) !== null && _f !== void 0 ? _f : 0;
441
468
  const ms = Date.now() - start;
442
469
  const expectedDim = tier.vectorDim || 384;
443
470
  if (dim === expectedDim) {
@@ -464,7 +491,7 @@ exports.doctor = new commander_1.Command("doctor")
464
491
  // caller most likely to be inside the sandbox this check is about.
465
492
  const sandbox = checkClaudeSandbox({
466
493
  home: os.homedir(),
467
- projectRoot: (_f = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _f !== void 0 ? _f : process.cwd(),
494
+ projectRoot: (_g = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _g !== void 0 ? _g : process.cwd(),
468
495
  });
469
496
  if (sandbox) {
470
497
  if (opts.agent) {
@@ -583,7 +610,7 @@ exports.doctor = new commander_1.Command("doctor")
583
610
  ? "LOW"
584
611
  : "ok";
585
612
  }
586
- catch (_p) { }
613
+ catch (_q) { }
587
614
  const staleChunkerProjects = projects.filter((p) => { var _a; return p.status === "indexed" &&
588
615
  ((_a = p.chunkerVersion) !== null && _a !== void 0 ? _a : 1) < config_1.CONFIG.CHUNKER_VERSION; });
589
616
  const staleEmbeddingProjects = projects.filter((p) => p.status === "indexed" &&
@@ -612,9 +639,9 @@ exports.doctor = new commander_1.Command("doctor")
612
639
  `schema_dim=${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "none"}`,
613
640
  `schema_dim_ok=${schemaGap ? "false" : "true"}`,
614
641
  `ann_index=${vectorIndex ? "present" : "absent"}`,
615
- `ann_indexed=${(_g = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.numIndexedRows) !== null && _g !== void 0 ? _g : 0}`,
616
- `ann_unindexed=${(_h = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.numUnindexedRows) !== null && _h !== void 0 ? _h : 0}`,
617
- `ann_metric=${(_j = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.distanceType) !== null && _j !== void 0 ? _j : "none"}`,
642
+ `ann_indexed=${(_h = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.numIndexedRows) !== null && _h !== void 0 ? _h : 0}`,
643
+ `ann_unindexed=${(_j = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.numUnindexedRows) !== null && _j !== void 0 ? _j : 0}`,
644
+ `ann_metric=${(_k = vectorIndexStats === null || vectorIndexStats === void 0 ? void 0 : vectorIndexStats.distanceType) !== null && _k !== void 0 ? _k : "none"}`,
618
645
  ];
619
646
  console.log(fields.join("\t"));
620
647
  if (schemaGap) {
@@ -669,7 +696,7 @@ exports.doctor = new commander_1.Command("doctor")
669
696
  console.log(`ok Schema: vector table is ${physicalDim}d`);
670
697
  }
671
698
  if (vectorIndexStats) {
672
- console.log(`ok ANN: ${vectorIndexStats.numIndexedRows.toLocaleString()} indexed, ${vectorIndexStats.numUnindexedRows.toLocaleString()} unindexed, metric=${(_k = vectorIndexStats.distanceType) !== null && _k !== void 0 ? _k : "unknown"}`);
699
+ console.log(`ok ANN: ${vectorIndexStats.numIndexedRows.toLocaleString()} indexed, ${vectorIndexStats.numUnindexedRows.toLocaleString()} unindexed, metric=${(_l = vectorIndexStats.distanceType) !== null && _l !== void 0 ? _l : "unknown"}`);
673
700
  }
674
701
  else {
675
702
  console.log("INFO ANN: vector index not built (set GMAX_ANN=1)");
@@ -798,7 +825,7 @@ exports.doctor = new commander_1.Command("doctor")
798
825
  yield mc.close();
799
826
  }
800
827
  }
801
- catch (_q) { }
828
+ catch (_r) { }
802
829
  }
803
830
  }
804
831
  // --fix auto-remediation
@@ -876,7 +903,7 @@ exports.doctor = new commander_1.Command("doctor")
876
903
  const done = yield sendStreamingCommand({ cmd: "index", root: p.root, reset: true }, () => { }).catch((e) => ({ ok: false, error: String(e) }));
877
904
  if (done.ok &&
878
905
  (!("degraded" in done) || done.degraded !== true)) {
879
- const chunks = (_l = done.indexed) !== null && _l !== void 0 ? _l : 0;
906
+ const chunks = (_m = done.indexed) !== null && _m !== void 0 ? _m : 0;
880
907
  if (opts.agent) {
881
908
  console.log(`stale_chunker_reindexed\tname=${name}\tchunks=${chunks}`);
882
909
  }
@@ -888,7 +915,7 @@ exports.doctor = new commander_1.Command("doctor")
888
915
  else if (!opts.agent) {
889
916
  const degraded = "degraded" in done && done.degraded === true;
890
917
  const reason = degraded
891
- ? `${(_m = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _m !== void 0 ? _m : 0} scan errors, ${(_o = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _o !== void 0 ? _o : 0} file failures`
918
+ ? `${(_o = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _o !== void 0 ? _o : 0} scan errors, ${(_p = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _p !== void 0 ? _p : 0} file failures`
892
919
  : done.error;
893
920
  console.log(`FAIL ${name}: reindex failed (${reason})`);
894
921
  }
@@ -51,6 +51,7 @@ const config_1 = require("../config");
51
51
  const embedding_status_1 = require("../lib/index/embedding-status");
52
52
  const index_config_1 = require("../lib/index/index-config");
53
53
  const index_state_footer_1 = require("../lib/output/index-state-footer");
54
+ const autostart_1 = require("../lib/utils/autostart");
54
55
  const daemon_client_1 = require("../lib/utils/daemon-client");
55
56
  const exit_1 = require("../lib/utils/exit");
56
57
  const filter_builder_1 = require("../lib/utils/filter-builder");
@@ -78,12 +79,13 @@ function loadStatusView(projects) {
78
79
  daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "status" }),
79
80
  render: (resp) => __awaiter(this, void 0, void 0, function* () {
80
81
  var _a;
82
+ var _b;
81
83
  const watchers = new Map();
82
84
  const entries = Array.isArray(resp.projects) ? resp.projects : [];
83
85
  const health = new Map();
84
86
  for (const entry of entries) {
85
87
  if (entry && typeof entry.root === "string") {
86
- watchers.set(entry.root, { status: (_a = entry.status) !== null && _a !== void 0 ? _a : "watching" });
88
+ watchers.set(entry.root, { status: (_b = entry.status) !== null && _b !== void 0 ? _b : "watching" });
87
89
  if (entry.indexState)
88
90
  health.set(entry.root, entry.indexState);
89
91
  }
@@ -93,15 +95,21 @@ function loadStatusView(projects) {
93
95
  // the in-process path does when the LanceDB query fails.
94
96
  const chunkCounts = new Map();
95
97
  for (const project of projects) {
98
+ if (((_a = resp.service) === null || _a === void 0 ? void 0 : _a.mode) === "paused")
99
+ break;
96
100
  const stats = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "project-stats", root: project.root }, { timeoutMs: 30000 });
97
101
  if (stats.ok && typeof stats.chunks === "number") {
98
102
  chunkCounts.set(project.root, stats.chunks);
99
103
  }
100
104
  }
101
- const daemon = Object.assign(Object.assign(Object.assign({}, (resp.compaction !== undefined
105
+ const daemon = Object.assign(Object.assign(Object.assign(Object.assign({}, (resp.compaction !== undefined
102
106
  ? { compaction: resp.compaction }
103
107
  : {})), (resp.resources !== undefined
104
108
  ? { resources: resp.resources }
109
+ : {})), (resp.service
110
+ ? {
111
+ service: resp.service,
112
+ }
105
113
  : {})), { pid: typeof resp.pid === "number" ? resp.pid : null, uptimeSec: typeof resp.uptime === "number" ? resp.uptime : null,
106
114
  // Older daemons do not report these.
107
115
  workers: typeof resp.workers === "number" ? resp.workers : null, workerThreads: resp.workerThreads &&
@@ -206,7 +214,9 @@ function buildStatusJson(input) {
206
214
  ? ((_c = input.countWorkers) !== null && _c !== void 0 ? _c : countWorkerProcesses)(d.pid)
207
215
  : null);
208
216
  }
209
- return Object.assign(Object.assign({}, (input.stores ? { stores: input.stores } : {})), { daemon: Object.assign(Object.assign({ running: d !== undefined, pid: (_d = d === null || d === void 0 ? void 0 : d.pid) !== null && _d !== void 0 ? _d : null, since: (d === null || d === void 0 ? void 0 : d.uptimeSec) != null ? now - d.uptimeSec * 1000 : null, workerThreads: (_e = d === null || d === void 0 ? void 0 : d.workerThreads) !== null && _e !== void 0 ? _e : null }, ((d === null || d === void 0 ? void 0 : d.resources) !== undefined ? { resources: d.resources } : {})), ((d === null || d === void 0 ? void 0 : d.compaction) !== undefined ? { compaction: d.compaction } : {})), settings: {
217
+ return Object.assign(Object.assign({}, (input.stores ? { stores: input.stores } : {})), { daemon: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ running: d !== undefined }, (input.startupBlockedReason != null
218
+ ? { startupBlockedReason: input.startupBlockedReason }
219
+ : {})), { pid: (_d = d === null || d === void 0 ? void 0 : d.pid) !== null && _d !== void 0 ? _d : null, since: (d === null || d === void 0 ? void 0 : d.uptimeSec) != null ? now - d.uptimeSec * 1000 : null, workerThreads: (_e = d === null || d === void 0 ? void 0 : d.workerThreads) !== null && _e !== void 0 ? _e : null }), ((d === null || d === void 0 ? void 0 : d.service) ? { service: d.service } : {})), ((d === null || d === void 0 ? void 0 : d.resources) !== undefined ? { resources: d.resources } : {})), ((d === null || d === void 0 ? void 0 : d.compaction) !== undefined ? { compaction: d.compaction } : {})), settings: {
210
220
  embedMode: globalConfig.embedMode,
211
221
  modelTier: globalConfig.modelTier,
212
222
  vectorDim: globalConfig.vectorDim,
@@ -263,8 +273,8 @@ Examples:
263
273
  gmax status --json The same, plus daemon and settings, as JSON
264
274
  `)
265
275
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
266
- var _a, _b;
267
- var _c, _d, _e, _f, _g, _h, _j;
276
+ var _a, _b, _c, _d;
277
+ var _e, _f, _g, _h, _j, _k, _l, _m;
268
278
  const globalConfig = (0, index_config_1.readGlobalConfig)();
269
279
  const projects = (0, project_registry_1.listProjects)();
270
280
  const indexing = (0, lock_1.isLocked)(config_1.PATHS.globalRoot);
@@ -283,6 +293,7 @@ Examples:
283
293
  throw err;
284
294
  }
285
295
  const { watchers, chunkCounts } = view;
296
+ const startupBlockedReason = (0, autostart_1.daemonStartDeniedReason)();
286
297
  if (opts.json) {
287
298
  const json = buildStatusJson({
288
299
  stores,
@@ -290,6 +301,7 @@ Examples:
290
301
  projects,
291
302
  globalConfig,
292
303
  indexing,
304
+ startupBlockedReason,
293
305
  workerThreads: (0, config_1.resolveWorkerThreads)({
294
306
  env: process.env.GMAX_WORKER_THREADS,
295
307
  configValue: globalConfig.workerThreads,
@@ -303,18 +315,30 @@ Examples:
303
315
  // Header
304
316
  console.log(`\n${style.bold("gmax")} · ${globalConfig.modelTier} (${globalConfig.vectorDim}d, ${globalConfig.embedMode})${indexing ? style.yellow(" · indexing...") : ""}`);
305
317
  }
318
+ if (((_b = (_a = view.daemon) === null || _a === void 0 ? void 0 : _a.service) === null || _b === void 0 ? void 0 : _b.mode) === "paused") {
319
+ const reason = ((_e = view.daemon.service.reason) !== null && _e !== void 0 ? _e : "host pressure").replace(/[\r\n\t]/g, " ");
320
+ console.log(opts.agent
321
+ ? `daemon_service\tmode=paused\treason=${reason}`
322
+ : style.yellow(`Bounded reads available; indexing and embeddings paused: ${reason}`));
323
+ }
324
+ else if (startupBlockedReason !== null) {
325
+ const reason = startupBlockedReason.replace(/[\r\n\t]/g, " ");
326
+ console.log(opts.agent
327
+ ? `daemon_startup\tblocked=true\treason=${reason}`
328
+ : style.red(`Daemon startup blocked: ${reason}`));
329
+ }
306
330
  for (const store of stores) {
307
331
  if (opts.agent) {
308
332
  console.log(`store\t${store.home}\t${store.state}\tprefixes=${store.prefixes.join(",")}${store.error ? `\terror=${store.error}` : ""}`);
309
333
  for (const project of store.projects)
310
- console.log(`store_project\t${store.home}\t${project.name}\t${(_c = project.chunkCount) !== null && _c !== void 0 ? _c : 0}\t${(_d = project.status) !== null && _d !== void 0 ? _d : "indexed"}\tcounts=cached`);
334
+ console.log(`store_project\t${store.home}\t${project.name}\t${(_f = project.chunkCount) !== null && _f !== void 0 ? _f : 0}\t${(_g = project.status) !== null && _g !== void 0 ? _g : "indexed"}\tcounts=cached`);
311
335
  }
312
336
  else {
313
337
  console.log(`\nStore: ${shortenPath(store.home)} — ${store.state}${store.error ? ` (${store.error})` : ""}`);
314
338
  if (store.state === "offline")
315
339
  console.log(" Connect the drive to list its projects.");
316
340
  for (const project of store.projects)
317
- console.log(` ${project.name} ${formatChunks(project.chunkCount)} chunks (cached) ${(_e = project.status) !== null && _e !== void 0 ? _e : "indexed"}`);
341
+ console.log(` ${project.name} ${formatChunks(project.chunkCount)} chunks (cached) ${(_h = project.status) !== null && _h !== void 0 ? _h : "indexed"}`);
318
342
  }
319
343
  }
320
344
  if (projects.length === 0) {
@@ -331,9 +355,9 @@ Examples:
331
355
  for (const project of projects) {
332
356
  const st = projectState(project, watchers.get(project.root));
333
357
  const isCurrent = project.root === currentRoot;
334
- const count = (_f = chunkCounts.get(project.root)) !== null && _f !== void 0 ? _f : project.chunkCount;
358
+ const count = (_j = chunkCounts.get(project.root)) !== null && _j !== void 0 ? _j : project.chunkCount;
335
359
  const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
336
- console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}${((_a = view.health) === null || _a === void 0 ? void 0 : _a.get(project.root)) ? `\t${(_g = (0, index_state_footer_1.formatIndexStateFooter)(view.health.get(project.root), { agent: true })) !== null && _g !== void 0 ? _g : ""}` : ""}`);
360
+ console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}${((_c = view.health) === null || _c === void 0 ? void 0 : _c.get(project.root)) ? `\t${(_k = (0, index_state_footer_1.formatIndexStateFooter)(view.health.get(project.root), { agent: true })) !== null && _k !== void 0 ? _k : ""}` : ""}`);
337
361
  }
338
362
  const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig), { agent: true });
339
363
  if (legacyNotice)
@@ -349,7 +373,7 @@ Examples:
349
373
  const watcher = watchers.get(project.root);
350
374
  // Status column
351
375
  let statusStr;
352
- const projectStatus = (_h = project.status) !== null && _h !== void 0 ? _h : "indexed";
376
+ const projectStatus = (_l = project.status) !== null && _l !== void 0 ? _l : "indexed";
353
377
  if (projectStatus === "pending") {
354
378
  statusStr = style.yellow("pending");
355
379
  }
@@ -369,7 +393,7 @@ Examples:
369
393
  statusStr = style.dim("idle");
370
394
  }
371
395
  // Chunks column
372
- const count = (_j = chunkCounts.get(project.root)) !== null && _j !== void 0 ? _j : project.chunkCount;
396
+ const count = (_m = chunkCounts.get(project.root)) !== null && _m !== void 0 ? _m : project.chunkCount;
373
397
  const chunks = `${formatChunks(count)} chunks`;
374
398
  // Age column
375
399
  const age = formatAge(project.lastIndexed);
@@ -378,7 +402,7 @@ Examples:
378
402
  const marker = isCurrent ? style.cyan(" ←") : "";
379
403
  const name = project.name.padEnd(nameWidth);
380
404
  console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr} embedding:${embedding}${marker}`);
381
- const health = (0, index_state_footer_1.formatIndexStateFooter)((_b = view.health) === null || _b === void 0 ? void 0 : _b.get(project.root), {
405
+ const health = (0, index_state_footer_1.formatIndexStateFooter)((_d = view.health) === null || _d === void 0 ? void 0 : _d.get(project.root), {
382
406
  agent: false,
383
407
  });
384
408
  if (health)
@@ -72,12 +72,18 @@ exports.watch = new commander_1.Command("watch")
72
72
  .description("Start background file watcher for live reindexing")
73
73
  .option("-b, --background", "Run watcher in background and exit")
74
74
  .option("-d, --daemon", "Run as centralized daemon watching projects that active sessions hold")
75
+ .option("--read-only", "Serve bounded reads without indexing, models, or clearing quarantine")
75
76
  .option("-p, --path <dir>", "Directory to watch (defaults to project root)")
76
77
  .option("--no-idle-timeout", "Disable the 30-minute idle shutdown")
77
78
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
78
79
  var _a, _b;
80
+ if (options.readOnly && !options.daemon) {
81
+ console.error("gmax: --read-only requires --daemon");
82
+ process.exitCode = 2;
83
+ return;
84
+ }
79
85
  const safetyDenied = (0, autostart_1.daemonStartDeniedReason)();
80
- if (safetyDenied !== null) {
86
+ if (safetyDenied !== null && !options.readOnly) {
81
87
  console.error(`gmax: ${safetyDenied}; preserving containment`);
82
88
  process.exitCode = 2;
83
89
  return;
@@ -114,6 +120,14 @@ exports.watch = new commander_1.Command("watch")
114
120
  const cliVersion = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf-8")).version;
115
121
  const resp = yield sendDaemonCommand({ cmd: "ping" });
116
122
  if (resp.version && resp.version === cliVersion) {
123
+ if (options.readOnly) {
124
+ const paused = yield sendDaemonCommand({
125
+ cmd: "pause",
126
+ reason: "read-only service requested",
127
+ });
128
+ if (!paused.ok)
129
+ throw new Error(String(paused.error));
130
+ }
117
131
  process.exit(0);
118
132
  }
119
133
  console.log(`Daemon version mismatch (${resp.version} → ${cliVersion}), restarting...`);
@@ -151,7 +165,9 @@ exports.watch = new commander_1.Command("watch")
151
165
  const { spawnDaemonProcess } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-launcher")));
152
166
  let started;
153
167
  try {
154
- started = yield spawnDaemonProcess();
168
+ started = options.readOnly
169
+ ? yield spawnDaemonProcess({ readOnly: true })
170
+ : yield spawnDaemonProcess();
155
171
  }
156
172
  catch (error) {
157
173
  const message = error instanceof Error ? error.message : String(error);
@@ -195,7 +211,10 @@ exports.watch = new commander_1.Command("watch")
195
211
  console.error("[daemon] unhandledRejection:", reason);
196
212
  });
197
213
  try {
198
- yield daemon.start();
214
+ if (options.readOnly)
215
+ yield daemon.start({ readOnly: true });
216
+ else
217
+ yield daemon.start();
199
218
  }
200
219
  catch (err) {
201
220
  const code = err === null || err === void 0 ? void 0 : err.code;