@saasontools/strauss-kb 0.1.13 → 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,63 +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_links` | `{ target, rel }` — a typed causal edge, source target. See [Links](#links). |
159
- | `strauss_assumption` | The claim has no source, said as a field rather than as a fake entry in `sources`. |
160
- | `strauss_answered` | Who resolved an open question, and when. |
161
- | `strauss_verify` | Checks that would confirm the record still holds. |
162
- | `strauss_materiality` / `strauss_confidence` / `strauss_owner` | `blocking`/`important`/`non-blocking`, `low`/`medium`/`high`, and a name. |
163
-
164
- Edges are markdown links in the body, as OKF specifies — untyped, with the kind
165
- conveyed by the surrounding prose. Broken links are legal: records are routinely
166
- written before the ones they point at exist. `strauss_links` adds a typed layer
167
- over that, described below.
168
-
169
- Twelve record types differ only in what their body answers and where they start
170
- in the lifecycle — `fact`, `requirement`, `constraint`, `decision`,
171
- `assumption`, `open-question`, `risk`, `contract`, `flow`, `affected-system`,
172
- `test-obligation`, `source-note`. `strauss-kb types` prints each one's purpose,
173
- body sections, and initial status; a section a type does not define is rejected
174
- rather than written.
175
-
176
- Do not work from memory on the frontmatter contract — `strauss-kb schema` emits
177
- JSON Schema generated from the code that enforces it, so it cannot drift from
178
- 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).
179
140
 
180
141
  ```yaml
181
142
  ---
@@ -230,50 +191,16 @@ has the rest.
230
191
 
231
192
  ## Writes
232
193
 
233
- Records are staged to a sibling file and published atomically, so a concurrent
234
- reader sees a whole record or none. Publication uses `link`, which fails when
235
- the name is taken two writers choosing one concept id is a 409 the caller must
236
- answer, by picking a more specific slug or by saying it meant to replace.
237
- `rename` is used only when the caller passes `overwrite`.
238
-
239
- Read-modify-write (`setStatus`, `answer`) checks a content digest immediately
240
- before publishing, which narrows the lost-update window rather than closing it.
241
- [ARCHITECTURE.md](./ARCHITECTURE.md) says why a lock was rejected.
242
-
243
- `supersede` writes both directions, so a backlink cannot drift in normal use and
244
- `validate` drops to catching hand-edits. A `write` (or `write-decision`) that
245
- carries `supersedes` does the same: the new record publishes first, then each
246
- prior record it names is marked superseded in turn — a crash between the two
247
- leaves an old record with no backlink, which `validate` already reports as
248
- "is not marked superseded", never a silent drift. A `supersedes` id naming a
249
- record that does not exist yet is legal and does not fail the write; `validate`
250
- is what reports a target that never resolves. A `supersedes` id naming the
251
- record's own concept id is a no-op rather than an error, duplicate ids mark
252
- once, and the array is capped at 32 entries.
253
-
254
- A concurrent writer marking the same target races the compare-and-swap check;
255
- that's retried a few times before giving up, and giving up is reported the
256
- same way as a target that doesn't exist yet — left out of `supersededIds` for
257
- `validate` to catch, not thrown, since the calling record is already
258
- published by that point. If two different records both name the same target
259
- in `supersedes`, the target's backlink points at whichever wrote last;
260
- `validate` doesn't see this as a problem because the target genuinely is
261
- superseded, but `kb_query`/`kb_load`'s adjudication surfaces the resulting
262
- fork as a warning at read time.
263
-
264
- `kb_write` and `kb_write_decision` return
265
- `{ conceptId, action: "created" | "superseded-prior", supersededIds }` —
266
- `supersededIds` is only the ids actually marked, not every id the input named.
267
- A 409 from a concept-id collision carries `action: "refused"` in its `details`,
268
- alongside the `conceptId`.
269
-
270
- Records are never deleted. Superseding keeps the earlier reasoning inspectable,
271
- which is what a later `trace` reads.
272
-
273
- `no-decision` records the explicit claim that a piece of work had nothing to
274
- decide (an idempotent `decision.none` record). It exists for workflow gates:
275
- "did you write a decision?" rewards writing a junk one, "did you answer?"
276
- 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).
277
204
 
