Skip to main content

CLI reference

strauss-kb [--bundle PATH] <command> [args]

Global​

Flag / variableEffect
--bundle PATHThe base to act on. Defaults to ./.strauss/kb. Accepted before or after the verb.
--jsonThe 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, --helpThe usage listing. Also printed when no verb is given.
-v, --versionThe installed package version — what makes plugin/CLI skew diagnosable, since neither updates the other.
STRAUSS_KB_ACTORNames 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_DIRWhere 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_GRAMMARSoff never downloads a grammar; the cache is still read. Same effect as --offline.
STRAUSS_KB_GRAMMARS_URLReplaces the scheme and host of every grammar URL grammars/manifest.json pins, for a mirror. Usually unset.
STRAUSS_KB_FETCH_TIMEOUT_MSPer-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.

FlagEffect
--repo-root <path>Where the anchored source lives. Defaults to the working directory.
--offlineRead foreign anchors from the repo cache only, never fetching.
--rebaselineAccept the current code as the new baseline.
--restampRefresh resolved_at on anchors that already match.
--checkReport 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.

KeyEffect
reasonRequired, non-blank. What was reviewed. Goes in the log.
anchorsThe 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.

FlagEffect
--resolveStamp every anchor against the current code in the same call.
--repo-root <path>Where the anchored source lives, for --resolve.
--offlineWith --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.

FlagEffect
--repo-root <path>Where the anchored source lives. Defaults to the working directory.
--with-diffRecover 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.

FlagDefaultEffect
--budget N25000Approximate token ceiling.
--all—Load everything regardless of size, bypassing the budget; mutually exclusive with --budget.
--repo-root PATHcwdWhere 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.

FlagDefaultEffect
--hops N2how far from the root the walk may reach
--max-nodes N20how many records the pack may hold, root included
--budget N25000approximate 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.

FlagDefaultEffect
--depth NunboundedHops out from the record. A walk this cuts reports truncated: true.
--rels a,ball causalComma-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 <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.

FlagEffect
--git <base>..<head>Read the range with git diff --unified=0. ... works too.
--stdinTake { files, symbolRanges? } as JSON instead.
--repo-root <path>Where the changed source lives. Defaults to the working directory.
--offlineResolve symbols from what is on disk, never fetching a grammar.
--include-non-currentReturn 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.

FlagEffect
--git <base>..<head>Read the range with git diff --unified=0 -M. ... works too.
--stdinTake { files } as JSON instead, in match's shape.
--repo-root <path>Where the changed source lives. Defaults to the working directory.
--offlineResolve 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.

FlagEffect
--since DIGESTPrints nothing and exits 0 when the digest still matches; otherwise reports the base.
--since FILEA 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.

FlagEffect
--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.
--forceOverwrite records the target base already holds.
--listName 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.

FlagEffect
--format madrOutput 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.

CheckReports
expiredstale_after is in the past — or is not a readable date, which is no better.
expiringstale_after falls inside the next --expiring-days.
unverifiedverified[] is empty and the record is over --unverified-days old.
agingStill open or proposed after --aging-days.
orphanedNothing points at it, by reference or supersession.
broken-supersessionA chain that does not resolve: no replacement, a missing one, a cycle, a fork.
superseded-but-citedA record that still holds, pointing at one that does not.
driftedA hash-carrying anchor whose code moved, or whose file or symbol is gone.
uncheckedAn anchor in another repository nothing could reach, grouped per repository.
FlagDefaultEffect
--expiring-days N30How far ahead expiring looks.
--unverified-days N90How old an unconfirmed record must be to be reported.
--aging-days N90How long a record may stay open or proposed.
--repo-root PATHcwdWhere 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_after expires 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".
  • orphaned counts incoming references only, and reads supersession one way. Shared anchors and sources are co-location rather than reference.
  • A reference is a strauss_links entry. A citation in prose that nothing mirrored is invisible here and reported by validate.
  • One finding per source/target pair, however many ways the pair is stated. superseded-but-cited names the rels when the pointer is typed, and every finding carries the edge as reference.
  • 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.

FlagEffect
--tag TAGRequired. Without it the command refuses; it never sweeps a whole base.
--terminalRequired. Names the only scope it deletes: the three terminal statuses.
--dry-runReport 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.

FlagEffect
--mode fullpreload the whole base into the block regardless of the full-under threshold
--mode indexnever upgrade to bodies
--profiles a,bcomma-separated context profiles this pin surfaces in. Absent: all of them.
--localwrite .strauss/kb-pins.local.json (personal, gitignored)
--userwrite ~/.strauss/kb-pins.json (every workspace)
(neither)write .strauss/kb-pins.json, the committed project manifest — the default
--frozenmark the base concluded: write commands refuse and context labels it read-only
--unfreezelift 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.

FlagEffect
--profile NAMEnamed budget set. Built-ins: session-start (full-under 1500), compact and turn (budget 2500). An unknown name falls through to defaults.
--budget Nceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000.
--full-under Nper-base threshold: a base whose complete load fits under this arrives as full records instead of index lines. Off by default.
--exclude-tag Trepeatable: records carrying the tag stay out of the block. The base stays pinned and stays readable through the tools.
--format jsonwrap the block in a JSON envelope, for hook protocols that require strict JSON on stdout.
--event NAMEthe 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