@gmickel/gno 2.10.1 → 2.12.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 (112) hide show
  1. package/README.md +11 -11
  2. package/assets/skill/SKILL.md +27 -15
  3. package/assets/skill/cli-reference.md +6 -5
  4. package/assets/skill/examples.md +16 -12
  5. package/assets/skill/mcp-reference.md +2 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.10.1.zip → gno-browser-clipper-v2.12.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.12.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +10 -5
  11. package/spec/cli.md +127 -35
  12. package/spec/evals-agentic.md +3 -1
  13. package/spec/mcp.md +36 -6
  14. package/spec/output-schemas/ask.schema.json +8 -0
  15. package/spec/output-schemas/claim-verification.schema.json +8 -2
  16. package/spec/output-schemas/context-capsule-v1.schema.json +6 -1
  17. package/spec/output-schemas/context-capsule-verification.schema.json +4 -2
  18. package/spec/output-schemas/get.schema.json +4 -0
  19. package/spec/output-schemas/metadata-summary.schema.json +60 -0
  20. package/spec/output-schemas/multi-get.schema.json +4 -0
  21. package/spec/output-schemas/search-result.schema.json +4 -0
  22. package/spec/output-schemas/search-results.schema.json +4 -0
  23. package/spec/output-schemas/status.schema.json +28 -0
  24. package/src/app/constants.ts +16 -0
  25. package/src/app/context-agent-projection.ts +10 -1
  26. package/src/app/context-format.ts +1 -0
  27. package/src/app/verified-ask.ts +2 -0
  28. package/src/cli/commands/agents/block.ts +10 -9
  29. package/src/cli/commands/agents/commands.ts +6 -5
  30. package/src/cli/commands/agents/engine.ts +15 -2
  31. package/src/cli/commands/agents/harnesses.ts +21 -14
  32. package/src/cli/commands/ask-format.ts +18 -1
  33. package/src/cli/commands/ask.ts +4 -5
  34. package/src/cli/commands/cleanup.ts +46 -6
  35. package/src/cli/commands/collection/remove.ts +1 -0
  36. package/src/cli/commands/collection/rename.ts +1 -0
  37. package/src/cli/commands/daemon.ts +5 -1
  38. package/src/cli/commands/doctor.ts +37 -0
  39. package/src/cli/commands/get.ts +11 -1
  40. package/src/cli/commands/mcp/paths.ts +2 -1
  41. package/src/cli/commands/metadata.ts +79 -0
  42. package/src/cli/commands/multi-get.ts +13 -0
  43. package/src/cli/commands/setup-semantic.ts +6 -1
  44. package/src/cli/commands/shared.ts +35 -8
  45. package/src/cli/commands/skill/paths.ts +20 -2
  46. package/src/cli/commands/status.ts +89 -15
  47. package/src/cli/format/search-results.ts +11 -0
  48. package/src/cli/options.ts +2 -0
  49. package/src/cli/program.ts +64 -3
  50. package/src/core/activation-probe-plan.ts +0 -1
  51. package/src/core/config-mutation.ts +72 -55
  52. package/src/core/context-capsule-schema.ts +2 -0
  53. package/src/core/context-capsule-verification.ts +2 -0
  54. package/src/core/context-compiler.ts +2 -5
  55. package/src/core/context-evidence.ts +1 -0
  56. package/src/core/context-verifier.ts +14 -1
  57. package/src/core/memory-remember.ts +0 -1
  58. package/src/core/metadata-discovery.ts +169 -0
  59. package/src/core/module-path.ts +33 -0
  60. package/src/core/runtime-entrypoint.ts +3 -1
  61. package/src/ingestion/file-processor.ts +6 -3
  62. package/src/ingestion/prepare-file.ts +14 -4
  63. package/src/ingestion/source-availability/root.ts +138 -0
  64. package/src/ingestion/sync.ts +35 -1
  65. package/src/ingestion/walker.ts +11 -3
  66. package/src/llm/native-worker/client.ts +8 -4
  67. package/src/llm/native-worker/errors.ts +26 -3
  68. package/src/mcp/http-egress.ts +1 -0
  69. package/src/mcp/http-transport.ts +7 -2
  70. package/src/mcp/server.ts +3 -0
  71. package/src/mcp/tool-descriptions-core.ts +3 -3
  72. package/src/mcp/tool-profile.ts +16 -1
  73. package/src/mcp/tools/get.ts +10 -0
  74. package/src/mcp/tools/index.ts +68 -4
  75. package/src/mcp/tools/multi-get.ts +15 -0
  76. package/src/mcp/tools/query.ts +5 -3
  77. package/src/mcp/tools/search.ts +5 -3
  78. package/src/mcp/tools/status.ts +17 -2
  79. package/src/mcp/tools/vsearch.ts +5 -3
  80. package/src/mcp/tools/workspace-write.ts +42 -34
  81. package/src/pipeline/answer-prompt.ts +15 -2
  82. package/src/pipeline/answer.ts +3 -0
  83. package/src/pipeline/claim-verification-schema.ts +6 -1
  84. package/src/pipeline/claim-verification.ts +54 -1
  85. package/src/pipeline/explain.ts +1 -1
  86. package/src/pipeline/fusion.ts +24 -4
  87. package/src/pipeline/hybrid.ts +5 -1
  88. package/src/pipeline/query-snippet.ts +266 -0
  89. package/src/pipeline/result-context.ts +29 -0
  90. package/src/pipeline/search.ts +154 -20
  91. package/src/pipeline/types.ts +4 -0
  92. package/src/pipeline/vsearch.ts +5 -1
  93. package/src/sdk/client.ts +46 -6
  94. package/src/sdk/documents.ts +5 -0
  95. package/src/sdk/types.ts +11 -1
  96. package/src/serve/public/components/ThoroughnessSelector.tsx +1 -1
  97. package/src/serve/public/globals.built.css +1 -1
  98. package/src/serve/public/pages/Collections.tsx +8 -0
  99. package/src/serve/public/pages/Search.tsx +9 -1
  100. package/src/serve/resident-request.ts +8 -0
  101. package/src/serve/resident-runtime.ts +25 -3
  102. package/src/serve/routes/api.ts +29 -1
  103. package/src/serve/server.ts +12 -0
  104. package/src/serve/spa-production-build.ts +7 -3
  105. package/src/serve/status-model.ts +2 -0
  106. package/src/serve/status.ts +29 -0
  107. package/src/sessions/config-refresh.ts +6 -0
  108. package/src/sessions/import-child.ts +4 -1
  109. package/src/store/sqlite/adapter.ts +241 -20
  110. package/src/store/types.ts +61 -7
  111. package/src/store/vector/variant-search.ts +112 -74
  112. package/browser-extension/artifacts/gno-browser-clipper-v2.10.1.zip.sha256 +0 -1