278
205
  ## Anchors and drift
279
206
 
@@ -368,17 +295,12 @@ strauss-kb [--bundle PATH] <command> [args]
368
295
  STRAUSS_KB_ACTOR names the writer in the log
369
296
  ```
370
297
 
371
- Results go to stdout as JSON `index`, `catalog` and `pack` are markdown, which is what
372
- they are, and `doctor` prints a table unless `--json` asks for the object
373
- behind it. `--json` is refused rather than ignored on the commands that have
374
- only one form, since a flag that quietly does nothing reads as one that
375
- worked; `--` ends flag parsing, for the verbs that end in free prose. Errors
376
- go to stderr and exit 1. `validate`, `anchor-resolve`, and `doctor --strict`
377
- are the commands whose exit code is not just "did it run": a check that
378
- reports a problem succeeded as a command and failed as a check, so each exits
379
- 1 with its findings on stdout. Only `validate` findings with `severity: "error"`
380
- do that — a base mid-write is full of links to records not written yet, so
381
- warnings alone exit 0.
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).
382
304
 
383
305
  A flag accepts either spelling — `--budget 4000` or `--budget=4000` — and a
384
306
  flag given no value is an error rather than a silent fallback to the default,
@@ -402,16 +324,6 @@ strauss-kb validate || echo "errors above" # warnings alone still exit 0
402
324
  ## MCP server
403
325
 
404
326
  `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
405
- Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
406
- `kb_status`, `kb_supersede`, `kb_answer`, `kb_verify`, `kb_anchor_resolve`,
407
- `kb_load`, `kb_catalog`, `kb_pack`, `kb_query`, `kb_trace`, `kb_impact`,
408
- `kb_backlinks`, `kb_list`,
409
- `kb_index`, `kb_log`, `kb_validate`, `kb_doctor`, `kb_schema`, `kb_types`,
410
- `kb_pin`, `kb_unpin`, `kb_pins`, `kb_context`. Most tools take a `bundlePath`;
411
- `kb_schema` and `kb_types` describe the format rather than any one base, and
412
- `kb_pins` and `kb_context` read the workspace pin manifests instead. The one
413
- CLI verb with no tool is `sync-instructions` — file plumbing for hooks, not an
414
- agent capability; the capability is `kb_context`.
415
327
 
416
328
  ```json
417
329
  {
@@ -421,13 +333,17 @@ agent capability; the capability is `kb_context`.
421
333
  }
422
334
  ```
423
335
 
424
- The tool descriptions carry the judgment a schema cannot: that an unsourced
425
- claim is an `assumption` and not a `fact` with a vague source, that a conflict
426
- between two records belongs in a `risk` or a superseding `decision` rather than
427
- 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`.
428
343
 
429
- `STRAUSS_KB_ACTOR` names the writer in the log. Diagnostics go to stderr,
430
- 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).
431
347
 
432
348
  ## Library
433
349
 
@@ -448,11 +364,8 @@ for (const hit of hits) {
448
364
  }
449
365
  ```
450
366
 
451
- `matchToDiff` answers a different question from `query`: given a structural
452
- description of a diff, which records are anchored to each hunk. It takes hunks
453
- and optional symbol ranges rather than a patch, so this package carries no diff
454
- parser, and it degrades to file-level precision — labelled as such — when a
455
- 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.
456
369
 
457
370
  ## Retrieval
458
371
 
@@ -465,111 +378,23 @@ problem:
465
378
  | Standing | `strauss_status`, the supersession chain | is this still what we hold? |
466
379
  | Freshness | `stale_after`, `verified[]` | has anyone confirmed it lately? |
467
380
 
