@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
package/spec/mcp.md CHANGED
@@ -7,7 +7,10 @@
7
7
  **Transport:** JSON-RPC 2.0 over stdio or resident Streamable HTTP
8
8
  **SDK:** `@modelcontextprotocol/server` 2.x (tool `inputSchema` /
9
9
  `outputSchema` carry the JSON Schema 2020-12 `$schema` stamp; an unknown tool
10
- name answers JSON-RPC `-32602`)
10
+ name answers JSON-RPC `-32602`). Tool inputs are closed objects: an argument
11
+ key a tool does not declare fails validation with an error naming the key, and
12
+ the tool does not run. Each `inputSchema` advertises `additionalProperties:
13
+ false` (per `oneOf` member for the discriminated-union inputs).
11
14
 
12
15
  This document specifies the MCP server interface for GNO.
13
16
 
@@ -153,7 +156,7 @@ the resident gateway applies one profile to every connected client.
153
156
 
154
157
  | Profile | Without `--enable-write` | With `--enable-write` |
155
158
  | ---------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
156
- | `full` (default) | Every read tool below (37) | Every read tool plus every write tool (59) |
159
+ | `full` (default) | Every read tool below (38) | Every read tool plus every write tool (60) |
157
160
  | `core` | `gno_query`, `gno_search`, `gno_get`, `gno_multi_get`, `gno_context`, `gno_changes`, `gno_recall` (7) | The 7 read tools plus `gno_capture`, `gno_remember` (9) |
158
161
 
159
162
  Both lists are exact: `core` advertises nothing else, and `full` is byte-for-byte
@@ -525,7 +528,7 @@ non-reflection contract as all MCP retrieval tools.
525
528
 
526
529
  `structuredContent` is the complete canonical Context Capsule object for
527
530
  application clients. Model-visible text is always one deterministic
528
- `gno-context-agent-v1` JSON projection, even when the compatibility `format`
531
+ `gno-context-agent-v2` JSON projection, even when the compatibility `format`
529
532
  field is present. The compact keys and tuple positions are part of that
530
533
  versioned contract:
531
534
 
@@ -537,8 +540,14 @@ versioned contract:
537
540
  or `null`, enabled capability names, fallbacks.
538
541
  - `e[]`: URI, start line, end line, source hash, mirror hash, passage hash,
539
542
  exact extractive text, title, heading, configured-context IDs, egress
540
- classification. Title/heading are nullable; egress is explicit even when the
541
- policy is unavailable.
543
+ classification, content date, `supersededBy`. Title/heading are nullable;
544
+ egress is explicit even when the policy is unavailable. The content date is
545
+ the document's own date (frontmatter date, a record's date, or a memory
546
+ fact's `memory.createdAt`), never the file modification time, and `null` when
547
+ the document has none. `supersededBy` is the current successor's URI when a
548
+ newer memory fact superseded this one, otherwise `null`; an agent must not
549
+ present superseded evidence as current. v2 appends these two positions to
550
+ the v1 tuple; every earlier position is unchanged.
542
551
  - `g`: evidence trust (`untrusted_data`), instruction boundary
543
552
  (`hard_delimited`), then configured-guidance tuples containing context ID,
544
553
  scope type, scope key, and exact guidance text. Evidence `contextIds` bind
@@ -907,7 +916,7 @@ Hybrid search combining BM25 and vector retrieval with optional expansion and re
907
916
  },
908
917
  "fast": {
909
918
  "type": "boolean",
910
- "description": "Fast mode: skip expansion and reranking (~0.7s)",
919
+ "description": "Fast mode: skip expansion and reranking (typically under a second on a warm server)",
911
920
  "default": false
912
921
  },