@@ -6,7 +6,7 @@
6
6
  * @module src/cli/commands/cleanup
7
7
  */
8
8
 
9
- import type { CleanupStats } from "../../store/types";
9
+ import type { CleanupStats, StaleCollection } from "../../store/types";
10
10
 
11
11
  import { getIndexDbPath } from "../../app/constants";
12
12
  import { isInitialized, loadConfig } from "../../config";
@@ -20,14 +20,26 @@ export interface CleanupOptions {
20
20
  configPath?: string;
21
21
  /** Index name */
22
22
  indexName?: string;
23
+ /** Called with the collections about to be deleted, before deleting them. */
24
+ onDropping?: (collections: StaleCollection[]) => void;
23
25
  }
24
26
 
25
27
  /**
26
28
  * Result of cleanup command.
27
29
  */
28
30
  export type CleanupResult =
29
- | { success: true; stats: CleanupStats }
30
- | { success: false; error: string };
31
+ | {
32
+ success: true;
33
+ stats: CleanupStats;
34
+ /** Indexed collections the config omits, deleted by this cleanup. */
35
+ droppedCollections: StaleCollection[];
36
+ }
37
+ | {
38
+ success: false;
39
+ error: string;
40
+ /** Collections already deleted when a later step failed. */
41
+ droppedCollections?: StaleCollection[];
42
+ };
31
43
 