468
- Freshness is tiered by who did the confirming. OKF's spec (§5.3) defines the
469
- trust tiers from the verifying actor's prefix: an empty `verified[]` is
470
- unverified, an agent-prefixed verifier makes the record machine-confirmed, and
471
- a `human:`-prefixed verifier makes it human-reviewed. Of that ladder, today's
472
- adjudication reports only the first rung — the warning it attaches when
473
- `verified[]` is empty; reporting the full tier is upcoming tooling. When it
474
- lands, the tier will be derived from the events at read time, never stored, so
475
- it cannot drift from the trail that justifies it.
476
-
477
- **Load before you search.** These bases run to a few thousand tokens — twenty
478
- records measured at about 3,000 — so the first thing to try is taking all of it.
479
- On nine questions whose wording appears in no record, a reader holding the whole
480
- base answered eight; embedding search over the same records answered four. Two
481
- of those differences are structural rather than matters of degree: a reader can
482
- say no record answers the question, where vector search returns its nearest
483
- neighbour whatever the distance; and a reader picks the record that answers the
484
- question rather than the one nearest the topic. The mechanism is simple:
485
- retrieval makes similarity the gatekeeper, and a match the ranker misses never
486
- reaches the model. A full read lets the model do the matching itself —
487
- synonymy, implication across records, aggregation — which no ranker does.
488
-
489
- Read for a question, not for a session: a base loaded at the start of a long
490
- conversation is summarised away by the end of it, and reloading costs about
491
- three thousand tokens. Read it again at the point of use.
492
-
493
- **The three rungs, in one rule.** While the base fits the budget, `load` it
494
- whole. Once `load` refuses, `catalog` then `pack` the record that matters. For
495
- 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.
496
386
 
