homegraph 1.5.8 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/extraction/grammars.d.ts +19 -0
  3. package/dist/extraction/grammars.js +44 -1
  4. package/dist/extraction/index.js +17 -12
  5. package/dist/extraction/languages/arkts.d.ts +40 -7
  6. package/dist/extraction/languages/arkts.js +290 -84
  7. package/dist/extraction/tree-sitter.js +12 -3
  8. package/dist/index.js +23 -12
  9. package/dist/mcp/arkts-evidence-packs.js +1 -0
  10. package/dist/mcp/daemon.d.ts +21 -3
  11. package/dist/mcp/daemon.js +60 -6
  12. package/dist/mcp/engine.d.ts +26 -0
  13. package/dist/mcp/engine.js +143 -7
  14. package/dist/mcp/index-availability.d.ts +40 -10
  15. package/dist/mcp/index-availability.js +89 -23
  16. package/dist/mcp/index.js +9 -0
  17. package/dist/mcp/indexable-root.d.ts +22 -0
  18. package/dist/mcp/indexable-root.js +140 -0
  19. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  20. package/dist/mcp/liveness-watchdog.js +17 -5
  21. package/dist/mcp/locate-contract.d.ts +50 -0
  22. package/dist/mcp/locate-contract.js +146 -0
  23. package/dist/mcp/server-instructions.d.ts +2 -6
  24. package/dist/mcp/server-instructions.js +15 -5
  25. package/dist/mcp/session.js +15 -0
  26. package/dist/mcp/tools.d.ts +52 -0
  27. package/dist/mcp/tools.js +750 -103
  28. package/dist/project-map/index.d.ts +45 -0
  29. package/dist/project-map/index.js +373 -45
  30. package/dist/resolution/callback-synthesizer.js +251 -0
  31. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  32. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  33. package/dist/resolution/index.js +25 -21
  34. package/dist/runtime-log.d.ts +52 -0
  35. package/dist/runtime-log.js +199 -0
  36. package/dist/search/query-plan-provider.js +3 -2
  37. package/dist/search/query-plan.js +21 -19
  38. package/dist/search/query-utils.d.ts +13 -0
  39. package/dist/search/query-utils.js +64 -0
  40. package/package.json +2 -2
@@ -20,9 +20,12 @@
20
20
  * so a SIGKILL'd host still reaps its proxy promptly; the proxy's socket
21
21
  * close then decrements the daemon's refcount.
22
22
  * - When the last client disconnects the daemon lingers for