32
44
  /**
33
45
  * Execute gno cleanup command.
@@ -64,17 +76,32 @@ export async function cleanup(
64
76
  try {
65
77
  // Drop collections no longer in the config first (their documents follow
66
78
  // by cascade), so their content, chunks and vectors count as orphans.
67
- const syncResult = await store.syncCollections(config.collections);
79
+ // This is the explicit removal path; it names what it deletes.
80
+ const stale = await store.listStaleCollections(
81
+ config.collections.map(({ name }) => name)
82
+ );
83
+ if (!stale.ok) {
84
+ return { success: false, error: stale.error.message };
85
+ }
86
+ const droppedCollections = stale.value;
87
+ if (droppedCollections.length > 0) options.onDropping?.(droppedCollections);
88
+ const syncResult = await store.syncCollections(config.collections, {
89
+ remove: "stale",
90
+ });
68
91
  if (!syncResult.ok) {
69
92
  return { success: false, error: syncResult.error.message };
70
93
  }
71
94
 
72
95
  const cleanupResult = await store.cleanupOrphans();
73
96
  if (!cleanupResult.ok) {
74
- return { success: false, error: cleanupResult.error.message };
97
+ return {
98
+ success: false,
99
+ error: cleanupResult.error.message,
100
+ droppedCollections,
101
+ };
75
102
  }
76
103
 
77
- return { success: true, stats: cleanupResult.value };
104
+ return { success: true, stats: cleanupResult.value, droppedCollections };
78
105
  } finally {
79
106
  await store.close();
80
107
  }
@@ -118,3 +145,16 @@ export function formatCleanup(result: CleanupResult): string {
118
145
 
119
146
  return lines.join("\n");
120
147
  }
148
+
149
+ /** Lines naming the collections cleanup is about to delete. */
150
+ export function formatDroppingCollections(
151
+ collections: readonly StaleCollection[]
152
+ ): string {
153
+ return [
154
+ "Deleting collections not in this config:",
155
+ ...collections.map(
156
+ (c) =>
157
+ ` ${c.name}: ${c.documents} ${c.documents === 1 ? "document" : "documents"}`
158
+ ),
159
+ ].join("\n");
160
+ }
@@ -19,6 +19,7 @@ export async function collectionRemove(
19
19
  configPath: options.configPath,
20
20
  indexName: options.indexName,
21
21
  syncConfig: false,
22
+ allowStaleCollections: true,
22
23
  allowEmptyCollections: true,
23
24
  });
24
25
  if (!initialized.ok) {
@@ -23,6 +23,7 @@ export async function collectionRename(
23
23
  configPath: options.configPath,
24
24
  indexName: options.indexName,
25
25
  syncConfig: false,
26
+ allowStaleCollections: true,
26
27
  allowEmptyCollections: true,
27
28
  });