497
- ```bash
498
- strauss-kb load # under the budget: everything, with standing
499
- strauss-kb catalog # past it: one line per record, ~30 tokens each
500
- strauss-kb pack decision.cursor-v2 # then the neighbourhood around the one that matters
501
- strauss-kb query cursor pagination # or a point lookup by wording
502
- ```
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`.
503
395
 
504
- A whole read gives perfect recall and can say _no record answers this_, which
505
- no ranker can. `catalog` keeps that at a fraction of the cost by naming every
506
- record instead of every body; `query` gives up both, returning its nearest hit
507
- whatever the distance.
508
-
509
- `load` refuses rather than truncating past its token budget (`--budget` /
510
- `budgetTokens`, 25,000 by default, held against the estimated size of what is
511
- handed back) — a truncated base reads as a complete one, so a caller would
512
- answer "never decided" from a slice it did not know was a slice. `context`
513
- refuses the same way at its own, tighter budget (4,000 by default). Superseded
514
- records come back as name, replacement and date only; `trace` still reaches
515
- them by id.
516
-
517
- A refusal reports `approxTokens` against `budgetTokens` and carries a `message`
518
- naming the budget and the next calls. A successful load reports `budgetTokens`
519
- too, so a caller can see how close it came before crossing the line.
520
-
521
- `--all` (`all: true` over MCP) bypasses the budget and hands back the entire
522
- bundle regardless of size. A loaded result carries `tokensLoaded`, and
523
- `budgetTokens: null` marks that no ceiling applied; `--all` is mutually
524
- exclusive with `--budget`. It is for an operator who has decided the size is
525
- worth the tokens — a narrower `type` filter, `catalog`, or `query` fits better
526
- when it is not.
527
-
528
- **Catalog is the rung that keeps the base knowable.** One line per record —
529
- concept id, type, title, standing, stale flag — sorted by type then title, at
530
- roughly thirty tokens each, so a base far past `load`'s budget still fits in
531
- one call. Superseded records show the replacement in place of a body. The
532
- header sums record counts by standing and reports staleness separately (a
533
- current record can be stale). Bodies live in `load`, `pack`, and `trace`.
534
-
535
- `catalog` alone has no ceiling and never refuses — cost is linear at roughly
536
- thirty tokens a record (a thousand-record base is about 30k, five thousand
537
- about 150k); narrow with `type` at that scale. Output is deterministic given
538
- a fixed clock — no timestamp, ordering total down to the concept id — so two
539
- catalogs of an unchanged base diff to nothing except a stale flag flipping as
540
- `stale_after` passes. Pass an explicit `now` (library callers) to hold
541
- byte-equality across that boundary.
542
-
543
- **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
544
- name it), `pack` hands over that record's bounded neighbourhood: everything
545
- within `--hops` of the root, walked over the base's edges — body links (a
546
- `relatedConceptIds` entry is one), supersession in both directions, shared
547
- code anchors, and shared sources — ranked and cut to `--max-nodes`. Standing
548
- travels with it: superseded neighbours arrive as the same stubs `load` emits.
549
- Every dropped record is named under Excluded, and past its own token budget
550
- `pack` refuses exactly as `load` does. Output is byte-identical across runs
551
- over an unchanged base below the header. With neither a size problem nor a
552
- root record in hand, the question is a point lookup — `query`.
553
-
554
- **Flag, never filter.** `query` returns every hit with its standing, because a
555
- filtered result set is invisible — the caller cannot tell it missed anything.
556
- The single exception is narrow: a superseded record is dropped only when its
557
- replacement is also in the results, so the thread is never lost.
558
-
559
- **Trace inverts the point query.** In a query a `rejected` record is the most
560
- dangerous thing retrievable — a well-formed assertion of what someone decided
561
- _not_ to do. In a history it is the content. `trace` follows supersession,
562
- shared code anchors, and shared sources, and orders by `generated.at`; ranking a
563
- history is meaningless when the sequence is the point.
564
-
565
- Chain resolution happens on read. A stored head would need rewriting on every
566
- ancestor whenever a chain grows, which is derived state that goes stale. The
567
- walk follows both pointers, so a hand-edit that left one side behind cannot
568
- return a record the base openly claims is replaced. A cycle terminates with
569
- `chain-cycle`; a fork reports every head rather than presenting a guess as a
570
- fact; a missing replacement is `broken-chain` with no head — the case that needs
571
- the most care, because returning the stale record unmarked looks exactly like
572
- success.
396
+ Worked flows:
397
+ [use-cases](https://saasontools.github.io/strauss-agent-tools/use-cases).
573
398
 
574
399
  **Placement is cache economics.** `load`'s output belongs in the stable
575
400
  prefix — system prompt or first turn; `query` and `pack` results belong at
@@ -581,98 +406,19 @@ caveats: <https://saasontools.github.io/strauss-agent-tools/mcp-reference>.
581
406
 
582
407
  ## Health
583
408
 
584
- `doctor` sweeps a whole base and reports what has decayed. It is read-only —
585
- nothing is re-dated, re-verified, superseded, or deleted — because every
586
- finding is a judgment somebody has to make: whether a claim still holds, which
587
- question is worth answering, which island to link or drop.
588
-
589
- It exists because decay is invisible from inside a single record. A stale
590
- record reads exactly like a live one, a question nobody answered reads exactly
591
- like one nobody asked, and a record nothing links to is reachable only by
592
- someone who already knows it is there. `validate` is the narrower neighbour:
593
- it asks only whether pointers between records agree.
594
-
595
- | Check | Reports |
596
- | ---------------------- | ------------------------------------------------------------------------------ |
597
- | `expired` | `stale_after` is in the past — or is not a readable date, which is no better. |
598
- | `expiring` | `stale_after` falls inside the next `--expiring-days` (30). |
599
- | `unverified` | `verified[]` is empty and the record is over `--unverified-days` (90) old. |
600
- | `aging` | Still `open` or `proposed` after `--aging-days` (90). |
601
- | `orphaned` | No other record links to it, by body link or supersession. |
602
- | `broken-supersession` | A chain that does not resolve: no replacement, a missing one, a cycle, a fork. |
603
- | `superseded-but-cited` | A record that still holds, whose body links to one that does not. |
604
- | `drifted` | A hash-carrying anchor whose code moved, or whose file or symbol is gone. |
605
-
606
- Pass `doctor --repo-root PATH` when the base does not sit inside the tree it
607
- describes; without it the sweep looks under the working directory.
608
-
609
- `superseded-but-cited`'s name is for its common case: a rejected target counts
610
- too, and is the worse half — a superseded record at least names its
611
- replacement, while a rejected one is a well-formed assertion of what someone
612
- decided _not_ to do, cited by a record the reader trusts.
613
-
614
- ```bash
615
- strauss-kb doctor # the table
616
- strauss-kb doctor --json # the object behind it
617
- strauss-kb doctor --strict # exit 1 if anything has expired
618
- strauss-kb doctor --unverified-days 30 # a stricter confirmation window
619
- strauss-kb doctor --repo-root ../app # where the anchored source lives
620
- ```
621
-
622
- All eight groups are reported even when empty. A check that found nothing and
623
- a check that never ran look identical in a report that only lists findings,
624
- which is the whole value of a sweep.
625
-
626
- Judgments the checks make, worth knowing before reading a report:
627
-
628
- - **Superseded and rejected records sit out the freshness checks.** A replaced
629
- record whose date has passed needs no repair, and reporting it would bury the
630
- records that do. They stay in the graph checks, where standing is not the
631
- question.
632
- - **A date-only `stale_after` expires at UTC midnight.** `2026-09-01` parses as
633
- `2026-09-01T00:00:00Z`, so a record goes stale at the start of its date: a
634
- sweep run at exactly that instant still calls it expiring, and one a minute
635
- later calls it expired. That is `adjudicate`'s comparison rather than a
636
- second one — two readings of the same field disagreeing about the day would
637
- be worse than either.
638
- - **Age is read from `generated.at`, exclusively.** A record carrying no
639
- timestamp is not reported as aging or unverified — without a start there is
640
- no duration, and inventing one would flag every foreign record as overdue
641
- (adjudication still warns `unverified` on it at read time). Exactly N days
642
- old is not yet "older than N".
643
- - **`orphaned` counts incoming links only, and reads supersession one way.** A
644
- record that cites five others and is cited by none is precisely the island:
645
- reachable if you already know it exists. The replacement references what it
646
- replaced, never the reverse — taken symmetrically, a dead record would vouch
647
- for its own replacement and an old→new pair nothing else touches would rescue
648
- itself. Shared anchors and shared sources are co-location rather than
649
- reference, so they do not rescue a record either.
650
- - **A record citing the one it replaced is not superseded-but-cited.** That
651
- link is the history working as designed, and reporting it would put a finding
652
- on every correctly performed supersession.
653
- - **A replacement pointer is checked whatever the status says.** The store
654
- writes `strauss_status` and `strauss_superseded_by` in one mutation, so a
655
- record left `accepted` while naming a replacement was hand-edited — and
656
- adjudication reads it as current no matter what the pointer says, which is
657
- what makes it worth naming.
658
- - **`drifted` reports and never repairs.** It reads the same comparison
659
- `kb_load` and `kb_query` do, so the sweep and the read paths cannot disagree
660
- about what drift is. Unresolvable anchors ride in this group rather than a
661
- ninth check, anchors carrying no hash are never read, and `--strict` does not
662
- gate on drift.
663
-
664
- `--strict` gates on expiry alone. The other seven report debt a reader decides
665
- about; an expired record is the base itself saying it would stop standing
666
- behind something, which is the one finding a pipeline can act on without a
667
- 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).
668
416
 
669
417
  ## Living in an agent session
670
418
 
671
- Long sessions lose a knowledge base twice over: attention decays, and
672
- compaction summarises away both the records loaded early and the instruction
673
- that said to consult them. The fix is two-tier: a small index is re-injected
674
- at every context birth, and record bodies are fetched by tool when a question
675
- 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.
676
422
 
677
423
  ```bash
