@saasontools/strauss-kb 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -156,6 +156,11 @@ before publishing, which narrows the lost-update window rather than closing it.
156
156
  Records are never deleted. Superseding keeps the earlier reasoning inspectable,
157
157
  which is what a later `trace` reads.
158
158
 
159
+ `no-decision` records the explicit claim that a piece of work had nothing to
160
+ decide (an idempotent `decision.none` record). It exists for workflow gates:
161
+ "did you write a decision?" rewards writing a junk one, "did you answer?"
162
+ does not — so silence has to be expressible.
163
+
159
164
  ## CLI
160
165
 
161
166
  ```
@@ -176,6 +181,11 @@ strauss-kb [--bundle PATH] <command> [args]
176
181
  validate Cross-record checks. Exits 1 when it reports a problem.
177
182
  schema JSON Schema for the format.
178
183
  types The twelve types, their sections and initial status.
184
+ pin [bundle-path] [flags] Pin a base. --mode, --profiles, --frozen; --local/--user pick the layer.
185
+ unpin [bundle-path] Remove a base from every manifest layer that holds it.
186
+ pins Every pinned base, with whether it resolves to records.
187
+ context [--profile NAME] [--budget N] The pinned-base index block, for injection at context birth.
188
+ sync-instructions <file> Plant the context block between sentinels in an instruction file.
179
189
 
180
190
  --bundle PATH defaults to ./.strauss/kb
181
191
  STRAUSS_KB_ACTOR names the writer in the log
@@ -206,9 +216,12 @@ strauss-kb validate || echo "problems above"
206
216
  `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
207
217
  Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
208
218
  `kb_status`, `kb_supersede`, `kb_answer`, `kb_load`, `kb_query`, `kb_trace`,
209
- `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`. Every
210
- tool but `kb_schema` and `kb_types` takes a `bundlePath`; those two describe the
211
- format rather than any one base.
219
+ `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`,
220
+ `kb_pin`, `kb_unpin`, `kb_pins`, `kb_context`. Most tools take a `bundlePath`;
221
+ `kb_schema` and `kb_types` describe the format rather than any one base, and
222
+ `kb_pins` and `kb_context` read the workspace pin manifests instead. The one
223
+ CLI verb with no tool is `sync-instructions` — file plumbing for hooks, not an
224
+ agent capability; the capability is `kb_context`.
212
225
 
