@saasontools/strauss-kb 0.1.22 → 0.1.23

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/mcp-main.cjs CHANGED
@@ -2390,8 +2390,8 @@ function selectDecisions(records) {
2390
2390
  );
2391
2391
  }
2392
2392
 
2393
- // src/commands/anchor-resolve.ts
2394
- var import_zod7 = require("zod");
2393
+ // src/commands/anchor-resolve/command.ts
2394
+ var import_zod8 = require("zod");
2395
2395
 
2396
2396
  // src/kb-errors.ts
2397
2397
  var KbRecordAlreadyExistsError = class extends BaseError {
@@ -2670,6 +2670,73 @@ var KbStampDigestBaselineError = class extends BaseError {
2670
2670
  since;
2671
2671
  };
2672
2672
 
2673
+ // src/commands/model.ts
2674
+ var import_zod5 = require("zod");
2675
+ var bundlePath = import_zod5.z.string().min(1).describe("Absolute path to the knowledge base directory.");
2676
+ var conceptId = import_zod5.z.string().min(1).describe("e.g. decision.cursor-v2");
2677
+ var TAGS = import_zod5.z.array(import_zod5.z.string().min(1)).optional().describe(
2678
+ "Keep only records carrying every one of these frontmatter tags. Matched exactly."
2679
+ );
2680
+ var REPO_ROOT = import_zod5.z.string().min(1).optional().describe(
2681
+ "Where the anchored source lives, for the drift check. Defaults to the working directory."
2682
+ );
2683
+ function define(command) {
2684
+ return command;
2685
+ }
2686
+ function argvFlag(argv, name) {
2687
+ const joined = argv.find((arg) => arg.startsWith(`${name}=`));
2688
+ if (joined !== void 0) {
2689
+ const value2 = joined.slice(name.length + 1);
2690
+ if (!value2) throw new KbMissingFlagValueError(name);
2691
+ return value2;
2692
+ }
2693
+ const at2 = argv.indexOf(name);
2694
+ if (at2 === -1) return void 0;
2695
+ const value = argv[at2 + 1];
2696
+ if (value === void 0 || value.startsWith("--")) {
2697
+ throw new KbMissingFlagValueError(name);
2698
+ }
2699
+ return value;
2700
+ }
2701
+ function argvFlags(argv, name) {
2702
+ const values = [];
2703
+ for (const [at2, arg] of argv.entries()) {
2704
+ if (arg.startsWith(`${name}=`)) {
2705
+ const value = arg.slice(name.length + 1);
2706
+ if (!value) throw new KbMissingFlagValueError(name);
2707
+ values.push(value);
2708
+ } else if (arg === name) {
2709
+ const value = argv[at2 + 1];
2710
+ if (value === void 0 || value.startsWith("--")) {
2711
+ throw new KbMissingFlagValueError(name);
2712
+ }
2713
+ values.push(value);
2714
+ }
2715
+ }
2716
+ return values;
2717
+ }
2718
+ function argvWithout(argv, ...names) {
2719
+ const kept = [];
2720
+ for (let at2 = 0; at2 < argv.length; at2 += 1) {
2721
+ const arg = argv[at2];
2722
+ if (names.some((name) => arg.startsWith(`${name}=`))) continue;
2723
+ if (names.includes(arg)) {
2724
+ at2 += 1;
2725
+ continue;
2726
+ }
2727
+ kept.push(arg);
2728
+ }
2729
+ return kept;
2730
+ }
2731
+ function argvPositional(argv, ...names) {
2732
+ return argvWithout(argv.slice(1), ...names).find(
2733
+ (arg) => !arg.startsWith("--")
2734
+ );
2735
+ }
2736
+
2737
+ // src/commands/anchor-resolve/apply.ts
2738
+ var import_zod7 = require("zod");
2739
+
2673
2740
  // src/kb-pins/budgets.ts
2674
2741
  function asBudgets(value) {
2675
2742
  if (value === null || typeof value !== "object") return {};
@@ -2731,14 +2798,14 @@ var import_node_path7 = require("path");
2731
2798
 
2732
2799
  // src/kb-pins/model.ts
2733
2800
  var import_node_path6 = require("path");
2734
- var import_zod5 = require("zod");
2801
+ var import_zod6 = require("zod");
2735
2802
  var PINS_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.json");
2736
2803
  var PINS_LOCAL_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.local.json");
2737
2804
  var PIN_LAYERS = ["project", "local", "user"];
2738
- var pinSchema = import_zod5.z.object({
2805
+ var pinSchema = import_zod6.z.object({
2739
2806
  /** Relative to the manifest's root, so the file is committable. */
2740
- path: import_zod5.z.string().min(1),
2741
- pinnedAt: import_zod5.z.string().min(1).optional(),
2807
+ path: import_zod6.z.string().min(1),
2808
+ pinnedAt: import_zod6.z.string().min(1).optional(),
2742
2809
  /**
2743
2810
  * How `context` renders this base. `full` preloads the whole base into
2744
2811
  * the block regardless of the full-under threshold — for a base whose
@@ -2748,7 +2815,7 @@ var pinSchema = import_zod5.z.object({
2748
2815
  * Absent: the profile's full-under threshold decides. Invalid values
2749
2816
  * degrade to absent rather than failing the manifest.
2750
2817
  */
2751
- mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
2818
+ mode: import_zod6.z.enum(["full", "index"]).optional().catch(void 0),
2752
2819
  /**
2753
2820
  * Context profiles this pin surfaces in (e.g. only at session-start,
2754
2821
  * not per turn). Absent: every profile. A run without a profile sees
@@ -2756,17 +2823,17 @@ var pinSchema = import_zod5.z.object({
2756
2823
  * that skill at point of use than pinned at all — pins are what every
2757
2824
  * session should see.
2758
2825
  */
2759
- profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
2826
+ profiles: import_zod6.z.array(import_zod6.z.string()).optional().catch(void 0),
2760
2827
  /**
2761
2828
  * The base is concluded — a finished piece of research, a frozen ADR
2762
2829
  * set. Write commands against it refuse while this workspace holds the
2763
2830
  * pin, and `context` labels it read-only. Workspace policy, not base
2764
2831
  * state: the base itself stays copyable and writable elsewhere.
2765
2832
  */
2766
- frozen: import_zod5.z.boolean().optional().catch(void 0)
2833
+ frozen: import_zod6.z.boolean().optional().catch(void 0)
2767
2834
  }).passthrough();
2768
- var pinsManifestSchema = import_zod5.z.object({
2769
- pins: import_zod5.z.array(pinSchema).default([]),
2835
+ var pinsManifestSchema = import_zod6.z.object({
2836
+ pins: import_zod6.z.array(pinSchema).default([]),
2770
2837
  /**
2771
2838
  * Per-repo budgets for the `context` command, keyed by profile —
2772
2839
  * `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
@@ -2775,7 +2842,7 @@ var pinsManifestSchema = import_zod5.z.object({
2775
2842
  * the index at every session start. `contextProfileBudgets` does the
2776
2843
  * tolerant read.
2777
2844
  */
2778
- context: import_zod5.z.unknown().optional()
2845
+ context: import_zod6.z.unknown().optional()
2779
2846
  }).passthrough();
2780
2847
 
2781
2848
  // src/kb-pins/layers.ts
@@ -2968,90 +3035,269 @@ async function unpinBase(workspaceDir, bundlePath2) {
2968
3035
  };
2969
3036
  }
2970
3037
 
2971
- // src/commands/model.ts
2972
- var import_zod6 = require("zod");
2973
- var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
2974
- var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
2975
- var TAGS = import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
2976
- "Keep only records carrying every one of these frontmatter tags. Matched exactly."
2977
- );
2978
- var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
2979
- "Where the anchored source lives, for the drift check. Defaults to the working directory."
2980
- );
2981
- function define(command) {
2982
- return command;
2983
- }
2984
- function argvFlag(argv, name) {
2985
- const joined = argv.find((arg) => arg.startsWith(`${name}=`));
2986
- if (joined !== void 0) {
2987
- const value2 = joined.slice(name.length + 1);
2988
- if (!value2) throw new KbMissingFlagValueError(name);
2989
- return value2;
2990
- }
2991
- const at2 = argv.indexOf(name);
2992
- if (at2 === -1) return void 0;
2993
- const value = argv[at2 + 1];
2994
- if (value === void 0 || value.startsWith("--")) {
2995
- throw new KbMissingFlagValueError(name);
3038
+ // src/commands/anchor-resolve/apply.ts
3039
+ async function baseFrozen(cwd, bundlePath2) {
3040
+ try {
3041
+ await assertBaseNotFrozen(cwd, bundlePath2);
3042
+ return false;
3043
+ } catch (caught) {
3044
+ if (!(caught instanceof KbBaseFrozenError)) throw caught;
3045
+ return true;
2996
3046
  }
2997
- return value;
2998
3047
  }
2999
- function argvFlags(argv, name) {
3000
- const values = [];
3001
- for (const [at2, arg] of argv.entries()) {
3002
- if (arg.startsWith(`${name}=`)) {
3003
- const value = arg.slice(name.length + 1);
3004
- if (!value) throw new KbMissingFlagValueError(name);
3005
- values.push(value);
3006
- } else if (arg === name) {
3007
- const value = argv[at2 + 1];
3008
- if (value === void 0 || value.startsWith("--")) {
3009
- throw new KbMissingFlagValueError(name);
3048
+ async function applyPlan(plans, target) {
3049
+ if (!plans.some((plan) => plan.write)) {
3050
+ return { results: plans.map((plan) => plan.finding) };
3051
+ }
3052
+ let failure = target.frozen ? "frozen" : void 0;
3053
+ let error;
3054
+ if (!failure) {
3055
+ try {
3056
+ await target.store.updateAnchors(
3057
+ target.bundlePath,
3058
+ target.conceptId,
3059
+ plans.map((plan) => plan.anchor),
3060
+ target.actor
3061
+ );
3062
+ } catch (caught) {
3063
+ if (caught instanceof BaseError || caught instanceof import_zod7.z.ZodError) {
3064
+ throw caught;
3010
3065
  }
3011
- values.push(value);
3066
+ failure = "write-failed";
3067
+ error = clamp(caught instanceof Error ? caught.message : String(caught));
3012
3068
  }
3013
3069
  }
3014
- return values;
3070
+ return {
3071
+ results: plans.map((plan) => settle(plan, failure)),
3072
+ ...error ? { error } : {}
3073
+ };
3015
3074
  }
3016
- function argvWithout(argv, ...names) {
3017
- const kept = [];
3018
- for (let at2 = 0; at2 < argv.length; at2 += 1) {
3019
- const arg = argv[at2];
3020
- if (names.some((name) => arg.startsWith(`${name}=`))) continue;
3021
- if (names.includes(arg)) {
3022
- at2 += 1;
3075
+ function settle(plan, failure) {
3076
+ if (!plan.write) return plan.finding;
3077
+ if (failure) {
3078
+ return { ...plan.finding, outcome: "failed", outcomeReason: failure };
3079
+ }
3080
+ if (plan.write === "refresh") return plan.finding;
3081
+ return plan.write === "stamp" ? { ...plan.finding, state: "stamped", outcome: "applied" } : { ...plan.finding, outcome: "applied", rebaselined: true };
3082
+ }
3083
+ function clamp(message) {
3084
+ const line = message.split("\n")[0] ?? "";
3085
+ return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
3086
+ }
3087
+
3088
+ // src/commands/anchor-resolve/sources.ts
3089
+ async function readSources(anchors, root, offline) {
3090
+ const origin = new LazyOrigin(root);
3091
+ if (anchors.some((anchor) => anchor.repo)) await origin.prime();
3092
+ const foreign = new Map(
3093
+ anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
3094
+ );
3095
+ const local = anchors.filter(
3096
+ (anchor) => !foreign.get(anchor) && anchor.side !== "old"
3097
+ );
3098
+ const committed = anchors.filter(
3099
+ (anchor) => !foreign.get(anchor) && anchor.side === "old"
3100
+ );
3101
+ const remote = anchors.filter((anchor) => foreign.get(anchor));
3102
+ const reads = await readAnchorFiles(
3103
+ local.map((anchor) => anchor.file),
3104
+ anchorFileReader(root)
3105
+ );
3106
+ const atRef = await readCommitted(root, committed);
3107
+ const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
3108
+ offline
3109
+ });
3110
+ const sources = /* @__PURE__ */ new Map();
3111
+ for (const anchor of local) {
3112
+ const read = reads.get(anchor.file);
3113
+ sources.set(
3114
+ anchor,
3115
+ read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3116
+ );
3117
+ }
3118
+ for (const anchor of committed) {
3119
+ const read = atRef.get(atRefKey(anchor));
3120
+ sources.set(
3121
+ anchor,
3122
+ read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3123
+ );
3124
+ }
3125
+ for (const anchor of remote) {
3126
+ const repo = anchor.repo;
3127
+ const key2 = normalizeRepoUrl(repo);
3128
+ const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
3129
+ const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
3130
+ if (!primary?.ok) {
3131
+ sources.set(anchor, {
3132
+ ok: false,
3133
+ reason: primary?.ok === false ? primary.reason : "remote-unreachable",
3134
+ repo
3135
+ });
3023
3136
  continue;
3024
3137
  }
3025
- kept.push(arg);
3138
+ sources.set(anchor, {
3139
+ ok: true,
3140
+ source: primary.source,
3141
+ repo,
3142
+ ...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
3143
+ });
3026
3144
  }
3027
- return kept;
3145
+ return sources;
3028
3146
  }
3029
- function argvPositional(argv, ...names) {
3030
- return argvWithout(argv.slice(1), ...names).find(
3031
- (arg) => !arg.startsWith("--")
3147
+
3148
+ // src/commands/anchor-resolve/plan.ts
3149
+ async function planAnchors(anchors, options) {
3150
+ const { root, offline, rebaseline, restamp, check, frozen, now } = options;
3151
+ const sources = await readSources(anchors, root, offline);
3152
+ const resolvers = defaultAnchorResolvers({ offline });
3153
+ await prepareResolvers(
3154
+ resolvers,
3155
+ anchors.map((anchor) => anchor.file)
3032
3156
  );
3157
+ const plans = [];
3158
+ for (const anchor of anchors) {
3159
+ const base2 = {
3160
+ file: anchor.file,
3161
+ ...anchor.symbol ? { symbol: anchor.symbol } : {},
3162
+ ...anchor.side === "old" ? { side: "old" } : {},
3163
+ // Carried onto unresolved findings too: an anchor that once hashed
3164
+ // and now resolves to nothing is a broken anchor, and the exit code
3165
+ // has to be able to tell it from one nobody ever stamped.
3166
+ ...anchor.hash ? { storedHash: anchor.hash } : {}
3167
+ };
3168
+ const source = sources.get(anchor);
3169
+ if (source.repo) base2.repo = source.repo;
3170
+ if (!source.ok) {
3171
+ plans.push({
3172
+ finding: { ...base2, state: "unresolved", reason: source.reason },
3173
+ anchor
3174
+ });
3175
+ continue;
3176
+ }
3177
+ const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
3178
+ if (!outcome.ok) {
3179
+ plans.push({
3180
+ finding: { ...base2, state: "unresolved", reason: outcome.reason },
3181
+ anchor
3182
+ });
3183
+ continue;
3184
+ }
3185
+ const resolved = outcome.span;
3186
+ const producedBy = outcome.resolver;
3187
+ const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
3188
+ const currentLines = resolved.endLine - resolved.startLine + 1;
3189
+ const stampedKind = outcome.normalized ? "ast" : "raw";
3190
+ const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
3191
+ const stamped = {
3192
+ ...anchor,
3193
+ hash: stampedHash,
3194
+ hash_kind: stampedKind,
3195
+ lines: currentLines,
3196
+ resolved_at: now(),
3197
+ ...producedBy ? { resolver: producedBy } : {}
3198
+ };
3199
+ const pinned = anchor.ref !== void 0 && source.repo !== void 0;
3200
+ if (!anchor.hash) {
3201
+ plans.push({
3202
+ finding: {
3203
+ ...base2,
3204
+ state: "unstamped",
3205
+ currentHash: stampedHash,
3206
+ hashKind: stampedKind,
3207
+ ...producedBy ? { resolver: producedBy } : {}
3208
+ },
3209
+ anchor: check ? anchor : stamped,
3210
+ ...check ? {} : { write: "stamp" }
3211
+ });
3212
+ continue;
3213
+ }
3214
+ if (anchor.hash !== currentHash) {
3215
+ plans.push({
3216
+ finding: {
3217
+ ...base2,
3218
+ state: "drifted",
3219
+ currentHash,
3220
+ hashKind: kind,
3221
+ diffSize: lineDelta(anchor, currentLines),
3222
+ ...producedBy ? { resolver: producedBy } : {},
3223
+ // A regex-stamped anchor re-read by tree-sitter drifts because the
3224
+ // resolver changed, not because the code did.
3225
+ ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
3226
+ ...pinned ? { remoteState: "drifted-from-ref" } : {}
3227
+ },
3228
+ anchor: rebaseline && !check ? stamped : anchor,
3229
+ ...rebaseline && !check ? { write: "rebaseline" } : {}
3230
+ });
3231
+ continue;
3232
+ }
3233
+ const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
3234
+ if (onDefault && onDefault.hash !== anchor.hash) {
3235
+ plans.push({
3236
+ finding: {
3237
+ ...base2,
3238
+ state: "drifted",
3239
+ currentHash: onDefault.hash,
3240
+ diffSize: lineDelta(anchor, onDefault.lines),
3241
+ remoteState: "drifted-on-default",
3242
+ ...rebaseline ? {
3243
+ outcome: "skipped",
3244
+ outcomeReason: "pinned-ref"
3245
+ } : {}
3246
+ },
3247
+ anchor
3248
+ });
3249
+ continue;
3250
+ }
3251
+ const backfill = anchor.resolved_at === void 0 && !frozen;
3252
+ const refresh = !check && (restamp || backfill);
3253
+ plans.push({
3254
+ finding: {
3255
+ ...base2,
3256
+ state: "match",
3257
+ currentHash,
3258
+ hashKind: kind,
3259
+ ...producedBy ? { resolver: producedBy } : {},
3260
+ ...pinned ? { remoteState: "matches-ref" } : {}
3261
+ },
3262
+ anchor: refresh ? { ...anchor, resolved_at: now() } : anchor,
3263
+ ...refresh ? { write: "refresh" } : {}
3264
+ });
3265
+ }
3266
+ return plans;
3267
+ }
3268
+ function lineDelta(anchor, current) {
3269
+ return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
3270
+ }
3271
+ function headHash(source, anchor, resolvers) {
3272
+ if (source.head === void 0) return void 0;
3273
+ const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
3274
+ if (!outcome.ok) return void 0;
3275
+ return {
3276
+ hash: hashAnchorText(outcome.span.text),
3277
+ lines: outcome.span.endLine - outcome.span.startLine + 1
3278
+ };
3033
3279
  }
3034
3280
 
3035
- // src/commands/anchor-resolve.ts
3281
+ // src/commands/anchor-resolve/command.ts
3036
3282
  var anchorResolveCommand = define({
3037
3283
  name: "anchor-resolve",
3038
3284
  tool: "kb_anchor_resolve",
3039
3285
  usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]",
3040
- description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Exits non-zero on drift.",
3041
- input: import_zod7.z.object({
3286
+ description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Each result says what it compared and whether the write applied.",
3287
+ input: import_zod8.z.object({
3042
3288
  bundlePath,
3043
3289
  conceptId,
3044
- repoRoot: import_zod7.z.string().min(1).optional(),
3045
- offline: import_zod7.z.boolean().optional().describe(
3290
+ repoRoot: import_zod8.z.string().min(1).optional(),
3291
+ offline: import_zod8.z.boolean().optional().describe(
3046
3292
  "Resolve foreign anchors from the local repo cache only, never fetching."
3047
3293
  ),
3048
- rebaseline: import_zod7.z.boolean().optional().describe(
3294
+ rebaseline: import_zod8.z.boolean().optional().describe(
3049
3295
  "Accept the current code as the new baseline for anchors that drifted."
3050
3296
  ),
3051
- restamp: import_zod7.z.boolean().optional().describe(
3297
+ restamp: import_zod8.z.boolean().optional().describe(
3052
3298
  "Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
3053
3299
  ),
3054
- check: import_zod7.z.boolean().optional().describe(
3300
+ check: import_zod8.z.boolean().optional().describe(
3055
3301
  "Resolve and report only: no hash, no `resolved_at`, no log entry."
3056
3302
  )
3057
3303
  }),
@@ -3090,246 +3336,87 @@ var anchorResolveCommand = define({
3090
3336
  note: "record has no anchors"
3091
3337
  };
3092
3338
  }
3093
- const results = [];
3094
- const updated = [];
3095
- let dirty = false;
3096
- const sources = await readSources(anchors, root, offline === true);
3097
- const resolvers = defaultAnchorResolvers({ offline: offline === true });
3098
- await prepareResolvers(
3099
- resolvers,
3100
- anchors.map((anchor) => anchor.file)
3101
- );
3102
- for (const anchor of anchors) {
3103
- const base2 = {
3104
- file: anchor.file,
3105
- ...anchor.symbol ? { symbol: anchor.symbol } : {},
3106
- ...anchor.side === "old" ? { side: "old" } : {},
3107
- // Carried onto unresolved findings too: an anchor that once hashed
3108
- // and now resolves to nothing is a broken anchor, and the exit code
3109
- // has to be able to tell it from one nobody ever stamped.
3110
- ...anchor.hash ? { storedHash: anchor.hash } : {}
3111
- };
3112
- const source = sources.get(anchor);
3113
- if (source.repo) base2.repo = source.repo;
3114
- if (!source.ok) {
3115
- results.push({ ...base2, state: "unresolved", reason: source.reason });
3116
- updated.push(anchor);
3117
- continue;
3118
- }
3119
- const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
3120
- if (!outcome.ok) {
3121
- results.push({
3122
- ...base2,
3123
- state: "unresolved",
3124
- reason: outcome.reason
3125
- });
3126
- updated.push(anchor);
3127
- continue;
3128
- }
3129
- const resolved = outcome.span;
3130
- const producedBy = outcome.resolver;
3131
- const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
3132
- const currentLines = resolved.endLine - resolved.startLine + 1;
3133
- const stampedKind = outcome.normalized ? "ast" : "raw";
3134
- const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
3135
- const stamped = {
3136
- ...anchor,
3137
- hash: stampedHash,
3138
- hash_kind: stampedKind,
3139
- lines: currentLines,
3140
- resolved_at: now(),
3141
- ...producedBy ? { resolver: producedBy } : {}
3142
- };
3143
- const pinned = anchor.ref !== void 0 && source.repo !== void 0;
3144
- if (!anchor.hash) {
3145
- results.push({
3146
- ...base2,
3147
- state: check ? "unstamped" : "stamped",
3148
- currentHash: stampedHash,
3149
- hashKind: stampedKind,
3150
- ...producedBy ? { resolver: producedBy } : {}
3151
- });
3152
- updated.push(stamped);
3153
- dirty = true;
3154
- continue;
3155
- }
3156
- if (anchor.hash !== currentHash) {
3157
- results.push({
3158
- ...base2,
3159
- state: "drifted",
3160
- currentHash,
3161
- hashKind: kind,
3162
- diffSize: lineDelta(anchor, currentLines),
3163
- ...producedBy ? { resolver: producedBy } : {},
3164
- // A regex-stamped anchor re-read by tree-sitter drifts because the
3165
- // resolver changed, not because the code did.
3166
- ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
3167
- ...pinned ? { remoteState: "drifted-from-ref" } : {},
3168
- ...rebaseline ? { rebaselined: true } : {}
3169
- });
3170
- updated.push(rebaseline ? stamped : anchor);
3171
- if (rebaseline) dirty = true;
3172
- continue;
3173
- }
3174
- const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
3175
- if (onDefault && onDefault.hash !== anchor.hash) {
3176
- results.push({
3177
- ...base2,
3178
- state: "drifted",
3179
- currentHash: onDefault.hash,
3180
- diffSize: lineDelta(anchor, onDefault.lines),
3181
- remoteState: "drifted-on-default"
3182
- });
3183
- updated.push(anchor);
3184
- continue;
3185
- }
3186
- results.push({
3187
- ...base2,
3188
- state: "match",
3189
- currentHash,
3190
- hashKind: kind,
3191
- ...producedBy ? { resolver: producedBy } : {},
3192
- ...pinned ? { remoteState: "matches-ref" } : {}
3193
- });
3194
- const refresh = restamp || anchor.resolved_at === void 0;
3195
- updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
3196
- if (refresh) dirty = true;
3197
- }
3198
- let frozen = false;
3199
- if (dirty && !check) {
3200
- try {
3201
- await assertBaseNotFrozen(process.cwd(), path);
3202
- } catch (error) {
3203
- if (!(error instanceof KbBaseFrozenError)) throw error;
3204
- frozen = true;
3205
- }
3206
- if (!frozen) await store.updateAnchors(path, id, updated, actor);
3207
- }
3208
- const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
3339
+ const frozen = check ? false : await baseFrozen(process.cwd(), path);
3340
+ const plans = await planAnchors(anchors, {
3341
+ root,
3342
+ offline: offline === true,
3343
+ rebaseline: rebaseline === true,
3344
+ restamp: restamp === true,
3345
+ check: check === true,
3346
+ frozen,
3347
+ now
3348
+ });
3349
+ const applied = await applyPlan(plans, {
3350
+ store,
3351
+ actor,
3352
+ bundlePath: path,
3353
+ conceptId: id,
3354
+ frozen
3355
+ });
3356
+ const results = applied.results;
3357
+ const refused = frozen && plans.some((plan) => plan.write);
3209
3358
  const hints = grammarHints();
3210
3359
  const hintNote = hints.length ? { hints } : {};
3211
3360
  const unreachable = results.filter(
3212
3361
  (entry) => isUncheckedReason(entry.reason)
3213
3362
  ).length;
3214
3363
  const matches3 = results.filter((entry) => entry.state === "match").length;
3215
- const note = `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable`;
3364
+ const note = [
3365
+ unreachable ? `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable` : "",
3366
+ refused ? "base is frozen: nothing was stamped" : "",
3367
+ applied.error ? `nothing was written: ${applied.error}` : ""
3368
+ ].filter(Boolean).join("; ");
3216
3369
  return {
3217
3370
  conceptId: id,
3218
3371
  results,
3219
- ...unreachable ? { note } : {},
3220
- ...frozenNote,
3372
+ ...note ? { note } : {},
3373
+ ...refused ? { frozen: true } : {},
3221
3374
  ...hintNote
3222
3375
  };
3223
3376
  },
3377
+ // Drift is a finding until a write settles it: a rebaseline the base took is
3378
+ // the answer to the drift it reports, while one refused, skipped, or never
3379
+ // asked for leaves the gate exactly what it was meant to catch.
3380
+ //
3224
3381
  // A stored hash that no longer resolves is a broken anchor, not an absence:
3225
- // the file was deleted or the symbol renamed, and exiting zero on it would
3226
- // let the one edit that destroys an anchor pass the gate that exists to
3227
- // catch it. An anchor nobody ever stamped is still just unstamped, and one
3228
- // whose remote nothing could reach was never checked — failing CI on either
3229
- // would gate on work this command did not do.
3382
+ // the file was deleted or the symbol renamed. An anchor nobody ever stamped
3383
+ // is still just unstamped, and one whose remote nothing could reach was
3384
+ // never checked failing CI on either would gate on work this command did
3385
+ // not do.
3230
3386
  failsWhen: (result) => result.results.some(
3231
- (entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
3387
+ (entry) => entry.outcome === "failed" || entry.outcome === "skipped" || entry.state === "drifted" && entry.outcome !== "applied" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
3232
3388
  )
3233
3389
  });
3234
- function lineDelta(anchor, current) {
3235
- return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
3236
- }
3237
- function headHash(source, anchor, resolvers) {
3238
- if (source.head === void 0) return void 0;
3239
- const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
3240
- if (!outcome.ok) return void 0;
3241
- return {
3242
- hash: hashAnchorText(outcome.span.text),
3243
- lines: outcome.span.endLine - outcome.span.startLine + 1
3244
- };
3245
- }
3246
- async function readSources(anchors, root, offline) {
3247
- const origin = new LazyOrigin(root);
3248
- if (anchors.some((anchor) => anchor.repo)) await origin.prime();
3249
- const foreign = new Map(
3250
- anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
3251
- );
3252
- const local = anchors.filter(
3253
- (anchor) => !foreign.get(anchor) && anchor.side !== "old"
3254
- );
3255
- const committed = anchors.filter(
3256
- (anchor) => !foreign.get(anchor) && anchor.side === "old"
3257
- );
3258
- const remote = anchors.filter((anchor) => foreign.get(anchor));
3259
- const reads = await readAnchorFiles(
3260
- local.map((anchor) => anchor.file),
3261
- anchorFileReader(root)
3262
- );
3263
- const atRef = await readCommitted(root, committed);
3264
- const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
3265
- offline
3266
- });
3267
- const sources = /* @__PURE__ */ new Map();
3268
- for (const anchor of local) {
3269
- const read = reads.get(anchor.file);
3270
- sources.set(
3271
- anchor,
3272
- read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3273
- );
3274
- }
3275
- for (const anchor of committed) {
3276
- const read = atRef.get(atRefKey(anchor));
3277
- sources.set(
3278
- anchor,
3279
- read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3280
- );
3281
- }
3282
- for (const anchor of remote) {
3283
- const repo = anchor.repo;
3284
- const key2 = normalizeRepoUrl(repo);
3285
- const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
3286
- const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
3287
- if (!primary?.ok) {
3288
- sources.set(anchor, {
3289
- ok: false,
3290
- reason: primary?.ok === false ? primary.reason : "remote-unreachable",
3291
- repo
3292
- });
3293
- continue;
3294
- }
3295
- sources.set(anchor, {
3296
- ok: true,
3297
- source: primary.source,
3298
- repo,
3299
- ...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
3300
- });
3301
- }
3302
- return sources;
3303
- }
3304
3390
 
3305
3391
  // src/commands/anchor-set/model.ts
3306
- var import_zod8 = require("zod");
3307
- var anchorSetInputSchema = import_zod8.z.object({
3308
- reason: import_zod8.z.string().refine((text) => text.trim().length > 0, {
3392
+ var import_zod9 = require("zod");
3393
+ var anchorSetInputSchema = import_zod9.z.object({
3394
+ reason: import_zod9.z.string().refine((text) => text.trim().length > 0, {
3309
3395
  message: "reason must say what was reviewed"
3310
3396
  }).describe(
3311
3397
  "What the reviewer read that makes these the right pointers. Recorded in the log."
3312
3398
  ),
3313
- anchors: import_zod8.z.array(kbAnchorWriteSchema).min(1).describe(
3399
+ anchors: import_zod9.z.array(kbAnchorWriteSchema).min(1).describe(
3314
3400
  "The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
3315
3401
  )
3316
3402
  }).strict();
3317
- var anchorSetCommandInput = import_zod8.z.object({
3403
+ var anchorSetCommandInput = import_zod9.z.object({
3318
3404
  bundlePath,
3319
3405
  conceptId,
3320
3406
  input: anchorSetInputSchema,
3321
- resolve: import_zod8.z.boolean().optional().describe(
3407
+ resolve: import_zod9.z.boolean().optional().describe(
3322
3408
  "Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
3323
3409
  ),
3324
- repoRoot: import_zod8.z.string().min(1).optional().describe(
3410
+ repoRoot: import_zod9.z.string().min(1).optional().describe(
3325
3411
  "Where the anchored source lives, for resolve. Defaults to the working directory."
3326
3412
  ),
3327
- offline: import_zod8.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
3413
+ offline: import_zod9.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
3328
3414
  });
3329
3415
 
3330
3416
  // src/commands/anchor-set/command.ts
3331
3417
  var NOTE = "pointers only: nothing was resolved or verified. Run anchor-resolve to check the new pointers, --rebaseline to accept the code, or pass resolve to do both here.";
3332
3418
  var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
3419
+ var INCOMPLETE_NOTE = "pointers set, but not every anchor was stamped: see each resolved entry's state and outcome.";
3333
3420
  var anchorSetCommand = define({
3334
3421
  name: "anchor-set",
3335
3422
  tool: "kb_anchor_set",
@@ -3386,33 +3473,39 @@ var anchorSetCommand = define({
3386
3473
  })
3387
3474
  );
3388
3475
  const after = await store.read(path, id);
3476
+ const stamped = resolved.results.every(
3477
+ (entry) => entry.state === "match" || entry.outcome === "applied"
3478
+ );
3389
3479
  return {
3390
3480
  conceptId: id,
3391
3481
  reason: input.reason,
3392
3482
  changes,
3393
3483
  anchors: after?.frontmatter.strauss_anchors ?? [],
3394
- baseline: "stamped",
3484
+ baseline: stamped ? "stamped" : "incomplete",
3395
3485
  resolved: resolved.results,
3396
- note: STAMPED_NOTE
3486
+ note: stamped ? STAMPED_NOTE : INCOMPLETE_NOTE
3397
3487
  };
3398
3488
  },
3399
3489
  // With `resolve`, a pointer that names nothing is a failed set, not a
3400
- // finding to read later. A remote nothing could reach was never checked, so
3401
- // it does not fail the same line anchor-resolve draws.
3402
- failsWhen: (result) => (result.resolved ?? []).some((entry) => {
3403
- const { state, reason } = entry;
3404
- return state === "unresolved" && !isUncheckedReason(reason);
3405
- })
3490
+ // finding to read later, and a stamp that did not land fails as it does in
3491
+ // anchor-resolve. A remote nothing could reach was never checked, so it does
3492
+ // not fail the same line anchor-resolve draws.
3493
+ failsWhen: (result, input) => {
3494
+ const resolved = result.resolved ?? [];
3495
+ return resolved.some(
3496
+ (entry) => entry.state === "unresolved" && !isUncheckedReason(entry.reason)
3497
+ ) || anchorResolveCommand.failsWhen?.({ results: resolved }, input) === true;
3498
+ }
3406
3499
  });
3407
3500
 
3408
3501
  // src/commands/answer.ts
3409
- var import_zod9 = require("zod");
3502
+ var import_zod10 = require("zod");
3410
3503
  var answerCommand = define({
3411
3504
  name: "answer",
3412
3505
  tool: "kb_answer",
3413
3506
  usage: "answer <concept-id> <answer...>",
3414
3507
  description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
3415
- input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
3508
+ input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
3416
3509
  fromArgv: (argv, path) => ({
3417
3510
  bundlePath: path,
3418
3511
  conceptId: argv[1],
@@ -3426,19 +3519,19 @@ var answerCommand = define({
3426
3519
  });
3427
3520
 
3428
3521
  // src/commands/backlinks.ts
3429
- var import_zod10 = require("zod");
3522
+ var import_zod11 = require("zod");
3430
3523
  var backlinksCommand = define({
3431
3524
  name: "backlinks",
3432
3525
  tool: "kb_backlinks",
3433
3526
  usage: "backlinks <concept-id>",
3434
3527
  description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
3435
- input: import_zod10.z.object({ bundlePath, conceptId }),
3528
+ input: import_zod11.z.object({ bundlePath, conceptId }),
3436
3529
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
3437
3530
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
3438
3531
  });
3439
3532
 
3440
3533
  // src/commands/catalog.ts
3441
- var import_zod11 = require("zod");
3534
+ var import_zod12 = require("zod");
3442
3535
 
3443
3536
  // src/adjudicate.ts
3444
3537
  var STANDING = {
@@ -3617,9 +3710,9 @@ var catalogCommand = define({
3617
3710
  tool: "kb_catalog",
3618
3711
  usage: "catalog [type] [--tag T]...",
3619
3712
  description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
3620
- input: import_zod11.z.object({
3713
+ input: import_zod12.z.object({
3621
3714
  bundlePath,
3622
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
3715
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
3623
3716
  tags: TAGS
3624
3717
  }),
3625
3718
  fromArgv: (argv, path) => {
@@ -3691,7 +3784,7 @@ function count(value, noun) {
3691
3784
  var import_node_buffer = require("buffer");
3692
3785
  var import_promises7 = require("fs/promises");
3693
3786
  var import_node_path10 = require("path");
3694
- var import_zod14 = require("zod");
3787
+ var import_zod15 = require("zod");
3695
3788
 
3696
3789
  // src/match-diff.ts
3697
3790
  function matchToDiff(files, records, options = {}) {
@@ -4295,7 +4388,7 @@ function claimOf(record) {
4295
4388
  }
4296
4389
 
4297
4390
  // src/commands/match/command.ts
4298
- var import_zod13 = require("zod");
4391
+ var import_zod14 = require("zod");
4299
4392
 
4300
4393
  // src/commands/match/errors.ts
4301
4394
  var KbMatchInputError = class extends BaseError {
@@ -4315,21 +4408,21 @@ var KbMatchInputError = class extends BaseError {
4315
4408
  };
4316
4409
 
4317
4410
  // src/commands/match/model.ts
4318
- var import_zod12 = require("zod");
4319
- var diffHunkSchema = import_zod12.z.object({
4320
- startLine: import_zod12.z.number().int().positive(),
4321
- endLine: import_zod12.z.number().int().positive(),
4322
- side: import_zod12.z.enum(["old", "new"]).optional()
4411
+ var import_zod13 = require("zod");
4412
+ var diffHunkSchema = import_zod13.z.object({
4413
+ startLine: import_zod13.z.number().int().positive(),
4414
+ endLine: import_zod13.z.number().int().positive(),
4415
+ side: import_zod13.z.enum(["old", "new"]).optional()
4323
4416
  }).passthrough();
4324
- var diffFileSchema = import_zod12.z.object({
4325
- filePath: import_zod12.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4326
- hunks: import_zod12.z.array(diffHunkSchema)
4417
+ var diffFileSchema = import_zod13.z.object({
4418
+ filePath: import_zod13.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4419
+ hunks: import_zod13.z.array(diffHunkSchema)
4327
4420
  });
4328
- var symbolRangeSchema = import_zod12.z.object({
4329
- file: import_zod12.z.string().min(1),
4330
- symbol: import_zod12.z.string().min(1),
4331
- startLine: import_zod12.z.number().int().positive(),
4332
- endLine: import_zod12.z.number().int().positive()
4421
+ var symbolRangeSchema = import_zod13.z.object({
4422
+ file: import_zod13.z.string().min(1),
4423
+ symbol: import_zod13.z.string().min(1),
4424
+ startLine: import_zod13.z.number().int().positive(),
4425
+ endLine: import_zod13.z.number().int().positive()
4333
4426
  });
4334
4427
 
4335
4428
  // src/commands/match/parse-unified-diff.ts
@@ -4578,17 +4671,17 @@ var matchCommand = define({
4578
4671
  tool: "kb_match",
4579
4672
  usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
4580
4673
  description: "Which records sit on each changed hunk: the anchored records per file range, current first, each with its standing and the anchor that matched. kb_load hands over a whole base; this narrows a diff. Symbol ranges resolve from repoRoot when omitted; non-current records need includeNonCurrent.",
4581
- input: import_zod13.z.object({
4674
+ input: import_zod14.z.object({
4582
4675
  bundlePath,
4583
- files: import_zod13.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4584
- symbolRanges: import_zod13.z.array(symbolRangeSchema).optional().describe(
4676
+ files: import_zod14.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4677
+ symbolRanges: import_zod14.z.array(symbolRangeSchema).optional().describe(
4585
4678
  "Symbol spans the caller already has. Resolved from repoRoot when omitted."
4586
4679
  ),
4587
4680
  repoRoot: REPO_ROOT,
4588
- offline: import_zod13.z.boolean().optional().describe(
4681
+ offline: import_zod14.z.boolean().optional().describe(
4589
4682
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4590
4683
  ),
4591
- includeNonCurrent: import_zod13.z.boolean().optional().describe(
4684
+ includeNonCurrent: import_zod14.z.boolean().optional().describe(
4592
4685
  "Return superseded, rejected and unsettled records too, each carrying its standing."
4593
4686
  )
4594
4687
  }),
@@ -4692,22 +4785,22 @@ function project(match, ranges, all) {
4692
4785
 
4693
4786
  // src/commands/classify.ts
4694
4787
  var classifyFileSchema = diffFileSchema.extend({
4695
- hunks: import_zod14.z.array(
4696
- diffHunkSchema.extend({ lines: import_zod14.z.array(import_zod14.z.string()).optional() })
4788
+ hunks: import_zod15.z.array(
4789
+ diffHunkSchema.extend({ lines: import_zod15.z.array(import_zod15.z.string()).optional() })
4697
4790
  ),
4698
- renamedFrom: import_zod14.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4699
- similarity: import_zod14.z.number().min(0).max(100).optional()
4791
+ renamedFrom: import_zod15.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4792
+ similarity: import_zod15.z.number().min(0).max(100).optional()
4700
4793
  });
4701
4794
  var classifyCommand = define({
4702
4795
  name: "classify",
4703
4796
  tool: "kb_classify",
4704
4797
  usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
4705
4798
  description: "What kind of change each file carries: test, config, ci, docs, lockfile, generated, boilerplate, rename or source, with the rule that decided it. Derived from the diff and never stored; a `review:generated`, `review:boilerplate` or `review:move` fact anchored on a file overrides the heuristic. kb_match says what sits on a hunk; this says whether to read it.",
4706
- input: import_zod14.z.object({
4799
+ input: import_zod15.z.object({
4707
4800
  bundlePath,
4708
- files: import_zod14.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4801
+ files: import_zod15.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4709
4802
  repoRoot: REPO_ROOT,
4710
- offline: import_zod14.z.boolean().optional().describe(
4803
+ offline: import_zod15.z.boolean().optional().describe(
4711
4804
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4712
4805
  )
4713
4806
  }),
@@ -4818,7 +4911,7 @@ function renderClassify(result) {
4818
4911
  }
4819
4912
 
4820
4913
  // src/commands/context.ts
4821
- var import_zod15 = require("zod");
4914
+ var import_zod16 = require("zod");
4822
4915
 
4823
4916
  // src/kb-context.ts
4824
4917
  var import_promises8 = require("fs/promises");
@@ -5089,23 +5182,23 @@ var contextCommand = define({
5089
5182
  tool: "kb_context",
5090
5183
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
5091
5184
  description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
5092
- input: import_zod15.z.object({
5093
- budgetTokens: import_zod15.z.number().int().positive().optional().describe(
5185
+ input: import_zod16.z.object({
5186
+ budgetTokens: import_zod16.z.number().int().positive().optional().describe(
5094
5187
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
5095
5188
  ),
5096
- fullUnderTokens: import_zod15.z.number().int().positive().optional().describe(
5189
+ fullUnderTokens: import_zod16.z.number().int().positive().optional().describe(
5097
5190
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
5098
5191
  ),
5099
- profile: import_zod15.z.string().optional().describe(
5192
+ profile: import_zod16.z.string().optional().describe(
5100
5193
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
5101
5194
  ),
5102
- excludeTags: import_zod15.z.array(import_zod15.z.string().min(1)).optional().describe(
5195
+ excludeTags: import_zod16.z.array(import_zod16.z.string().min(1)).optional().describe(
5103
5196
  "Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
5104
5197
  ),
5105
- format: import_zod15.z.enum(["markdown", "json"]).optional().describe(
5198
+ format: import_zod16.z.enum(["markdown", "json"]).optional().describe(
5106
5199
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
5107
5200
  ),
5108
- event: import_zod15.z.string().optional().describe(
5201
+ event: import_zod16.z.string().optional().describe(
5109
5202
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
5110
5203
  )
5111
5204
  }),
@@ -5144,7 +5237,7 @@ var contextCommand = define({
5144
5237
  });
5145
5238
 
5146
5239
  // src/commands/doctor.ts
5147
- var import_zod17 = require("zod");
5240
+ var import_zod18 = require("zod");
5148
5241
 
5149
5242
  // src/kb-edges.ts
5150
5243
  var KB_EDGE_KINDS = [
@@ -5660,17 +5753,17 @@ function ageInDays(record, now) {
5660
5753
  }
5661
5754
 
5662
5755
  // src/commands/reassess.ts
5663
- var import_zod16 = require("zod");
5756
+ var import_zod17 = require("zod");
5664
5757
  var reassessCommand = define({
5665
5758
  name: "reassess",
5666
5759
  tool: "kb_reassess",
5667
5760
  usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
5668
5761
  description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
5669
- input: import_zod16.z.object({
5762
+ input: import_zod17.z.object({
5670
5763
  bundlePath,
5671
5764
  conceptId,
5672
5765
  repoRoot: REPO_ROOT,
5673
- withDiff: import_zod16.z.boolean().optional().describe(
5766
+ withDiff: import_zod17.z.boolean().optional().describe(
5674
5767
  "Recover each anchor's committed span and render the diff. Reads git history."
5675
5768
  )
5676
5769
  }),
@@ -5815,13 +5908,13 @@ function at(file, symbol) {
5815
5908
  }
5816
5909
 
5817
5910
  // src/commands/doctor.ts
5818
- var days = (what, fallback) => import_zod17.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5911
+ var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5819
5912
  var doctorCommand = define({
5820
5913
  name: "doctor",
5821
5914
  tool: "kb_doctor",
5822
5915
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
5823
5916
  description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
5824
- input: import_zod17.z.object({
5917
+ input: import_zod18.z.object({
5825
5918
  bundlePath,
5826
5919
  repoRoot: REPO_ROOT,
5827
5920
  expiringDays: days(
@@ -5836,16 +5929,16 @@ var doctorCommand = define({
5836
5929
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
5837
5930
  DEFAULT_AGING_DAYS
5838
5931
  ),
5839
- offline: import_zod17.z.boolean().optional().describe(
5932
+ offline: import_zod18.z.boolean().optional().describe(
5840
5933
  "Read foreign anchors from the local repo cache only, never fetching."
5841
5934
  ),
5842
- strict: import_zod17.z.boolean().optional().describe(
5935
+ strict: import_zod18.z.boolean().optional().describe(
5843
5936
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
5844
5937
  ),
5845
- drifted: import_zod17.z.boolean().optional().describe(
5938
+ drifted: import_zod18.z.boolean().optional().describe(
5846
5939
  "Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
5847
5940
  ),
5848
- withDiff: import_zod17.z.boolean().optional().describe(
5941
+ withDiff: import_zod18.z.boolean().optional().describe(
5849
5942
  "With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
5850
5943
  )
5851
5944
  }),
@@ -6022,7 +6115,7 @@ function renderPackets(result) {
6022
6115
  // src/commands/export.ts
6023
6116
  var import_promises9 = require("fs/promises");
6024
6117
  var import_node_path11 = require("path");
6025
- var import_zod18 = require("zod");
6118
+ var import_zod19 = require("zod");
6026
6119
  var NUMBERED = /^(\d{4})-(.+)\.md$/;
6027
6120
  var MARKER = "<!-- strauss-kb export: ";
6028
6121
  var exportCommand = define({
@@ -6030,10 +6123,10 @@ var exportCommand = define({
6030
6123
  tool: "kb_export",
6031
6124
  usage: "export --format madr --to <dir>",
6032
6125
  description: "Write the base's decisions out as numbered MADR files, one per decision, for a repository that keeps ADRs of its own. Numbering is by slug, so a re-run rewrites its own files in place. A superseded decision is exported with what replaced it.",
6033
- input: import_zod18.z.object({
6126
+ input: import_zod19.z.object({
6034
6127
  bundlePath,
6035
- format: import_zod18.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
6036
- to: import_zod18.z.string().min(1).describe("Directory the ADR files are written into.")
6128
+ format: import_zod19.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
6129
+ to: import_zod19.z.string().min(1).describe("Directory the ADR files are written into.")
6037
6130
  }),
6038
6131
  fromArgv: (argv, path) => ({
6039
6132
  bundlePath: path,
@@ -6155,19 +6248,19 @@ function bodySections(body) {
6155
6248
  }
6156
6249
 
6157
6250
  // src/commands/impact.ts
6158
- var import_zod19 = require("zod");
6251
+ var import_zod20 = require("zod");
6159
6252
  var impactCommand = define({
6160
6253
  name: "impact",
6161
6254
  tool: "kb_impact",
6162
6255
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
6163
6256
  description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
6164
- input: import_zod19.z.object({
6257
+ input: import_zod20.z.object({
6165
6258
  bundlePath,
6166
6259
  conceptId,
6167
- depth: import_zod19.z.number().int().positive().optional().describe(
6260
+ depth: import_zod20.z.number().int().positive().optional().describe(
6168
6261
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
6169
6262
  ),
6170
- rels: import_zod19.z.array(import_zod19.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6263
+ rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6171
6264
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
6172
6265
  )
6173
6266
  }),
@@ -6188,15 +6281,15 @@ var impactCommand = define({
6188
6281
  });
6189
6282
 
6190
6283
  // src/commands/list.ts
6191
- var import_zod20 = require("zod");
6284
+ var import_zod21 = require("zod");
6192
6285
  var listCommand = define({
6193
6286
  name: "list",
6194
6287
  tool: "kb_list",
6195
6288
  usage: "list [type] [--tag T]...",
6196
6289
  description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
6197
- input: import_zod20.z.object({
6290
+ input: import_zod21.z.object({
6198
6291
  bundlePath,
6199
- type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
6292
+ type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
6200
6293
  tags: TAGS
6201
6294
  }),
6202
6295
  fromArgv: (argv, path) => {
@@ -6220,17 +6313,17 @@ var listCommand = define({
6220
6313
  });
6221
6314
 
6222
6315
  // src/commands/load.ts
6223
- var import_zod21 = require("zod");
6316
+ var import_zod22 = require("zod");
6224
6317
  var loadCommand = define({
6225
6318
  name: "load",
6226
6319
  tool: "kb_load",
6227
6320
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
6228
6321
  description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
6229
- input: import_zod21.z.object({
6322
+ input: import_zod22.z.object({
6230
6323
  bundlePath,
6231
- type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
6232
- budgetTokens: import_zod21.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6233
- all: import_zod21.z.boolean().optional().describe(
6324
+ type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
6325
+ budgetTokens: import_zod22.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6326
+ all: import_zod22.z.boolean().optional().describe(
6234
6327
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
6235
6328
  ),
6236
6329
  repoRoot: REPO_ROOT
@@ -6272,25 +6365,25 @@ var loadCommand = define({
6272
6365
  });
6273
6366
 
6274
6367
  // src/commands/log.ts
6275
- var import_zod22 = require("zod");
6368
+ var import_zod23 = require("zod");
6276
6369
  var logCommand = define({
6277
6370
  name: "log",
6278
6371
  tool: "kb_log",
6279
6372
  usage: "log",
6280
6373
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
6281
- input: import_zod22.z.object({ bundlePath }),
6374
+ input: import_zod23.z.object({ bundlePath }),
6282
6375
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6283
6376
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
6284
6377
  });
6285
6378
 
6286
6379
  // src/commands/no-decision.ts
6287
- var import_zod23 = require("zod");
6380
+ var import_zod24 = require("zod");
6288
6381
  var noDecisionCommand = define({
6289
6382
  name: "no-decision",
6290
6383
  tool: "kb_no_decision",
6291
6384
  usage: "no-decision <reason...>",
6292
6385
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
6293
- input: import_zod23.z.object({ bundlePath, reason: import_zod23.z.string().min(1) }),
6386
+ input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
6294
6387
  fromArgv: (argv, path) => ({
6295
6388
  bundlePath: path,
6296
6389
  reason: argv.slice(1).join(" ").trim()
@@ -6307,20 +6400,20 @@ var noDecisionCommand = define({
6307
6400
  });
6308
6401
 
6309
6402
  // src/commands/pack.ts
6310
- var import_zod24 = require("zod");
6403
+ var import_zod25 = require("zod");
6311
6404
  var packCommand = define({
6312
6405
  name: "pack",
6313
6406
  tool: "kb_pack",
6314
6407
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
6315
6408
  description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
6316
- input: import_zod24.z.object({
6409
+ input: import_zod25.z.object({
6317
6410
  bundlePath,
6318
6411
  conceptId,
6319
- hops: import_zod24.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6320
- maxNodes: import_zod24.z.number().int().positive().optional().describe(
6412
+ hops: import_zod25.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6413
+ maxNodes: import_zod25.z.number().int().positive().optional().describe(
6321
6414
  "How many records the pack may hold, root included. Defaults to 20."
6322
6415
  ),
6323
- budgetTokens: import_zod24.z.number().int().positive().optional().describe(
6416
+ budgetTokens: import_zod25.z.number().int().positive().optional().describe(
6324
6417
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
6325
6418
  )
6326
6419
  }),
@@ -6407,22 +6500,22 @@ function warningLabel(warning) {
6407
6500
  }
6408
6501
 
6409
6502
  // src/commands/pin.ts
6410
- var import_zod25 = require("zod");
6503
+ var import_zod26 = require("zod");
6411
6504
  var pinCommand = define({
6412
6505
  name: "pin",
6413
6506
  tool: "kb_pin",
6414
6507
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
6415
6508
  description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
6416
- input: import_zod25.z.object({
6509
+ input: import_zod26.z.object({
6417
6510
  bundlePath,
6418
- mode: import_zod25.z.enum(["full", "index"]).optional().describe(
6511
+ mode: import_zod26.z.enum(["full", "index"]).optional().describe(
6419
6512
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
6420
6513
  ),
6421
- profiles: import_zod25.z.array(import_zod25.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6422
- layer: import_zod25.z.enum(["project", "local", "user"]).optional().describe(
6514
+ profiles: import_zod26.z.array(import_zod26.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6515
+ layer: import_zod26.z.enum(["project", "local", "user"]).optional().describe(
6423
6516
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
6424
6517
  ),
6425
- frozen: import_zod25.z.boolean().optional().describe(
6518
+ frozen: import_zod26.z.boolean().optional().describe(
6426
6519
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
6427
6520
  )
6428
6521
  }),
@@ -6451,13 +6544,13 @@ var pinCommand = define({
6451
6544
  });
6452
6545
 
6453
6546
  // src/commands/pins.ts
6454
- var import_zod26 = require("zod");
6547
+ var import_zod27 = require("zod");
6455
6548
  var pinsCommand = define({
6456
6549
  name: "pins",
6457
6550
  tool: "kb_pins",
6458
6551
  usage: "pins",
6459
6552
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
6460
- input: import_zod26.z.object({}),
6553
+ input: import_zod27.z.object({}),
6461
6554
  fromArgv: () => ({}),
6462
6555
  run: ({ store }) => listPins(store, process.cwd())
6463
6556
  });
@@ -6728,16 +6821,16 @@ function recordType(conceptId2) {
6728
6821
  }
6729
6822
 
6730
6823
  // src/commands/promote/model.ts
6731
- var import_zod27 = require("zod");
6732
- var promoteInputSchema = import_zod27.z.object({
6824
+ var import_zod28 = require("zod");
6825
+ var promoteInputSchema = import_zod28.z.object({
6733
6826
  bundlePath,
6734
- conceptIds: import_zod27.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6735
- to: import_zod27.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6736
- source: import_zod27.z.string().min(1).optional().describe(
6827
+ conceptIds: import_zod28.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6828
+ to: import_zod28.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6829
+ source: import_zod28.z.string().min(1).optional().describe(
6737
6830
  "Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
6738
6831
  ),
6739
- force: import_zod27.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6740
- list: import_zod27.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6832
+ force: import_zod28.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6833
+ list: import_zod28.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6741
6834
  }).refine((input) => input.list === true || input.to !== void 0, {
6742
6835
  message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
6743
6836
  path: ["to"]
@@ -6874,17 +6967,17 @@ function renderPromote(result) {
6874
6967
  }
6875
6968
 
6876
6969
  // src/commands/query.ts
6877
- var import_zod28 = require("zod");
6970
+ var import_zod29 = require("zod");
6878
6971
  var queryCommand = define({
6879
6972
  name: "query",
6880
6973
  tool: "kb_query",
6881
6974
  usage: "query <text...> [--tag T]... [--repo-root PATH]",
6882
6975
  description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
6883
- input: import_zod28.z.object({
6976
+ input: import_zod29.z.object({
6884
6977
  bundlePath,
6885
- text: import_zod28.z.string().optional(),
6886
- type: import_zod28.z.enum(KB_RECORD_TYPES).optional(),
6887
- includeNonCurrent: import_zod28.z.boolean().optional(),
6978
+ text: import_zod29.z.string().optional(),
6979
+ type: import_zod29.z.enum(KB_RECORD_TYPES).optional(),
6980
+ includeNonCurrent: import_zod29.z.boolean().optional(),
6888
6981
  tags: TAGS,
6889
6982
  repoRoot: REPO_ROOT
6890
6983
  }),
@@ -6918,32 +7011,32 @@ var queryCommand = define({
6918
7011
  });
6919
7012
 
6920
7013
  // src/commands/read-index.ts
6921
- var import_zod29 = require("zod");
7014
+ var import_zod30 = require("zod");
6922
7015
  var readIndexCommand = define({
6923
7016
  name: "index",
6924
7017
  tool: "kb_index",
6925
7018
  usage: "index",
6926
7019
  description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
6927
- input: import_zod29.z.object({ bundlePath }),
7020
+ input: import_zod30.z.object({ bundlePath }),
6928
7021
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6929
7022
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
6930
7023
  });
6931
7024
 
6932
7025
  // src/commands/schema.ts
6933
- var import_zod32 = require("zod");
7026
+ var import_zod33 = require("zod");
6934
7027
 
6935
7028
  // src/json-schema.ts
6936
- var import_zod31 = require("zod");
7029
+ var import_zod32 = require("zod");
6937
7030
 
6938
7031
  // src/kb-log.ts
6939
- var import_zod30 = require("zod");
7032
+ var import_zod31 = require("zod");
6940
7033
  var LOG_FILE = "log.jsonl";
6941
- var kbLogAnchorChangeSchema = import_zod30.z.object({
6942
- op: import_zod30.z.enum(["move", "add", "drop"]),
7034
+ var kbLogAnchorChangeSchema = import_zod31.z.object({
7035
+ op: import_zod31.z.enum(["move", "add", "drop"]),
6943
7036
  from: kbAnchorLocatorSchema.optional(),
6944
7037
  to: kbAnchorLocatorSchema.optional()
6945
7038
  }).strict();
6946
- var kbLogEntryFields = import_zod30.z.object({
7039
+ var kbLogEntryFields = import_zod31.z.object({
6947
7040
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
6948
7041
  // below), and a value that isn't actually chronological — a Unix
6949
7042
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -6952,23 +7045,23 @@ var kbLogEntryFields = import_zod30.z.object({
6952
7045
  // and rejects everything else, including a non-`Z` offset — so a
6953
7046
  // malformed `at` is reported the same way a malformed line already is,
6954
7047
  // rather than silently sorting into the wrong place.
6955
- at: import_zod30.z.iso.datetime(),
6956
- by: import_zod30.z.string().min(1),
6957
- operation: import_zod30.z.string().min(1),
6958
- conceptId: import_zod30.z.string().min(1),
7048
+ at: import_zod31.z.iso.datetime(),
7049
+ by: import_zod31.z.string().min(1),
7050
+ operation: import_zod31.z.string().min(1),
7051
+ conceptId: import_zod31.z.string().min(1),
6959
7052
  /**
6960
7053
  * The operation's other end, where it has one: a second concept id for
6961
7054
  * supersession, the other base's path for promotion.
6962
7055
  */
6963
- target: import_zod30.z.string().min(1).optional(),
7056
+ target: import_zod31.z.string().min(1).optional(),
6964
7057
  /**
6965
7058
  * Why the operation was performed, where the operation demands one.
6966
7059
  * `anchor-set` does: a pointer moved by a reader is only auditable if
6967
7060
  * the reading is recorded beside it.
6968
7061
  */
6969
- reason: import_zod30.z.string().min(1).optional(),
7062
+ reason: import_zod31.z.string().min(1).optional(),
6970
7063
  /** What `anchor-set` changed, derived from the record before and after. */
6971
- anchors: import_zod30.z.array(kbLogAnchorChangeSchema).optional()
7064
+ anchors: import_zod31.z.array(kbLogAnchorChangeSchema).optional()
6972
7065
  });
6973
7066
  var kbLogEntrySchema = kbLogEntryFields.passthrough();
6974
7067
  var kbLogEntryWriteSchema = kbLogEntryFields.strict();
@@ -7014,11 +7107,11 @@ function parseLog(raw) {
7014
7107
  // src/json-schema.ts
7015
7108
  function kbJsonSchemas() {
7016
7109
  return {
7017
- recordFrontmatter: import_zod31.z.toJSONSchema(kbRecordFrontmatterSchema, {
7110
+ recordFrontmatter: import_zod32.z.toJSONSchema(kbRecordFrontmatterSchema, {
7018
7111
  io: "input"
7019
7112
  }),
7020
- composeInput: import_zod31.z.toJSONSchema(composeInputSchema, { io: "input" }),
7021
- logEntry: import_zod31.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
7113
+ composeInput: import_zod32.z.toJSONSchema(composeInputSchema, { io: "input" }),
7114
+ logEntry: import_zod32.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
7022
7115
  };
7023
7116
  }
7024
7117
 
@@ -7028,25 +7121,25 @@ var schemaCommand = define({
7028
7121
  tool: "kb_schema",
7029
7122
  usage: "schema",
7030
7123
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
7031
- input: import_zod32.z.object({}),
7124
+ input: import_zod33.z.object({}),
7032
7125
  fromArgv: () => ({}),
7033
7126
  run: () => Promise.resolve(kbJsonSchemas())
7034
7127
  });
7035
7128
 
7036
7129
  // src/commands/stamp.ts
7037
7130
  var import_promises10 = require("fs/promises");
7038
- var import_zod33 = require("zod");
7131
+ var import_zod34 = require("zod");
7039
7132
  var DIGEST = /^[0-9a-f]{64}$/;
7040
7133
  var stampCommand = define({
7041
7134
  name: "stamp",
7042
7135
  tool: "kb_stamp",
7043
7136
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
7044
7137
  description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
7045
- input: import_zod33.z.object({
7046
- bundlePath: import_zod33.z.string().min(1).optional().describe(
7138
+ input: import_zod34.z.object({
7139
+ bundlePath: import_zod34.z.string().min(1).optional().describe(
7047
7140
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
7048
7141
  ),
7049
- since: import_zod33.z.string().min(1).optional().describe(
7142
+ since: import_zod34.z.string().min(1).optional().describe(
7050
7143
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
7051
7144
  )
7052
7145
  }),
@@ -7132,16 +7225,16 @@ async function readBaseline(since) {
7132
7225
  }
7133
7226
 
7134
7227
  // src/commands/status.ts
7135
- var import_zod34 = require("zod");
7228
+ var import_zod35 = require("zod");
7136
7229
  var statusCommand = define({
7137
7230
  name: "status",
7138
7231
  tool: "kb_status",
7139
7232
  usage: "status <concept-id> <status>",
7140
7233
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
7141
- input: import_zod34.z.object({
7234
+ input: import_zod35.z.object({
7142
7235
  bundlePath,
7143
7236
  conceptId,
7144
- status: import_zod34.z.enum(KB_RECORD_STATUSES)
7237
+ status: import_zod35.z.enum(KB_RECORD_STATUSES)
7145
7238
  }),
7146
7239
  fromArgv: (argv, path) => ({
7147
7240
  bundlePath: path,
@@ -7156,13 +7249,13 @@ var statusCommand = define({
7156
7249
  });
7157
7250
 
7158
7251
  // src/commands/supersede.ts
7159
- var import_zod35 = require("zod");
7252
+ var import_zod36 = require("zod");
7160
7253
  var supersedeCommand = define({
7161
7254
  name: "supersede",
7162
7255
  tool: "kb_supersede",
7163
7256
  usage: "supersede <concept-id> <replacement-id>",
7164
7257
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
7165
- input: import_zod35.z.object({ bundlePath, conceptId, replacementId: conceptId }),
7258
+ input: import_zod36.z.object({ bundlePath, conceptId, replacementId: conceptId }),
7166
7259
  fromArgv: (argv, path) => ({
7167
7260
  bundlePath: path,
7168
7261
  conceptId: argv[1],
@@ -7176,7 +7269,7 @@ var supersedeCommand = define({
7176
7269
  });
7177
7270
 
7178
7271
  // src/commands/sweep.ts
7179
- var import_zod36 = require("zod");
7272
+ var import_zod37 = require("zod");
7180
7273
  var TERMINAL = [
7181
7274
  "resolved",
7182
7275
  "rejected",
@@ -7187,15 +7280,15 @@ var sweepCommand = define({
7187
7280
  tool: "kb_sweep",
7188
7281
  usage: "sweep --tag <tag> --terminal [--dry-run]",
7189
7282
  description: "Delete tagged records that are resolved, rejected or superseded. Refuses without --tag, keeps any record a surviving record still points at, and logs each deletion.",
7190
- input: import_zod36.z.object({
7283
+ input: import_zod37.z.object({
7191
7284
  bundlePath,
7192
- tag: import_zod36.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
7193
- terminal: import_zod36.z.literal(true, {
7285
+ tag: import_zod37.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
7286
+ terminal: import_zod37.z.literal(true, {
7194
7287
  error: "sweep needs --terminal: it deletes only settled records"
7195
7288
  }).describe(
7196
7289
  "Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
7197
7290
  ),
7198
- dryRun: import_zod36.z.boolean().optional().describe("Report what would go, and delete nothing.")
7291
+ dryRun: import_zod37.z.boolean().optional().describe("Report what would go, and delete nothing.")
7199
7292
  }),
7200
7293
  fromArgv: (argv, path) => ({
7201
7294
  bundlePath: path,
@@ -7312,16 +7405,16 @@ function renderSweep(result) {
7312
7405
  }
7313
7406
 
7314
7407
  // src/commands/sync-instructions.ts
7315
- var import_zod37 = require("zod");
7408
+ var import_zod38 = require("zod");
7316
7409
  var syncInstructionsCommand = define({
7317
7410
  name: "sync-instructions",
7318
7411
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
7319
7412
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
7320
- input: import_zod37.z.object({
7321
- file: import_zod37.z.string().min(1).describe("The instruction file to edit in place."),
7322
- budgetTokens: import_zod37.z.number().int().positive().optional(),
7323
- fullUnderTokens: import_zod37.z.number().int().positive().optional(),
7324
- profile: import_zod37.z.string().optional()
7413
+ input: import_zod38.z.object({
7414
+ file: import_zod38.z.string().min(1).describe("The instruction file to edit in place."),
7415
+ budgetTokens: import_zod38.z.number().int().positive().optional(),
7416
+ fullUnderTokens: import_zod38.z.number().int().positive().optional(),
7417
+ profile: import_zod38.z.string().optional()
7325
7418
  }),
7326
7419
  fromArgv: (argv) => {
7327
7420
  const budget = argvFlag(argv, "--budget");
@@ -7347,7 +7440,7 @@ var syncInstructionsCommand = define({
7347
7440
  });
7348
7441
 
7349
7442
  // src/commands/trace.ts
7350
- var import_zod38 = require("zod");
7443
+ var import_zod39 = require("zod");
7351
7444
 
7352
7445
  // src/trace.ts
7353
7446
  var TRACE_EDGES = [
@@ -7403,11 +7496,11 @@ var traceCommand = define({
7403
7496
  tool: "kb_trace",
7404
7497
  usage: "trace <concept-id> [edges...]",
7405
7498
  description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
7406
- input: import_zod38.z.object({
7499
+ input: import_zod39.z.object({
7407
7500
  bundlePath,
7408
7501
  conceptId,
7409
- edges: import_zod38.z.array(import_zod38.z.enum(TRACE_EDGES)).optional(),
7410
- depth: import_zod38.z.number().int().positive().optional()
7502
+ edges: import_zod39.z.array(import_zod39.z.enum(TRACE_EDGES)).optional(),
7503
+ depth: import_zod39.z.number().int().positive().optional()
7411
7504
  }),
7412
7505
  fromArgv: (argv, path) => ({
7413
7506
  bundlePath: path,
@@ -7429,37 +7522,37 @@ var traceCommand = define({
7429
7522
  });
7430
7523
 
7431
7524
  // src/commands/types.ts
7432
- var import_zod39 = require("zod");
7525
+ var import_zod40 = require("zod");
7433
7526
  var typesCommand = define({
7434
7527
  name: "types",
7435
7528
  tool: "kb_types",
7436
7529
  usage: "types",
7437
7530
  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.",
7438
- input: import_zod39.z.object({}),
7531
+ input: import_zod40.z.object({}),
7439
7532
  fromArgv: () => ({}),
7440
7533
  run: () => Promise.resolve(RECORD_TYPES)
7441
7534
  });
7442
7535
 
7443
7536
  // src/commands/unpin.ts
7444
- var import_zod40 = require("zod");
7537
+ var import_zod41 = require("zod");
7445
7538
  var unpinCommand = define({
7446
7539
  name: "unpin",
7447
7540
  tool: "kb_unpin",
7448
7541
  usage: "unpin [bundle-path]",
7449
7542
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
7450
- input: import_zod40.z.object({ bundlePath }),
7543
+ input: import_zod41.z.object({ bundlePath }),
7451
7544
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
7452
7545
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
7453
7546
  });
7454
7547
 
7455
7548
  // src/commands/validate.ts
7456
- var import_zod41 = require("zod");
7549
+ var import_zod42 = require("zod");
7457
7550
  var validateCommand = define({
7458
7551
  name: "validate",
7459
7552
  tool: "kb_validate",
7460
7553
  usage: "validate",
7461
7554
  description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
7462
- input: import_zod41.z.object({ bundlePath }),
7555
+ input: import_zod42.z.object({ bundlePath }),
7463
7556
  fromArgv: (_argv, path) => ({ bundlePath: path }),
7464
7557
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
7465
7558
  // Warnings never fail the exit code; every other severity does.
@@ -7469,16 +7562,16 @@ var validateCommand = define({
7469
7562
  });
7470
7563
 
7471
7564
  // src/commands/verify.ts
7472
- var import_zod42 = require("zod");
7565
+ var import_zod43 = require("zod");
7473
7566
  var verifyCommand = define({
7474
7567
  name: "verify",
7475
7568
  tool: "kb_verify",
7476
7569
  usage: "verify <concept-id> --note <text>",
7477
7570
  description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
7478
- input: import_zod42.z.object({
7571
+ input: import_zod43.z.object({
7479
7572
  bundlePath,
7480
7573
  conceptId,
7481
- note: import_zod42.z.string().refine((s) => s.trim().length > 0, {
7574
+ note: import_zod43.z.string().refine((s) => s.trim().length > 0, {
7482
7575
  message: "note must say what the check found"
7483
7576
  })
7484
7577
  }),
@@ -7498,15 +7591,15 @@ var verifyCommand = define({
7498
7591
  });
7499
7592
 
7500
7593
  // src/commands/write.ts
7501
- var import_zod43 = require("zod");
7594
+ var import_zod44 = require("zod");
7502
7595
  var writeCommand = define({
7503
7596
  name: "write",
7504
7597
  tool: "kb_write",
7505
7598
  usage: "write <type> < record.json",
7506
7599
  description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
7507
- input: import_zod43.z.object({
7600
+ input: import_zod44.z.object({
7508
7601
  bundlePath,
7509
- type: import_zod43.z.enum(KB_RECORD_TYPES),
7602
+ type: import_zod44.z.enum(KB_RECORD_TYPES),
7510
7603
  input: composeInputSchema
7511
7604
  }),
7512
7605
  fromArgv: async (argv, path, stdin) => ({
@@ -7530,13 +7623,13 @@ var writeCommand = define({
7530
7623
  });
7531
7624
 
7532
7625
  // src/commands/write-decision.ts
7533
- var import_zod44 = require("zod");
7626
+ var import_zod45 = require("zod");
7534
7627
  var writeDecisionCommand = define({
7535
7628
  name: "write-decision",
7536
7629
  tool: "kb_write_decision",
7537
7630
  usage: "write-decision < decision.json",
7538
7631
  description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
7539
- input: import_zod44.z.object({ bundlePath, input: decisionInputSchema }),
7632
+ input: import_zod45.z.object({ bundlePath, input: decisionInputSchema }),
7540
7633
  fromArgv: async (_argv, path, stdin) => ({
7541
7634
  bundlePath: path,
7542
7635
  input: JSON.parse(await stdin())
@@ -8699,7 +8792,7 @@ function assertActor(actor, { named = false } = {}) {
8699
8792
  }
8700
8793
 
8701
8794
  // src/version.ts
8702
- var VERSION = true ? "0.1.22" : "0.0.0-dev";
8795
+ var VERSION = true ? "0.1.23" : "0.0.0-dev";
8703
8796
 
8704
8797
  // src/mcp.ts
8705
8798
  function createKbMcpServer() {