913
922
  "thorough": {
@@ -1937,6 +1946,27 @@ paths or session content.
1937
1946
  archive lock; `SESSIONS_RUNTIME_FAILURE` (fixed, path-free message) for filesystem or
1938
1947
  index failures.
1939
1948
 
1949
+ ### gno_list_metadata
1950
+
1951
+ List the typed metadata keys indexed documents carry, with observed types,
1952
+ document counts and most common values (array members count individually).
1953
+
1954
+ **Input Schema:**
1955
+
1956
+ ```json
1957
+ {
1958
+ "type": "object",
1959
+ "additionalProperties": false,
1960
+ "properties": {
1961
+ "collection": { "type": "string" },
1962
+ "values": { "type": "integer", "minimum": 1, "maximum": 100 }
1963
+ }
1964
+ }
1965
+ ```
1966
+
1967
+ **Output Schema:** `gno://schemas/metadata-summary`. Text content is the same
1968
+ summary as `gno metadata`. Full profile only.
1969
+
1940
1970
  ### gno_list_tags
1941
1971
 
1942
1972
  List all tags with document counts.
@@ -169,6 +169,10 @@
169
169
  "description": "Resolved user-configured retrieval guidance, ordered from global to collection to matching path prefixes"
170
170
  },
171
171
  "egressLineage": { "$ref": "#/$defs/egressLineage" },
172
+ "supersededBy": {
173
+ "type": "string",
174
+ "description": "URI of the current fact when a newer memory fact superseded this result"
175
+ },
172
176
  "snippet": {
173
177
  "type": "string",
174
178
  "description": "Matching text snippet from document mirror"
@@ -447,6 +451,10 @@
447
451
  "type": "integer",
448
452
  "description": "Citation end line",
449
453
  "minimum": 1
454
+ },
455
+ "supersededBy": {
456
+ "type": "string",
457
+ "description": "URI of the current fact when a newer memory fact superseded the cited one"
450
458
  }
451
459
  }
452
460
  }
@@ -186,7 +186,8 @@
186
186
  "semantic_entailment",
187
187
  "semantic_contradiction",
188
188
  "no_valid_evidence",
189
- "semantic_judgment_unavailable"
189
+ "semantic_judgment_unavailable",
190
+ "quote_not_in_evidence"
190
191
  ]
191
192
  },
192
193
  "verifierFingerprint": {
@@ -260,7 +261,12 @@
260
261
  "if": { "properties": { "status": { "const": "uncertain" } } },
261
262
  "then": {
262
263
  "properties": {
263
- "rationaleCode": { "const": "semantic_judgment_unavailable" },
264
+ "rationaleCode": {
265
+ "enum": [
266
+ "semantic_judgment_unavailable",
267
+ "quote_not_in_evidence"
268
+ ]
269
+ },
264
270
  "evidence": { "type": "array", "minItems": 1 }
265
271
  }
266
272
  }
@@ -924,7 +924,12 @@
924
924
  "enum": ["local_only", "lan", "remote", "unclassified", "unavailable"]
925
925
  },
926
926
  "egressLineage": { "$ref": "#/definitions/egressLineage" },
927
- "record": { "$ref": "#/definitions/recordEvidenceMetadata" }
927
+ "record": { "$ref": "#/definitions/recordEvidenceMetadata" },
928
+ "supersededBy": {
929
+ "type": "string",
930
+ "pattern": "^gno://",
931
+ "description": "Current successor when a newer memory fact superseded this one"
932
+ }
928
933
  }
929
934
  },
930
935
  "coverage": {
@@ -1229,7 +1229,8 @@
1229
1229
  "chunk_missing",
1230
1230
  "chunk_corrupt",
1231
1231
  "source_missing",
1232
- "mirror_missing"
1232
+ "mirror_missing",
1233
+ "superseded"
1233
1234
  ]
1234
1235
  },
1235
1236
  "rankingStatus": {
@@ -1273,7 +1274,8 @@
1273
1274
  "mirror_corrupt",
1274
1275
  "passage_stale",
1275
1276
  "chunk_missing",
1276
- "chunk_corrupt"
1277
+ "chunk_corrupt",
1278
+ "superseded"
1277
1279
  ]
1278
1280
  },
1279
1281
  "currentSourceHash": { "$ref": "#/definitions/sha256" },
@@ -129,6 +129,10 @@
129
129
  "reason": { "type": "string" }
130
130
  }
131
131
  },
