@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/ARCHITECTURE.md +39 -50
- package/README.md +139 -424
- package/dist/{chunk-XALWG3EZ.js → chunk-43KALLFU.js} +21 -36
- package/dist/chunk-43KALLFU.js.map +1 -0
- package/dist/{chunk-ZICKDZGY.js → chunk-MBXNCZ4V.js} +2 -2
- package/dist/{chunk-WZODZNR6.js → chunk-PYA5E7FL.js} +2 -2
- package/dist/cli-main.cjs +20 -35
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +20 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/mcp-main.cjs +20 -35
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-XALWG3EZ.js.map +0 -1
- /package/dist/{chunk-ZICKDZGY.js.map → chunk-MBXNCZ4V.js.map} +0 -0
- /package/dist/{chunk-WZODZNR6.js.map → chunk-PYA5E7FL.js.map} +0 -0
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
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
38
|
-
| Project-local | `pnpm add -D @saasontools/strauss-kb` then `pnpm exec strauss-kb` | Pinned per repository
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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`
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
`fact.auth-retries` — `<type>.<slug>`, both halves kebab-case.
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
[
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
430
|
-
|
|
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`
|
|
452
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
a
|
|
472
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
`--
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
731
|
-
standing
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
747
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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.
|
|
776
|
-
|
|
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
|
|