678
424
  strauss-kb pin docs/kb # mark a base every session should see
@@ -680,23 +426,13 @@ strauss-kb context # emit the pinned index block
680
426
  strauss-kb sync-instructions AGENTS.md # or keep it in an instruction file
681
427
  ```
682
428
 
683
- Pins live in `.strauss/kb-pins.json`, committed with the repo. Two more
684
- layers exist: `.strauss/kb-pins.local.json` (personal, gitignore it) and
685
- `~/.strauss/kb-pins.json` (every workspace). Nearest layer wins per base,
686
- `--local`/`--user` write the other layers, and `unpin` removes from all
687
- three. A malformed layer is skipped on read and refused on write.
688
-
689
- Per pin:
690
-
691
- - `--mode full` — inject the records themselves, not just the index. For
692
- small or critical bases (ADRs). Falls back to a labelled index when it
693
- cannot fit the block budget.
694
- - `--mode index` — never inject bodies.
695
- - `--profiles a,b` — only inject in the named profiles.
696
- - `--frozen` — the base is concluded; write commands refuse until `--unfreeze`.
697
-
698
- Budgets are named profiles — `session-start`, `compact`, `turn` — with
699
- 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:
700
436
 
701
437
  ```json
702
438
  {
@@ -705,16 +441,9 @@ per-repo overrides in the manifest, so hook commands never carry numbers:
705
441
  }