132
+ "supersededBy": {
133
+ "type": "string",
134
+ "description": "URI of the current fact when a newer memory fact superseded this document"
135
+ },
132
136
  "record": {
133
137
  "type": "object",
134
138
  "additionalProperties": false,
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "gno://schemas/metadata-summary",
4
+ "title": "Typed metadata summary",
5
+ "description": "Typed metadata keys indexed documents carry: observed types, document counts and the most common values. Returned by `gno metadata --json`, MCP gno_list_metadata, GET /api/metadata and SDK listMetadata.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["collection", "documents", "documentsWithMetadata", "keys"],
9
+ "properties": {
10
+ "collection": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
11
+ "documents": { "type": "integer", "minimum": 0 },
12
+ "documentsWithMetadata": { "type": "integer", "minimum": 0 },
13
+ "keys": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["key", "types", "documents", "values", "valuesTruncated"],
19
+ "properties": {
20
+ "key": { "type": "string", "minLength": 1 },
21
+ "types": {
22
+ "type": "array",
23
+ "minItems": 1,
24
+ "items": {
25
+ "enum": [
26
+ "string",
27
+ "number",
28
+ "boolean",
29
+ "string[]",
30
+ "number[]",
31
+ "boolean[]"
32
+ ]
33
+ }
34
+ },
35
+ "documents": { "type": "integer", "minimum": 1 },
36
+ "values": {
37
+ "type": "array",
38
+ "maxItems": 100,
39
+ "items": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["value", "documents"],
43
+ "properties": {
44
+ "value": {
45
+ "oneOf": [
46
+ { "type": "string" },
47
+ { "type": "number" },
48
+ { "type": "boolean" }
49
+ ]
50
+ },
51
+ "documents": { "type": "integer", "minimum": 1 }
52
+ }
53
+ }
54
+ },
55
+ "valuesTruncated": { "type": "boolean" }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
@@ -49,6 +49,10 @@
49
49
  "ext": { "type": "string", "pattern": "^\\.[a-zA-Z0-9]+$" }
50
50
  }
51
51
  },
52
+ "supersededBy": {
53
+ "type": "string",
54
+ "description": "URI of the current fact when a newer memory fact superseded this document"
55
+ },
52
56
  "record": {
53
57
  "type": "object",
54
58
  "additionalProperties": false,
@@ -50,6 +50,10 @@
50
50
  "type": "string",
51
51
  "description": "Resolved user-configured retrieval guidance, ordered from global to collection to matching path prefixes"
52
52
  },
53
+ "supersededBy": {
54
+ "type": "string",
55
+ "description": "URI of the current fact when a newer memory fact superseded this result"
56
+ },
53
57
  "snippet": {
54
58
  "type": "string",
55
59
  "description": "Matching text snippet from document mirror"
@@ -84,6 +84,10 @@
84
84
  "description": "Resolved user-configured retrieval guidance, ordered from global to collection to matching path prefixes"
85
85
  },
86
86
  "egressLineage": { "$ref": "#/$defs/egressLineage" },
87
+ "supersededBy": {
88
+ "type": "string",
89
+ "description": "URI of the current fact when a newer memory fact superseded this result"
90
+ },
87
91
  "snippet": {
88
92
  "type": "string",
89
93
  "description": "Matching text snippet from document mirror"
@@ -140,6 +140,21 @@
140
140
  "type": "integer",
141
141
  "description": "Number of chunks with embeddings",
142
142
  "minimum": 0
143
+ },
144
+ "rootError": {
145
+ "type": "object",
146
+ "description": "Present only while the collection root is missing or unreadable, checked when status is read; the collection's indexed documents are kept and still counted. The message names the filesystem error but not the path.",
147
+ "additionalProperties": false,
148
+ "required": ["code", "message"],
149
+ "properties": {
150
+ "code": {
151
+ "type": "string",
152
+ "const": "SOURCE_AVAILABILITY_UNKNOWN"
153
+ },
154
+ "message": {
155
+ "type": "string"
156
+ }
157
+ }
143
158
  }
144
159
  }
145
160
  }
@@ -257,6 +272,19 @@
257
272
  }