213
226
  ```json
214
227
  {
@@ -269,7 +282,10 @@ base answered eight; embedding search over the same records answered four. Two
269
282
  of those differences are structural rather than matters of degree: a reader can
270
283
  say no record answers the question, where vector search returns its nearest
271
284
  neighbour whatever the distance; and a reader picks the record that answers the
272
- question rather than the one nearest the topic.
285
+ question rather than the one nearest the topic. The mechanism is simple:
286
+ retrieval makes similarity the gatekeeper, and a match the ranker misses never
287
+ reaches the model. A full read lets the model do the matching itself —
288
+ synonymy, implication across records, aggregation — which no ranker does.
273
289
 
274
290
  Read for a question, not for a session: a base loaded at the start of a long
275
291
  conversation is summarised away by the end of it, and reloading costs about
@@ -278,7 +294,8 @@ three thousand tokens. Read it again at the point of use.
278
294
  `load` refuses rather than truncating when a base exceeds its budget (25,000
279
295
  tokens by default). A truncated base is indistinguishable from a complete one,
280
296
  so a caller would answer "that was never decided" from a slice it did not know
281
- was a slice. Superseded records come back as name, replacement and date only
297
+ was a slice. `context` refuses the same way at its own, tighter budget (4,000
298
+ by default). Superseded records come back as name, replacement and date only —
282
299
  their bodies no longer hold, and a body read later in a long session outlives
283
300
  the qualifier that said so. `trace` still reaches them by id.
284
301
 
@@ -302,6 +319,80 @@ fact; a missing replacement is `broken-chain` with no head — the case that nee
302
319
  the most care, because returning the stale record unmarked looks exactly like
303
320
  success.
304
321
 
322
+ ## Living in an agent session
323
+
324
+ Long sessions lose a knowledge base twice over: attention decays, and
325
+ compaction summarises away both the records loaded early and the instruction
326
+ that said to consult them. The fix is two-tier: a small index is re-injected
327
+ at every context birth, and record bodies are fetched by tool when a question
328
+ actually needs them.
329
+
330
+ ```bash
331
+ strauss-kb pin docs/kb # mark a base every session should see
332
+ strauss-kb context # emit the pinned index block
333
+ strauss-kb sync-instructions AGENTS.md # or keep it in an instruction file
334
+ ```
335
+
336
+ Pins live in `.strauss/kb-pins.json`, committed with the repo. Two more
337
+ layers exist: `.strauss/kb-pins.local.json` (personal, gitignore it) and
338
+ `~/.strauss/kb-pins.json` (every workspace). Nearest layer wins per base,
339
+ `--local`/`--user` write the other layers, and `unpin` removes from all
340
+ three. A malformed layer is skipped on read and refused on write.
341
+
342
+ Per pin:
343
+
344
+ - `--mode full` — inject the records themselves, not just the index. For
345
+ small or critical bases (ADRs). Falls back to a labelled index when it
346
+ cannot fit the block budget.
347
+ - `--mode index` — never inject bodies.
348
+ - `--profiles a,b` — only inject in the named profiles.
349
+ - `--frozen` — the base is concluded; write commands refuse until `--unfreeze`.
350
+
351
+ Budgets are named profiles — `session-start`, `compact`, `turn` — with
352
+ per-repo overrides in the manifest, so hook commands never carry numbers:
353
+
354
+ ```json
355
+ {
356
+ "pins": [{ "path": "docs/adr", "mode": "full" }],
357
+ "context": { "compact": { "budgetTokens": 1500 } }
358
+ }
359
+ ```
360
+
361
+ Flags beat the manifest, the manifest beats the built-ins, and invalid values
362
+ fall back to defaults instead of silencing the index. Past its budget,
363
+ `context` refuses like `load` does — never truncates — and its refusal says
364
+ what to load directly and how to shrink the block.
365
+
366
+ `sync-instructions <file>` keeps the same block between
367
+ `<!-- strauss-kb:begin/end -->` sentinels in AGENTS.md or CLAUDE.md, touching
368
+ nothing outside them. Re-run it when pins change; it is idempotent. This is
369
+ the mechanism for runtimes without a reliable post-compaction hook, since
370
+ instruction files are re-read where conversation history is not.
371
+
372
+ What each runtime gets (configs in the
373
+ [plugin's adapters](../../plugins/strauss-kb/adapters/)):
374
+
375
+ | Layer | Claude Code | Codex CLI | Antigravity CLI |
376
+ | ------------------------- | ------------------ | ------------------------------------------- | -------------------------- |
377
+ | MCP tool descriptions | ✓ | ✓ | ✓ |
378
+ | Session-start injection | SessionStart hook | SessionStart hook | PreInvocation, per turn |
379
+ | Post-compact re-injection | ✓ `compact` source | ✓ client-side; instruction-only when hosted | moot — injected every turn |
380
+ | File-read blocking | opt-in PreToolUse | ✗ (shell is the side door) | opt-in PreToolUse, JSON |
381
+ | Instruction file | CLAUDE.md | AGENTS.md | AGENTS.md + rules/ |
382
+
383
+ One more thing agents add: file tools. A raw read of a record file bypasses
384
+ standing entirely — a superseded record reads exactly like a current one — so
385
+ bases are read through the tools, and a workspace can enforce that with deny
386
+ rules or the plugin's opt-in PreToolUse script:
387
+
388
+ ```json
389
+ {
390
+ "permissions": {
391
+ "deny": ["Read(.strauss/kb/**)", "Read(**/.strauss/kb/**)"]
392
+ }
393
+ }
394
+ ```
395
+
305
396
  ## Optional search tier
306
397
 
307
398
  `@tobilu/qmd` is an **optional peer dependency** providing BM25 (`searchLex`,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  KB_COMMANDS,
3
3
  KbStore
4
- } from "./chunk-ZSYSHJVZ.js";
4
+ } from "./chunk-HYNAEAPM.js";
5
5
 
6
6
  // src/mcp.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -18,6 +18,7 @@ function createKbMcpServer() {
18
18
  now: () => (/* @__PURE__ */ new Date()).toISOString()
19
19
  };
20
20
  for (const command of KB_COMMANDS) {
21
+ if (!command.tool) continue;
21
22
  server.registerTool(
22
23
  command.tool,
23
24
  { description: command.description, inputSchema: command.input.shape },
@@ -44,4 +45,4 @@ export {
44
45
  createKbMcpServer,
45
46
  runKbMcpServer
46
47
  };
47
- //# sourceMappingURL=chunk-WFHYWZX5.js.map
48
+ //# sourceMappingURL=chunk-FSI4Q2FD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { KB_COMMANDS } from \"./commands/index.js\";\nimport { KbStore } from \"./kb-store.js\";\n\n/**\n * A knowledge base's own MCP server, over stdio.\n *\n * Standalone because a base is self-contained: a directory of markdown that\n * needs no database, no HTTP surface, and no running service to read. Folding\n * these tools into a larger server would make every consumer start that server\n * to open files it could open itself.\n *\n * Every tool is a projection of `KB_COMMANDS`, which the CLI also projects, so\n * the two cannot drift.\n */\nexport function createKbMcpServer(): McpServer {\n const server = new McpServer({ name: \"strauss-kb\", version: \"0.1.0\" });\n const store = new KbStore({\n warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}\\n`),\n });\n const ctx = {\n store,\n actor: process.env.STRAUSS_KB_ACTOR ?? \"mcp\",\n now: () => new Date().toISOString(),\n };\n\n for (const command of KB_COMMANDS) {\n // CLI-only plumbing (sync-instructions) edits files for hooks and\n // instruction blocks; the agent capability it serves is kb_context.\n if (!command.tool) continue;\n server.registerTool(\n command.tool,\n { description: command.description, inputSchema: command.input.shape },\n async (args: unknown) => {\n const result = await command.run(ctx, command.input.parse(args));\n return {\n content: [\n {\n type: \"text\" as const,\n text:\n typeof result === \"string\"\n ? result\n : JSON.stringify(result, null, 2),\n },\n ],\n };\n },\n );\n }\n\n return server;\n}\n\nexport async function runKbMcpServer(): Promise<void> {\n await createKbMcpServer().connect(new StdioServerTransport());\n}\n"],"mappings":";;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AAe9B,SAAS,oBAA+B;AAC7C,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,cAAc,SAAS,QAAQ,CAAC;AACrE,QAAM,QAAQ,IAAI,QAAQ;AAAA,IACxB,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AAAA,EACpE,CAAC;AACD,QAAM,MAAM;AAAA,IACV;AAAA,IACA,OAAO,QAAQ,IAAI,oBAAoB;AAAA,IACvC,KAAK,OAAM,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AAEA,aAAW,WAAW,aAAa;AAGjC,QAAI,CAAC,QAAQ,KAAM;AACnB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,EAAE,aAAa,QAAQ,aAAa,aAAa,QAAQ,MAAM,MAAM;AAAA,MACrE,OAAO,SAAkB;AACvB,cAAM,SAAS,MAAM,QAAQ,IAAI,KAAK,QAAQ,MAAM,MAAM,IAAI,CAAC;AAC/D,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MACE,OAAO,WAAW,WACd,SACA,KAAK,UAAU,QAAQ,MAAM,CAAC;AAAA,YACtC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAsB,iBAAgC;AACpD,QAAM,kBAAkB,EAAE,QAAQ,IAAI,qBAAqB,CAAC;AAC9D;","names":[]}