@saasontools/strauss-kb 0.1.7 → 0.1.8

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/dist/index.cjs CHANGED
@@ -35,9 +35,12 @@ __export(index_exports, {
35
35
  CONTEXT_END: () => CONTEXT_END,
36
36
  CONTEXT_PROFILES: () => CONTEXT_PROFILES,
37
37
  DECISION_TYPE: () => DECISION_TYPE,
38
+ DEFAULT_AGING_DAYS: () => DEFAULT_AGING_DAYS,
39
+ DEFAULT_EXPIRING_DAYS: () => DEFAULT_EXPIRING_DAYS,
38
40
  DEFAULT_LOAD_BUDGET: () => DEFAULT_LOAD_BUDGET,
39
41
  DEFAULT_PACK_HOPS: () => DEFAULT_PACK_HOPS,
40
42
  DEFAULT_PACK_MAX_NODES: () => DEFAULT_PACK_MAX_NODES,
43
+ DEFAULT_UNVERIFIED_DAYS: () => DEFAULT_UNVERIFIED_DAYS,
41
44
  ErrorTypes: () => ErrorTypes,
42
45
  Fault: () => Fault,
43
46
  INDEX_FILE: () => INDEX_FILE,
@@ -46,6 +49,7 @@ __export(index_exports, {
46
49
  KB_CONCEPT_ID_PATTERN: () => KB_CONCEPT_ID_PATTERN,
47
50
  KB_CONFIDENCES: () => KB_CONFIDENCES,
48
51
  KB_DIR: () => KB_DIR,
52
+ KB_DOCTOR_CHECKS: () => KB_DOCTOR_CHECKS,
49
53
  KB_EDGE_KINDS: () => KB_EDGE_KINDS,
50
54
  KB_MATERIALITIES: () => KB_MATERIALITIES,
51
55
  KB_RECORD_STATUSES: () => KB_RECORD_STATUSES,
@@ -78,6 +82,7 @@ __export(index_exports, {
78
82
  contextProfileBudgets: () => contextProfileBudgets,
79
83
  createKbMcpServer: () => createKbMcpServer,
80
84
  decisionInputSchema: () => decisionInputSchema,
85
+ doctor: () => doctor,
81
86
  edgeNeighbours: () => edgeNeighbours,
82
87
  indexIsStale: () => indexIsStale,
83
88
  isKbRecordType: () => isKbRecordType,
@@ -2128,6 +2133,258 @@ function validateBundle(records) {
2128
2133
  return problems;
2129
2134
  }
2130
2135
 
2136
+ // src/doctor.ts
2137
+ var DEFAULT_EXPIRING_DAYS = 30;
2138
+ var DEFAULT_UNVERIFIED_DAYS = 90;
2139
+ var DEFAULT_AGING_DAYS = 90;
2140
+ var KB_DOCTOR_CHECKS = [
2141
+ "expired",
2142
+ "expiring",
2143
+ "unverified",
2144
+ "aging",
2145
+ "orphaned",
2146
+ "broken-supersession",
2147
+ "superseded-but-cited"
2148
+ ];
2149
+ var CHECK_HEADLINES = {
2150
+ expired: "past its stale_after date",
2151
+ expiring: "stale_after falls within the window",
2152
+ unverified: "nobody has ever confirmed it, and it is old enough to matter",
2153
+ aging: "still open or still proposed long after it was written",
2154
+ orphaned: "no other record links to it",
2155
+ "broken-supersession": "the supersession pointers do not resolve",
2156
+ "superseded-but-cited": "a live record's body links to one that no longer holds"
2157
+ };
2158
+ var DAY_MS = 864e5;
2159
+ function doctor(bundle, options = {}) {
2160
+ const thresholds = {
2161
+ expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
2162
+ unverifiedDays: options.unverifiedDays ?? DEFAULT_UNVERIFIED_DAYS,
2163
+ agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
2164
+ };
2165
+ const now = options.now ?? /* @__PURE__ */ new Date();
2166
+ const adjudicated = adjudicate(bundle, bundle, now);
2167
+ const standings = new Map(
2168
+ adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
2169
+ );
2170
+ const inForce = adjudicated.filter(
2171
+ (hit) => hit.standing !== "superseded" && hit.standing !== "rejected"
2172
+ );
2173
+ const groups = [
2174
+ group("expired", expired(inForce, now)),
2175
+ group("expiring", expiring(inForce, now, thresholds.expiringDays)),
2176
+ group("unverified", unverified(inForce, now, thresholds.unverifiedDays)),
2177
+ group("aging", aging(inForce, now, thresholds.agingDays)),
2178
+ group("orphaned", orphaned(bundle)),
2179
+ group("broken-supersession", brokenSupersession(bundle, adjudicated)),
2180
+ group("superseded-but-cited", supersededButCited(bundle, standings))
2181
+ ];
2182
+ const counts = Object.fromEntries(
2183
+ groups.map((entry) => [entry.check, entry.count])
2184
+ );
2185
+ const findingCount = groups.reduce((total, entry) => total + entry.count, 0);
2186
+ return {
2187
+ recordCount: bundle.length,
2188
+ thresholds,
2189
+ counts,
2190
+ groups,
2191
+ findingCount,
2192
+ healthy: findingCount === 0
2193
+ };
2194
+ }
2195
+ function group(check, findings) {
2196
+ return {
2197
+ check,
2198
+ headline: CHECK_HEADLINES[check],
2199
+ count: findings.length,
2200
+ findings
2201
+ };
2202
+ }
2203
+ function expired(hits, now) {
2204
+ const findings = [];
2205
+ for (const hit of hits) {
2206
+ const raw = hit.record.frontmatter.stale_after;
2207
+ if (!raw) continue;
2208
+ const at = Date.parse(raw);
2209
+ if (Number.isNaN(at)) {
2210
+ findings.push(
2211
+ finding(hit.record, `stale_after "${raw}" is not a readable date`)
2212
+ );
2213
+ continue;
2214
+ }
2215
+ if (at < now.getTime()) {
2216
+ findings.push(
2217
+ finding(
2218
+ hit.record,
2219
+ `stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
2220
+ )
2221
+ );
2222
+ }
2223
+ }
2224
+ return findings;
2225
+ }
2226
+ function expiring(hits, now, withinDays) {
2227
+ const horizon = now.getTime() + withinDays * DAY_MS;
2228
+ const findings = [];
2229
+ for (const hit of hits) {
2230
+ const raw = hit.record.frontmatter.stale_after;
2231
+ if (!raw) continue;
2232
+ const at = Date.parse(raw);
2233
+ if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
2234
+ findings.push(
2235
+ finding(
2236
+ hit.record,
2237
+ `goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
2238
+ )
2239
+ );
2240
+ }
2241
+ return findings;
2242
+ }
2243
+ function unverified(hits, now, olderThanDays) {
2244
+ const findings = [];
2245
+ for (const hit of hits) {
2246
+ if (hit.record.frontmatter.verified?.length) continue;
2247
+ const age = ageInDays(hit.record, now);
2248
+ if (age === null || age <= olderThanDays) continue;
2249
+ findings.push(
2250
+ finding(hit.record, `never verified, written ${age} days ago`)
2251
+ );
2252
+ }
2253
+ return findings;
2254
+ }
2255
+ function aging(hits, now, olderThanDays) {
2256
+ const findings = [];
2257
+ for (const hit of hits) {
2258
+ const status = hit.record.frontmatter.strauss_status;
2259
+ if (status !== "open" && status !== "proposed") continue;
2260
+ const age = ageInDays(hit.record, now);
2261
+ if (age === null || age <= olderThanDays) continue;
2262
+ findings.push(
2263
+ finding(
2264
+ hit.record,
2265
+ status === "open" ? `open for ${age} days` : `proposed ${age} days ago and still unsettled`
2266
+ )
2267
+ );
2268
+ }
2269
+ return findings.sort(
2270
+ (left, right) => left.conceptId.localeCompare(right.conceptId)
2271
+ );
2272
+ }
2273
+ function orphaned(bundle) {
2274
+ const present = new Set(bundle.map((record) => record.conceptId));
2275
+ const referenced = /* @__PURE__ */ new Set();
2276
+ for (const record of bundle) {
2277
+ for (const neighbour of edgeNeighbours(record, bundle, "body-link")) {
2278
+ referenced.add(neighbour.conceptId);
2279
+ }
2280
+ for (const replaced of record.frontmatter.strauss_supersedes ?? []) {
2281
+ referenced.add(replaced);
2282
+ }
2283
+ const replacement = record.frontmatter.strauss_superseded_by;
2284
+ if (replacement && present.has(replacement)) {
2285
+ referenced.add(record.conceptId);
2286
+ }
2287
+ }
2288
+ return bundle.filter((record) => !referenced.has(record.conceptId)).map((record) => finding(record, "no other record links to it"));
2289
+ }
2290
+ var SUPERSESSION_CHECKS = /* @__PURE__ */ new Set([
2291
+ "superseded_by",
2292
+ "supersedes",
2293
+ "backlink"
2294
+ ]);
2295
+ function brokenSupersession(bundle, adjudicated) {
2296
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
2297
+ const findings = [];
2298
+ const seen = /* @__PURE__ */ new Set();
2299
+ const add = (record, note) => {
2300
+ const key2 = `${record.conceptId}\0${note}`;
2301
+ if (seen.has(key2)) return;
2302
+ seen.add(key2);
2303
+ findings.push(finding(record, note));
2304
+ };
2305
+ for (const problem of validateBundle(bundle)) {
2306
+ if (!SUPERSESSION_CHECKS.has(problem.check)) continue;
2307
+ const record = byId.get(problem.conceptId);
2308
+ if (record) add(record, problem.note);
2309
+ }
2310
+ for (const record of bundle) {
2311
+ const replacement = record.frontmatter.strauss_superseded_by;
2312
+ if (!replacement) continue;
2313
+ if (!byId.has(replacement)) {
2314
+ add(record, `replacement ${replacement} is missing`);
2315
+ } else if (record.frontmatter.strauss_status !== "superseded") {
2316
+ add(
2317
+ record,
2318
+ `names ${replacement} as its replacement but is not marked superseded`
2319
+ );
2320
+ }
2321
+ }
2322
+ for (const hit of adjudicated) {
2323
+ for (const warning of hit.warnings) {
2324
+ if (warning.kind === "broken-chain") {
2325
+ add(hit.record, `replacement ${warning.missing} is missing`);
2326
+ } else if (warning.kind === "chain-cycle") {
2327
+ add(
2328
+ hit.record,
2329
+ `supersession chain cycles through ${warning.through.join(" \u2192 ")}`
2330
+ );
2331
+ } else if (warning.kind === "forked-chain") {
2332
+ add(
2333
+ hit.record,
2334
+ `two records claim to replace it: ${warning.heads.join(", ")}`
2335
+ );
2336
+ }
2337
+ }
2338
+ }
2339
+ return findings.sort(
2340
+ (left, right) => left.conceptId.localeCompare(right.conceptId)
2341
+ );
2342
+ }
2343
+ function supersededButCited(bundle, standings) {
2344
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
2345
+ const findings = [];
2346
+ for (const record of bundle) {
2347
+ const standing = standings.get(record.conceptId);
2348
+ if (standing === "superseded" || standing === "rejected") continue;
2349
+ for (const target of edgeNeighbours(record, bundle, "body-link")) {
2350
+ const targetStanding = standings.get(target.conceptId);
2351
+ if (targetStanding !== "superseded" && targetStanding !== "rejected") {
2352
+ continue;
2353
+ }
2354
+ if (replaces(record, target)) continue;
2355
+ const replacement = target.frontmatter.strauss_superseded_by;
2356
+ findings.push(
2357
+ finding(
2358
+ record,
2359
+ `cites ${targetStanding} ${target.conceptId}${targetStanding === "superseded" && replacement && byId.has(replacement) ? ` \u2014 replaced by ${replacement}` : ""}`
2360
+ )
2361
+ );
2362
+ }
2363
+ }
2364
+ return findings;
2365
+ }
2366
+ function replaces(later, earlier) {
2367
+ return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
2368
+ }
2369
+ function finding(record, note) {
2370
+ return {
2371
+ conceptId: record.conceptId,
2372
+ title: record.frontmatter.title ?? null,
2373
+ status: record.frontmatter.strauss_status,
2374
+ note
2375
+ };
2376
+ }
2377
+ function daysBetween(from, to) {
2378
+ return Math.max(0, Math.floor((to - from) / DAY_MS));
2379
+ }
2380
+ function ageInDays(record, now) {
2381
+ const at = record.frontmatter.generated?.at;
2382
+ if (!at) return null;
2383
+ const written = Date.parse(at);
2384
+ if (Number.isNaN(written)) return null;
2385
+ return daysBetween(written, now.getTime());
2386
+ }
2387
+
2131
2388
  // src/decision-record.ts
2132
2389
  var import_zod6 = require("zod");
2133
2390
  var DECISION_TYPE = "decision";
@@ -2264,14 +2521,104 @@ var contextCommand = define({
2264
2521
  }
2265
2522
  });
2266
2523
 
2267
- // src/commands/list.ts
2524
+ // src/commands/doctor.ts
2268
2525
  var import_zod10 = require("zod");
2526
+ var days = (what, fallback) => import_zod10.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
2527
+ var doctorCommand = define({
2528
+ name: "doctor",
2529
+ tool: "kb_doctor",
2530
+ usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
2531
+ description: "A health sweep over a whole base: what the calendar has already retired, what nobody ever confirmed, what has been open or proposed long enough that the status is now the answer, and what the graph has dropped on the floor. Read-only \u2014 it never writes, never supersedes, and never re-dates anything; every finding names a record for a person to repair. Seven checks, grouped and counted: expired (past `stale_after`), expiring (inside the window), unverified (an empty `verified[]` on a record old enough to matter), aging (still `open` or `proposed`), orphaned (no other record links to it), broken supersession (a chain that does not resolve), and superseded-but-cited (a live record whose body links to a record that no longer holds). Every group is 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.\n\nThis is the question no reader thinks to ask, which is why it needs a command: decay is invisible from inside a single record \u2014 a stale one reads exactly like a live one, and a question nobody answered reads exactly like one nobody asked. Reach for it when picking up a base someone else kept, before trusting a base you have not touched in months, or on a schedule; kb_validate is the narrower neighbour, checking only whether pointers between records agree.",
2532
+ input: import_zod10.z.object({
2533
+ bundlePath,
2534
+ expiringDays: days(
2535
+ "How far ahead `expiring` looks, in days.",
2536
+ DEFAULT_EXPIRING_DAYS
2537
+ ),
2538
+ unverifiedDays: days(
2539
+ "How old an unconfirmed record must be before `unverified` reports it, in days.",
2540
+ DEFAULT_UNVERIFIED_DAYS
2541
+ ),
2542
+ agingDays: days(
2543
+ "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
2544
+ DEFAULT_AGING_DAYS
2545
+ ),
2546
+ strict: import_zod10.z.boolean().optional().describe(
2547
+ "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
2548
+ )
2549
+ }),
2550
+ // Presence, not truthiness: `--expiring-days ""` is a caller who meant
2551
+ // something and mistyped it, and a falsy test would answer by quietly
2552
+ // sweeping at the default. Passed through as given, the schema rejects it
2553
+ // and says which field.
2554
+ fromArgv: (argv, path) => {
2555
+ const expiring2 = argvFlag(argv, "--expiring-days");
2556
+ const unverified2 = argvFlag(argv, "--unverified-days");
2557
+ const agingDays = argvFlag(argv, "--aging-days");
2558
+ return {
2559
+ bundlePath: path,
2560
+ ...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
2561
+ ...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
2562
+ ...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
2563
+ ...argv.includes("--strict") ? { strict: true } : {}
2564
+ };
2565
+ },
2566
+ run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
2567
+ const checkedAt = now();
2568
+ const report = doctor(await store.list(path), {
2569
+ ...expiringDays !== void 0 ? { expiringDays } : {},
2570
+ ...unverifiedDays !== void 0 ? { unverifiedDays } : {},
2571
+ ...agingDays !== void 0 ? { agingDays } : {},
2572
+ now: new Date(checkedAt)
2573
+ });
2574
+ return { bundlePath: path, checkedAt, ...report };
2575
+ },
2576
+ render: (result) => render(result),
2577
+ // Only expiry, and only under --strict. The other six checks report debt a
2578
+ // reader decides about; an expired record is the base asserting something it
2579
+ // already said it would stop standing behind, which is the one finding a
2580
+ // pipeline can act on without a judgment call.
2581
+ failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
2582
+ });
2583
+ function render(result) {
2584
+ const { thresholds } = result;
2585
+ const lines = [
2586
+ `# KB Doctor \u2014 ${result.bundlePath}`,
2587
+ `records: ${result.recordCount}`,
2588
+ `thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
2589
+ `checked: ${result.checkedAt}`,
2590
+ ""
2591
+ ];
2592
+ const width = Math.max(...result.groups.map((group2) => group2.check.length));
2593
+ for (const group2 of result.groups) {
2594
+ lines.push(
2595
+ ` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
2596
+ );
2597
+ }
2598
+ for (const group2 of result.groups) {
2599
+ if (!group2.count) continue;
2600
+ lines.push("", `## ${group2.check} (${group2.count})`);
2601
+ for (const found of group2.findings) {
2602
+ lines.push(
2603
+ `- ${found.conceptId}${found.title ? ` \u2014 ${found.title}` : ""}: ${found.note}`
2604
+ );
2605
+ }
2606
+ }
2607
+ lines.push(
2608
+ "",
2609
+ result.healthy ? "Nothing to repair." : `${result.findingCount} finding${result.findingCount === 1 ? "" : "s"} across ${result.groups.filter((group2) => group2.count).length} of ${result.groups.length} checks.`
2610
+ );
2611
+ return lines.join("\n");
2612
+ }
2613
+
2614
+ // src/commands/list.ts
2615
+ var import_zod11 = require("zod");
2269
2616
  var listCommand = define({
2270
2617
  name: "list",
2271
2618
  tool: "kb_list",
2272
2619
  usage: "list [type]",
2273
2620
  description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
2274
- input: import_zod10.z.object({ bundlePath, type: import_zod10.z.enum(KB_RECORD_TYPES).optional() }),
2621
+ input: import_zod11.z.object({ bundlePath, type: import_zod11.z.enum(KB_RECORD_TYPES).optional() }),
2275
2622
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
2276
2623
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
2277
2624
  conceptId: record.conceptId,
@@ -2283,17 +2630,17 @@ var listCommand = define({
2283
2630
  });
2284
2631
 
2285
2632
  // src/commands/load.ts
2286
- var import_zod11 = require("zod");
2633
+ var import_zod12 = require("zod");
2287
2634
  var loadCommand = define({
2288
2635
  name: "load",
2289
2636
  tool: "kb_load",
2290
2637
  usage: "load [type] [--budget N | --all]",
2291
2638
  description: "Load the whole knowledge base at once, each record with its standing. Prefer this over searching: these bases run to a few thousand tokens, and a reader holding all of it has perfect recall and knows why it is asking, which no ranker does. Superseded records arrive under `superseded` as name, replacement and date only \u2014 their bodies no longer hold, and reading one later in a long session is the mistake this prevents; pass the id to kb_trace when you need the history. Rejected and unresolved records arrive whole: what was turned down, and what is still open, is the part a diff cannot show you. Refuses with a count rather than truncating when the base is too large \u2014 a truncated base is indistinguishable from a complete one, and would have you conclude something was never decided from a slice you did not know was a slice. Call at the point of use, not once per session: a base loaded early is summarised away by compaction, so if the visible context holds no records from this base and the question at hand is one it might govern, load before answering \u2014 never conclude nothing was decided from a context with no KB content in it. This tool (with kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.\n\nThat refusal is the default guardrail, meant for an agent that would otherwise burn its whole context on one call. `all` bypasses it and loads everything regardless of size: a deliberate operator with the budget to spend, not something to reach for automatically. It is mutually exclusive with `budgetTokens`. When the reader does not need everything, kb_query or a narrower `type` filter is the better fit than either.",
2292
- input: import_zod11.z.object({
2639
+ input: import_zod12.z.object({
2293
2640
  bundlePath,
2294
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
2295
- budgetTokens: import_zod11.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
2296
- all: import_zod11.z.boolean().optional().describe(
2641
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
2642
+ budgetTokens: import_zod12.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
2643
+ all: import_zod12.z.boolean().optional().describe(
2297
2644
  "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
2298
2645
  )
2299
2646
  }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
@@ -2331,25 +2678,25 @@ var loadCommand = define({
2331
2678
  });
2332
2679
 
2333
2680
  // src/commands/log.ts
2334
- var import_zod12 = require("zod");
2681
+ var import_zod13 = require("zod");
2335
2682
  var logCommand = define({
2336
2683
  name: "log",
2337
2684
  tool: "kb_log",
2338
2685
  usage: "log",
2339
2686
  description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
2340
- input: import_zod12.z.object({ bundlePath }),
2687
+ input: import_zod13.z.object({ bundlePath }),
2341
2688
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2342
2689
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
2343
2690
  });
2344
2691
 
2345
2692
  // src/commands/no-decision.ts
2346
- var import_zod13 = require("zod");
2693
+ var import_zod14 = require("zod");
2347
2694
  var noDecisionCommand = define({
2348
2695
  name: "no-decision",
2349
2696
  tool: "kb_no_decision",
2350
2697
  usage: "no-decision <reason...>",
2351
2698
  description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
2352
- input: import_zod13.z.object({ bundlePath, reason: import_zod13.z.string().min(1) }),
2699
+ input: import_zod14.z.object({ bundlePath, reason: import_zod14.z.string().min(1) }),
2353
2700
  fromArgv: (argv, path) => ({
2354
2701
  bundlePath: path,
2355
2702
  reason: argv.slice(1).join(" ").trim()
@@ -2366,20 +2713,20 @@ var noDecisionCommand = define({
2366
2713
  });
2367
2714
 
2368
2715
  // src/commands/pack.ts
2369
- var import_zod14 = require("zod");
2716
+ var import_zod15 = require("zod");
2370
2717
  var packCommand = define({
2371
2718
  name: "pack",
2372
2719
  tool: "kb_pack",
2373
2720
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
2374
2721
  description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
2375
- input: import_zod14.z.object({
2722
+ input: import_zod15.z.object({
2376
2723
  bundlePath,
2377
2724
  conceptId,
2378
- hops: import_zod14.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
2379
- maxNodes: import_zod14.z.number().int().positive().optional().describe(
2725
+ hops: import_zod15.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
2726
+ maxNodes: import_zod15.z.number().int().positive().optional().describe(
2380
2727
  "How many records the pack may hold, root included. Defaults to 20."
2381
2728
  ),
2382
- budgetTokens: import_zod14.z.number().int().positive().optional().describe(
2729
+ budgetTokens: import_zod15.z.number().int().positive().optional().describe(
2383
2730
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
2384
2731
  )
2385
2732
  }),
@@ -2401,10 +2748,10 @@ var packCommand = define({
2401
2748
  ...maxNodes !== void 0 ? { maxNodes } : {},
2402
2749
  ...budgetTokens !== void 0 ? { budgetTokens } : {}
2403
2750
  });
2404
- return render(result, path, now());
2751
+ return render2(result, path, now());
2405
2752
  }
2406
2753
  });
2407
- function render(result, bundle, at) {
2754
+ function render2(result, bundle, at) {
2408
2755
  const lines = [
2409
2756
  `# KB Pack \u2014 ${result.root}`,
2410
2757
  `bundle: ${bundle}`,
@@ -2466,22 +2813,22 @@ function warningLabel(warning) {
2466
2813
  }
2467
2814
 
2468
2815
  // src/commands/pin.ts
2469
- var import_zod15 = require("zod");
2816
+ var import_zod16 = require("zod");
2470
2817
  var pinCommand = define({
2471
2818
  name: "pin",
2472
2819
  tool: "kb_pin",
2473
2820
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
2474
2821
  description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
2475
- input: import_zod15.z.object({
2822
+ input: import_zod16.z.object({
2476
2823
  bundlePath,
2477
- mode: import_zod15.z.enum(["full", "index"]).optional().describe(
2824
+ mode: import_zod16.z.enum(["full", "index"]).optional().describe(
2478
2825
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
2479
2826
  ),
2480
- profiles: import_zod15.z.array(import_zod15.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2481
- layer: import_zod15.z.enum(["project", "local", "user"]).optional().describe(
2827
+ profiles: import_zod16.z.array(import_zod16.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2828
+ layer: import_zod16.z.enum(["project", "local", "user"]).optional().describe(
2482
2829
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
2483
2830
  ),
2484
- frozen: import_zod15.z.boolean().optional().describe(
2831
+ frozen: import_zod16.z.boolean().optional().describe(
2485
2832
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
2486
2833
  )
2487
2834
  }),
@@ -2510,29 +2857,29 @@ var pinCommand = define({
2510
2857
  });
2511
2858
 
2512
2859
  // src/commands/pins.ts
2513
- var import_zod16 = require("zod");
2860
+ var import_zod17 = require("zod");
2514
2861
  var pinsCommand = define({
2515
2862
  name: "pins",
2516
2863
  tool: "kb_pins",
2517
2864
  usage: "pins",
2518
2865
  description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
2519
- input: import_zod16.z.object({}),
2866
+ input: import_zod17.z.object({}),
2520
2867
  fromArgv: () => ({}),
2521
2868
  run: ({ store }) => listPins(store, process.cwd())
2522
2869
  });
2523
2870
 
2524
2871
  // src/commands/query.ts
2525
- var import_zod17 = require("zod");
2872
+ var import_zod18 = require("zod");
2526
2873
  var queryCommand = define({
2527
2874
  name: "query",
2528
2875
  tool: "kb_query",
2529
2876
  usage: "query <text...>",
2530
2877
  description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. Prefer kb_load when the base fits its budget: on this package's measurements, a reader holding the whole base answered eight of nine questions whose wording appears in no record, where embedding search answered four. Never read record files directly \u2014 this tool (with kb_load and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
2531
- input: import_zod17.z.object({
2878
+ input: import_zod18.z.object({
2532
2879
  bundlePath,
2533
- text: import_zod17.z.string().optional(),
2534
- type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
2535
- includeNonCurrent: import_zod17.z.boolean().optional()
2880
+ text: import_zod18.z.string().optional(),
2881
+ type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
2882
+ includeNonCurrent: import_zod18.z.boolean().optional()
2536
2883
  }),
2537
2884
  fromArgv: (argv, path) => ({
2538
2885
  bundlePath: path,
@@ -2554,40 +2901,40 @@ var queryCommand = define({
2554
2901
  });
2555
2902
 
2556
2903
  // src/commands/read-index.ts
2557
- var import_zod18 = require("zod");
2904
+ var import_zod19 = require("zod");
2558
2905
  var readIndexCommand = define({
2559
2906
  name: "index",
2560
2907
  tool: "kb_index",
2561
2908
  usage: "index",
2562
2909
  description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
2563
- input: import_zod18.z.object({ bundlePath }),
2910
+ input: import_zod19.z.object({ bundlePath }),
2564
2911
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2565
2912
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
2566
2913
  });
2567
2914
 
2568
2915
  // src/commands/schema.ts
2569
- var import_zod19 = require("zod");
2916
+ var import_zod20 = require("zod");
2570
2917
  var schemaCommand = define({
2571
2918
  name: "schema",
2572
2919
  tool: "kb_schema",
2573
2920
  usage: "schema",
2574
2921
  description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
2575
- input: import_zod19.z.object({}),
2922
+ input: import_zod20.z.object({}),
2576
2923
  fromArgv: () => ({}),
2577
2924
  run: () => Promise.resolve(kbJsonSchemas())
2578
2925
  });
2579
2926
 
2580
2927
  // src/commands/status.ts
2581
- var import_zod20 = require("zod");
2928
+ var import_zod21 = require("zod");
2582
2929
  var statusCommand = define({
2583
2930
  name: "status",
2584
2931
  tool: "kb_status",
2585
2932
  usage: "status <concept-id> <status>",
2586
2933
  description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
2587
- input: import_zod20.z.object({
2934
+ input: import_zod21.z.object({
2588
2935
  bundlePath,
2589
2936
  conceptId,
2590
- status: import_zod20.z.enum(KB_RECORD_STATUSES)
2937
+ status: import_zod21.z.enum(KB_RECORD_STATUSES)
2591
2938
  }),
2592
2939
  fromArgv: (argv, path) => ({
2593
2940
  bundlePath: path,
@@ -2602,13 +2949,13 @@ var statusCommand = define({
2602
2949
  });
2603
2950
 
2604
2951
  // src/commands/supersede.ts
2605
- var import_zod21 = require("zod");
2952
+ var import_zod22 = require("zod");
2606
2953
  var supersedeCommand = define({
2607
2954
  name: "supersede",
2608
2955
  tool: "kb_supersede",
2609
2956
  usage: "supersede <concept-id> <replacement-id>",
2610
2957
  description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
2611
- input: import_zod21.z.object({ bundlePath, conceptId, replacementId: conceptId }),
2958
+ input: import_zod22.z.object({ bundlePath, conceptId, replacementId: conceptId }),
2612
2959
  fromArgv: (argv, path) => ({
2613
2960
  bundlePath: path,
2614
2961
  conceptId: argv[1],
@@ -2622,16 +2969,16 @@ var supersedeCommand = define({
2622
2969
  });
2623
2970
 
2624
2971
  // src/commands/sync-instructions.ts
2625
- var import_zod22 = require("zod");
2972
+ var import_zod23 = require("zod");
2626
2973
  var syncInstructionsCommand = define({
2627
2974
  name: "sync-instructions",
2628
2975
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
2629
2976
  description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
2630
- input: import_zod22.z.object({
2631
- file: import_zod22.z.string().min(1).describe("The instruction file to edit in place."),
2632
- budgetTokens: import_zod22.z.number().int().positive().optional(),
2633
- fullUnderTokens: import_zod22.z.number().int().positive().optional(),
2634
- profile: import_zod22.z.string().optional()
2977
+ input: import_zod23.z.object({
2978
+ file: import_zod23.z.string().min(1).describe("The instruction file to edit in place."),
2979
+ budgetTokens: import_zod23.z.number().int().positive().optional(),
2980
+ fullUnderTokens: import_zod23.z.number().int().positive().optional(),
2981
+ profile: import_zod23.z.string().optional()
2635
2982
  }),
2636
2983
  fromArgv: (argv) => {
2637
2984
  const budget = argvFlag(argv, "--budget");
@@ -2657,17 +3004,17 @@ var syncInstructionsCommand = define({
2657
3004
  });
2658
3005
 
2659
3006
  // src/commands/trace.ts
2660
- var import_zod23 = require("zod");
3007
+ var import_zod24 = require("zod");
2661
3008
  var traceCommand = define({
2662
3009
  name: "trace",
2663
3010
  tool: "kb_trace",
2664
3011
  usage: "trace <concept-id> [edges...]",
2665
3012
  description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
2666
- input: import_zod23.z.object({
3013
+ input: import_zod24.z.object({
2667
3014
  bundlePath,
2668
3015
  conceptId,
2669
- edges: import_zod23.z.array(import_zod23.z.enum(TRACE_EDGES)).optional(),
2670
- depth: import_zod23.z.number().int().positive().optional()
3016
+ edges: import_zod24.z.array(import_zod24.z.enum(TRACE_EDGES)).optional(),
3017
+ depth: import_zod24.z.number().int().positive().optional()
2671
3018
  }),
2672
3019
  fromArgv: (argv, path) => ({
2673
3020
  bundlePath: path,
@@ -2689,53 +3036,53 @@ var traceCommand = define({
2689
3036
  });
2690
3037
 
2691
3038
  // src/commands/types.ts
2692
- var import_zod24 = require("zod");
3039
+ var import_zod25 = require("zod");
2693
3040
  var typesCommand = define({
2694
3041
  name: "types",
2695
3042
  tool: "kb_types",
2696
3043
  usage: "types",
2697
3044
  description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
2698
- input: import_zod24.z.object({}),
3045
+ input: import_zod25.z.object({}),
2699
3046
  fromArgv: () => ({}),
2700
3047
  run: () => Promise.resolve(RECORD_TYPES)
2701
3048
  });
2702
3049
 
2703
3050
  // src/commands/unpin.ts
2704
- var import_zod25 = require("zod");
3051
+ var import_zod26 = require("zod");
2705
3052
  var unpinCommand = define({
2706
3053
  name: "unpin",
2707
3054
  tool: "kb_unpin",
2708
3055
  usage: "unpin [bundle-path]",
2709
3056
  description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
2710
- input: import_zod25.z.object({ bundlePath }),
3057
+ input: import_zod26.z.object({ bundlePath }),
2711
3058
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
2712
3059
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
2713
3060
  });
2714
3061
 
2715
3062
  // src/commands/validate.ts
2716
- var import_zod26 = require("zod");
3063
+ var import_zod27 = require("zod");
2717
3064
  var validateCommand = define({
2718
3065
  name: "validate",
2719
3066
  tool: "kb_validate",
2720
3067
  usage: "validate",
2721
3068
  description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
2722
- input: import_zod26.z.object({ bundlePath }),
3069
+ input: import_zod27.z.object({ bundlePath }),
2723
3070
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2724
3071
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
2725
3072
  failsWhen: (result) => Array.isArray(result) && result.length > 0
2726
3073
  });
2727
3074
 
2728
3075
  // src/commands/verify.ts
2729
- var import_zod27 = require("zod");
3076
+ var import_zod28 = require("zod");
2730
3077
  var verifyCommand = define({
2731
3078
  name: "verify",
2732
3079
  tool: "kb_verify",
2733
3080
  usage: "verify <concept-id> --note <text>",
2734
3081
  description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
2735
- input: import_zod27.z.object({
3082
+ input: import_zod28.z.object({
2736
3083
  bundlePath,
2737
3084
  conceptId,
2738
- note: import_zod27.z.string().refine((s) => s.trim().length > 0, {
3085
+ note: import_zod28.z.string().refine((s) => s.trim().length > 0, {
2739
3086
  message: "note must say what the check found"
2740
3087
  })
2741
3088
  }),
@@ -2755,7 +3102,7 @@ var verifyCommand = define({
2755
3102
  });
2756
3103
 
2757
3104
  // src/commands/write.ts
2758
- var import_zod28 = require("zod");
3105
+ var import_zod29 = require("zod");
2759
3106
  var writeCommand = define({
2760
3107
  name: "write",
2761
3108
  tool: "kb_write",
@@ -2769,9 +3116,9 @@ var writeCommand = define({
2769
3116
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
2770
3117
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
2771
3118
  ].join("\n"),
2772
- input: import_zod28.z.object({
3119
+ input: import_zod29.z.object({
2773
3120
  bundlePath,
2774
- type: import_zod28.z.enum(KB_RECORD_TYPES),
3121
+ type: import_zod29.z.enum(KB_RECORD_TYPES),
2775
3122
  input: composeInputSchema
2776
3123
  }),
2777
3124
  fromArgv: async (argv, path, stdin) => ({
@@ -2795,7 +3142,7 @@ var writeCommand = define({
2795
3142
  });
2796
3143
 
2797
3144
  // src/commands/write-decision.ts
2798
- var import_zod29 = require("zod");
3145
+ var import_zod30 = require("zod");
2799
3146
  var writeDecisionCommand = define({
2800
3147
  name: "write-decision",
2801
3148
  tool: "kb_write_decision",
@@ -2808,7 +3155,7 @@ var writeDecisionCommand = define({
2808
3155
  "- `alternative` is what you turned down and why, not a list of everything considered.",
2809
3156
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
2810
3157
  ].join("\n"),
2811
- input: import_zod29.z.object({ bundlePath, input: decisionInputSchema }),
3158
+ input: import_zod30.z.object({ bundlePath, input: decisionInputSchema }),
2812
3159
  fromArgv: async (_argv, path, stdin) => ({
2813
3160
  bundlePath: path,
2814
3161
  input: JSON.parse(await stdin())
@@ -2845,6 +3192,7 @@ var KB_COMMANDS = [
2845
3192
  readIndexCommand,
2846
3193
  logCommand,
2847
3194
  validateCommand,
3195
+ doctorCommand,
2848
3196
  schemaCommand,
2849
3197
  pinCommand,
2850
3198
  unpinCommand,
@@ -2862,7 +3210,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
2862
3210
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
2863
3211
 
2864
3212
  // src/version.ts
2865
- var VERSION = true ? "0.1.7" : "0.0.0-dev";
3213
+ var VERSION = true ? "0.1.8" : "0.0.0-dev";
2866
3214
 
2867
3215
  // src/mcp.ts
2868
3216
  function createKbMcpServer() {
@@ -2903,8 +3251,9 @@ async function runKbMcpServer() {
2903
3251
  // src/cli.ts
2904
3252
  var import_node_path7 = require("path");
2905
3253
  async function runKbCli(argv) {
2906
- const { bundle, rest } = takeBundle(argv);
2907
- const name = rest[0] ?? "";
3254
+ const { flags, literal } = takeLiteral(argv);
3255
+ const { bundle, rest: withFlags } = takeBundle(flags);
3256
+ const name = withFlags[0] ?? "";
2908
3257
  if (!name || name === "-h" || name === "--help") {
2909
3258
  process.stdout.write(usage());
2910
3259
  return;
@@ -2916,6 +3265,14 @@ async function runKbCli(argv) {
2916
3265
  }
2917
3266
  const command = KB_COMMANDS_BY_NAME.get(name);
2918
3267
  if (!command) die(`unknown command ${name}`);
3268
+ const json = withFlags.includes("--json");
3269
+ if (json && !command.render) {
3270
+ die(`${name} takes no --json: its result is already the machine shape`);
3271
+ }
3272
+ const rest = [
3273
+ ...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
3274
+ ...literal
3275
+ ];
2919
3276
  const raw = await command.fromArgv(rest, bundle, readStdin);
2920
3277
  const parsed = command.input.safeParse(raw);
2921
3278
  if (!parsed.success) {
@@ -2935,13 +3292,16 @@ async function runKbCli(argv) {
2935
3292
  },
2936
3293
  parsed.data
2937
3294
  );
2938
- if (command.failsWhen?.(result)) process.exitCode = 1;
3295
+ if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
2939
3296
  if (result === "") return;
2940
- process.stdout.write(
2941
- typeof result === "string" ? result.endsWith("\n") ? result : `${result}
2942
- ` : `${JSON.stringify(result, null, 2)}
2943
- `
2944
- );
3297
+ const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
3298
+ process.stdout.write(text.endsWith("\n") ? text : `${text}
3299
+ `);
3300
+ }
3301
+ function takeLiteral(argv) {
3302
+ const at = argv.indexOf("--");
3303
+ if (at === -1) return { flags: argv, literal: [] };
3304
+ return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
2945
3305
  }
2946
3306
  function takeBundle(argv) {
2947
3307
  const at = argv.indexOf("--bundle");
@@ -2983,6 +3343,8 @@ function usage() {
2983
3343
  ),
2984
3344
  "",
2985
3345
  ` --bundle PATH defaults to ./${KB_DIR}`,
3346
+ " --json the machine shape, where a command prints a table",
3347
+ " -- everything after it is text, not flags",
2986
3348
  " --version the installed package version",
2987
3349
  " STRAUSS_KB_ACTOR names the writer in the log",
2988
3350
  ""
@@ -2995,9 +3357,12 @@ function usage() {
2995
3357
  CONTEXT_END,
2996
3358
  CONTEXT_PROFILES,
2997
3359
  DECISION_TYPE,
3360
+ DEFAULT_AGING_DAYS,
3361
+ DEFAULT_EXPIRING_DAYS,
2998
3362
  DEFAULT_LOAD_BUDGET,
2999
3363
  DEFAULT_PACK_HOPS,
3000
3364
  DEFAULT_PACK_MAX_NODES,
3365
+ DEFAULT_UNVERIFIED_DAYS,
3001
3366
  ErrorTypes,
3002
3367
  Fault,
3003
3368
  INDEX_FILE,
@@ -3006,6 +3371,7 @@ function usage() {
3006
3371
  KB_CONCEPT_ID_PATTERN,
3007
3372
  KB_CONFIDENCES,
3008
3373
  KB_DIR,
3374
+ KB_DOCTOR_CHECKS,
3009
3375
  KB_EDGE_KINDS,
3010
3376
  KB_MATERIALITIES,
3011
3377
  KB_RECORD_STATUSES,
@@ -3038,6 +3404,7 @@ function usage() {
3038
3404
  contextProfileBudgets,
3039
3405
  createKbMcpServer,
3040
3406
  decisionInputSchema,
3407
+ doctor,
3041
3408
  edgeNeighbours,
3042
3409
  indexIsStale,
3043
3410
  isKbRecordType,