258
273
  }
259
274
  },
275
+ "staleCollections": {
276
+ "type": "array",
277
+ "description": "Indexed collections the config does not list. Commands that sync the config refuse to open the index (nothing is deleted) until the collections are added back or `gno cleanup` deletes their index data. Omitted when there are none.",
278
+ "items": {
279
+ "type": "object",
280
+ "additionalProperties": false,
281
+ "required": ["name", "documents"],
282
+ "properties": {
283
+ "name": { "type": "string" },
284
+ "documents": { "type": "integer", "minimum": 1 }
285
+ }
286
+ }
287
+ },
260
288
  "recentErrors": {
261
289
  "type": "integer",
262
290
  "description": "Recent ingest errors in the last 24 hours",
@@ -361,6 +361,22 @@ export function decorateUriForIndex(uri: string, indexName?: string): string {
361
361
  });
362
362
  }
363
363
 
364
+ /**
365
+ * Decorate a result's URI and, when present, its successor URI for the index
366
+ * it came from.
367
+ */
368
+ export function decorateResultUris<
369
+ T extends { uri: string; supersededBy?: string },
370
+ >(item: T, indexName?: string): T {
371
+ return {
372
+ ...item,
373
+ uri: decorateUriForIndex(item.uri, indexName),
374
+ ...(item.supersededBy
375
+ ? { supersededBy: decorateUriForIndex(item.supersededBy, indexName) }
376
+ : {}),
377
+ };
378
+ }
379
+
364
380
  /**
365
381
  * Remove output-only index metadata from a gno:// URI.
366
382
  */
@@ -3,7 +3,7 @@
3
3
  import type { ContextCapsuleV1 } from "../core/context-capsule";
4
4
 
5
5
  export const CONTEXT_AGENT_PROJECTION_SCHEMA_VERSION =
6
- "gno-context-agent-v1" as const;
6
+ "gno-context-agent-v2" as const;
7
7
 
8
8
  export interface ContextAgentProjectionEvidence {
9
9
  uri: string;
@@ -17,6 +17,9 @@ export interface ContextAgentProjectionEvidence {
17
17
  contextIds: readonly string[];
18
18
  egress: "local_only" | "lan" | "remote" | "unclassified" | "unavailable";
19
19
  text: string;
20
+ /** Content date (frontmatter or record date), never the file mtime. */
21
+ documentDate: string | null;
22
+ supersededBy: string | null;
20
23
  }
21
24
 
22
25
  export interface ContextAgentProjectionGuidance {
@@ -97,6 +100,8 @@ type EvidenceTuple = readonly [
97
100
  heading: string | null,
98
101
  contextIds: readonly string[],
99
102
  egress: ContextAgentProjectionEvidence["egress"],
103
+ documentDate: string | null,
104
+ supersededBy: string | null,
100
105
  ];
101
106
 
102
107
  type GuidanceTuple = readonly [
@@ -207,6 +212,8 @@ const projection = (
207
212
  item.heading,
208
213
  item.contextIds,
209
214
  item.egress,
215
+ item.documentDate,
216
+ item.supersededBy,
210
217
  ]),
211
218
  g: [
212
219
  source.guidance.evidenceTrust,
@@ -275,6 +282,8 @@ export const projectContextCapsuleForAgent = (
275
282
  contextIds: item.contextIds,
276
283
  egress: item.egress,
277
284
  text: item.text,
285
+ documentDate: item.documentDate,
286
+ supersededBy: item.supersededBy ?? null,
278
287
  })),
279
288
  coverage: {
280
289
  requestedFacets: capsule.coverage.requestedFacets,
@@ -98,6 +98,7 @@ const evidenceBlock = (
98
98
  `- Selection rank: ${item.selectionRank}`,
99
99
  `- Modified: ${nullable(item.modifiedAt)}`,
100
100
  `- Document date: ${nullable(item.documentDate)}`,
101
+ ...(item.supersededBy ? [`- Superseded by: ${item.supersededBy}`] : []),
101
102
  `- Observed: ${nullable(item.observedAt)}`,
102
103
  `- Facets: ${item.facets.length > 0 ? item.facets.join(", ") : "none"}`,
103
104
  `- Trust: ${item.trust}`,
@@ -49,6 +49,8 @@ const generationSources = (capsule: ContextCapsuleV1) => {
49
49
  return text ? [text] : [];
50
50
  })
51
51
  .join("\n"),
52
+ date: evidence.documentDate ?? undefined,
53
+ supersededBy: evidence.supersededBy,
52
54
  }));