28
29
  if (!initialized.ok) {
@@ -99,7 +99,11 @@ export function logSessionAutomationResult(
99
99
  }
100
100
 
101
101
  function formatCollectionSyncSummary(result: CollectionSyncResult): string {
102
- return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
102
+ const collectionErrors = result.errors
103
+ .filter((error) => error.relPath === "")
104
+ .map((error) => `; [${error.code}] ${error.message}`)
105
+ .join("");
106
+ return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors${collectionErrors}`;
103
107
  }
104
108
 
105
109
  export const authorizeDaemonStatus = async (
@@ -33,6 +33,7 @@ import {
33
33
  import { formatVectorPartitionLines } from "../../core/vector-partition-status";
34
34
  import { getEmbeddingFingerprint } from "../../embed/fingerprint";
35
35
  import { getCodeChunkingStatus } from "../../ingestion/chunker";
36
+ import { checkCollectionRoots } from "../../ingestion/source-availability/root";
36
37
  import { ModelCache } from "../../llm/cache";
37
38
  import { getActivePreset, resolveModelUri } from "../../llm/registry";
38
39
  import { SqliteAdapter } from "../../store/sqlite/adapter";
@@ -739,6 +740,37 @@ export async function checkFindingsPass(
739
740
  };
740
741
  }
741
742
 
743
+ /** Configured collection roots that are missing or unreadable right now. */
744
+ export async function checkCollectionRootAccess(
745
+ config: Config
746
+ ): Promise<DoctorCheck> {
747
+ const name = "collection-roots";
748
+ const errors = await checkCollectionRoots(config.collections);
749
+ const unavailable = config.collections.flatMap((collection) => {
750
+ const error = errors[collection.name];
751
+ return error ? [{ collection, error }] : [];
752
+ });
753
+ if (unavailable.length === 0) {
754
+ return {
755
+ name,
756
+ status: "ok",
757
+ message: `${config.collections.length} collection root(s) available`,
758
+ };
759
+ }
760
+ return {
761
+ name,
762
+ status: "warn",
763
+ message: `${unavailable.length} collection root(s) missing or unreadable: ${unavailable.map(({ collection }) => collection.name).join(", ")}`,
764
+ details: [
765
+ ...unavailable.map(
766
+ ({ collection, error }) =>
767
+ `${collection.name}: ${collection.path} [${error.code}] ${error.message}`
768
+ ),
769
+ "Reconnect the drive or restore the folder; if it moved, change the collection's path in the config, then run: gno update",
770
+ ],
771
+ };
772
+ }
773
+
742
774
  // ─────────────────────────────────────────────────────────────────────────────
743
775
  // Implementation
744
776
  // ─────────────────────────────────────────────────────────────────────────────
@@ -779,6 +811,10 @@ export async function doctor(
779
811
  // Code chunking capability
780
812
  checks.push(checkCodeChunking());
781
813
 
814
+ // Collection roots, checked now (documents stay indexed while one is away)
815
+ const rootsCheck = await checkCollectionRootAccess(config);
816
+ checks.push(rootsCheck);
817
+
782
818
  // Embedding fingerprint freshness
783
819
  checks.push(...(await checkEmbeddingFingerprints(config, options.indexName)));
784
820
 
@@ -801,6 +837,7 @@ export async function doctor(
801
837
  return {
802
838
  healthy:
803
839
  !hasErrors &&
840
+ rootsCheck.status === "ok" &&
804
841
  activation.healthy &&
805
842
  isConnectorActivationComplete(activation),
806
843
  checks,
@@ -29,6 +29,7 @@ import {
29
29
  evidenceFromExactDocument,
30
30
  RetrievalTraceSession as TraceSession,
31
31
  } from "../../core/retrieval-trace-session";
32
+ import { supersedingUri } from "../../pipeline/result-context";
32
33
  import { parseRef } from "./ref-parser";
33
34
  import { initStore } from "./shared";
34
35
 
@@ -90,6 +91,8 @@ export interface GetResponse {
90
91
  mirrorHash?: string;
91
92
  };
92
93
  record?: RecordEvidenceMetadata;
94
+ /** URI of the current fact when a newer memory fact superseded this one. */
95
+ supersededBy?: string;
93
96
  capabilities: DocumentCapabilities;
94
97
  }
95
98
 
@@ -269,7 +272,7 @@ async function fetchDocument(
269
272
  return { success: false, error: "Mirror content unavailable" };
270
273
  }
271
274
 
272
- return buildResponse({
275
+ const response = buildResponse({
273
276
  doc,
274
277
  fullContent: contentResult.value,
275
278
  parsed,
@@ -277,6 +280,10 @@ async function fetchDocument(
277
280
  config,
278
281
  indexName,
279
282
  });
283
+ const successor = await supersedingUri(store, doc.uri);
284
+ if (response.success && successor)
285
+ response.data.supersededBy = decorateUriForIndex(successor, indexName);
286
+ return response;
280
287
  }
281
288
 
282
289
  interface BuildResponseContext {
@@ -467,6 +474,9 @@ function formatMarkdown(data: GetResponse, options: GetCommandOptions): string {
467
474
  lines.push("");
468
475
  lines.push(`- **URI**: \`${data.uri}\``);
469
476
  lines.push(`- **DocID**: \`${data.docid}\``);
477
+ if (data.supersededBy) {
478
+ lines.push(`- **Superseded by**: \`${data.supersededBy}\``);
479
+ }
470
480
  if (data.returnedLines) {
471
481
  lines.push(
472
482
  `- **Lines**: ${data.returnedLines.start}-${data.returnedLines.end} of ${data.totalLines}`
@@ -12,6 +12,7 @@ import type { ConnectorWorkspaceEnvironment } from "../../../core/connector-envi
12
12
 
13
13
  import { resolveDirs } from "../../../app/constants";
14
14
  import { assertValidIndexName } from "../../../app/index-name";
15
+ import { modulePath } from "../../../core/module-path";
15
16
  import { getCurrentGnoEntrypoint } from "../../../core/runtime-entrypoint";
16
17
  import {
17
18
  DEFAULT_MCP_TOOL_PROFILE,
@@ -459,7 +460,7 @@ export function resolveLaunchPaths(): {
459
460
  entrypoint: LaunchPath;
460
461
  } {
461
462
  const entrypoint = getCurrentGnoEntrypoint();
462
- if (isBunfsPath(import.meta.path)) {
463
+ if (isBunfsPath(modulePath(import.meta))) {
463
464
  return {
464
465
  bun: { path: process.execPath, pinned: false },
465
466
  entrypoint: { path: entrypoint, pinned: false },
@@ -0,0 +1,79 @@
1
+ /**
2
+ * gno metadata - list the typed metadata keys indexed documents carry.
3
+ *
4
+ * @module src/cli/commands/metadata
5
+ */
6
+
7
+ import {
8
+ formatMetadataSummary,
9
+ type MetadataSummary,
10
+ readMetadataSummary,
11
+ } from "../../core/metadata-discovery";
12
+ import { initStore } from "./shared";
13
+
14
+ export interface MetadataListOptions {
15
+ configPath?: string;
16
+ indexName?: string;
17
+ collection?: string;
18
+ values?: number;
19
+ }
20
+
21
+ export type MetadataListResult =
22
+ | { success: true; data: MetadataSummary }
23
+ | { success: false; error: string; isValidation?: boolean };
24
+
25
+ export async function metadataList(
26
+ options: MetadataListOptions = {}
27
+ ): Promise<MetadataListResult> {
28
+ const initResult = await initStore({
29
+ configPath: options.configPath,
30
+ indexName: options.indexName,
31
+ });
32
+ if (!initResult.ok) return { success: false, error: initResult.error };
33
+ const { store } = initResult;
34
+ try {
35
+ return {
36
+ success: true,
37
+ data: await readMetadataSummary(store, {
38
+ collection: options.collection,
39
+ values: options.values,
40
+ }),
41
+ };
42
+ } catch (cause) {
43
+ return {
44
+ success: false,
45
+ error: cause instanceof Error ? cause.message : String(cause),
46
+ isValidation: cause instanceof RangeError,
47
+ };
48
+ } finally {
49
+ await store.close();
50
+ }
51
+ }
52
+
53
+ export function formatMetadataList(
54
+ data: MetadataSummary,
55
+ options: { json?: boolean; md?: boolean }
56
+ ): string {
57
+ if (options.json) return JSON.stringify(data, null, 2);
58
+ if (!options.md) return formatMetadataSummary(data);
59
+ const lines = [
60
+ "# Typed metadata",
61
+ "",
62
+ `*${data.documentsWithMetadata} of ${data.documents} documents${data.collection ? ` in \`${data.collection}\`` : ""}*`,
63
+ ];
64
+ if (data.keys.length === 0) return lines.join("\n");
65
+ lines.push(
66
+ "",
67
+ "| Key | Types | Documents | Common values |",
68
+ "|---|---|---|---|"
69
+ );
70
+ for (const key of data.keys) {
71
+ const values = key.values
72
+ .map((item) => `\`${JSON.stringify(item.value)}\` (${item.documents})`)
73
+ .join(", ");
74
+ lines.push(
75
+ `| \`${key.key}\` | ${key.types.join(", ")} | ${key.documents} | ${values}${key.valuesTruncated ? ", ..." : ""} |`
76
+ );
77
+ }
78
+ return lines.join("\n");
79
+ }
@@ -21,6 +21,7 @@ import {
21
21
  projectRecordEvidenceMetadata,
22
22
  type RecordEvidenceMetadata,
23
23
  } from "../../core/record-metadata";
24
+ import { supersedingUri } from "../../pipeline/result-context";
24
25
  import { isGlobPattern, parseRef, splitRefs } from "./ref-parser";
25
26
  import { initStore } from "./shared";
26
27
 
@@ -58,6 +59,8 @@ export interface MultiGetDocument {
58
59
  totalLines?: number;
59
60
  source: { absPath?: string; relPath: string; mime: string; ext: string };
60
61
  record?: RecordEvidenceMetadata;
62
+ /** URI of the current fact when a newer memory fact superseded this one. */
63
+ supersededBy?: string;
61
64
  }
62
65
 
63
66
  export interface SkippedDoc {
@@ -236,7 +239,11 @@ async function fetchSingleDocument(
236
239
  const coll = ctx.config.collections.find((c) => c.name === doc.collection);
237
240
 
238
241
  const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
242
+ const successor = await supersedingUri(ctx.store, doc.uri);
239
243
  ctx.documents.push({
244
+ ...(successor
245
+ ? { supersededBy: decorateUriForIndex(successor, ctx.indexName) }
246
+ : {}),
240
247
  docid: doc.docid,
241
248
  uri: decorateUriForIndex(doc.uri, ctx.indexName),
242
249
  title: doc.title ?? undefined,
@@ -422,6 +429,9 @@ function formatMarkdown(
422
429
  for (const doc of data.documents) {
423
430
  lines.push(`## ${doc.title || doc.source.relPath}`);
424
431
  lines.push(`- **URI**: \`${doc.uri}\``);
432
+ if (doc.supersededBy) {
433
+ lines.push(`- **Superseded by**: \`${doc.supersededBy}\``);
434
+ }
425
435
  if (doc.truncated) {
426
436
  lines.push(`- **Truncated**: yes (max ${data.meta.maxBytes} bytes)`);
427
437
  }
@@ -450,6 +460,9 @@ function formatTerminal(
450
460
 
451
461
  for (const doc of data.documents) {
452
462
  lines.push(`=== ${doc.uri} ===`);
463
+ if (doc.supersededBy) {
464
+ lines.push(`Superseded by: ${doc.supersededBy}`);
465
+ }
453
466
  lines.push(formatContent(doc.content, Boolean(options.lineNumbers)));
454
467
  lines.push("");
455
468
  }
@@ -21,6 +21,7 @@ import type { FolderSetupReceipt } from "../../core/setup-receipt";
21
21
  import { VERSION } from "../../app/constants";
22
22
  import { canonicalizeIndexName } from "../../app/index-name";
23
23
  import { withWriteLock } from "../../core/file-lock";
24
+ import { moduleDir } from "../../core/module-path";
24
25
  import {
25
26
  setupFingerprint,
26
27
  setupRootFingerprint,
@@ -427,7 +428,11 @@ function createSemanticReceipt(
427
428
  async function defaultSpawnWorker(
428
429
  receipt: SetupSemanticReceipt
429
430
  ): Promise<SpawnedSemanticWorker> {
430
- const workerPath = join(import.meta.dir, "..", "setup-semantic-worker.ts");
431
+ const workerPath = join(
432
+ moduleDir(import.meta),
433
+ "..",
434
+ "setup-semantic-worker.ts"
435
+ );
431
436
  const descriptor = openSync(receipt.logPath, "a", 0o600);
432
437
  try {
433
438
  const child = Bun.spawn({
@@ -11,7 +11,7 @@ import type { SearchResults } from "../../pipeline/types";
11
11
 
12
12
  import {
13
13
  DEFAULT_INDEX_NAME,
14
- decorateUriForIndex,
14
+ decorateResultUris,
15
15
  getIndexDbPath,
16
16
  } from "../../app/constants";
17
17
  import {
@@ -22,7 +22,10 @@ import {
22
22
  } from "../../config";
23
23
  import { tracePhase } from "../../core/phase-trace";
24
24
  import { BUDGET_ERROR_CODES } from "../../ingestion/file-processor";
25
- import { SqliteAdapter } from "../../store/sqlite/adapter";
25
+ import {
26
+ SqliteAdapter,
27
+ staleCollectionsMessage,
28
+ } from "../../store/sqlite/adapter";
26
29
  import { assertCliSessionBinding } from "../session-binding";
27
30
 
28
31
  /**
@@ -53,6 +56,10 @@ export interface InitStoreOptions {
53
56
  syncConfig?: boolean;
54
57
  /** Permit management commands that only need an existing index database */
55
58
  allowEmptyCollections?: boolean;
59
+ /** Open without syncing even when the index holds collections the config
60
+ * omits: config mutations report them through their own sync, and a
61
+ * connector proof reads one collection through a subset config. */
62
+ allowStaleCollections?: boolean;
56
63
  }
57
64
 
58
65
  /**
@@ -132,7 +139,25 @@ export async function initStore(
132
139
  return { ok: false, error: openResult.error.message };
133
140
  }
134
141
 
142
+ if (options.syncConfig === false && options.allowStaleCollections) {
143
+ return { ok: true, store, config, collections, actualConfigPath };
144
+ }
135
145
  if (options.syncConfig === false) {
146
+ // Reads skip the sync but must not search collections another config
147
+ // indexed; they fail the same way a sync would.
148
+ const stale = await store.listStaleCollections(
149
+ config.collections.map(({ name }) => name)
150
+ );
151
+ const indexed = stale.ok ? stale.value.filter((c) => c.documents > 0) : [];
152
+ if (!stale.ok || indexed.length > 0) {
153
+ await store.close();
154
+ return {
155
+ ok: false,
156
+ error: stale.ok
157
+ ? staleCollectionsMessage(indexed)
158
+ : stale.error.message,
159
+ };
160
+ }
136
161
  return { ok: true, store, config, collections, actualConfigPath };
137
162
  }
138
163
 
@@ -164,10 +189,9 @@ export function decorateSearchResultsForIndex(
164
189
  }
165
190
  return {
166
191
  ...data,
167
- results: data.results.map((result) => ({
168
- ...result,
169
- uri: decorateUriForIndex(result.uri, indexName),
170
- })),
192
+ results: data.results.map((result) =>
193
+ decorateResultUris(result, indexName)
194
+ ),
171
195
  };
172
196
  }
173
197
 
@@ -217,10 +241,13 @@ export function formatSyncResultLines(
217
241
  lines.push(` ${c.filesErrored} errors`);
218
242
  }
219
243
  // A file stopped at its conversion budget is always named, not only in
220
- // verbose output: it stays unindexed until a later run finishes it.
244
+ // verbose output: it stays unindexed until a later run finishes it. So is
245
+ // a collection-level error such as an unavailable root.
221
246
  if (!options.verbose) {
222
247
  for (const err of c.errors) {
223
- if (BUDGET_ERROR_CODES.has(err.code)) {
248
+ if (err.relPath === "") {
249
+ lines.push(` [${err.code}] ${err.message}`);
250
+ } else if (BUDGET_ERROR_CODES.has(err.code)) {
224
251
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
225
252
  }
226
253
  }
@@ -11,6 +11,11 @@ import { existsSync, realpathSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { dirname, isAbsolute, join, normalize, relative, sep } from "node:path";
13
13
 
14
+ import {
15
+ harnessConfigDirFromEnv,
16
+ type HarnessId,
17
+ } from "../agents/harnesses.js";
18
+
14
19
  // ─────────────────────────────────────────────────────────────────────────────
15
20
  // Environment Variables
16
21
  // ─────────────────────────────────────────────────────────────────────────────
@@ -84,6 +89,11 @@ interface TargetPathConfig {
84
89
  userBase: string; // e.g., ".claude" (joined with homedir) or ".config/opencode"
85
90
  skillsSubdir: string; // e.g., "skills" or "skill"
86
91
  envVar: string;
92
+ /**
93
+ * Harness whose config-dir env var (CLAUDE_CONFIG_DIR, CODEX_HOME) replaces
94
+ * the user base, resolved exactly as `gno agents` does.
95
+ */
96
+ configDirHarness?: HarnessId;
87
97
  }
88
98
 
89
99
  const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
@@ -92,12 +102,14 @@ const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
92
102
  userBase: ".claude",
93
103
  skillsSubdir: "skills",
94
104
  envVar: ENV_CLAUDE_SKILLS_DIR,
105
+ configDirHarness: "claude",
95
106
  },
96
107
  codex: {
97
108
  projectBase: ".codex",
98
109
  userBase: ".codex",
99
110
  skillsSubdir: "skills",
100
111
  envVar: ENV_CODEX_SKILLS_DIR,
112
+ configDirHarness: "codex",
101
113
  },
102
114
  opencode: {
103
115
  projectBase: ".opencode",
@@ -150,8 +162,14 @@ export function resolveSkillPaths(opts: SkillPathOptions): SkillPaths {
150
162
  let base: string;
151
163
 
152
164
  if (scope === "user") {
153
- const home = homeDir ?? process.env[ENV_SKILLS_HOME_OVERRIDE] ?? homedir();
154
- base = join(home, config.userBase);
165
+ // Any home override (option or env) is an isolation request and
166
+ // suppresses the harness config-dir env vars, as in `gno agents`.
167
+ const explicitHome = homeDir ?? process.env[ENV_SKILLS_HOME_OVERRIDE];
168
+ const configDir =
169
+ explicitHome === undefined && config.configDirHarness
170
+ ? harnessConfigDirFromEnv(config.configDirHarness)
171
+ : undefined;
172
+ base = configDir ?? join(explicitHome ?? homedir(), config.userBase);
155
173
  } else {
156
174
  const projectRoot = cwd ?? process.cwd();
157
175
  base = join(projectRoot, config.projectBase);