@saasontools/strauss-kb 0.1.12 → 0.1.14

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.
package/README.md CHANGED
@@ -1,19 +1,15 @@
1
1
  # @saasontools/strauss-kb
2
2
 
3
- A knowledge base is a directory of small markdown records. Copy the directory
3
+ A knowledge base is a directory of small markdown records: copy the directory
4
4
  and you have the whole thing — nothing outside it is needed to read, search,
5
- adjudicate, or trace it.
5
+ adjudicate, or trace it. This package is that directory's library, command line,
6
+ and MCP server; all three project one command table.
6
7
 
7
- This package is that directory's library, its command line, and its MCP server.
8
- All three project one command table, so a capability exists in every surface or
9
- in none.
8
+ The point of the format is **standing**, not storage: not only "does this
9
+ match?" but "is this still what we hold?" Results are flagged, never filtered.
10
10
 
11
- The point of the format is **standing**, not storage. A search engine answers
12
- "does this match?"; a knowledge base also has to answer "is this still what we
13
- hold?" — and the two disagree in a predictable direction, because a superseded
14
- record is usually the older, longer, more general one and its replacement is
15
- usually a narrowing. Every result therefore arrives flagged rather than
16
- filtered.
11
+ Reference:
12
+ [docs](https://saasontools.github.io/strauss-agent-tools/overview).
17
13
 
18
14
  ## Install
19
15
 
@@ -21,21 +17,15 @@ filtered.
21
17
  npm install -g @saasontools/strauss-kb
22
18
  ```
23
19
 
24
- Global install is the supported path. The consumers of the CLI are agent skills
25
- that shell out to `strauss-kb` by name, many times per session and from whatever
26
- directory the work happens to be in — so the binary has to be on `PATH` without
27
- a per-project setup step, and per-call resolution latency is paid on every call.
28
- The trade-off accepted is that the version is machine-wide and not pinned by the
29
- consuming project; the on-disk format is the compatibility contract, and the
30
- reader is deliberately tolerant of records it did not write (unknown frontmatter
31
- keys are preserved, a missing status defaults).
20
+ Global install is the supported path: agent skills shell out to `strauss-kb` by
21
+ name, from any directory.
32
22
 
33
23
  Two alternatives work and are not the documented convention:
34
24
 
35
- | | Command | When it fits |
36
- | -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
37
- | Per invocation | `npx -y @saasontools/strauss-kb@0.1` | Pinned and zero-install; adds resolution latency to every call and needs a warm npx cache or a network. |
38
- | Project-local | `pnpm add -D @saasontools/strauss-kb` then `pnpm exec strauss-kb` | Pinned per repository and offline after install; bare `strauss-kb` does not resolve outside that repository, so skills cannot use one spelling. |
25
+ | | Command | When it fits |
26
+ | -------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
27
+ | Per invocation | `npx -y @saasontools/strauss-kb@0.1` | Pinned, zero-install; per-call latency, needs a warm cache or net. |
28
+ | Project-local | `pnpm add -D @saasontools/strauss-kb` then `pnpm exec strauss-kb` | Pinned per repository, offline; bare `strauss-kb` resolves only inside it, so skills cannot use one spelling. |
39
29
 
40
30
  As a library:
41
31
 
@@ -43,9 +33,7 @@ As a library:
43
33
  npm install @saasontools/strauss-kb
44
34
  ```
45
35
 
46
- Ships ESM and CommonJS. A consumer that transpiles per-file to CommonJS without
47
- bundling can `require()` it without depending on its Node version honouring
48
- `require(esm)`.
36
+ ESM and CommonJS.
49
37
 
50
38
  ## What is in a base
51
39
 
@@ -58,24 +46,17 @@ bundling can `require()` it without depending on its Node version honouring
58
46
  .index.sqlite search derived, gitignored
59
47
  ```
60
48
 
61
- The default base is `.strauss/kb` relative to the working directory;
62
- `--bundle PATH` addresses any other. A scratch base under a worktree and a
63
- committed base versioned beside the code it describes are the same format with
64
- different lifetimes. Nothing promotes one to the other.
49
+ The default base is `.strauss/kb`; `--bundle PATH` names another.
50
+ `INDEX.md` and `log.jsonl` are store-owned and differ in kind:
65
51
 
66
- `INDEX.md` and `log.jsonl` are store-owned and differ in kind — treating them
67
- alike is how the history gets lost:
52
+ | | `INDEX.md` | `log.jsonl` |
53
+ | ------- | -------------------------- | ------------------------------ |
54
+ | Nature | derived — from frontmatter | primary — events nothing holds |
55
+ | Write | full regenerate | append |
56
+ | Repair | rebuilt when it disagrees | malformed lines reported |
57
+ | If lost | reconstructed free | gone |
68
58
 
69
- | | `INDEX.md` | `log.jsonl` |
70
- | ------- | ------------------------------------------ | ------------------------------------------- |
71
- | Nature | derived — recomputable from frontmatter | primary — records events nothing else holds |
72
- | Write | full regenerate | append |
73
- | Repair | rebuilt when it disagrees with the records | malformed lines reported, never rewritten |
74
- | If lost | reconstructed free | gone |
75
-
76
- Repair-on-read, not coordination, is what lets both exist without a lock. The
77
- index is _eventually_ correct: a writer whose scan predated another's record
78
- publishes a briefly stale index, and the next read through the store settles it.
59
+ Repair-on-read, not coordination, lets both exist without a lock.
79
60
 
80
61
  ### Cross-worktree writes
81
62
 
@@ -119,61 +100,43 @@ GitHub itself performs.
119
100
 
120
101
  ## Records
121
102
 
122
- The filename is the identity. `fact.auth-retries.md` has concept id
123
- `fact.auth-retries` — `<type>.<slug>`, both halves kebab-case. One record per
124
- file, so parallel writers never merge; they only choose distinct names.
103
+ The filename is the identity: `fact.auth-retries.md` has concept id
104
+ `fact.auth-retries` — `<type>.<slug>`, both halves kebab-case.
125
105
 
126
106
  Records are [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog)
127
107
  concepts. `type` is the only key OKF requires; `title`, `description`,
128
108
  `resource`, `tags`, `sources`, `generated`, `verified`, and `stale_after` are
129
- OKF's. Unknown keys are preserved rather than stripped, as OKF requires of
130
- consumers.
131
-
132
- `verified` is the record's append-only trail of checks. Each entry is OKF's
133
- actor stamp `{ by, at }` and the entries this package's `verify` writes add
134
- a `note`: what the check actually found, not just that one happened. The `note`
135
- is a strauss extension key on the entries this tool writes, not an OKF
136
- requirement on the array, so noteless entries a foreign producer wrote remain
137
- readable, and prior entries are spread forward untouched rather than reshaped.
138
-
139
- Who may append is the point. A verifier whose actor equals the record's
140
- `generated.by` compared case-insensitively over the whole actor, so case
141
- drift cannot mint a distinct verifier identity — is refused unless the actor
142
- is `human:`-prefixed: trust that can be self-granted is not trust, and a
143
- generator re-reading its own output is not an independent check. The refusal
144
- is recorded in the log as `verify:refused`, so an audit sees the attempt as
145
- well as the rule. The `human:` prefix itself is an honor-system label actor
146
- identity is self-declared through `STRAUSS_KB_ACTOR`, not an authenticated
147
- identity claim which is worth knowing when deciding how much weight a
148
- human-verified event carries.
149
-
150
- Anything prefixed `strauss_` is this package's extension, namespaced so a later
151
- OKF version defining the same name cannot collide:
152
-
153
- | Key | Meaning |
154
- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
- | `strauss_status` | `draft`, `proposed`, `accepted`, `open`, `resolved`, `rejected`, `superseded`. Parses with a default of `draft`. |
156
- | `strauss_supersedes` / `strauss_superseded_by` | Both directions of a supersession, written together. |
157
- | `strauss_anchors` | `{ file, symbol?, hash?, lines?, resolved_at? }` — where the record attaches in the code, and optionally what that code looked like when it did. See [Anchors and drift](#anchors-and-drift). |
158
- | `strauss_assumption` | The claim has no source, said as a field rather than as a fake entry in `sources`. |
159
- | `strauss_answered` | Who resolved an open question, and when. |
160
- | `strauss_verify` | Checks that would confirm the record still holds. |
161
- | `strauss_materiality` / `strauss_confidence` / `strauss_owner` | `blocking`/`important`/`non-blocking`, `low`/`medium`/`high`, and a name. |
162
-
163
- Edges are markdown links in the body, as OKF specifies — untyped, with the kind
164
- conveyed by the surrounding prose. Broken links are legal: records are routinely
165
- written before the ones they point at exist.
166
-
167
- Twelve record types differ only in what their body answers and where they start
168
- in the lifecycle — `fact`, `requirement`, `constraint`, `decision`,
169
- `assumption`, `open-question`, `risk`, `contract`, `flow`, `affected-system`,
170
- `test-obligation`, `source-note`. `strauss-kb types` prints each one's purpose,
171
- body sections, and initial status; a section a type does not define is rejected
172
- rather than written.
173
-
174
- Do not work from memory on the frontmatter contract — `strauss-kb schema` emits
175
- JSON Schema generated from the code that enforces it, so it cannot drift from
176
- what a write will accept.
109
+ OKF's. Unknown keys are preserved rather than stripped.
110
+
111
+ `verified` is an append-only trail of checks, each an actor stamp `{ by, at }`
112
+ plus a `note`. A verifier whose actor equals the record's `generated.by` is
113
+ refused unless the actor is `human:`-prefixed, and the refusal is logged as
114
+ `verify:refused`; the prefix is honor-system, since actor identity is
115
+ self-declared through `STRAUSS_KB_ACTOR`.
116
+
117
+ Anything prefixed `strauss_` is this package's extension, namespaced against a
118
+ later OKF key of the same name:
119
+
120
+ | Key | Meaning |
121
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
+ | `strauss_status` | `draft`, `proposed`, `accepted`, `open`, `resolved`, `rejected`, `superseded`. Default `draft`. |
123
+ | `strauss_supersedes` / `strauss_superseded_by` | Both directions of a supersession, written together. |
124
+ | `strauss_anchors` | `{ file, symbol?, hash?, lines?, resolved_at? }` where the record attaches, and what that code looked like then. See [Anchors and drift](#anchors-and-drift). |
125
+ | `strauss_links` | `{ target, rel }` a typed causal edge, source target. See [Links](#links). |
126
+ | `strauss_assumption` | The claim has no source, as a field rather than a fake entry in `sources`. |
127
+ | `strauss_answered` | Who resolved an open question, and when. |
128
+ | `strauss_verify` | Checks that would confirm the record still holds. |
129
+ | `strauss_materiality` / `strauss_confidence` / `strauss_owner` | `blocking`/`important`/`non-blocking`, `low`/`medium`/`high`, and a name. |
130
+
131
+ Twelve types differ in what their body answers and where they start in the
132
+ lifecycle: `fact`, `requirement`, `constraint`, `decision`, `assumption`,
133
+ `open-question`, `risk`, `contract`, `flow`, `affected-system`,
134
+ `test-obligation`, `source-note`.
135
+
136
+ `strauss-kb types` prints each one's purpose, sections, and initial status;
137
+ `strauss-kb schema` emits JSON Schema from the code that enforces it.
138
+ Body sections, edges, and the frontmatter contract:
139
+ [specification](https://saasontools.github.io/strauss-agent-tools/specification).
177
140
 
178
141
  ```yaml
179
142
  ---
@@ -197,52 +160,47 @@ A lock file. It closes the window and adds a stale-hold failure mode that is
197
160
  worse than the residue.
198
161
  ```
199
162
 
163
+ ## Links
164
+
165
+ `strauss_links` is a typed causal edge, `[{ target, rel }]` on the source
166
+ record, read source → target: `A depends_on B` means A needs B. The vocabulary
167
+ is closed: eight rels. The **dependant** — the end that breaks when the other
168
+ changes — is per-rel.
169
+
170
+ | `rel` | `A <rel> B` means | Dependant |
171
+ | ------------- | ---------------------------- | --------- |
172
+ | `depends_on` | A needs B to hold | A |
173
+ | `verified_by` | B confirms A | A |
174
+ | `satisfies` | A discharges B's requirement | A |
175
+ | `constrains` | A bounds what B may do | B |
176
+ | `informs` | A shaped B, not binding | B |
177
+ | `blocks` | B waits on A | B |
178
+ | `invalidates` | A makes B no longer hold | B |
179
+ | `related_to` | no dependence | — |
180
+
181
+ `kb_impact` (`strauss-kb impact <id>`): the transitive set of dependants, each
182
+ rel followed in its own direction, never `related_to`.
183
+
184
+ `kb_backlinks` (`strauss-kb backlinks <id>`): every inbound edge, one hop, any rel.
185
+
186
+ `kb_validate`: unknown rel or malformed target is an `error`, absent target a
187
+ `warning`. Only errors fail the exit code.
188
+
189
+ The [specification](https://saasontools.github.io/strauss-agent-tools/specification)
190
+ has the rest.
191
+
200
192
  ## Writes
201
193
 
202
- Records are staged to a sibling file and published atomically, so a concurrent
203
- reader sees a whole record or none. Publication uses `link`, which fails when
204
- the name is taken two writers choosing one concept id is a 409 the caller must
205
- answer, by picking a more specific slug or by saying it meant to replace.
206
- `rename` is used only when the caller passes `overwrite`.
207
-
208
- Read-modify-write (`setStatus`, `answer`) checks a content digest immediately
209
- before publishing, which narrows the lost-update window rather than closing it.
210
- [ARCHITECTURE.md](./ARCHITECTURE.md) says why a lock was rejected.
211
-
212
- `supersede` writes both directions, so a backlink cannot drift in normal use and
213
- `validate` drops to catching hand-edits. A `write` (or `write-decision`) that
214
- carries `supersedes` does the same: the new record publishes first, then each
215
- prior record it names is marked superseded in turn — a crash between the two
216
- leaves an old record with no backlink, which `validate` already reports as
217
- "is not marked superseded", never a silent drift. A `supersedes` id naming a
218
- record that does not exist yet is legal and does not fail the write; `validate`
219
- is what reports a target that never resolves. A `supersedes` id naming the
220
- record's own concept id is a no-op rather than an error, duplicate ids mark
221
- once, and the array is capped at 32 entries.
222
-
223
- A concurrent writer marking the same target races the compare-and-swap check;
224
- that's retried a few times before giving up, and giving up is reported the
225
- same way as a target that doesn't exist yet — left out of `supersededIds` for
226
- `validate` to catch, not thrown, since the calling record is already
227
- published by that point. If two different records both name the same target
228
- in `supersedes`, the target's backlink points at whichever wrote last;
229
- `validate` doesn't see this as a problem because the target genuinely is
230
- superseded, but `kb_query`/`kb_load`'s adjudication surfaces the resulting
231
- fork as a warning at read time.
232
-
233
- `kb_write` and `kb_write_decision` return
234
- `{ conceptId, action: "created" | "superseded-prior", supersededIds }` —
235
- `supersededIds` is only the ids actually marked, not every id the input named.
236
- A 409 from a concept-id collision carries `action: "refused"` in its `details`,
237
- alongside the `conceptId`.
238
-
239
- Records are never deleted. Superseding keeps the earlier reasoning inspectable,
240
- which is what a later `trace` reads.
241
-
242
- `no-decision` records the explicit claim that a piece of work had nothing to
243
- decide (an idempotent `decision.none` record). It exists for workflow gates:
244
- "did you write a decision?" rewards writing a junk one, "did you answer?"
245
- does not — so silence has to be expressible.
194
+ Records are staged to a sibling file and published atomically: a reader sees a
195
+ whole record or none. A concept-id collision is a 409 the caller answers with a
196
+ narrower slug or by saying it meant to replace. `supersede` writes both
197
+ directions of the link, and `write`/`write-decision` do the same for every id in
198
+ `supersedes`. Records are never deleted; superseding keeps earlier reasoning
199
+ inspectable for `trace`.
200
+
201
+ Crash, race, and fork edge cases:
202
+ [specification](https://saasontools.github.io/strauss-agent-tools/specification).
203
+ Why a lock was rejected: [ARCHITECTURE.md](./ARCHITECTURE.md).
246
204
 
247
205
  ## Anchors and drift
248
206
 
@@ -314,6 +272,9 @@ strauss-kb [--bundle PATH] <command> [args]
314
272
  The bounded neighbourhood around one record, every cut named.
315
273
  query <text...> [--repo-root PATH] Search; every match arrives flagged with its standing.
316
274
  trace <concept-id> [edges...] How a position was arrived at, as a timeline.
275
+ impact <concept-id> [--depth N] [--rels a,b]
276
+ What breaks if this changes: its dependants, transitively.
277
+ backlinks <concept-id> Who points at this record — one hop, every rel.
317
278
  list [type] Every record, optionally narrowed to one type.
318
279
  index The index, rebuilt if it disagrees with the records.
319
280
  log What touched what, and when.
@@ -334,15 +295,12 @@ strauss-kb [--bundle PATH] <command> [args]
334
295
  STRAUSS_KB_ACTOR names the writer in the log
335
296
  ```
336
297
 
337
- Results go to stdout as JSON `index`, `catalog` and `pack` are markdown, which is what
338
- they are, and `doctor` prints a table unless `--json` asks for the object
339
- behind it. `--json` is refused rather than ignored on the commands that have
340
- only one form, since a flag that quietly does nothing reads as one that
341
- worked; `--` ends flag parsing, for the verbs that end in free prose. Errors
342
- go to stderr and exit 1. `validate`, `anchor-resolve`, and `doctor --strict`
343
- are the commands whose exit code is not just "did it run": a check that
344
- reports a problem succeeded as a command and failed as a check, so each exits
345
- 1 with its findings on stdout.
298
+ Results go to stdout as JSON; `index`, `catalog` and `pack` are markdown, and
299
+ `doctor` prints a table unless `--json`. `--json` is refused where a command has
300
+ one form; `--` ends flag parsing. Errors go to stderr with exit 1; `validate`,
301
+ `anchor-resolve` and `doctor --strict` exit 1 with findings on stdout; only
302
+ `validate` findings with `severity: "error"` do, so warnings alone exit 0. Per-command flags:
303
+ [cli-reference](https://saasontools.github.io/strauss-agent-tools/cli-reference).
346
304
 
347
305
  A flag accepts either spelling — `--budget 4000` or `--budget=4000` — and a
348
306
  flag given no value is an error rather than a silent fallback to the default,
@@ -360,21 +318,12 @@ strauss-kb --bundle .strauss/kb write fact <<'JSON'
360
318
  JSON
361
319
 
362
320
  strauss-kb query cache key region
363
- strauss-kb validate || echo "problems above"
321
+ strauss-kb validate || echo "errors above" # warnings alone still exit 0
364
322
  ```
365
323
 
366
324
  ## MCP server
367
325
 
368
326
  `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
369
- Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
370
- `kb_status`, `kb_supersede`, `kb_answer`, `kb_verify`, `kb_anchor_resolve`,
371
- `kb_load`, `kb_catalog`, `kb_pack`, `kb_query`, `kb_trace`, `kb_list`,
372
- `kb_index`, `kb_log`, `kb_validate`, `kb_doctor`, `kb_schema`, `kb_types`,
373
- `kb_pin`, `kb_unpin`, `kb_pins`, `kb_context`. Most tools take a `bundlePath`;
374
- `kb_schema` and `kb_types` describe the format rather than any one base, and
375
- `kb_pins` and `kb_context` read the workspace pin manifests instead. The one
376
- CLI verb with no tool is `sync-instructions` — file plumbing for hooks, not an
377
- agent capability; the capability is `kb_context`.
378
327
 
379
328
  ```json
380
329
  {
@@ -384,13 +333,17 @@ agent capability; the capability is `kb_context`.
384
333
  }
385
334
  ```
386
335
 
387
- The tool descriptions carry the judgment a schema cannot: that an unsourced
388
- claim is an `assumption` and not a `fact` with a vague source, that a conflict
389
- between two records belongs in a `risk` or a superseding `decision` rather than
390
- being quietly resolved, and that `kb_load` is usually the right first call.
336
+ Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
337
+ `kb_status`, `kb_supersede`, `kb_answer`, `kb_verify`, `kb_anchor_resolve`, `kb_load`, `kb_catalog`,
338
+ `kb_pack`,
339
+ `kb_query`, `kb_trace`, `kb_impact`, `kb_backlinks`, `kb_list`, `kb_index`, `kb_log`, `kb_validate`,
340
+ `kb_doctor`, `kb_schema`, `kb_types`, `kb_pin`, `kb_unpin`, `kb_pins`,
341
+ `kb_context`. Most take a `bundlePath`. The one CLI verb with no tool is
342
+ `sync-instructions`; the agent capability is `kb_context`.
391
343
 
392
- `STRAUSS_KB_ACTOR` names the writer in the log. Diagnostics go to stderr,
393
- because stdout is the JSON-RPC transport.
344
+ `STRAUSS_KB_ACTOR` names the writer in the log; diagnostics go to stderr, since
345
+ stdout is the JSON-RPC transport. Per-tool schemas:
346
+ [mcp-reference](https://saasontools.github.io/strauss-agent-tools/mcp-reference).
394
347
 
395
348
  ## Library
396
349
 
@@ -411,11 +364,8 @@ for (const hit of hits) {
411
364
  }
412
365
  ```
413
366
 
414
- `matchToDiff` answers a different question from `query`: given a structural
415
- description of a diff, which records are anchored to each hunk. It takes hunks
416
- and optional symbol ranges rather than a patch, so this package carries no diff
417
- parser, and it degrades to file-level precision — labelled as such — when a
418
- symbol cannot be resolved.
367
+ `matchToDiff` takes hunks and optional symbol ranges rather than a patch,
368
+ answering which records are anchored to each hunk.
419
369
 
420
370
  ## Retrieval
421
371
 
@@ -428,111 +378,23 @@ problem:
428
378
  | Standing | `strauss_status`, the supersession chain | is this still what we hold? |
429
379
  | Freshness | `stale_after`, `verified[]` | has anyone confirmed it lately? |
430
380
 
431
- Freshness is tiered by who did the confirming. OKF's spec (§5.3) defines the
432
- trust tiers from the verifying actor's prefix: an empty `verified[]` is
433
- unverified, an agent-prefixed verifier makes the record machine-confirmed, and
434
- a `human:`-prefixed verifier makes it human-reviewed. Of that ladder, today's
435
- adjudication reports only the first rung — the warning it attaches when
436
- `verified[]` is empty; reporting the full tier is upcoming tooling. When it
437
- lands, the tier will be derived from the events at read time, never stored, so
438
- it cannot drift from the trail that justifies it.
439
-
440
- **Load before you search.** These bases run to a few thousand tokens — twenty
441
- records measured at about 3,000 — so the first thing to try is taking all of it.
442
- On nine questions whose wording appears in no record, a reader holding the whole
443
- base answered eight; embedding search over the same records answered four. Two
444
- of those differences are structural rather than matters of degree: a reader can
445
- say no record answers the question, where vector search returns its nearest
446
- neighbour whatever the distance; and a reader picks the record that answers the
447
- question rather than the one nearest the topic. The mechanism is simple:
448
- retrieval makes similarity the gatekeeper, and a match the ranker misses never
449
- reaches the model. A full read lets the model do the matching itself —
450
- synonymy, implication across records, aggregation — which no ranker does.
451
-
452
- Read for a question, not for a session: a base loaded at the start of a long
453
- conversation is summarised away by the end of it, and reloading costs about
454
- three thousand tokens. Read it again at the point of use.
455
-
456
- **The three rungs, in one rule.** While the base fits the budget, `load` it
457
- whole. Once `load` refuses, `catalog` then `pack` the record that matters. For
458
- a lookup by wording, `query`.
381
+ **Load before you search.** These bases run to a few thousand tokens, and a
382
+ reader holding the whole base out-answers embedding search over the same records
383
+ ([ARCHITECTURE.md](./ARCHITECTURE.md#load-beats-retrieval-while-the-base-fits)).
384
+ Read for a question, not for a session a base loaded early in a long
385
+ conversation is summarised away by the end.
459
386
 
460
- ```bash
461
- strauss-kb load # under the budget: everything, with standing
462
- strauss-kb catalog # past it: one line per record, ~30 tokens each
463
- strauss-kb pack decision.cursor-v2 # then the neighbourhood around the one that matters
464
- strauss-kb query cursor pagination # or a point lookup by wording
465
- ```
387
+ - `load` refuses rather than truncating past its budget (25,000 tokens default).
388
+ - `--all` (`all: true` over MCP) bypasses that refusal; it excludes `--budget`.
389
+ - Superseded records return as name, replacement and date only.
390
+ - `catalog` is the rung with no ceiling: one line per record, ~30 tokens each.
391
+ - `pack` is the middle rung: one record's neighbourhood, every cut named.
392
+ - `query` flags rather than filters — every hit arrives with its standing.
393
+ - `trace` orders by `generated.at` rather than ranking.
394
+ - Chains resolve on read: `chain-cycle`, forked heads, `broken-chain`.
466
395
 
467
- A whole read gives perfect recall and can say _no record answers this_, which
468
- no ranker can. `catalog` keeps that at a fraction of the cost by naming every
469
- record instead of every body; `query` gives up both, returning its nearest hit
470
- whatever the distance.
471
-
472
- `load` refuses rather than truncating past its token budget (`--budget` /
473
- `budgetTokens`, 25,000 by default, held against the estimated size of what is
474
- handed back) — a truncated base reads as a complete one, so a caller would
475
- answer "never decided" from a slice it did not know was a slice. `context`
476
- refuses the same way at its own, tighter budget (4,000 by default). Superseded
477
- records come back as name, replacement and date only; `trace` still reaches
478
- them by id.
479
-
480
- A refusal reports `approxTokens` against `budgetTokens` and carries a `message`
481
- naming the budget and the next calls. A successful load reports `budgetTokens`
482
- too, so a caller can see how close it came before crossing the line.
483
-
484
- `--all` (`all: true` over MCP) bypasses the budget and hands back the entire
485
- bundle regardless of size. A loaded result carries `tokensLoaded`, and
486
- `budgetTokens: null` marks that no ceiling applied; `--all` is mutually
487
- exclusive with `--budget`. It is for an operator who has decided the size is
488
- worth the tokens — a narrower `type` filter, `catalog`, or `query` fits better
489
- when it is not.
490
-
491
- **Catalog is the rung that keeps the base knowable.** One line per record —
492
- concept id, type, title, standing, stale flag — sorted by type then title, at
493
- roughly thirty tokens each, so a base far past `load`'s budget still fits in
494
- one call. Superseded records show the replacement in place of a body. The
495
- header sums record counts by standing and reports staleness separately (a
496
- current record can be stale). Bodies live in `load`, `pack`, and `trace`.
497
-
498
- `catalog` alone has no ceiling and never refuses — cost is linear at roughly
499
- thirty tokens a record (a thousand-record base is about 30k, five thousand
500
- about 150k); narrow with `type` at that scale. Output is deterministic given
501
- a fixed clock — no timestamp, ordering total down to the concept id — so two
502
- catalogs of an unchanged base diff to nothing except a stale flag flipping as
503
- `stale_after` passes. Pass an explicit `now` (library callers) to hold
504
- byte-equality across that boundary.
505
-
506
- **Pack is the middle rung.** Under the budget, load the base whole. Past it, when the work centres on a record you can name (`catalog` is how you
507
- name it), `pack` hands over that record's bounded neighbourhood: everything
508
- within `--hops` of the root, walked over the base's edges — body links (a
509
- `relatedConceptIds` entry is one), supersession in both directions, shared
510
- code anchors, and shared sources — ranked and cut to `--max-nodes`. Standing
511
- travels with it: superseded neighbours arrive as the same stubs `load` emits.
512
- Every dropped record is named under Excluded, and past its own token budget
513
- `pack` refuses exactly as `load` does. Output is byte-identical across runs
514
- over an unchanged base below the header. With neither a size problem nor a
515
- root record in hand, the question is a point lookup — `query`.
516
-
517
- **Flag, never filter.** `query` returns every hit with its standing, because a
518
- filtered result set is invisible — the caller cannot tell it missed anything.
519
- The single exception is narrow: a superseded record is dropped only when its
520
- replacement is also in the results, so the thread is never lost.
521
-
522
- **Trace inverts the point query.** In a query a `rejected` record is the most
523
- dangerous thing retrievable — a well-formed assertion of what someone decided
524
- _not_ to do. In a history it is the content. `trace` follows supersession,
525
- shared code anchors, and shared sources, and orders by `generated.at`; ranking a
526
- history is meaningless when the sequence is the point.
527
-
528
- Chain resolution happens on read. A stored head would need rewriting on every
529
- ancestor whenever a chain grows, which is derived state that goes stale. The
530
- walk follows both pointers, so a hand-edit that left one side behind cannot
531
- return a record the base openly claims is replaced. A cycle terminates with
532
- `chain-cycle`; a fork reports every head rather than presenting a guess as a
533
- fact; a missing replacement is `broken-chain` with no head — the case that needs
534
- the most care, because returning the stale record unmarked looks exactly like
535
- success.
396
+ Worked flows:
397
+ [use-cases](https://saasontools.github.io/strauss-agent-tools/use-cases).
536
398
 
537
399
  **Placement is cache economics.** `load`'s output belongs in the stable
538
400
  prefix — system prompt or first turn; `query` and `pack` results belong at
@@ -544,98 +406,19 @@ caveats: <https://saasontools.github.io/strauss-agent-tools/mcp-reference>.
544
406
 
545
407
  ## Health
546
408
 
547
- `doctor` sweeps a whole base and reports what has decayed. It is read-only —
548
- nothing is re-dated, re-verified, superseded, or deleted — because every
549
- finding is a judgment somebody has to make: whether a claim still holds, which
550
- question is worth answering, which island to link or drop.
551
-
552
- It exists because decay is invisible from inside a single record. A stale
553
- record reads exactly like a live one, a question nobody answered reads exactly
554
- like one nobody asked, and a record nothing links to is reachable only by
555
- someone who already knows it is there. `validate` is the narrower neighbour:
556
- it asks only whether pointers between records agree.
557
-
558
- | Check | Reports |
559
- | ---------------------- | ------------------------------------------------------------------------------ |
560
- | `expired` | `stale_after` is in the past — or is not a readable date, which is no better. |
561
- | `expiring` | `stale_after` falls inside the next `--expiring-days` (30). |
562
- | `unverified` | `verified[]` is empty and the record is over `--unverified-days` (90) old. |
563
- | `aging` | Still `open` or `proposed` after `--aging-days` (90). |
564
- | `orphaned` | No other record links to it, by body link or supersession. |
565
- | `broken-supersession` | A chain that does not resolve: no replacement, a missing one, a cycle, a fork. |
566
- | `superseded-but-cited` | A record that still holds, whose body links to one that does not. |
567
- | `drifted` | A hash-carrying anchor whose code moved, or whose file or symbol is gone. |
568
-
569
- Pass `doctor --repo-root PATH` when the base does not sit inside the tree it
570
- describes; without it the sweep looks under the working directory.
571
-
572
- `superseded-but-cited`'s name is for its common case: a rejected target counts
573
- too, and is the worse half — a superseded record at least names its
574
- replacement, while a rejected one is a well-formed assertion of what someone
575
- decided _not_ to do, cited by a record the reader trusts.
576
-
577
- ```bash
578
- strauss-kb doctor # the table
579
- strauss-kb doctor --json # the object behind it
580
- strauss-kb doctor --strict # exit 1 if anything has expired
581
- strauss-kb doctor --unverified-days 30 # a stricter confirmation window
582
- strauss-kb doctor --repo-root ../app # where the anchored source lives
583
- ```
584
-
585
- All eight groups are reported even when empty. A check that found nothing and
586
- a check that never ran look identical in a report that only lists findings,
587
- which is the whole value of a sweep.
588
-
589
- Judgments the checks make, worth knowing before reading a report:
590
-
591
- - **Superseded and rejected records sit out the freshness checks.** A replaced
592
- record whose date has passed needs no repair, and reporting it would bury the
593
- records that do. They stay in the graph checks, where standing is not the
594
- question.
595
- - **A date-only `stale_after` expires at UTC midnight.** `2026-09-01` parses as
596
- `2026-09-01T00:00:00Z`, so a record goes stale at the start of its date: a
597
- sweep run at exactly that instant still calls it expiring, and one a minute
598
- later calls it expired. That is `adjudicate`'s comparison rather than a
599
- second one — two readings of the same field disagreeing about the day would
600
- be worse than either.
601
- - **Age is read from `generated.at`, exclusively.** A record carrying no
602
- timestamp is not reported as aging or unverified — without a start there is
603
- no duration, and inventing one would flag every foreign record as overdue
604
- (adjudication still warns `unverified` on it at read time). Exactly N days
605
- old is not yet "older than N".
606
- - **`orphaned` counts incoming links only, and reads supersession one way.** A
607
- record that cites five others and is cited by none is precisely the island:
608
- reachable if you already know it exists. The replacement references what it
609
- replaced, never the reverse — taken symmetrically, a dead record would vouch
610
- for its own replacement and an old→new pair nothing else touches would rescue
611
- itself. Shared anchors and shared sources are co-location rather than
612
- reference, so they do not rescue a record either.
613
- - **A record citing the one it replaced is not superseded-but-cited.** That
614
- link is the history working as designed, and reporting it would put a finding
615
- on every correctly performed supersession.
616
- - **A replacement pointer is checked whatever the status says.** The store
617
- writes `strauss_status` and `strauss_superseded_by` in one mutation, so a
618
- record left `accepted` while naming a replacement was hand-edited — and
619
- adjudication reads it as current no matter what the pointer says, which is
620
- what makes it worth naming.
621
- - **`drifted` reports and never repairs.** It reads the same comparison
622
- `kb_load` and `kb_query` do, so the sweep and the read paths cannot disagree
623
- about what drift is. Unresolvable anchors ride in this group rather than a
624
- ninth check, anchors carrying no hash are never read, and `--strict` does not
625
- gate on drift.
626
-
627
- `--strict` gates on expiry alone. The other seven report debt a reader decides
628
- about; an expired record is the base itself saying it would stop standing
629
- behind something, which is the one finding a pipeline can act on without a
630
- judgment call.
409
+ `doctor` sweeps a base read-only for `expired`, `expiring`, `unverified`,
410
+ `aging`, `orphaned`, `broken-supersession`, `superseded-but-cited`, and
411
+ `drifted`. All eight groups are reported even when empty; `--strict` gates on
412
+ expiry alone, not on drift. Pass `--repo-root PATH` when the base does not sit
413
+ inside the tree it describes.
414
+ Windows and judgments:
415
+ [cli-reference](https://saasontools.github.io/strauss-agent-tools/cli-reference).
631
416
 
632
417
  ## Living in an agent session
633
418
 
634
- Long sessions lose a knowledge base twice over: attention decays, and
635
- compaction summarises away both the records loaded early and the instruction
636
- that said to consult them. The fix is two-tier: a small index is re-injected
637
- at every context birth, and record bodies are fetched by tool when a question
638
- actually needs them.
419
+ Compaction summarises away the records loaded early and the instruction to
420
+ consult them. The fix is two-tier: a small index re-injected at every context
421
+ birth, bodies fetched by tool when needed.
639
422
 
640
423
  ```bash
641
424
  strauss-kb pin docs/kb # mark a base every session should see
@@ -643,23 +426,13 @@ strauss-kb context # emit the pinned index block
643
426
  strauss-kb sync-instructions AGENTS.md # or keep it in an instruction file
644
427
  ```
645
428
 
646
- Pins live in `.strauss/kb-pins.json`, committed with the repo. Two more
647
- layers exist: `.strauss/kb-pins.local.json` (personal, gitignore it) and
648
- `~/.strauss/kb-pins.json` (every workspace). Nearest layer wins per base,
649
- `--local`/`--user` write the other layers, and `unpin` removes from all
650
- three. A malformed layer is skipped on read and refused on write.
651
-
652
- Per pin:
653
-
654
- - `--mode full` — inject the records themselves, not just the index. For
655
- small or critical bases (ADRs). Falls back to a labelled index when it
656
- cannot fit the block budget.
657
- - `--mode index` — never inject bodies.
658
- - `--profiles a,b` — only inject in the named profiles.
659
- - `--frozen` — the base is concluded; write commands refuse until `--unfreeze`.
660
-
661
- Budgets are named profiles — `session-start`, `compact`, `turn` — with
662
- per-repo overrides in the manifest, so hook commands never carry numbers:
429
+ Pins live in `.strauss/kb-pins.json`, committed with the repo, over
430
+ `.strauss/kb-pins.local.json` (personal) and `~/.strauss/kb-pins.json` (every
431
+ workspace); nearest layer wins per base. Per pin: `--mode full` injects bodies,
432
+ `--mode index` never does, `--profiles a,b` narrows to named profiles, and
433
+ `--frozen` refuses writes until `--unfreeze`. Budgets are named profiles
434
+ `session-start`, `compact`, `turn` — overridable per repo, so hook commands
435
+ carry no numbers:
663
436
 
664
437
  ```json
665
438
  {
@@ -668,16 +441,9 @@ per-repo overrides in the manifest, so hook commands never carry numbers:
668
441
  }
669
442
  ```
670
443
 
671
- Flags beat the manifest, the manifest beats the built-ins, and invalid values
672
- fall back to defaults instead of silencing the index. Past its budget,
673
- `context` refuses like `load` does never truncates — and its refusal says
674
- what to load directly and how to shrink the block.
675
-
676
- `sync-instructions <file>` keeps the same block between
677
- `<!-- strauss-kb:begin/end -->` sentinels in AGENTS.md or CLAUDE.md, touching
678
- nothing outside them. Re-run it when pins change; it is idempotent. This is
679
- the mechanism for runtimes without a reliable post-compaction hook, since
680
- instruction files are re-read where conversation history is not.
444
+ `sync-instructions <file>` keeps that block between
445
+ `<!-- strauss-kb:begin/end -->` sentinels in AGENTS.md or CLAUDE.md; it is
446
+ idempotent, and covers runtimes without a reliable post-compaction hook.
681
447
 
682
448
  What each runtime gets (configs in the
683
449
  [plugin's adapters](../../plugins/strauss-kb/adapters/)):
@@ -688,12 +454,15 @@ What each runtime gets (configs in the
688
454
  | Session-start injection | SessionStart hook | SessionStart hook | PreInvocation, per turn |
689
455
  | Post-compact re-injection | ✓ `compact` source | ✓ client-side; instruction-only when hosted | moot — injected every turn |
690
456
  | File-read blocking | opt-in PreToolUse | ✗ (shell is the side door) | opt-in PreToolUse, JSON |
457
+ | Manual-edit validation | opt-in PostToolUse | ✗ | ✗ |
458
+ | Generated-file edit guard | opt-in PreToolUse | ✗ | ✗ |
691
459
  | Instruction file | CLAUDE.md | AGENTS.md | AGENTS.md + rules/ |
692
460
 
693
- One more thing agents add: file tools. A raw read of a record file bypasses
694
- standing entirely a superseded record reads exactly like a current one so
695
- bases are read through the tools, and a workspace can enforce that with deny
696
- rules or the plugin's opt-in PreToolUse script:
461
+ Never read record files directly read through the tools; a raw read bypasses
462
+ standing, and a superseded record reads exactly like a current one. Enforce it
463
+ with deny rules or the plugin's
464
+ [opt-in hook scripts](../../plugins/strauss-kb/README.md#opt-in-workspace-hooks),
465
+ which also cover manual edits to a bundle:
697
466
 
698
467
  ```json
699
468
  {
@@ -705,45 +474,28 @@ rules or the plugin's opt-in PreToolUse script:
705
474
 
706
475
  ## Optional search tier
707
476
 
708
- `@tobilu/qmd` is an **optional peer dependency** providing BM25 (`searchLex`,
709
- no model download) over a `.index.sqlite` per base, rebuilt when a record is
710
- newer than the index.
477
+ `@tobilu/qmd` is an **optional peer dependency** providing BM25 (`searchLex`, no
478
+ model download) over a `.index.sqlite` per base, rebuilt when a record is newer
479
+ than the index.
711
480
 
712
481
  ```bash
713
482
  npm install -g @tobilu/qmd # alongside a global strauss-kb
714
483
  ```
715
484
 
716
- With it absent — the default — `query` falls back to a substring scan over
717
- concept ids, titles, descriptions, and bodies. Nothing throws, no answer changes
718
- shape, and only recall degrades. Measured against that fallback on a
719
- twenty-record base, the lexical tier wins on word forms (`pages` finds a record
720
- saying only `page`) and on little else: eight of nine probe queries returned
721
- exactly what substring returned.
722
-
723
- The vector tier is deliberately off. It does close the semantic gap — "why not
724
- just use a mutex" finds a record about compare-and-swap that no lexical match
725
- can — but its scores do not separate right from wrong. A wrong hit scored 0.318
726
- against a correct one at 0.295, and any threshold that drops the first drops the
727
- second. Scores are evidence for a reader to weigh, not a filter to apply before
728
- one.
729
-
730
- qmd is used as a library, never through its own MCP server: that would let a
731
- caller reach a base without going through the store, and its default markdown
732
- glob returns `INDEX.md` as a search hit.
485
+ Absent — the default — `query` falls back to a substring scan over concept ids,
486
+ titles, descriptions, and bodies: nothing throws, only recall degrades. The
487
+ vector tier is off, since its scores do not separate right from wrong
488
+ ([ARCHITECTURE.md](./ARCHITECTURE.md#what-happens-when-a-base-outgrows-a-context)).
489
+ qmd is used as a library, never through its own MCP server, which would bypass
490
+ the store.
733
491
 
734
492
  ## Constraints worth knowing
735
493
 
736
- **The store is the sole accessor, not merely the sole writer.** Excluding
494
+ The store is the sole accessor, not merely the sole writer: excluding
737
495
  store-owned files from listings and repairing the index on read hold only while
738
- everything goes through one door. Reading one record by a concept id you already
739
- hold is the exception no invariant, deterministic path.
740
-
741
- **Cross-base questions are unaskable.** Supersession, traces, and search stop at
742
- the directory boundary. "Was this settled somewhere else?" is answered by a
743
- person choosing which base to open. That is the price of a base that can be
744
- copied, deleted, or handed over whole, and it is what keeps the search index
745
- disposable. [ARCHITECTURE.md](./ARCHITECTURE.md) covers the registry that would
746
- lift it, and why it is unbuilt.
496
+ everything goes through one door. Cross-base questions are unaskable
497
+ supersession, traces, and search stop at the directory boundary
498
+ ([ARCHITECTURE.md](./ARCHITECTURE.md)).
747
499
 
748
500
  ## License
749
501