53
55
  };
54
56
 
@@ -17,7 +17,7 @@ import { CliError } from "../../errors.js";
17
17
  // ─────────────────────────────────────────────────────────────────────────────
18
18
 
19
19
  /** Version of the protocol block content. Bump on any content change. */
20
- export const BLOCK_VERSION = 3;
20
+ export const BLOCK_VERSION = 5;
21
21
 
22
22
  /** Stable across block versions — never change these once shipped. */
23
23
  export const BEGIN_MARKER = "<!-- gno:agents:begin -->";
@@ -43,17 +43,18 @@ Local knowledge search over indexed collections. Source files are the truth; the
43
43
 
44
44
  Ladder — scope to a collection first (\`--collection <name>\`):
45
45
 
46
- 1. Exact term/identifier/quote/error: \`gno search "<text>"\`
47
- 2. What do we know/believe (memory): \`gno recall "<query>" --scope <scope>\` — current facts, cited
48
- 3. Entity or known document: \`gno query "<question>" --fast -n 10\`
49
- 4. Multi-document evidence: \`gno context build "<goal>" --budget 12000\`
50
- 5. Change/dependency questions: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
51
- 6. Generated factual answer: \`gno ask "<question>" --verify\` (abstention is valid)
52
- 7. Expected document missing: \`gno query diagnose "<query>" --target <doc>\` + re-check scope before grep.
46
+ 1. Exact term/identifier/quote/error you already have verbatim: \`gno search "<text>"\`. A question, or a name or topic taken from it, is not exact text: use rung 3
47
+ 2. What do we know/believe (memory): \`gno recall "<query>" --scope <scope>\`
48
+ 3. Any other question: \`gno query "<question>" --fast -n 10\` (hybrid)
49
+ 4. Not found or vague: \`gno query "<question>" -n 10\` (adds expansion + rerank)
50
+ 5. Multi-document evidence: \`gno context build "<goal>" --budget 12000\`
51
+ 6. Changes/dependencies: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
52
+ 7. Generated answer: \`gno ask "<question>" --verify\` (abstention is valid)
53
+ 8. Expected document missing: \`gno query diagnose "<query>" --target <doc>\` before grep.
53
54
 
54
55
  Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title, provenance) — never an update API. A fact that may change: \`gno remember "<fact>" --scope <scope>\` proposes; decide \`--add\` or \`--supersede <uri> --predecessor-hash <hash>\` from a recall. Recalled spans are context, not new facts: pass the receipt (\`--receipt\`). After writes: reindex the collection, verify retrieval.
55
56
 
56
- Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks) and memory recipes live in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
57
+ Cite with gno:// URIs. Filters, structured queries, backlinks and memory recipes live in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
57
58
  }
58
59
 
59
60
  /** SHA-256 hex digest of the body, truncated for the stamp line. */
@@ -170,7 +170,7 @@ async function runMutation(
170
170
  // the operator the exact block to paste (install/update only — uninstall
171
171
  // guidance is in the per-target detail).
172
172
  const manualBlock =
173
- failedPaths.length > 0 && mode === "install" ? renderBlock() : undefined;
173
+ failedPaths.length > 0 && mode !== "uninstall" ? renderBlock() : undefined;
174
174
 
175
175
  if (json) {
176
176
  process.stdout.write(
@@ -217,15 +217,16 @@ async function runMutation(
217
217
  }
218
218
 
219
219
  /**
220
- * Install or refresh the protocol block. Both verbs converge the block to
221
- * the current release: install appends when absent, update replaces an
222
- * older/stale block in place; a current block is a no-op for either.
220
+ * Install or refresh the protocol block. Both verbs replace an older/stale
221
+ * block in place and treat a current block as a no-op; only install appends
222
+ * where no block exists. Update reports such a file `absent` (with an
223
+ * install hint) and never writes or creates it.
223
224
  */
224
225
  export function installAgents(
225
226
  opts: AgentsOptions = {},
226
227
  verb: "install" | "update" = "install"
227
228
  ): Promise<void> {
228
- return runMutation(verb, "install", opts);
229
+ return runMutation(verb, verb, opts);
229
230
  }
230
231
 
231
232
  /** Remove the protocol block and its markers, leaving the rest untouched. */
@@ -35,12 +35,16 @@ export type PlanAction =
35
35
  | "update" // block present but differs — replace in place
36
36
  | "current" // block present and identical — no-op
37
37
  | "remove" // uninstall: block present — remove it
38
- | "absent" // uninstall: no block — no-op
38
+ | "absent" // update/uninstall: no block — no-op, file untouched
39
39
  | "covered" // import chain or same real file — another target owns it
40
40
  | "not-detected" // harness not installed on this machine — skipped
41
41
  | "error"; // fail-closed (malformed markers, unreadable file)
42
42
 
43
- export type PlanMode = "install" | "uninstall";
43
+ /** `update` refreshes existing blocks only; it never appends or creates. */
44
+ export type PlanMode = "install" | "update" | "uninstall";
45
+
46
+ const UPDATE_ABSENT_DETAIL =
47
+ "no GNO agents block to refresh; run `gno agents install` to add one";
44
48
 
45
49
  export interface TargetPlan {
46
50
  target: ResolvedTarget;
@@ -261,6 +265,15 @@ export async function planTargets(
261
265
  );
262
266
  continue;
263
267
  }
268
+ if (mode === "update" && !extraction.found) {
269
+ plans.push({
270
+ ...base,
271
+ action: "absent",
272
+ detail: UPDATE_ABSENT_DETAIL,
273
+ newContent: content,
274
+ });
275
+ continue;
276
+ }
264
277
 
265
278
  const newContent = withBlock(
266
279
  content,
@@ -183,25 +183,32 @@ function isDirectory(path: string): boolean {
183
183
  }
184
184
  }
185
185
 
186
+ /**
187
+ * The harness's documented config-dir override from the environment
188
+ * (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`): trimmed, unset when empty, and
189
+ * rejected unless absolute. Shared with `gno skill` user-scope resolution.
190
+ */
191
+ export function harnessConfigDirFromEnv(id: HarnessId): string | undefined {
192
+ const envVar = HARNESS_DEFS[id].configDirEnvVar;
193
+ const value = envVar ? process.env[envVar]?.trim() : undefined;
194
+ if (!(envVar && value)) {
195
+ return undefined;
196
+ }
197
+ if (!isAbsolute(value)) {
198
+ throw new CliError("VALIDATION", `${envVar} must be an absolute path`);
199
+ }
200
+ return normalize(value);
201
+ }
202
+
186
203
  function resolveHarness(
187
204
  def: HarnessDef,
188
205
  home: string,
189
206
  explicitHome: boolean
190
207
  ): ResolvedTarget {
191
- let configDir = join(home, def.configDir);
192
-
193
- if (!explicitHome && def.configDirEnvVar) {
194
- const envOverride = process.env[def.configDirEnvVar];
195
- if (envOverride) {
196
- if (!isAbsolute(envOverride)) {
197
- throw new CliError(
198
- "VALIDATION",
199
- `${def.configDirEnvVar} must be an absolute path`
200
- );
201
- }
202
- configDir = normalize(envOverride);
203
- }
204
- }
208
+ const envOverride = explicitHome
209
+ ? undefined
210
+ : harnessConfigDirFromEnv(def.id);
211
+ const configDir = envOverride ?? join(home, def.configDir);
205
212
 
206
213
  // A covered harness (grok → claude) reads its covering harness's file and
207
214
  // has none of its own, so that is the file its rows report.
@@ -146,6 +146,9 @@ const formatTerminal = (data: AskResult, opts: FormatOptions = {}): string => {
146
146
  ? ""
147
147
  : `:L${citation.startLine}${citation.endLine === undefined || citation.endLine === citation.startLine ? "" : `-L${citation.endLine}`}`;
148
148
  lines.push(` [${index + 1}] ${citation.uri}${range}`);
149
+ if (citation.supersededBy) {
150
+ lines.push(` Superseded by: ${citation.supersededBy}`);
151
+ }
149
152
  }
150
153
  lines.push("");
151
154
  }
