CLI reference
strauss-kb [--bundle PATH] <command> [args]
Global
| Flag / variable | Effect |
|---|---|
--bundle PATH | The base to act on. Defaults to ./.strauss/kb. Accepted before or after the verb. |
--json | The machine shape, on commands that print a table. Refused, not ignored, on commands with only one form. |
-- | Ends flag parsing; everything after it is text, for the verbs that end in free prose. |
-h, --help | The usage listing. Also printed when no verb is given. |
-v, --version | The installed package version — what makes plugin/CLI skew diagnosable, since neither updates the other. |
STRAUSS_KB_ACTOR | Names the writer in the log and in generated.by / verified[].by: kind or kind:name, else every write refuses. Defaults to unknown. |
STRAUSS_KB_GRAMMARS_DIR | Where downloaded language packs — grammar and tags query alike — are cached. Defaults to ~/.strauss/grammars. Usually unset. For CI or air-gapped hosts, set it in the MCP server's env block (.mcp.json / plugin mcp.json) or the shell profile. |
STRAUSS_KB_GRAMMARS | off never downloads a grammar; the cache is still read. Same effect as --offline. |
STRAUSS_KB_GRAMMARS_URL | Replaces the scheme and host of every grammar URL grammars/manifest.json pins, for a mirror. Usually unset. |
STRAUSS_KB_FETCH_TIMEOUT_MS | Per-request timeout for remote reads and grammar downloads. Defaults to 30000. |
Results go to stdout as JSON; index, catalog, and pack emit markdown,
context emits the block itself, and doctor prints a table unless --json.
A flag taking a value accepts --budget 4000 or --budget=4000; given no
value it is an error rather than a fall back to the default.
--tag T is repeatable on list, query, and catalog — see
tags.
Errors go to stderr and exit 1. validate and doctor --strict exit 1 with
their findings still on stdout: a check that reports a problem succeeded as a
command and failed as a check. context prints nothing at all when nothing is
pinned, since even a bare newline is noise in a fresh context.
Every write verb refuses outright when the base is pinned --frozen in this
workspace: write, write-decision, no-decision, status, supersede,
answer, verify, anchor-set, and sweep (except under --dry-run).
anchor-resolve stamps nothing on a frozen base: it reports the refusal on
each anchor, and exits non-zero only where the caller asked for that write.
The write path
write
write <type> < record.json
Write one record of any type; the record is JSON on stdin,
<type> is the only positional, and types lists the sections each type
accepts. The stdin object is the
write input: slug, title and why
required; sections, anchors, sources, assumption, stale_after,
verify, tags, relatedConceptIds, links, supersedes, materiality,
confidence, and owner optional. Unknown keys are rejected.
Two anchors at one address are refused, as in anchor-set.
strauss-kb write fact <<'JSON'
{
"slug": "cache-key-includes-region",
"title": "The cache key includes the region",
"why": "A region-less key serves another region's data.",
"sections": { "Claim": "Every key is prefixed with the region." }
}
JSON
Returns { conceptId, action, supersededIds }.
write-decision
write-decision < decision.json
Write a decision, with the rejected alternative as a field: the same stdin
object as write minus sections, plus alternative and impact.
strauss-kb write-decision <<'JSON'
{
"slug": "cas-not-lock",
"title": "Compare-and-swap rather than a lock",
"why": "A stale lock hold blocks every later writer.",
"alternative": "A lock file, which adds a stale-hold failure mode.",
"impact": "Read-modify-write callers must retry on conflict."
}
JSON
Returns { conceptId, action, supersededIds }.
no-decision
no-decision <reason...>
Claim in one sentence that there was nothing to decide, writing the idempotent
decision.none record.
strauss-kb no-decision "Renamed a private helper; the diff answers it."
status
status <concept-id> <status>
Move a record's status, leaving everything else alone, with a compare-and-swap.
<status> is one of draft, proposed, accepted, open, resolved,
rejected, superseded.
strauss-kb status requirement.export-csv accepted
supersede
supersede <concept-id> <replacement-id>
Mark a record superseded by another, linking both directions.
strauss-kb supersede decision.cursor-v1 decision.cursor-v2
answer
answer <concept-id> <answer...>
Resolve an open question: sets the status, stamps who answered and when, and appends an Answer section. Remaining arguments are joined into the answer text.
strauss-kb answer open-question.retry-budget "Three retries with full jitter."
verify
verify <concept-id> --note <text>
Append one verified[] event. --note is required and must say what the check
found. Refuses the actor unknown, and a record's own generator unless the
actor is human:-prefixed.
STRAUSS_KB_ACTOR="human:assaf" strauss-kb verify decision.cas-not-lock \
--note "Re-read KbStore.setStatus; the digest check is still before publish."
Returns { conceptId, verified }, the new event count.
anchor-resolve
anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]
Resolve a record's anchors: stamp a hash onto anchors that lack one, and report drift where the code moved. An anchor naming another repository is read from that remote; everything else from the working tree. An unreadable file or unreachable remote is a finding, not an error.
| Flag | Effect |
|---|---|
--repo-root <path> | Where the anchored source lives. Defaults to the working directory. |
--offline | Read foreign anchors from the repo cache only, never fetching. |
--rebaseline | Accept the current code as the new baseline. |
--restamp | Refresh resolved_at on anchors that already match. |
--check | Report only: no hash, no resolved_at, no log entry. |
Exits 1 when an anchor drifted and no write settled it, when one carrying a
hash no longer resolves, or when a write this run asked for did not land, so a
CI gate can run it. A --rebaseline the base took exits 0; an anchor
nothing could reach does not fail it either way, and a frozen base plans no
resolved_at backfill, so a record whose anchors all match still exits 0
there. Never writes verified[]
(why); a judgment is verify.
--check refuses --rebaseline and --restamp.
strauss-kb anchor-resolve decision.cas-not-lock --repo-root /repo --rebaseline
Returns { conceptId, results }, each result
{ file, symbol?, side?, state, storedHash?, currentHash?, diffSize?, reason?, resolver?, outcome?, outcomeReason?, rebaselined?, repo?, remoteState? }.
state is the comparison, outcome what the write did: applied once the
record holds it, skipped where a rule forbids the write (outcomeReason: pinned-ref), failed where it was refused (frozen, write-failed).
rebaselined is set with applied, never before. An anchor with no hash is
unstamped until a stamp lands, so --check, a frozen base, and a refused
write all report it that way. side is set only for an
anchor read at its ref rather than in the working tree. resolver names
which resolver produced the span — see
symbol resolution. A result whose
reason is resolver-changed drifted because the resolver changed, not the
code; --rebaseline is the whole fix.
--rebaseline writes the new hashes and still exits 1 on the drift it just
accepted; a second run reports match. Being fixed under
SAA-822.
anchor-set
anchor-set <concept-id> [--resolve] [--repo-root <path>] [--offline] < anchors.json
Set a record's anchors after a refactor someone read: the new pointers, and a reason. The object is JSON on stdin.
| Key | Effect |
|---|---|
reason | Required, non-blank. What was reviewed. Goes in the log. |
anchors | The complete new set, at least one. |
The set is taken as given; only two anchors at one address are refused. An
anchor keeps its baseline by carrying its hash (and hash_kind, lines,
resolved_at, resolver) forward — do that when you have not read the new
code yet, so it still reports drift. Omit the hash and anchor-resolve stamps
the current code. Whether the code was read is your claim; the log records who
made it and why.
| Flag | Effect |
|---|---|
--resolve | Stamp every anchor against the current code in the same call. |
--repo-root <path> | Where the anchored source lives, for --resolve. |
--offline | With --resolve, read foreign anchors from the repo cache only. |
Choosing a pointer is reading the code behind it, so --resolve rebaselines
every anchor, carried hashes included, and returns anchor-resolve's results as
resolved. It also writes its own anchor-resolve log entry. baseline is
stamped only when every anchor matched or its write was applied, else
incomplete. Exits 1 when an anchor does not resolve — a typo'd symbol, a
missing file — or its stamp did not land, but not when a remote could not be
reached. Without it, nothing is stamped: run anchor-resolve
next.
strauss-kb anchor-set decision.export-retention <<'JSON'
{
"reason": "Reviewed the refactor: isExportExpired replaces shouldDeleteExport; retentionDays owns the shared setting.",
"anchors": [
{
"file": "src/cleanup.mjs",
"symbol": "isExportExpired",
"hash": "sha256:5c7242b8…",
"hash_kind": "ast",
"resolved_at": "2026-09-17T20:06:25.829Z",
"lines": 3,
"resolver": "tree-sitter"
},
{
"file": "src/download.mjs",
"symbol": "canDownloadExport",
"hash": "sha256:b4be493b…",
"hash_kind": "ast",
"resolved_at": "2026-09-17T20:06:25.830Z",
"lines": 3,
"resolver": "tree-sitter"
},
{ "file": "src/retention.mjs", "symbol": "retentionDays" }
]
}
JSON
Only symbol moved on the first anchor; the second is the one it read, back
unchanged; the third is new.
{
"conceptId": "decision.export-retention",
"reason": "Reviewed the refactor: isExportExpired replaces shouldDeleteExport; retentionDays owns the shared setting.",
"changes": [
{
"op": "move",
"from": { "file": "src/cleanup.mjs", "symbol": "shouldDeleteExport" },
"to": { "file": "src/cleanup.mjs", "symbol": "isExportExpired" }
},
{
"op": "add",
"to": { "file": "src/retention.mjs", "symbol": "retentionDays" }
}
],
"anchors": ["…the three anchors as stored…"],
"baseline": "unchanged",
"note": "pointers only: nothing was resolved, rebaselined or verified. Run anchor-resolve to check the new pointers, --rebaseline to accept the code, and verify separately."
}
Hashes elided; everything else is the run's own output. changes is derived
from the record before and after, not from what the caller declared, so the log
records what happened. An unchanged anchor is not listed.
changes are computed inside the write, against the record as it stands.
Exits 0 on a set that applied. A validation failure, a write
conflict, a missing record or a frozen base leaves the record and the log
untouched.
One anchor-set entry lands in the log with the actor, the reason and
every change. It is not verification — that is verify.
reassess
reassess <concept-id> [--repo-root <path>] [--with-diff]
One record, turned into something a reader can judge without opening the
repository: the record's claim, each anchor's
drift class, the record's
impact set, and the references it makes or receives that no longer
hold.
| Flag | Effect |
|---|---|
--repo-root <path> | Where the anchored source lives. Defaults to the working directory. |
--with-diff | Recover each anchor's committed span and render the diff. |
Anchors whose code only moved are rebaselined — file and symbol are
updated, the hash is not — and dropped from the packet; cosmetic ones are
counted and dropped. Never verifies, never supersedes, never moves standing:
what to do about a real change is the
skill's protocol.
References are the second half, and code drift is not required for them. A
record that still holds is asked what it points at that does not, reading
strauss_links and related_to with it. A record that has itself stopped
holding is asked the inverse: who still points at it, so the reader settling
its replacement finds the open risks resting on the old answer. A record with
neither drift nor an unresolved reference returns packet: null.
strauss-kb reassess fact.region-key --with-diff
Returns { conceptId, packet, rebaselined, cosmetic }. Each packet anchor
carries { file, symbol?, class, storedHash, diffSize, movedTo?, diff? }, and
diff is either { status: "ok", source, ref, unified, added, removed, truncated } or { status: "unrecoverable" }. packet.references carries
outgoing ({ from, target, targetStanding, rels, replacedBy }) and
incoming ({ from, title, standing, rels }).
## References that no longer hold (1)
- decision.retention [superseded] (related_to) — replaced by decision.retention-seven-days
The read path
load
load [type] [--budget N] [--all] [--repo-root PATH]
Hand over the whole base, each record with its standing; the optional positional narrows to one record type.
| Flag | Default | Effect |
|---|---|---|
--budget N | 25000 | Approximate token ceiling. |
--all | — | Load everything regardless of size, bypassing the budget; mutually exclusive with --budget. |
--repo-root PATH | cwd | Where the anchored source lives, for the drift check. |
Refuses with counts rather than truncating when the base trips the budget
ceiling, pointing at the next rung down in message. Every result carries a
digest for
cache-stable placement.
strauss-kb load decision --budget 8000
strauss-kb load --all
catalog
catalog [type] [--tag T]...
Every record in one line — concept id, type, title, standing, and a stale flag —
sorted by type then title, at roughly thirty tokens each. Emits markdown; the
optional positional narrows to one record type and --tag narrows further. Both
are named in the heading, so an empty result cannot read as an empty base.
strauss-kb catalog open-question
strauss-kb catalog --tag review --tag review:extract
# KB Catalog
bundle: /repo/.strauss/kb
3 records: 2 current · 1 superseded
- decision.retry-timeouts-only · decision · Retry timeouts only · current
- open-question.retry-scope · open-question · Which failures should the client retry? · superseded → decision.retry-timeouts-only
The tier-one listing, and what to reach for when load refuses. Superseded
records are listed with their replacement. Alone among the read paths it has
no ceiling and never refuses; its cost is linear, roughly 3k tokens per
hundred records. Output is deterministic given a fixed clock, so two catalogs of
an unchanged base diff to nothing.
pack
pack <conceptId> [--hops N] [--max-nodes N] [--budget N]
The bounded neighbourhood around one record: everything within --hops of the
root, ranked and cut to --max-nodes, with every cut record named under
Excluded. Emits markdown.
| Flag | Default | Effect |
|---|---|---|
--hops N | 2 | how far from the root the walk may reach |
--max-nodes N | 20 | how many records the pack may hold, root included |
--budget N | 25000 | approximate token ceiling over what is emitted |
strauss-kb pack decision.cursor-v2 --hops 2 --max-nodes 20
query
query <text...> [--tag T]... [--repo-root PATH]
Search and return each match with its standing, flagged and never filtered; the
remaining arguments are joined into the query text. The narrowest of the
three retrieval rungs: a query cannot tell you that nothing was decided, so when
the question is what exists, use catalog. --repo-root PATH
says where the anchored source lives for the
drift check; it and --tag are spliced out of the
argv before the remaining words become the query text.
strauss-kb query cache key region
The CLI always includes non-current records; the type filter is an MCP-side
parameter of kb_query.
trace
trace <concept-id> [edges...]
How a position was arrived at, as a timeline ordered by generated.at,
deliberately including rejected, draft, and superseded records. Trailing
arguments naming an edge — supersession, anchor, source — narrow the walk;
anything else is ignored, and with none given all three are followed.
strauss-kb trace decision.cursor-v2 supersession anchor
impact
impact <concept-id> [--depth N] [--rels a,b]
What breaks if this record changes: its transitive set of dependants, asked before superseding, contradicting, or narrowing a record.
| Flag | Default | Effect |
|---|---|---|
--depth N | unbounded | Hops out from the record. A walk this cuts reports truncated: true. |
--rels a,b | all causal | Comma-separated rels to follow. Defaults to every rel but related_to. |
This is not simply "inbound links": the walk follows each rel in whichever
direction its dependence runs — see the
direction table. Naming related_to,
or an unknown rel, in --rels is an error rather than an empty result. A
superseded or rejected record is reported and not walked through, named
under stopped.
strauss-kb impact fact.region-key --depth 2 --rels depends_on,satisfies
Returns { root, impacted, stopped, truncated, unexpanded }, each impacted
record { conceptId, title, standing, warnings, depth, via } with via naming
every edge that reached it, nearest first.
backlinks
backlinks <concept-id>
Who points at this record: every inbound typed link, one hop, every rel
including related_to, each with its rel and the standing of the record that
made it. No flags. It is the one to reach for when you need the exact edges
rather than impact's causal closure.
strauss-kb backlinks fact.region-key
Returns { target, backlinks }, each { from, rel, title, standing, warnings },
ordered by source id then rel.
match
match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]
Which records sit on each changed hunk. The diff arrives one of two ways: a
commit range this reads itself, or the same JSON kb_match
takes on stdin — files is [{ filePath, hunks: [{ startLine, endLine, side? }] }],
1-based and inclusive, numbered on the hunk's side ("old" or "new";
absent means post-change). --git emits an old-side hunk for every hunk that
removed lines, so a record anchored side: "old" surfaces on the code that
went away.
| Flag | Effect |
|---|---|
--git <base>..<head> | Read the range with git diff --unified=0. ... works too. |
--stdin | Take { files, symbolRanges? } as JSON instead. |
--repo-root <path> | Where the changed source lives. Defaults to the working directory. |
--offline | Resolve symbols from what is on disk, never fetching a grammar. |
--include-non-current | Return superseded, rejected and unsettled records too. |
Symbol anchors are resolved through the same
chain anchor-resolve uses, over the
files the diff names and no others; pass symbolRanges on stdin to skip that.
A symbol nothing resolved degrades to its whole file rather than dropping the
record; precision says which of the two happened.
strauss-kb match --git origin/main...HEAD --repo-root /repo
Returns [{ filePath, hunk, precision, records }], each record
{ conceptId, type, title, standing, status, supersededBy, materiality?, confidence?, tags?, anchor? } — current first, no bodies. A hunk with nothing
on it is absent, as is a binary file or a rename that changed no line.
classify
classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]
What kind of change each changed file carries, so a reviewer knows what to
skim. One class per file from a closed set — test, config, ci, docs,
lockfile, generated, boilerplate, rename, source — and the name of the
rule that decided it.
| Flag | Effect |
|---|---|
--git <base>..<head> | Read the range with git diff --unified=0 -M. ... works too. |
--stdin | Take { files } as JSON instead, in match's shape. |
--repo-root <path> | Where the changed source lives. Defaults to the working directory. |
--offline | Resolve symbols from what is on disk, never fetching a grammar. |
Rules fire in order: a KB override, then a generator's banner (@generated,
DO NOT EDIT, Code generated by, This file was automatically generated) in
the file's first 20 lines, then the path table, then a rename -M matched at
90% or better that changed no line, then a file whose changed lines are 80%
import, re-export or punctuation. Anything left is source.
The override is a fact tagged review:generated, review:boilerplate or
review:move and anchored on the file: the one
input a script cannot derive. Anchored to a symbol it covers only the hunks
that symbol spans, and those appear in hunks; the symbol is resolved from
--repo-root through the chain match
uses, over the files the diff names and no others.
strauss-kb classify --git origin/main...HEAD --repo-root /repo
generated src/protocol/generated/index.ts (kb-override fact.protocol-generated)
test src/checkout/checkout.spec.ts (test-path)
source src/checkout/pay.ts (default)
--json returns { files: [{ filePath, class, reason, renamedFrom?, hunks? }] },
each hunk { startLine, endLine, class, reason } and present only where some
hunk disagrees with its file.
list
list [type] [--tag T]...
Every record, optionally narrowed to one type or tag — for enumerating, where
query is for a question. Returns concept id, title, description, status, and
anchors per record.
strauss-kb list open-question
strauss-kb list --tag review
index
index
The index, rebuilt if it disagrees with the records — title, type, status, and description per record, in a few hundred tokens. Emits markdown.
strauss-kb index
log
log
What touched what, and when. Returns { entries, malformed, conflicted }.
Malformed lines are reported with their 1-based position and never repaired;
conflicted is true when the log still carries merge markers, which the read
skips past.
strauss-kb log
An entry is { at, by, operation, conceptId }, plus target where the
operation has another end, and reason and anchors on an
anchor-set. Both are optional, so entries written before
they existed read unchanged.
{
"at": "2026-09-17T17:51:45.636Z",
"by": "agent:author",
"operation": "anchor-set",
"conceptId": "decision.export-retention",
"reason": "Reviewed the refactor: isExportExpired replaces shouldDeleteExport; retentionDays owns the shared setting.",
"anchors": [
{
"op": "replace",
"from": { "file": "src/cleanup.mjs", "symbol": "shouldDeleteExport" },
"to": { "file": "src/cleanup.mjs", "symbol": "isExportExpired" }
},
{
"op": "add",
"to": { "file": "src/retention.mjs", "symbol": "retentionDays" }
}
]
}
stamp
stamp [--bundle PATH] [--since DIGEST|FILE]
The base's content stamp without its bodies: load's
digest, record and superseded counts,
the newest record date, a digest per record, and drifted: how many records
have an anchor whose code no longer matches its hash. Drift is counted but stays
out of the digest, so a stamp and a load of the same base always agree.
With no --bundle it stamps every pinned base — the list context injects.
| Flag | Effect |
|---|---|
--since DIGEST | Prints nothing and exits 0 when the digest still matches; otherwise reports the base. |
--since FILE | A prior stamp --json (or a hook's session state): reports the changed bases and their ids. |
strauss-kb stamp --json
strauss-kb stamp --bundle docs/kb --since 9f2c…
Promotion and export
promote
promote <concept-id...> --to <bundle> [--source <url>] [--force]
promote --list
Copy records into another base under the same ids: what a review base settled, lifted into the base that outlives the pull request. The originals stay where they are.
| Flag | Effect |
|---|---|
--to <bundle> | The base being promoted into. Required unless --list. |
--source <url> | Where the promotion came from, usually the pull request. Recorded as a source on every copy. |
--force | Overwrite records the target base already holds. |
--list | Name the source base's candidates instead of promoting. |
A copy that arrived draft, proposed or accepted is written accepted —
settling is what promotion means — while open and resolved carry unchanged.
The review tag and every review:<id> tag are dropped, and so is verified[],
which recorded a check nothing in the target ran. Typed links are kept when their
target was promoted in the same run and dropped otherwise — a typed edge cannot
point out of its base — and every dropped edge is named in the result.
Supersession is not carried. Anchors are, so they keep pointing at the source
repository's files: a target in another repository reports drift until the record
is re-anchored there.
The pre-flight refuses the whole run when the target base already holds any of
the records, when an id is not <type>.<slug>, when a record is superseded or
rejected where it was written, or when either base is pinned --frozen; an
I/O failure once writing has begun stops at that record and names the ids that
landed. Both bases are logged — the source takes promote-out naming the target
base, the target takes promote-in naming the source.
--list names what is usually worth promoting — decisions no longer tagged
review, constraints still proposed, every contract, requirements something
satisfies, and blocking risks still
open.
strauss-kb promote decision.cursor-v2 contract.page-token \
--to ../repo/.strauss/kb --source https://github.com/org/repo/pull/59
Returns { mode: "promote", to, promoted }, each entry
{ conceptId, droppedLinks }; --list returns { mode: "list", candidates },
each { conceptId, type, title, why }.
export
export --format madr --to <dir>
Write the base's decisions out as MADR files, one per decision, for a repository that keeps ADRs of its own.
| Flag | Effect |
|---|---|
--format madr | Output layout. madr is the only one so far. |
--to <dir> | Directory the files are written into. Created if missing. |
Each file is NNNN-<slug>.md: the title, ## Status, ## Context and Problem Statement from the record's why, ## Considered Options from Rejected,
## Decision Outcome from Decision, and ## Consequences from Impact. A
heading whose field is empty is left out, as it is in the record. A superseded
decision is exported with superseded by <id> as its status.
Numbering is keyed by slug: a decision keeps the number it was first exported
under and new ones append, because an ADR is cited by its number. That holds for
as long as the exported file stays in --to — delete one and the next run
renumbers that decision.
Every file written carries <!-- strauss-kb export: <conceptId> --> as its last
line. A NNNN-<slug>.md without that marker was written by something else, so
the decision holding that slug is skipped and reported as foreign rather than
overwritten.
strauss-kb export --format madr --to docs/adr
Returns { to, format, exported, foreign }, each exported entry
{ conceptId, file, status } and each foreign entry { conceptId, file }.
Format and housekeeping
validate
validate
Cross-record checks: supersession links that disagree between the two records,
typed causal links whose rel is outside the closed vocabulary or whose target is
not in the bundle, assumptions that cite sources, and anchors carrying two
addresses (symbol and span), a malformed span, or a side: "old" with no
ref. Per-record shape is enforced on every read, so a problem here means
someone edited a file by hand.
An unknown rel is an error and a link to a record that does not exist yet is
a warning. A body citation with no strauss_links entry beside it is also a
warning — prose is not an edge, and this is the one place it is read. Exits 1 on an error; warnings alone exit 0.
strauss-kb validate || echo "errors above" # warnings alone still exit 0
doctor
doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]
[--drifted [--with-diff]]
A health sweep over a whole base. Read-only — it never writes, supersedes, or re-dates; every finding names a record for a person to repair.
| Check | Reports |
|---|---|
expired | stale_after is in the past — or is not a readable date, which is no better. |
expiring | stale_after falls inside the next --expiring-days. |
unverified | verified[] is empty and the record is over --unverified-days old. |
aging | Still open or proposed after --aging-days. |
orphaned | Nothing points at it, by reference or supersession. |
broken-supersession | A chain that does not resolve: no replacement, a missing one, a cycle, a fork. |
superseded-but-cited | A record that still holds, pointing at one that does not. |
drifted | A hash-carrying anchor whose code moved, or whose file or symbol is gone. |
unchecked | An anchor in another repository nothing could reach, grouped per repository. |
| Flag | Default | Effect |
|---|---|---|
--expiring-days N | 30 | How far ahead expiring looks. |
--unverified-days N | 90 | How old an unconfirmed record must be to be reported. |
--aging-days N | 90 | How long a record may stay open or proposed. |
--repo-root PATH | cwd | Where the anchored source lives, for drifted. |
--offline | — | Read foreign anchors from the repo cache only. |
--strict | — | Exit 1 if anything has expired. |
--drifted | — | Report only drift, as a reassess packet per record. |
--with-diff | — | With --drifted: each anchor's old-vs-new span diff. |
strauss-kb doctor --json # the object behind the table
strauss-kb doctor --strict # exit 1 if anything has expired
strauss-kb doctor --unverified-days 30 # a stricter confirmation window
strauss-kb doctor --drifted --with-diff # only the records whose code moved
--drifted stays read-only like the rest of the sweep: it names the records
carrying a moved anchor under rebaselinable and leaves the write to
reassess.
The header carries lines the checks do not: how many hashed anchors each
resolver stamped, span among them, and old-side anchors on a line of their
own. A base still leaning on regex has weaker evidence than one resolved by
tree-sitter, but a regex-stamped anchor is not a finding — see
symbol resolution.
All groups are reported even when empty, because a check that found nothing and a check that never ran look identical in a report that only lists findings. Judgments worth knowing before reading one:
- Superseded and rejected records sit out the freshness checks, staying in the graph checks where standing is not the question.
- A date-only
stale_afterexpires at UTC midnight. - Age is read from
generated.at, exclusively, so a record with no timestamp is never reported as aging or unverified, and exactly N days old is not yet "older than N". orphanedcounts incoming references only, and reads supersession one way. Shared anchors and sources are co-location rather than reference.- A reference is a
strauss_linksentry. A citation in prose that nothing mirrored is invisible here and reported byvalidate. - One finding per source/target pair, however many ways the pair is stated.
superseded-but-citednames the rels when the pointer is typed, and every finding carries the edge asreference. - A record citing the one it replaced is not
superseded-but-cited.
--strict gates on expiry alone, the one finding a pipeline can act on
without a judgment call. validate is the narrower neighbour, checking only
whether pointers between records agree.
sweep
sweep --tag <tag> --terminal [--dry-run]
Deletes records carrying --tag that are also resolved, rejected or
superseded. See the one deletion.
Run it weekly or per release as one PR, --dry-run first; never as a
post-merge commit.
| Flag | Effect |
|---|---|
--tag TAG | Required. Without it the command refuses; it never sweeps a whole base. |
--terminal | Required. Names the only scope it deletes: the three terminal statuses. |
--dry-run | Report what would go, and delete nothing. |
A record another surviving record points at — by typed link or by
supersession — is kept and reported under skipped, with the ids holding it;
a citation only in prose holds nothing. An
id the run could not remove is reported under failed. Each deletion is one
sweep log entry; afterwards the index is rebuilt and the search index dropped.
strauss-kb sweep --tag review --terminal --dry-run
strauss-kb sweep --tag review --terminal
schema
schema
JSON Schema for the frontmatter, the write input, and log entries, generated from the code that enforces them.
strauss-kb schema > kb.schema.json
types
types
Every record type with its purpose, body sections, and starting status. Read this before writing rather than guessing headings.
strauss-kb types
Workspace pins
These read and write the workspace pin manifests. pins and context take no
--bundle: which bases a session should see is workspace state.
pin
pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]
Pin a base into a pin manifest, so context surfaces it at every context birth.
The positional path wins over --bundle, and with neither the default base is
pinned. Idempotent — re-pinning changes nothing unless a flag below is given.
| Flag | Effect |
|---|---|
--mode full | preload the whole base into the block regardless of the full-under threshold |
--mode index | never upgrade to bodies |
--profiles a,b | comma-separated context profiles this pin surfaces in. Absent: all of them. |
--local | write .strauss/kb-pins.local.json (personal, gitignored) |
--user | write ~/.strauss/kb-pins.json (every workspace) |
| (neither) | write .strauss/kb-pins.json, the committed project manifest — the default |
--frozen | mark the base concluded: write commands refuse and context labels it read-only |
--unfreeze | lift a freeze |
A path with no records yet succeeds with a warning, and the pinned base itself is never touched — not even its log.
strauss-kb pin docs/adr --mode full
unpin
unpin [bundle-path]
Remove a base from every manifest layer that holds it — project, local, and user — reporting which were touched, because unpinned means gone rather than still injected from another file.
strauss-kb unpin docs/adr
pins
pins
Every pinned base across the layers, each with its layer and whether it currently resolves to readable records.
strauss-kb pins
context
context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]
The pinned-base index block, for injection at every context birth — startup, clear, resume, and after compaction. An index, not the content.
| Flag | Effect |
|---|---|
--profile NAME | named budget set. Built-ins: session-start (full-under 1500), compact and turn (budget 2500). An unknown name falls through to defaults. |
--budget N | ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000. |
--full-under N | per-base threshold: a base whose complete load fits under this arrives as full records instead of index lines. Off by default. |
--exclude-tag T | repeatable: records carrying the tag stay out of the block. The base stays pinned and stays readable through the tools. |
--format json | wrap the block in a JSON envelope, for hook protocols that require strict JSON on stdout. |
--event NAME | the hookEventName stamped into that envelope. Only meaningful with --format json. |
Budgets and exclusions resolve most-specific-first: explicit flags, then the
manifests' context tables (per profile, over their default), then the
built-in profile, then package defaults. No profile excludes a tag by default.
strauss-kb context --format json --event SessionStart
strauss-kb context --profile session-start --exclude-tag review
sync-instructions
sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]
Idempotently plant the context block between <!-- strauss-kb:begin --> and
<!-- strauss-kb:end --> sentinels in an instruction file, leaving everything
outside them alone. CLI-only — the one verb with no MCP tool; the capability
it serves is kb_context.
strauss-kb sync-instructions CLAUDE.md --profile session-start