23
- * `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default 300s) so back-to-back agent
24
- * runs in the same project don't repay startup, then exits cleanly. This is
25
- * what keeps a single-agent session from leaking a daemon forever (#277).
23
+ * `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default **60s**, Spec 0047) so a
24
+ * quick reconnect does not repay startup — but **only after** any in-flight
25
+ * auto-init/full index finishes (`building_fast` / `indexing`). While a
26
+ * client is connected (refcount > 0) the daemon never idle-exits. This is
27
+ * what keeps a single-agent session from leaking a daemon forever (#277)
28
+ * without killing a half-built graph when the IDE just closed.
26
29
  *
27
30
  * What this file owns:
28
31
  * - Listening on the daemon socket and spawning per-connection sessions.
@@ -41,6 +44,21 @@
41
44
  */
42
45
  import * as net from 'net';
43
46
  import { DaemonLockInfo } from './daemon-paths';
47
+ /**
48
+ * Spec 0047: phases that block daemon idle-exit when clients=0.
49
+ * Routine watch/sync is NOT included.
50
+ */
51
+ export declare function isBuildPhaseBlockingIdle(phase: string | null | undefined): boolean;
52
+ /**
53
+ * Decide idle-timer action when the linger timer fires (refcount already 0).
54
+ * Pure helper for unit tests.
55
+ */
56
+ export declare function decideIdleExitAction(opts: {
57
+ clientCount: number;
58
+ buildBlocking: boolean;
59
+ }): 'exit' | 'rearm-idle' | 'poll-build';
60
+ /** Default idle linger after last client (exported for tests). */
61
+ export declare function defaultDaemonIdleTimeoutMs(): number;
44
62
  /**
45
63
  * Finalize daemon shutdown. On POSIX, exit immediately — it's clean and fast.
46
64
  * On Windows, do NOT force an exit while watchers may still be closing (that
@@ -21,9 +21,12 @@
21
21
  * so a SIGKILL'd host still reaps its proxy promptly; the proxy's socket
22
22
  * close then decrements the daemon's refcount.
23
23
  * - When the last client disconnects the daemon lingers for
24
- * `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default 300s) so back-to-back agent
25
- * runs in the same project don't repay startup, then exits cleanly. This is
26
- * what keeps a single-agent session from leaking a daemon forever (#277).
24
+ * `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default **60s**, Spec 0047) so a
25
+ * quick reconnect does not repay startup — but **only after** any in-flight
26
+ * auto-init/full index finishes (`building_fast` / `indexing`). While a
27
+ * client is connected (refcount > 0) the daemon never idle-exits. This is
28
+ * what keeps a single-agent session from leaking a daemon forever (#277)
29
+ * without killing a half-built graph when the IDE just closed.
27
30
  *
28
31
  * What this file owns:
29
32
  * - Listening on the daemon socket and spawning per-connection sessions.
@@ -75,6 +78,9 @@ var __importStar = (this && this.__importStar) || (function () {
75
78
  })();
76
79
  Object.defineProperty(exports, "__esModule", { value: true });
77
80
  exports.MAX_HELLO_LINE_BYTES = exports.Daemon = void 0;
81
+ exports.isBuildPhaseBlockingIdle = isBuildPhaseBlockingIdle;
82
+ exports.decideIdleExitAction = decideIdleExitAction;
83
+ exports.defaultDaemonIdleTimeoutMs = defaultDaemonIdleTimeoutMs;
78
84
  exports.finalizeDaemonExit = finalizeDaemonExit;
79
85
  exports.tryAcquireDaemonLock = tryAcquireDaemonLock;
80
86
  exports.acquireLockViaExclusiveOpen = acquireLockViaExclusiveOpen;
@@ -92,8 +98,32 @@ const transport_1 = require("./transport");
92
98
  const daemon_paths_1 = require("./daemon-paths");
93
99
  const version_1 = require("./version");
94
100
  const daemon_registry_1 = require("./daemon-registry");
95
- /** Default idle linger after the last client disconnects. */
96
- const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
101
+ /** How often to re-check build phase while deferring idle exit (Spec 0047). */
102
+ const BUILD_IDLE_POLL_MS = 2_000;
103
+ /** Default idle linger after the last client disconnects (Spec 0047: 60s). */
104
+ const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
105
+ /**
106
+ * Spec 0047: phases that block daemon idle-exit when clients=0.
107
+ * Routine watch/sync is NOT included.
108
+ */
109
+ function isBuildPhaseBlockingIdle(phase) {
110
+ return phase === 'building_fast' || phase === 'indexing';
111
+ }
112
+ /**
113
+ * Decide idle-timer action when the linger timer fires (refcount already 0).
114
+ * Pure helper for unit tests.
115
+ */
116
+ function decideIdleExitAction(opts) {
117
+ if (opts.clientCount > 0)
118
+ return 'rearm-idle';
119
+ if (opts.buildBlocking)
120
+ return 'poll-build';
121
+ return 'exit';
122
+ }
123
+ /** Default idle linger after last client (exported for tests). */
124
+ function defaultDaemonIdleTimeoutMs() {
125
+ return DEFAULT_IDLE_TIMEOUT_MS;
126
+ }
97
127
  /**
98
128
  * Hard ceiling on how long the daemon stays up with clients connected but no
99
129
  * inbound traffic. A backstop (#692): if a client's socket-close is never
@@ -372,15 +402,39 @@ class Daemon {
372
402
  return;
373
403
  if (this.idleTimeoutMs <= 0)
374
404
  return; // 0 = never idle-exit
405
+ // Spec 0047: if the last window closed mid-index, wait for build to finish
406
+ // before starting the idle linger — never exit while clients>0 either.
407
+ if (this.clients.size === 0 && this.engine.isIndexBuildInProgress()) {
408
+ this.idleTimer = setTimeout(() => {
409
+ this.idleTimer = null;
410
+ if (this.clients.size > 0)
411
+ return; // reconnect disarmed the need
412
+ if (this.engine.isIndexBuildInProgress()) {
413
+ this.armIdleTimer(); // keep polling until build settles
414
+ return;
415
+ }
416
+ this.armIdleTimer(); // build done → arm full idle linger
417
+ }, BUILD_IDLE_POLL_MS);
418
+ this.idleTimer.unref?.();
419
+ return;
420
+ }
375
421
  this.idleTimer = setTimeout(() => {
376
422
  this.idleTimer = null;
377
423
  // Last-second sanity check: if a connection landed between the timer
378
424
  // firing and now, don't exit. (setImmediate-ordering is the only way
379
425
  // this races; cheap to defend against.)
380
- if (this.clients.size > 0) {
426
+ const action = decideIdleExitAction({
427
+ clientCount: this.clients.size,
428
+ buildBlocking: this.engine.isIndexBuildInProgress(),
429
+ });
430
+ if (action === 'rearm-idle') {
381
431
  this.armIdleTimer();
382
432
  return;
383
433
  }
434
+ if (action === 'poll-build') {
435
+ this.armIdleTimer(); // switches to build-poll branch above
436
+ return;
437
+ }
384
438
  void this.stop('idle timeout');
385
439
  }, this.idleTimeoutMs);
386
440
  // Don't keep the event loop alive just for this — the net.Server keeps the
@@ -48,6 +48,13 @@ export declare class MCPEngine {
48
48
  private opts;
49
49
  private closed;
50
50
  private queryPool;
51
+ /**
52
+ * Spec 0049: auto-init deferred because the root was empty. Probe until
53
+ * indexable, then run the normal create-DB path once.
54
+ */
55
+ private deferredAutoInitRoot;
56
+ private deferProbeTimer;
57
+ private deferAutoInitPromise;
51
58
  constructor(opts?: MCPEngineOptions);
52
59
  /**
53
60
  * Start the worker-thread query pool once a default project is open (daemon
@@ -67,8 +74,21 @@ export declare class MCPEngine {
67
74
  getProjectPath(): string | null;
68
75
  /** Shared ToolHandler — sessions delegate tool dispatch through this. */
69
76
  getToolHandler(): ToolHandler;
77
+ /**
78
+ * Spec 0047: true while auto-init / full symbol build is in flight.
79
+ * Used by the daemon idle timer — never idle-exit mid-build when clients=0.
80
+ * Does not include routine watch/sync increments.
81
+ */
82
+ isIndexBuildInProgress(): boolean;
70
83
  /** Whether the default project's HomeGraph is open. */
71
84
  hasDefaultHomeGraph(): boolean;
85
+ /** Spec 0049: true while auto-init is waiting for an empty root to grow sources. */
86
+ isAutoInitDeferred(): boolean;
87
+ /**
88
+ * Spec 0049: on tool call (or timer), re-check the deferred root and start
89
+ * the normal auto-init once it becomes indexable. Idempotent.
90
+ */
91
+ kickDeferredAutoInit(): Promise<boolean>;
72
92
  /**
73
93
  * Walk up from `searchFrom` to find the nearest `.homegraph/` and open it.
74
94
  * Idempotent: concurrent callers share one in-flight init; subsequent
@@ -100,6 +120,11 @@ export declare class MCPEngine {
100
120
  * @returns true if the engine is fully wired to a new (or racing) index
101
121
  */
102
122
  private tryAutoInit;
123
+ /** Spec 0049: create DB + fast map + background full (shared by first hit and kick). */
124
+ private runAutoInitCreate;
125
+ /** Spec 0049: remember empty root and (optionally) poll until indexable. */
126
+ private armDeferProbe;
127
+ private clearDeferProbe;
103
128
  /**
104
129
  * Open an index another process just created (Spec 0032 cold-start race).
105
130
  * Brief retries cover the window between mkdir and db file create.
@@ -143,6 +168,7 @@ export declare class MCPEngine {
143
168
  * Range: 1s … 30min. Used by product hosts (e.g. DevEco Code = 5 minutes).
144
169
  */
145
170
  export declare function parseFixedWindowEnv(raw: string | undefined): number | undefined;
171
+ export { isIndexableRoot, parseDeferProbeMs } from './indexable-root';
146
172
  /**
147
173
  * Parse and clamp the HOMEGRAPH_WATCH_DEBOUNCE_MS env override.
148
174
  *
@@ -44,7 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.MCPEngine = void 0;
47
+ exports.parseDeferProbeMs = exports.isIndexableRoot = exports.MCPEngine = void 0;
48
48
  exports.parseFixedWindowEnv = parseFixedWindowEnv;
49
49
  exports.parseDebounceEnv = parseDebounceEnv;
50
50
  const os = __importStar(require("os"));
@@ -57,13 +57,28 @@ const memory_budget_1 = require("./memory-budget");
57
57
  const db_1 = require("../db");
58
58
  const graph_sources_1 = require("../graph-sources");
59
59
  const utils_1 = require("../utils");
60
+ const runtime_log_1 = require("../runtime-log");
61
+ const indexable_root_1 = require("./indexable-root");
60
62
  // Lazy-load the heavy HomeGraph chain (sqlite + query/graph/context layers) OFF
61
63
  // the MCP startup path. It's only needed once a tool actually opens a project —
62
64
  // not to answer initialize/tools-list — so deferring it lets `serve mcp` (and
63
65
  // the daemon it spawns) bind + register tools in ~Node-startup time instead of
64
66
  // ~800ms, closing the "No such tool available" cold-start race that made headless
65
- // agents flounder. require() is sync + cached on the CommonJS build.
66
- const loadHomeGraph = () => require('../index').default;
67
+ // agents flounder. Prefer dynamic import (vitest + CJS); sync require remains for
68
+ // the sync retry path after an async load has warmed the cache (or under dist/).
69
+ let homeGraphCtor = null;
70
+ const loadHomeGraph = async () => {
71
+ if (!homeGraphCtor) {
72
+ homeGraphCtor = (await Promise.resolve().then(() => __importStar(require('../index')))).default;
73
+ }
74
+ return homeGraphCtor;
75
+ };
76
+ const loadHomeGraphSync = () => {
77
+ if (homeGraphCtor)
78
+ return homeGraphCtor;
79
+ homeGraphCtor = require('../index').default;
80
+ return homeGraphCtor;
81
+ };
67
82
  /**
68
83
  * Shared MCP engine. Thread-safe in the sense that multiple sessions can
69
84
  * call its methods concurrently — internally it serializes initialization
@@ -85,6 +100,13 @@ class MCPEngine {
85
100
  // Off-loop read-tool pool (daemon mode only). Created lazily once the default
86
101
  // project is open — workers each hold their own WAL read connection.
87
102
  queryPool = null;
103
+ /**
104
+ * Spec 0049: auto-init deferred because the root was empty. Probe until
105
+ * indexable, then run the normal create-DB path once.
106
+ */
107
+ deferredAutoInitRoot = null;
108
+ deferProbeTimer = null;
109
+ deferAutoInitPromise = null;
88
110
  constructor(opts = {}) {
89
111
  this.opts = { watch: opts.watch ?? true, queryPool: opts.queryPool ?? false, rootFloor: opts.rootFloor ?? null };
90
112
  this.toolHandler = new tools_1.ToolHandler(null);
@@ -132,10 +154,42 @@ class MCPEngine {
132
154
  getToolHandler() {
133
155
  return this.toolHandler;
134
156
  }
157
+ /**
158
+ * Spec 0047: true while auto-init / full symbol build is in flight.
159
+ * Used by the daemon idle timer — never idle-exit mid-build when clients=0.
160
+ * Does not include routine watch/sync increments.
161
+ */
162
+ isIndexBuildInProgress() {
163
+ if (!this.cg)
164
+ return false;
165
+ try {
166
+ const phase = this.cg.getBuildPhase();
167
+ return phase === 'building_fast' || phase === 'indexing';
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
135
173
  /** Whether the default project's HomeGraph is open. */
136
174
  hasDefaultHomeGraph() {
137
175
  return this.toolHandler.hasDefaultHomeGraph();
138
176
  }
177
+ /** Spec 0049: true while auto-init is waiting for an empty root to grow sources. */
178
+ isAutoInitDeferred() {
179
+ return this.deferredAutoInitRoot !== null && !this.toolHandler.hasDefaultHomeGraph();
180
+ }
181
+ /**
182
+ * Spec 0049: on tool call (or timer), re-check the deferred root and start
183
+ * the normal auto-init once it becomes indexable. Idempotent.
184
+ */
185
+ async kickDeferredAutoInit() {
186
+ if (this.closed || this.toolHandler.hasDefaultHomeGraph())
187
+ return false;
188
+ const root = this.deferredAutoInitRoot;
189
+ if (!root)
190
+ return false;
191
+ return this.tryAutoInit(root);
192
+ }
139
193
  /**
140
194
  * Walk up from `searchFrom` to find the nearest `.homegraph/` and open it.
141
195
  * Idempotent: concurrent callers share one in-flight init; subsequent
@@ -194,7 +248,7 @@ class MCPEngine {
194
248
  catch { /* ignore */ }
195
249
  this.cg = null;
196
250
  }
197
- this.cg = loadHomeGraph().openSync(resolvedRoot, { sources: mode });
251
+ this.cg = loadHomeGraphSync().openSync(resolvedRoot, { sources: mode });
198
252
  this.projectPath = resolvedRoot;
199
253
  this.toolHandler.setDefaultHomeGraph(this.cg);
200
254
  this.startWatching();
@@ -213,6 +267,7 @@ class MCPEngine {
213
267
  if (this.closed)
214
268
  return;
215
269
  this.closed = true;
270
+ this.clearDeferProbe();
216
271
  // Detach + terminate the worker pool first so no tool call routes to a
217
272
  // worker mid-teardown; outstanding pool calls resolve with graceful guidance.
218
273
  this.toolHandler.setQueryPool(null);
@@ -252,7 +307,7 @@ class MCPEngine {
252
307
  process.stderr.write(`[HomeGraph MCP] Graph sources=${mode} — skipping project/SDK open (tools return guidance).\n`);
253
308
  return;
254
309
  }
255
- this.cg = await loadHomeGraph().open(resolvedRoot, { sources: mode });
310
+ this.cg = await (await loadHomeGraph()).open(resolvedRoot, { sources: mode });
256
311
  this.toolHandler.setDefaultHomeGraph(this.cg);
257
312
  this.healBuildPhase(this.cg);
258
313
  this.startWatching();
@@ -282,11 +337,34 @@ class MCPEngine {
282
337
  // Race: another process finished init before we entered — open, don't bail
283
338
  // (Spec 0032). Returning false here left the session with no cg forever.
284
339
  if ((0, directory_1.isInitialized)(root)) {
340
+ this.clearDeferProbe();
285
341
  return this.openAfterAutoInitRace(root);
286
342
  }
343
+ // Spec 0049: empty workspace — do not create `.homegraph/` (blocks
344
+ // in-place `devecocli create`). Probe later / on tool kick.
345
+ if (!(0, indexable_root_1.isIndexableRoot)(root)) {
346
+ this.armDeferProbe(root);
347
+ process.stderr.write(`[HomeGraph MCP] Auto-init deferred — empty root at ${root} ` +
348
+ `(no build-profile.json5 / indexable sources yet)\n`);
349
+ (0, runtime_log_1.logLifecycle)('auto-init.deferred', { projectRoot: root });
350
+ this.projectPath = root;
351
+ return false;
352
+ }
353
+ if (this.deferAutoInitPromise) {
354
+ return this.deferAutoInitPromise;
355
+ }
356
+ this.deferAutoInitPromise = this.runAutoInitCreate(root).finally(() => {
357
+ this.deferAutoInitPromise = null;
358
+ });
359
+ return this.deferAutoInitPromise;
360
+ }
361
+ /** Spec 0049: create DB + fast map + background full (shared by first hit and kick). */
362
+ async runAutoInitCreate(root) {
363
+ this.clearDeferProbe();
287
364
  try {
288
365
  process.stderr.write(`[HomeGraph MCP] Auto-init at ${root}\n`);
289
- const HomeGraph = loadHomeGraph();
366
+ (0, runtime_log_1.logLifecycle)('auto-init.start', { projectRoot: root });
367
+ const HomeGraph = await loadHomeGraph();
290
368
  // Create DB immediately so tools/open succeed; index in background.
291
369
  const cg = await HomeGraph.init(root, { index: false });
292
370
  // Pin empty state before returning so the first tool call cannot see `none`
@@ -312,6 +390,7 @@ class MCPEngine {
312
390
  catch (err) {
313
391
  const msg = err instanceof Error ? err.message : String(err);
314
392
  process.stderr.write(`[HomeGraph MCP] Fast build failed: ${msg}\n`);
393
+ (0, runtime_log_1.logLifecycleError)('auto-init.fail', { projectRoot: root, phase: 'fast', msg });
315
394
  }
316
395
  this.startBackgroundFullBuild(cg);
317
396
  })();
@@ -324,9 +403,51 @@ class MCPEngine {
324
403
  }
325
404
  const msg = err instanceof Error ? err.message : String(err);
326
405
  process.stderr.write(`[HomeGraph MCP] Auto-init failed: ${msg}\n`);
406
+ (0, runtime_log_1.logLifecycleError)('auto-init.fail', { projectRoot: root, msg });
407
+ // Keep probing so a later scaffold (or retry) can still land.
408
+ if (!(0, directory_1.isInitialized)(root)) {
409
+ this.armDeferProbe(root);
410
+ }
327
411
  return false;
328
412
  }
329
413
  }
414
+ /** Spec 0049: remember empty root and (optionally) poll until indexable. */
415
+ armDeferProbe(root) {
416
+ this.deferredAutoInitRoot = root;
417
+ if (this.deferProbeTimer)
418
+ return;
419
+ const ms = (0, indexable_root_1.parseDeferProbeMs)(process.env.HOMEGRAPH_DEFER_PROBE_MS);
420
+ if (ms <= 0)
421
+ return;
422
+ this.deferProbeTimer = setInterval(() => {
423
+ if (this.closed || this.toolHandler.hasDefaultHomeGraph()) {
424
+ this.clearDeferProbe();
425
+ return;
426
+ }
427
+ const target = this.deferredAutoInitRoot;
428
+ if (!target) {
429
+ this.clearDeferProbe();
430
+ return;
431
+ }
432
+ if (!(0, indexable_root_1.isIndexableRoot)(target))
433
+ return;
434
+ void this.tryAutoInit(target).then((ok) => {
435
+ if (ok)
436
+ this.clearDeferProbe();
437
+ });
438
+ }, ms);
439
+ // Don't keep the process alive solely for the probe in direct mode.
440
+ if (typeof this.deferProbeTimer.unref === 'function') {
441
+ this.deferProbeTimer.unref();
442
+ }
443
+ }
444
+ clearDeferProbe() {
445
+ this.deferredAutoInitRoot = null;
446
+ if (this.deferProbeTimer) {
447
+ clearInterval(this.deferProbeTimer);
448
+ this.deferProbeTimer = null;
449
+ }
450
+ }
330
451
  /**
331
452
  * Open an index another process just created (Spec 0032 cold-start race).
332
453
  * Brief retries cover the window between mkdir and db file create.
@@ -342,7 +463,7 @@ class MCPEngine {
342
463
  }
343
464
  try {
344
465
  process.stderr.write(`[HomeGraph MCP] Auto-init raced; opening existing index at ${root}\n`);
345
- this.cg = await loadHomeGraph().open(root, { sources: mode });
466
+ this.cg = await (await loadHomeGraph()).open(root, { sources: mode });
346
467
  this.projectPath = root;
347
468
  this.toolHandler.setDefaultHomeGraph(this.cg);
348
469
  this.healBuildPhase(this.cg);
@@ -400,6 +521,14 @@ class MCPEngine {
400
521
  startBackgroundFullBuild(cg) {
401
522
  cg.setBuildPhase('indexing');
402
523
  process.stderr.write('[HomeGraph MCP] Full build starting in-process (background)\n');
524
+ let projectRoot;
525
+ try {
526
+ projectRoot = cg.getProjectRoot();
527
+ }
528
+ catch {
529
+ projectRoot = this.projectPath ?? undefined;
530
+ }
531
+ (0, runtime_log_1.logLifecycle)('index.start', { projectRoot, via: 'auto-init' });
403
532
  void cg
404
533
  .indexAll()
405
534
  .then((result) => {
@@ -420,16 +549,20 @@ class MCPEngine {
420
549
  cg.setBuildPhase('full');
421
550
  process.stderr.write(`[HomeGraph MCP] Full build complete — files=${files || '?'} nodes=${nodes}` +
422
551
  (ok ? '\n' : ' (soft-fail but symbols present)\n'));
552
+ (0, runtime_log_1.logLifecycle)('index.done', { projectRoot, files, nodes });
553
+ (0, runtime_log_1.logLifecycle)('auto-init.done', { projectRoot, files, nodes });
423
554
  }
424
555
  else {
425
556
  cg.setBuildPhase('fast');
426
557
  process.stderr.write('[HomeGraph MCP] Full build finished with no symbols — staying on fast map\n');
558
+ (0, runtime_log_1.logLifecycle)('index.done', { projectRoot, files: 0, nodes: 0, note: 'no-symbols' });
427
559
  }
428
560
  this.startWatchingAfterAutoInit();
429
561
  })
430
562
  .catch((err) => {
431
563
  const msg = err instanceof Error ? err.message : String(err);
432
564
  process.stderr.write(`[HomeGraph MCP] Full build failed: ${msg}\n`);
565
+ (0, runtime_log_1.logLifecycleError)('index.fail', { projectRoot, msg });
433
566
  try {
434
567
  let nodes = 0;
435
568
  try {
@@ -575,6 +708,9 @@ function parseFixedWindowEnv(raw) {
575
708
  return undefined;
576
709
  return n;
577
710
  }
711
+ var indexable_root_2 = require("./indexable-root");
712
+ Object.defineProperty(exports, "isIndexableRoot", { enumerable: true, get: function () { return indexable_root_2.isIndexableRoot; } });
713
+ Object.defineProperty(exports, "parseDeferProbeMs", { enumerable: true, get: function () { return indexable_root_2.parseDeferProbeMs; } });
578
714
  /**
579
715
  * Parse and clamp the HOMEGRAPH_WATCH_DEBOUNCE_MS env override.
580
716
  *
@@ -1,23 +1,53 @@
1
1
  /**
2
- * Product-facing index availability for MCP tool results (Spec 0032).
2
+ * Product-facing index availability for MCP tool results (Spec 0032 + 0035).
3
3
  *
4
- * Internal `build_phase` stays as-is; this layer maps it (+ write-lock / busy)
5
- * to the four states hosts and agents should see in tool text.
4
+ * Internal `build_phase` stays as-is; this layer maps it (+ write-lock / busy /
5
+ * pending dirty files) to the five states hosts and agents should see.
6
6
  */
7
7
  import type HomeGraph from '../index';
8
8
  /** Agent-visible index readiness (tool return copy — not tools/list gating). */
9
- export type ProductIndexState = 'empty' | 'fast' | 'full' | 'syncing';
10
- export declare function productIndexGuidance(state: Exclude<ProductIndexState, 'full'>): string;
9
+ export type ProductIndexState = 'empty' | 'fast' | 'full' | 'dirty' | 'syncing';
10
+ /** One-line glossary for MCP initialize / tool surface (Spec 0035). */
11
+ export declare const PRODUCT_STATUS_GLOSSARY = "status: empty=not ready \u00B7 fast=map only (homegraph_project) \u00B7 full=fresh \u00B7 dirty=usable but listed paths outdated \u00B7 syncing=write lock, retry";
12
+ /** Whole-response guidance when the tool cannot usefully answer yet. */
13
+ export declare function productIndexGuidance(state: Extract<ProductIndexState, 'empty' | 'fast' | 'syncing'>): string;
14
+ /**
15
+ * Single status line for tool footers (and for guidance-only replies).
16
+ * `pendingPaths` only used when `state === 'dirty'`.
17
+ */
18
+ export declare function formatProductStatusLine(state: ProductIndexState, opts?: {
19
+ pendingPaths?: string[];
20
+ }): string;
11
21
  /**
12
22
  * Resolve the product state from a live HomeGraph handle.
13
23
  *
14
- * - empty: fast map not ready (`none` / `building_fast`)
15
- * - fast: map ready, full index not done (`fast` / `indexing`)
16
- * - full: symbol index ready
17
- * - syncing: full (or empty-while-contended-init) and a writer holds the lock /
18
- * this process is indexing — reads may fail or see a moving target
24
+ * Priority: syncing > empty > fast > dirty > full
19
25
  */
20
26
  export declare function resolveProductIndexState(cg: HomeGraph): ProductIndexState;
21
27
  /** True when an error message indicates SQLite writer contention. */
22
28
  export declare function isSqliteBusyMessage(message: string): boolean;
29
+ /** True when text is already a pure (or leading) HomeGraph status line. */
30
+ export declare function textAlreadyHasProductStatus(text: string): boolean;
31
+ /** Marker line for Spec 0038 project-root path hint (idempotent prepend). */
32
+ export declare const PROJECT_ROOT_HINT_MARKER = "HomeGraph project root:";
33
+ /**
34
+ * Short preamble: absolute project root + how to join repo-relative paths (Spec 0038).
35
+ * `absRoot` should already be resolved (platform-native absolute path).
36
+ */
37
+ export declare function formatProjectRootPathHint(absRoot: string): string;
38
+ /** True when text already carries a Spec 0038 project-root hint. */
39
+ export declare function textAlreadyHasProjectRootHint(text: string): boolean;
40
+ /** Marker for Spec 0043 bound-root projectPath soft-pin notice (idempotent prepend). */
41
+ export declare const BOUND_PROJECT_PATH_PIN_MARKER = "This MCP session is bound to";
42
+ /**
43
+ * English preamble when a tool `projectPath` resolves to a different index root
44
+ * than the MCP session's default bound root (Spec 0043). Success-shaped — not isError.
45
+ */
46
+ export declare function formatBoundProjectPathPinNotice(opts: {
47
+ boundRoot: string;
48
+ requestedPath: string;
49
+ resolvedRoot: string | null;
50
+ }): string;
51
+ /** True when text already carries a Spec 0043 pin notice. */
52
+ export declare function textAlreadyHasBoundProjectPathPinNotice(text: string): boolean;
23
53
  //# sourceMappingURL=index-availability.d.ts.map