@@ -158,12 +161,18 @@ const formatTerminal = (data: AskResult, opts: FormatOptions = {}): string => {
158
161
  ` [${evidence.docid}] ${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)}`
159
162
  );
160
163
  if (evidence.title) lines.push(` ${evidence.title}`);
164
+ if (evidence.supersededBy) {
165
+ lines.push(` Superseded by: ${evidence.supersededBy}`);
166
+ }
161
167
  }
162
168
  } else if (showAllSources && data.results.length > 0) {
163
169
  lines.push(hasAnswer ? "All Retrieved Sources:" : "Sources:");
164
170
  for (const result of data.results) {
165
171
  lines.push(` [${result.docid}] ${result.uri}`);
166
172
  if (result.title) lines.push(` ${result.title}`);
173
+ if (result.supersededBy) {
174
+ lines.push(` Superseded by: ${result.supersededBy}`);
175
+ }
167
176
  }
168
177
  } else if (hasAnswer && data.results.length > 0) {
169
178
  const citedCount = data.citations?.length ?? 0;
@@ -196,7 +205,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
196
205
  citation.startLine === undefined
197
206
  ? ""
198
207
  : `:L${citation.startLine}${citation.endLine === undefined || citation.endLine === citation.startLine ? "" : `-L${citation.endLine}`}`;
199
- lines.push(`**[${index + 1}]** \`${citation.uri}${range}\``);
208
+ lines.push(
209
+ `**[${index + 1}]** \`${citation.uri}${range}\`${citation.supersededBy ? ` (superseded by \`${citation.supersededBy}\`)` : ""}`
210
+ );
200
211
  }
201
212
  lines.push("");
202
213
  }
@@ -216,6 +227,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
216
227
  `${index + 1}. **${evidence.title || evidence.uri}**`,
217
228
  ` - URI: \`${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)}\``
218
229
  );
230
+ if (evidence.supersededBy) {
231
+ lines.push(` - Superseded by: \`${evidence.supersededBy}\``);
232
+ }
219
233
  }