706
442
  ```
707
443
 
708
- Flags beat the manifest, the manifest beats the built-ins, and invalid values
709
- fall back to defaults instead of silencing the index. Past its budget,
710
- `context` refuses like `load` does never truncates — and its refusal says
711
- what to load directly and how to shrink the block.
712
-
713
- `sync-instructions <file>` keeps the same block between
714
- `<!-- strauss-kb:begin/end -->` sentinels in AGENTS.md or CLAUDE.md, touching
715
- nothing outside them. Re-run it when pins change; it is idempotent. This is
716
- the mechanism for runtimes without a reliable post-compaction hook, since
717
- 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.
718
447
 
719
448
  What each runtime gets (configs in the
720
449
  [plugin's adapters](../../plugins/strauss-kb/adapters/)):
@@ -725,12 +454,15 @@ What each runtime gets (configs in the
725
454
  | Session-start injection | SessionStart hook | SessionStart hook | PreInvocation, per turn |
726
455
  | Post-compact re-injection | ✓ `compact` source | ✓ client-side; instruction-only when hosted | moot — injected every turn |
727
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 | ✗ | ✗ |
728
459
  | Instruction file | CLAUDE.md | AGENTS.md | AGENTS.md + rules/ |
729
460
 
730
- One more thing agents add: file tools. A raw read of a record file bypasses
731
- standing entirely a superseded record reads exactly like a current one so
732
- bases are read through the tools, and a workspace can enforce that with deny
733
- 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:
734
466
 
735
467
  ```json
736
468
  {
@@ -742,45 +474,28 @@ rules or the plugin's opt-in PreToolUse script:
742
474
 
743
475
  ## Optional search tier
744
476
 
745
- `@tobilu/qmd` is an **optional peer dependency** providing BM25 (`searchLex`,
746
- no model download) over a `.index.sqlite` per base, rebuilt when a record is
747
- 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.
748
480
 
749
481
  ```bash
750
482
  npm install -g @tobilu/qmd # alongside a global strauss-kb
751
483
  ```
752
484
 
753
- With it absent — the default — `query` falls back to a substring scan over
754
- concept ids, titles, descriptions, and bodies. Nothing throws, no answer changes
755
- shape, and only recall degrades. Measured against that fallback on a
756
- twenty-record base, the lexical tier wins on word forms (`pages` finds a record
757
- saying only `page`) and on little else: eight of nine probe queries returned
758
- exactly what substring returned.
759
-
760
- The vector tier is deliberately off. It does close the semantic gap — "why not
761
- just use a mutex" finds a record about compare-and-swap that no lexical match
762
- can — but its scores do not separate right from wrong. A wrong hit scored 0.318
763
- against a correct one at 0.295, and any threshold that drops the first drops the
764
- second. Scores are evidence for a reader to weigh, not a filter to apply before
765
- one.
766
-
767
- qmd is used as a library, never through its own MCP server: that would let a
768
- caller reach a base without going through the store, and its default markdown
769
- 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.
770
491
 
771
492
  ## Constraints worth knowing
772
493
 
773
- **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
774
495
  store-owned files from listings and repairing the index on read hold only while
775
- everything goes through one door. Reading one record by a concept id you already
776
- hold is the exception no invariant, deterministic path.
777
-
778
- **Cross-base questions are unaskable.** Supersession, traces, and search stop at
779
- the directory boundary. "Was this settled somewhere else?" is answered by a
780
- person choosing which base to open. That is the price of a base that can be
781
- copied, deleted, or handed over whole, and it is what keeps the search index
782
- disposable. [ARCHITECTURE.md](./ARCHITECTURE.md) covers the registry that would
783
- 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)).
784
499
 
785
500
  ## License
786
501