220
234
  } else {
221
235
  for (const [index, result] of data.results.entries()) {
@@ -223,6 +237,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
223
237
  `${index + 1}. **${result.title || result.source.relPath}**`
224
238
  );
225
239
  lines.push(` - URI: \`${result.uri}\``);
240
+ if (result.supersededBy) {
241
+ lines.push(` - Superseded by: \`${result.supersededBy}\``);
242
+ }
226
243
  lines.push(` - Score: ${result.score.toFixed(2)}`);
227
244
  }
228
245
  if (data.results.length === 0) lines.push("*No relevant sources found.*");
@@ -15,7 +15,7 @@ import type {
15
15
  } from "../../llm/types";
16
16
  import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
17
17
 
18
- import { decorateUriForIndex } from "../../app/constants";
18
+ import { decorateResultUris } from "../../app/constants";
19
19
  import { buildVerifiedAsk } from "../../app/verified-ask";
20
20
  import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
21
21
  import {
@@ -89,10 +89,9 @@ export function decorateAskUris(
89
89
  result: AskResult,
90
90
  indexName: string | undefined
91
91
  ): AskResult {
92
- const decorate = <T extends { uri: string }>(item: T): T => ({
93
- ...item,
94
- uri: decorateUriForIndex(item.uri, indexName),
95
- });
92
+ const decorate = <T extends { uri: string; supersededBy?: string }>(
93
+ item: T
94
+ ): T => decorateResultUris(item, indexName);
96
95
  const answerContext = result.meta.answerContext;
97
96
  return {
98
97
  ...result,