@rayul/kordis 0.15.1 → 0.16.0

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.js CHANGED
@@ -134,12 +134,32 @@ var init_enums = __esm({
134
134
  }
135
135
  });
136
136
 
137
+ // ../../packages/contracts/src/refresh-race.ts
138
+ function isRefreshRace(body) {
139
+ if (typeof body !== "object" || body === null) return false;
140
+ const error = body.error;
141
+ if (typeof error !== "object" || error === null) return false;
142
+ const { code, flags } = error;
143
+ if (code !== "not_authenticated" || !Array.isArray(flags)) return false;
144
+ return flags.some(
145
+ (flag2) => typeof flag2 === "object" && flag2 !== null && flag2.code === REFRESH_RACE_FLAG
146
+ );
147
+ }
148
+ var REFRESH_RACE_FLAG;
149
+ var init_refresh_race = __esm({
150
+ "../../packages/contracts/src/refresh-race.ts"() {
151
+ "use strict";
152
+ REFRESH_RACE_FLAG = "refresh_rotated_by_another_request";
153
+ }
154
+ });
155
+
137
156
  // ../../packages/contracts/src/errors.ts
138
157
  import { z as z3 } from "zod";
139
158
  var ErrorCodeSchema, ERROR_EXIT_CODES, ReviewFlagSchema, ApiErrorSchema;
140
159
  var init_errors = __esm({
141
160
  "../../packages/contracts/src/errors.ts"() {
142
161
  "use strict";
162
+ init_refresh_race();
143
163
  ErrorCodeSchema = z3.enum([
144
164
  "generic_failure",
145
165
  // 1
@@ -1159,16 +1179,33 @@ var init_api = __esm({
1159
1179
  */
1160
1180
  content: z11.string().min(50).max(1e5),
1161
1181
  /**
1162
- * Inline overrides for frontmatter fields. Merged into parsed frontmatter
1163
- * server-side; existing frontmatter wins if both present.
1182
+ * Optional, and refused when it disagrees. core-api stores the title,
1183
+ * description and type it reads from the frontmatter of `content`, which must
1184
+ * carry all three (the title may come from `name:`). A value here that
1185
+ * differs from the frontmatter's, compared trimmed, is refused as
1186
+ * `usage_error` (HTTP 400) with a hint naming the frontmatter key; absent
1187
+ * means no claim. A `type` the registry cannot publish at all (`scoped-rule`,
1188
+ * `mdc-rule`, `plugin`) names no key to write: its hint says the type cannot
1189
+ * be published yet and to drop `--type`. `kordis push` ≤0.15.2 sends
1190
+ * `--type` here, and writes `type: skill` into the frontmatter of a SKILL.md
1191
+ * or *.skill.md file, a folder or a .skill bundle that names no type — so
1192
+ * `--type hook` on one is refused, where until 2026-10 it published a skill.
1193
+ * Later versions send no `type`: they write `--type` into the frontmatter.
1164
1194
  */
1165
1195
  title: z11.string().min(1).max(200).optional(),
1166
1196
  description: z11.string().min(1).max(2048).optional(),
1167
1197
  type: HarnessTypeSchema.optional(),
1198
+ /**
1199
+ * A new package is published under this slug instead of the one derived from
1200
+ * the frontmatter title. An update keeps the slug of the package it updates:
1201
+ * a slug here that differs from the target's is refused (`usage_error`).
1202
+ */
1168
1203
  slug: SlugSchema.optional(),
1169
1204
  /**
1170
- * When updating, the target full_slug. Omitted for new packages (server
1171
- * derives slug from title or accepts inline `slug`).
1205
+ * When updating, the target full_slug; its presence is what makes a publish
1206
+ * an update (`PUT /api/packages/:u/:s` sets it from the path). Omitted for a
1207
+ * new package, whose slug is `slug` or else derived from the frontmatter
1208
+ * title — never derived on an update.
1172
1209
  */
1173
1210
  target_full_slug: FullSlugSchema.optional(),
1174
1211
  /**
@@ -1176,6 +1213,12 @@ var init_api = __esm({
1176
1213
  */
1177
1214
  bump: z11.enum(["patch", "minor", "major"]).optional(),
1178
1215
  changelog: z11.string().max(2e3).optional(),
1216
+ /**
1217
+ * Refused when non-empty (`usage_error`): a package's dependencies are read
1218
+ * from its frontmatter's `depends:` and `recommends:` lists only. Kept in the
1219
+ * schema so the server sees them to refuse them — this object is non-strict,
1220
+ * and an unlisted key would be stripped without a word.
1221
+ */
1179
1222
  depends: z11.array(z11.string()).optional(),
1180
1223
  recommends: z11.array(z11.string()).optional(),
1181
1224
  /**
@@ -1675,9 +1718,122 @@ var init_telemetry = __esm({
1675
1718
  });
1676
1719
 
1677
1720
  // ../../packages/contracts/src/review-patterns.ts
1721
+ var STAGE_1_REJECT_PATTERNS, STAGE_1_5_AUP_PATTERNS, DETERMINISTIC_REJECT_PATTERNS;
1678
1722
  var init_review_patterns = __esm({
1679
1723
  "../../packages/contracts/src/review-patterns.ts"() {
1680
1724
  "use strict";
1725
+ STAGE_1_REJECT_PATTERNS = [
1726
+ {
1727
+ code: "curl_pipe_shell",
1728
+ pattern: /\bcurl\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
1729
+ hint: "Replace `curl \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
1730
+ },
1731
+ {
1732
+ code: "wget_pipe_shell",
1733
+ pattern: /\bwget\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
1734
+ hint: "Replace `wget \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
1735
+ },
1736
+ {
1737
+ code: "shell_process_substitution",
1738
+ pattern: /(?:bash|sh|zsh)\s*<\(\s*(?:curl|wget)\b/i,
1739
+ hint: "Replace `bash <(curl \u2026)` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
1740
+ },
1741
+ {
1742
+ code: "eval_atob",
1743
+ pattern: /\beval\s*\(\s*atob\b/i,
1744
+ hint: "Encoded eval (`eval(atob(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
1745
+ },
1746
+ {
1747
+ code: "eval_buffer_from",
1748
+ pattern: /\beval\s*\(\s*Buffer\.from\b/i,
1749
+ hint: "Encoded eval (`eval(Buffer.from(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
1750
+ },
1751
+ {
1752
+ code: "aws_access_key",
1753
+ pattern: /\b(?:AKIA|ASIA|AGPA)[0-9A-Z]{16}\b/,
1754
+ hint: "Remove the AWS access key from the body. Reference credentials via env var (`AWS_ACCESS_KEY_ID`); rotate the leaked key immediately if it was real."
1755
+ },
1756
+ {
1757
+ code: "private_key_block",
1758
+ pattern: /-----BEGIN\s+(?:[A-Z0-9]+\s+)?PRIVATE\s+KEY(?:\s+BLOCK)?-----/,
1759
+ hint: "Remove the private key block from the body. Reference keys via env var or a secret manager; rotate the leaked key immediately if it was real."
1760
+ },
1761
+ {
1762
+ code: "google_api_key",
1763
+ pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
1764
+ hint: "Remove the Google API key from the body. Reference credentials via env var; rotate the leaked key immediately if it was real."
1765
+ },
1766
+ {
1767
+ code: "slack_token",
1768
+ pattern: /\b(?:xox[abpors]|xapp|xoxe)-[0-9a-zA-Z-]{10,}\b/,
1769
+ hint: "Remove the Slack token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
1770
+ },
1771
+ {
1772
+ code: "github_token",
1773
+ pattern: /\b(?:gh[pousr]_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/,
1774
+ hint: "Remove the GitHub token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
1775
+ }
1776
+ ];
1777
+ STAGE_1_5_AUP_PATTERNS = [
1778
+ {
1779
+ // Stolen-card / bank-credential trafficking storefronts. Anchored to the
1780
+ // trafficking jargon that only appears in carding markets, not in the
1781
+ // defensive "how to detect carding" literature. `dumps` is also an
1782
+ // ordinary developer verb — "this hook dumps vendor files", "dumps store
1783
+ // state", "dumps market data to CSV" all matched the shared noun list —
1784
+ // so after `dumps` only sale framing (`shop`, `for sale`) counts, while
1785
+ // `cvv` and `fullz` keep the full list. Narrowed when comments started
1786
+ // running this table (2026-10-14).
1787
+ code: "aup_stolen_financial_data_market",
1788
+ pattern: /\b(?:cvv|fullz)\s+(?:shop|store|for\s+sale|market|vendor)\b|\bdumps\s+(?:shop|for\s+sale)\b|\bdumps?\s+with\s+pin\b|\bsell(?:ing)?\s+(?:cvv|fullz|dumps)\b/i,
1789
+ hint: "This reads as a storefront for trafficking stolen financial data, which violates the Kordis AUP. If the package is about DETECTING or DEFENDING against card fraud, reword it so it does not read as an offer to buy/sell stolen data."
1790
+ },
1791
+ {
1792
+ // DDoS-for-hire (booter/stresser) services sold on a subscription.
1793
+ code: "aup_ddos_for_hire",
1794
+ pattern: /\b(?:booter|stresser)\s+(?:service|panel|subscription)\b|\b(?:ddos|dstat)\s+for\s+hire\b|\bhire\s+(?:a\s+)?(?:booter|stresser|ddos)\b/i,
1795
+ hint: "This reads as a DDoS-for-hire (booter/stresser) service, which violates the Kordis AUP. Load-testing tooling is fine \u2014 describe it as testing infrastructure you own/are authorized to test, not a for-hire attack service."
1796
+ },
1797
+ {
1798
+ // Malware/stealer/ransomware sold or distributed as a product ("builder
1799
+ // for sale", "buy our stealer"). A skill that DEFENDS against these, or a
1800
+ // malware-analysis skill, does not use buy/sell framing.
1801
+ code: "aup_malware_sale",
1802
+ pattern: /\b(?:ransomware|stealer|infostealer|rat|keylogger|botnet)\s+(?:builder|kit|panel|source)\s+(?:for\s+sale|for\s+rent)\b|\b(?:buy|sell(?:ing)?|rent)\s+(?:our\s+)?(?:ransomware|stealer|infostealer|botnet)\b/i,
1803
+ hint: "This reads as an offer to sell/rent malware, which violates the Kordis AUP. Malware ANALYSIS and DEFENSE content is welcome \u2014 reword so it does not read as distributing a weaponized build."
1804
+ },
1805
+ {
1806
+ // Child sexual abuse material. Unlike the rest of this table this entry is
1807
+ // NOT anchored to solicitation: it matches the spelled-out phrases ("child
1808
+ // porn", "child pornography", "child sexual abuse material") wherever they
1809
+ // appear, child-safety and moderation education included. Only the
1810
+ // acronym "CSAM" on its own does not match ("how platforms detect CSAM
1811
+ // for NCMEC reporting" passes). Kept this broad on purpose (2026-10-14):
1812
+ // comments have no Stage-2 review after this table, so a miss costs more
1813
+ // than a false positive.
1814
+ code: "aup_csam",
1815
+ pattern: /\bchild\s+(?:porn(?:ography)?|sexual\s+abuse\s+material)\b/i,
1816
+ hint: "This reads as solicitation or distribution of child sexual abuse material, which is illegal and violates the Kordis AUP. This is a terminal, non-appealable rejection."
1817
+ },
1818
+ {
1819
+ // Drug / untraceable-weapon brokering with explicit no-id / untraceable /
1820
+ // dark-web fulfillment language — not a bare mention of a substance or
1821
+ // firearm topic (e.g. harm-reduction or policy content does not use this
1822
+ // buy/sell + untraceable-fulfillment framing together).
1823
+ code: "aup_drug_weapon_brokering",
1824
+ pattern: /\b(?:buy|sell(?:ing)?)\b[\s\S]{0,40}\b(?:cocaine|heroin|fentanyl|methamphetamine|untraceable firearms?|untraceable guns?)\b[\s\S]{0,40}\b(?:no id|untraceable|worldwide shipping|dark ?web)\b/i,
1825
+ hint: "This reads as brokering illegal drugs or untraceable weapons for sale, which violates the Kordis AUP. If the package is about DETECTING or REPORTING such activity, reword it so it does not read as an offer to buy/sell."
1826
+ },
1827
+ {
1828
+ // A directive to dox or swat a specific, named/tagged target — not a
1829
+ // bare mention of doxxing/swatting as a topic (e.g. "how to protect
1830
+ // yourself from doxxing" does not match).
1831
+ code: "aup_targeted_harassment_directive",
1832
+ pattern: /\b(?:dox|swat)\s+(?:this (?:guy|girl|person|user)|him|her|@[\w.]+)\b/i,
1833
+ hint: "This reads as a directive to dox or swat a specific target, which violates the Kordis AUP. Remove the targeted-harassment instruction."
1834
+ }
1835
+ ];
1836
+ DETERMINISTIC_REJECT_PATTERNS = [...STAGE_1_REJECT_PATTERNS, ...STAGE_1_5_AUP_PATTERNS];
1681
1837
  }
1682
1838
  });
1683
1839
 
@@ -2443,13 +2599,17 @@ var init_sections = __esm({
2443
2599
 
2444
2600
  // ../../packages/memory-engine/src/schema.ts
2445
2601
  import { z as z22 } from "zod";
2446
- var MappingModeSchema, ScopeSourceSchema, SCOPE_LIMIT, TELEMETRY_ERROR_KINDS, TELEMETRY_MESSAGE_LIMIT, TelemetryErrorPayloadSchema2, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
2602
+ function isGlobalScope(scope) {
2603
+ return scope != null && scope.trim().toLowerCase() === GLOBAL_SCOPE;
2604
+ }
2605
+ var MappingModeSchema, ScopeSourceSchema, SCOPE_LIMIT, GLOBAL_SCOPE, TELEMETRY_ERROR_KINDS, TELEMETRY_MESSAGE_LIMIT, TelemetryErrorPayloadSchema2, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
2447
2606
  var init_schema = __esm({
2448
2607
  "../../packages/memory-engine/src/schema.ts"() {
2449
2608
  "use strict";
2450
2609
  MappingModeSchema = z22.enum(["explicit", "heuristic"]);
2451
2610
  ScopeSourceSchema = z22.enum(["explicit", "path", "none"]);
2452
2611
  SCOPE_LIMIT = 128;
2612
+ GLOBAL_SCOPE = "global";
2453
2613
  TELEMETRY_ERROR_KINDS = [
2454
2614
  "network_unreachable",
2455
2615
  "server_error",
@@ -2529,6 +2689,26 @@ var init_schema = __esm({
2529
2689
  scope: z22.string().min(1).nullable(),
2530
2690
  /** Id of the claim that is current for this memory, when one is declared. */
2531
2691
  current_claim: z22.string().min(1).nullable(),
2692
+ /**
2693
+ * The end date the NOTE declares, for the notes that state no claim.
2694
+ *
2695
+ * `valid_until` lived only on `Claim`, and the mapper mints a claim only for
2696
+ * a note carrying a `claim:` key — which `remember` has never written. So on
2697
+ * a real brain most notes could not carry an end date that anything read:
2698
+ * sampled six binding rules on a production brain and four had no claim at
2699
+ * all, the two that did having been through `revise`. A date in the
2700
+ * frontmatter of the other four was simply ignored.
2701
+ *
2702
+ * Optional rather than nullable, like `kind` and `binding` above and for the
2703
+ * same reason: a note written before this existed must map to exactly the
2704
+ * `Memory` it always did, or the regeneration test's byte comparison rewrites
2705
+ * every cache in existence on upgrade.
2706
+ *
2707
+ * The claim's copy WINS where both exist — it is the more specific statement,
2708
+ * and a note whose conclusion carries its own dates is saying something about
2709
+ * that conclusion rather than about the file.
2710
+ */
2711
+ valid_until: z22.string().min(1).optional(),
2532
2712
  status: MemoryStatusSchema,
2533
2713
  /**
2534
2714
  * Whether this memory binds what an agent does, rather than informing it.
@@ -2610,6 +2790,104 @@ var init_schema = __esm({
2610
2790
  }
2611
2791
  });
2612
2792
 
2793
+ // ../../packages/memory-engine/src/layout.ts
2794
+ var OBSERVATIONS_DIR, DERIVED_DIR, DERIVED_BLUEPRINT;
2795
+ var init_layout = __esm({
2796
+ "../../packages/memory-engine/src/layout.ts"() {
2797
+ "use strict";
2798
+ OBSERVATIONS_DIR = "_observations";
2799
+ DERIVED_DIR = "_derived";
2800
+ DERIVED_BLUEPRINT = "_blueprint.json";
2801
+ }
2802
+ });
2803
+
2804
+ // ../../packages/memory-engine/src/rule-budget.ts
2805
+ function ruleLane(scope, project) {
2806
+ if (scope == null || scope.trim() === "" || isGlobalScope(scope)) return "global";
2807
+ if (project === null) return "elsewhere";
2808
+ return scope.trim().toLowerCase() === project.trim().toLowerCase() ? "project" : "elsewhere";
2809
+ }
2810
+ function declaredScopeOf(memory) {
2811
+ return memory.provenance.scopeSource === "explicit" ? memory.scope : null;
2812
+ }
2813
+ function splitRuleBudget(ranked, scopeOf, project, budget = DEFAULT_RULE_BUDGET) {
2814
+ const globalLane = [];
2815
+ const projectLane = [];
2816
+ const elsewhere = [];
2817
+ for (const item of ranked) {
2818
+ const lane = ruleLane(scopeOf(item), project);
2819
+ if (lane === "global") globalLane.push(item);
2820
+ else if (lane === "project") projectLane.push(item);
2821
+ else elsewhere.push(item);
2822
+ }
2823
+ const total = budget.global + budget.project;
2824
+ const globalRoom = Math.max(budget.global, total - Math.min(projectLane.length, budget.project));
2825
+ const projectRoom = Math.max(budget.project, total - Math.min(globalLane.length, budget.global));
2826
+ return {
2827
+ global: globalLane.slice(0, globalRoom),
2828
+ project: projectLane.slice(0, projectRoom),
2829
+ elsewhere,
2830
+ // Everything either budget COULD have shown, so "5 of 12" is honest about
2831
+ // the cap without counting another project's rules as things withheld.
2832
+ eligible: globalLane.length + projectLane.length
2833
+ };
2834
+ }
2835
+ var DEFAULT_RULE_BUDGET;
2836
+ var init_rule_budget = __esm({
2837
+ "../../packages/memory-engine/src/rule-budget.ts"() {
2838
+ "use strict";
2839
+ init_schema();
2840
+ DEFAULT_RULE_BUDGET = { global: 5, project: 5 };
2841
+ }
2842
+ });
2843
+
2844
+ // ../../packages/memory-engine/src/rule-text.ts
2845
+ function ruleTextOf(entry) {
2846
+ const intro = (entry.chunks ?? []).find((chunk) => chunk.label === null)?.text.trim();
2847
+ if (intro !== void 0 && intro !== "") return intro;
2848
+ return entry.memory.summary ?? entry.memory.title;
2849
+ }
2850
+ function ruleWidthFor(count) {
2851
+ if (count <= 0) return RULE_WIDTH_MIN;
2852
+ const share = Math.floor(RULE_BLOCK_BUDGET / count);
2853
+ return Math.min(RULE_WIDTH_MAX, Math.max(RULE_WIDTH_MIN, share));
2854
+ }
2855
+ function rulePassage(text, max) {
2856
+ const flat = text.replace(/\s+/g, " ").trim();
2857
+ const chars = [...flat];
2858
+ if (chars.length <= max) return flat;
2859
+ if (max <= 1) return "\u2026";
2860
+ const budget = max - 1;
2861
+ if (budget < 2) return `${chars.slice(0, budget).join("")}\u2026`;
2862
+ const tailBudget = Math.min(budget - 1, Math.max(1, Math.floor(budget * 0.6)));
2863
+ const tail = [...trailingSentences(flat, tailBudget)];
2864
+ const head = chars.slice(0, budget - tail.length).join("").trimEnd();
2865
+ return `${head}\u2026${tail.join("")}`;
2866
+ }
2867
+ function trailingSentences(flat, budget) {
2868
+ const starts = [...flat.matchAll(/[.!?。!?](?=\s)/g)].map(
2869
+ (match) => match.index + match[0].length
2870
+ );
2871
+ let best = null;
2872
+ for (let i = starts.length - 1; i >= 0; i -= 1) {
2873
+ const candidate = flat.slice(starts[i]).trim();
2874
+ if ([...candidate].length > budget) break;
2875
+ if (candidate !== "") best = candidate;
2876
+ }
2877
+ if (best !== null) return best;
2878
+ return [...flat].slice(-budget).join("");
2879
+ }
2880
+ var RULE_WIRE_WIDTH, RULE_WIDTH_MIN, RULE_WIDTH_MAX, RULE_BLOCK_BUDGET;
2881
+ var init_rule_text = __esm({
2882
+ "../../packages/memory-engine/src/rule-text.ts"() {
2883
+ "use strict";
2884
+ RULE_WIRE_WIDTH = 600;
2885
+ RULE_WIDTH_MIN = 200;
2886
+ RULE_WIDTH_MAX = 600;
2887
+ RULE_BLOCK_BUDGET = RULE_WIDTH_MIN * 10;
2888
+ }
2889
+ });
2890
+
2613
2891
  // ../../packages/memory-engine/src/boundary.ts
2614
2892
  function normalizeForMatch(value) {
2615
2893
  return value.normalize("NFC").replace(ZERO_WIDTH, "").replace(WHITESPACE_RUN, " ").trim();
@@ -2753,8 +3031,8 @@ function scopeFromPath(path2) {
2753
3031
  return parts.length >= 2 ? parts[parts.length - 2] ?? null : null;
2754
3032
  }
2755
3033
  function titleFromPath(path2) {
2756
- const base = path2.split("/").pop() ?? path2;
2757
- return base.replace(/\.[^.]+$/, "") || base;
3034
+ const base2 = path2.split("/").pop() ?? path2;
3035
+ return base2.replace(/\.[^.]+$/, "") || base2;
2758
3036
  }
2759
3037
  function h1Of(intro) {
2760
3038
  for (const line of intro.split("\n")) {
@@ -2868,8 +3146,8 @@ function mapNoteToMemory(input) {
2868
3146
  const ns = explicit ? nsRaw : {};
2869
3147
  const id = asString(ns["id"]) ?? path2;
2870
3148
  const title = asString(ns["title"]) ?? asString(frontmatter?.["title"]) ?? h1Of(outline.intro) ?? titleFromPath(path2);
2871
- const declaredScope = asString(ns["scope"]);
2872
- const scope = declaredScope ?? scopeFromPath(path2);
3149
+ const declaredScope2 = asString(ns["scope"]);
3150
+ const scope = declaredScope2 ?? scopeFromPath(path2);
2873
3151
  const summary = asString(ns["summary"]) ?? outline.description ?? firstParagraph(outline.intro) ?? firstSectionParagraph(outline.sections) ?? null;
2874
3152
  const memory = {
2875
3153
  id,
@@ -2913,11 +3191,17 @@ function mapNoteToMemory(input) {
2913
3191
  aliases: asStringArray(ns["aliases"]),
2914
3192
  entities: asStringArray(ns["entities"]),
2915
3193
  updated_at: asString(ns["updated_at"]) ?? asString(frontmatter?.["updated_at"]),
3194
+ // Carried onto the memory as well as onto the claim below, because a note
3195
+ // that states no claim gets no claim object at all — and `remember` has
3196
+ // never written a `claim:` key, so that is most of a real brain. Written
3197
+ // only when present, so a note without one maps byte-identically to what
3198
+ // it always did.
3199
+ ...asString(ns["valid_until"]) === null ? {} : { valid_until: asString(ns["valid_until"]) },
2916
3200
  provenance: {
2917
3201
  sourcePath: path2,
2918
3202
  sourceRevision: revision,
2919
3203
  mapping: explicit ? "explicit" : "heuristic",
2920
- scopeSource: declaredScope !== null ? "explicit" : scope === null ? "none" : "path"
3204
+ scopeSource: declaredScope2 !== null ? "explicit" : scope === null ? "none" : "path"
2921
3205
  }
2922
3206
  };
2923
3207
  const claims = [];
@@ -2943,22 +3227,6 @@ function mapNoteToMemory(input) {
2943
3227
  issues
2944
3228
  };
2945
3229
  }
2946
- function isUntouchedBase(path2, content) {
2947
- if (!/^01_BASE\//.test(path2)) return false;
2948
- const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
2949
- const lines = body.split(/\r?\n/).map((l) => l.trimEnd());
2950
- let seenHeading = false;
2951
- for (const line of lines) {
2952
- if (line.trim() === "") continue;
2953
- if (!seenHeading && line.startsWith("# ")) {
2954
- seenHeading = true;
2955
- continue;
2956
- }
2957
- if (/^-\s*$/.test(line)) continue;
2958
- if (/^[-*+]\s+\S/.test(line) || /^#{1,6}\s/.test(line)) return false;
2959
- }
2960
- return seenHeading;
2961
- }
2962
3230
  var NAMESPACE, isRecord, asString, asStringArray, AUTHORABLE;
2963
3231
  var init_markdown_mapping = __esm({
2964
3232
  "../../packages/memory-engine/src/markdown-mapping.ts"() {
@@ -3048,7 +3316,7 @@ var init_cache_io = __esm({
3048
3316
  "../../packages/memory-engine/src/view/cache-io.ts"() {
3049
3317
  "use strict";
3050
3318
  init_schema();
3051
- SCHEMA_VERSION = 6;
3319
+ SCHEMA_VERSION = 7;
3052
3320
  POLICY_VERSION = 1;
3053
3321
  CacheEntrySchema = z23.object({
3054
3322
  path: z23.string().min(1),
@@ -3663,6 +3931,12 @@ function terms(text) {
3663
3931
  (match) => splitMixedScript(match[0])
3664
3932
  );
3665
3933
  }
3934
+ function contentTerms(text) {
3935
+ const all = terms(text);
3936
+ if (all.some((t) => HANGUL.test(t))) return all;
3937
+ const content = all.filter((t) => !LATIN_STOPWORDS.has(t));
3938
+ return content.length > 0 ? content : all;
3939
+ }
3666
3940
  function indexTerms(text) {
3667
3941
  return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap((match) => {
3668
3942
  const token = match[0];
@@ -3735,8 +4009,9 @@ function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false)
3735
4009
  return score;
3736
4010
  }
3737
4011
  function searchMemories(memories, query) {
3738
- const q = terms(query);
4012
+ const q = contentTerms(query);
3739
4013
  if (q.length === 0) return [];
4014
+ const scale = terms(query).length;
3740
4015
  const scored = [];
3741
4016
  for (const memory of memories) {
3742
4017
  let score = fieldScore(q, memory.title, WEIGHTS.title, true);
@@ -3744,13 +4019,13 @@ function searchMemories(memories, query) {
3744
4019
  for (const entity of memory.entities) score += fieldScore(q, entity, WEIGHTS.entity);
3745
4020
  score += fieldScore(q, memory.summary, WEIGHTS.summary);
3746
4021
  score += fieldScore(q, memory.scope, WEIGHTS.scope);
3747
- if (score > 0) scored.push({ memory, score: score / q.length });
4022
+ if (score > 0) scored.push({ memory, score: score / scale });
3748
4023
  }
3749
4024
  return scored.sort(
3750
4025
  (a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
3751
4026
  );
3752
4027
  }
3753
- var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
4028
+ var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, LATIN_STOPWORDS, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
3754
4029
  var init_search = __esm({
3755
4030
  "../../packages/memory-engine/src/recall/search.ts"() {
3756
4031
  "use strict";
@@ -3759,6 +4034,120 @@ var init_search = __esm({
3759
4034
  COMPONENT_SEPARATOR = /[._/-]+/u;
3760
4035
  HAS_COMPONENT_SEPARATOR = /[._/-]/u;
3761
4036
  LATIN_HANGUL_BOUNDARY = new RegExp("(?<=\\p{Script=Latin})(?=\\p{Script=Hangul})|(?<=\\p{Script=Hangul})(?=\\p{Script=Latin})", "u");
4037
+ LATIN_STOPWORDS = /* @__PURE__ */ new Set([
4038
+ // pronouns and possessives
4039
+ "i",
4040
+ "me",
4041
+ "my",
4042
+ "mine",
4043
+ "myself",
4044
+ "we",
4045
+ "us",
4046
+ "our",
4047
+ "ours",
4048
+ "ourselves",
4049
+ "you",
4050
+ "your",
4051
+ "yours",
4052
+ "yourself",
4053
+ "yourselves",
4054
+ "he",
4055
+ "him",
4056
+ "his",
4057
+ "himself",
4058
+ "she",
4059
+ "her",
4060
+ "hers",
4061
+ "herself",
4062
+ "it",
4063
+ "its",
4064
+ "itself",
4065
+ "they",
4066
+ "them",
4067
+ "their",
4068
+ "theirs",
4069
+ "themselves",
4070
+ // determiners
4071
+ "a",
4072
+ "an",
4073
+ "the",
4074
+ "this",
4075
+ "that",
4076
+ "these",
4077
+ "those",
4078
+ "some",
4079
+ "any",
4080
+ "each",
4081
+ "every",
4082
+ // auxiliaries and modals
4083
+ "am",
4084
+ "is",
4085
+ "are",
4086
+ "was",
4087
+ "were",
4088
+ "be",
4089
+ "been",
4090
+ "being",
4091
+ "do",
4092
+ "does",
4093
+ "did",
4094
+ "have",
4095
+ "has",
4096
+ "had",
4097
+ "will",
4098
+ "would",
4099
+ "shall",
4100
+ "should",
4101
+ "can",
4102
+ "could",
4103
+ "may",
4104
+ "might",
4105
+ "must",
4106
+ // question words
4107
+ "what",
4108
+ "which",
4109
+ "who",
4110
+ "whom",
4111
+ "whose",
4112
+ "when",
4113
+ "where",
4114
+ "why",
4115
+ "how",
4116
+ // prepositions
4117
+ "about",
4118
+ "above",
4119
+ "after",
4120
+ "at",
4121
+ "before",
4122
+ "by",
4123
+ "for",
4124
+ "from",
4125
+ "in",
4126
+ "into",
4127
+ "of",
4128
+ "off",
4129
+ "on",
4130
+ "onto",
4131
+ "over",
4132
+ "to",
4133
+ "under",
4134
+ "with",
4135
+ "without",
4136
+ "through",
4137
+ "during",
4138
+ // conjunctions and particles
4139
+ "and",
4140
+ "but",
4141
+ "or",
4142
+ "nor",
4143
+ "so",
4144
+ "if",
4145
+ "then",
4146
+ "than",
4147
+ "as",
4148
+ "there",
4149
+ "here"
4150
+ ]);
3762
4151
  SUBSTRING_MIN = 2;
3763
4152
  PARTICLES = [
3764
4153
  "\uC774\uB77C\uB3C4",
@@ -3909,7 +4298,7 @@ function freshnessOf(memory, claims, now) {
3909
4298
  if (memory.status === "superseded") return "superseded";
3910
4299
  if (memory.status === "retired") return "retired";
3911
4300
  const current = claims.find((c2) => c2.id === memory.current_claim) ?? claims[0];
3912
- const until = current?.valid_until;
4301
+ const until = current?.valid_until ?? memory.valid_until ?? null;
3913
4302
  if (until != null) {
3914
4303
  const t = Date.parse(until);
3915
4304
  if (Number.isFinite(t) && t < now.getTime()) return "expired";
@@ -3939,7 +4328,23 @@ function recall(entries, query, opts = {}) {
3939
4328
  (e) => (e.chunks ?? []).map((c2) => ({ memoryId: e.memory.id, label: c2.label, text: c2.text }))
3940
4329
  )
3941
4330
  ).search(queryShape.lexical);
3942
- const semanticHits = depth === "exact" ? [] : opts.semantic ?? [];
4331
+ const offeredSemantic = depth === "exact" ? [] : opts.semantic ?? [];
4332
+ const reachedByWords = /* @__PURE__ */ new Set([
4333
+ ...cardHits.map((hit) => hit.memory.id),
4334
+ ...chunkHits.map((hit) => hit.memoryId)
4335
+ ]);
4336
+ const floor = opts.meaningFloor;
4337
+ const bestSimilarity = offeredSemantic.reduce(
4338
+ (best, hit) => Math.max(best, Math.min(1, Math.max(0, hit.similarity))),
4339
+ 0
4340
+ );
4341
+ const semanticHits = floor === void 0 ? offeredSemantic : offeredSemantic.filter((hit) => {
4342
+ if (reachedByWords.has(hit.id)) return true;
4343
+ const s = Math.min(1, Math.max(0, hit.similarity));
4344
+ if (floor.absolute !== void 0 && s < floor.absolute) return false;
4345
+ if (floor.gap !== void 0 && bestSimilarity - s > floor.gap) return false;
4346
+ return true;
4347
+ });
3943
4348
  const scoreById = /* @__PURE__ */ new Map();
3944
4349
  for (const hit of cardHits) scoreById.set(hit.memory.id, hit.score);
3945
4350
  for (const hit of chunkHits) {
@@ -3949,6 +4354,14 @@ function recall(entries, query, opts = {}) {
3949
4354
  const similarity = Math.min(1, Math.max(0, hit.similarity));
3950
4355
  scoreById.set(hit.id, (scoreById.get(hit.id) ?? 0) + similarity * SEMANTIC_WEIGHT);
3951
4356
  }
4357
+ const wordIds = reachedByWords;
4358
+ const meaningIds = new Set(semanticHits.map((hit) => hit.id));
4359
+ const matchedOf = (id) => {
4360
+ if (opts.semantic === void 0 || depth === "exact") return void 0;
4361
+ const words = wordIds.has(id);
4362
+ const meaning = meaningIds.has(id);
4363
+ return words && meaning ? "both" : words ? "words" : meaning ? "meaning" : void 0;
4364
+ };
3952
4365
  const memoryById = new Map(allMemories.map((m) => [m.id, m]));
3953
4366
  const hits = [...scoreById.entries()].flatMap(([id, score]) => {
3954
4367
  const memory = memoryById.get(id);
@@ -4049,7 +4462,11 @@ function recall(entries, query, opts = {}) {
4049
4462
  type: memory.type,
4050
4463
  // The handle IS the progressive part. No claim text here, by
4051
4464
  // construction rather than by remembering to strip it.
4052
- detail_handle: `recall://${memory.id}`
4465
+ detail_handle: `recall://${memory.id}`,
4466
+ ...(() => {
4467
+ const matched = matchedOf(memory.id);
4468
+ return matched === void 0 ? {} : { matched };
4469
+ })()
4053
4470
  };
4054
4471
  }
4055
4472
  }
@@ -4080,7 +4497,7 @@ function resolveDetail(entries, handle) {
4080
4497
  const entry = entries.find((e) => e.memory.id === id);
4081
4498
  return entry ? { memory: entry.memory, claims: entry.claims, relations: entry.relations } : null;
4082
4499
  }
4083
- var RECALL_DEPTHS, RecallOptionError, SEMANTIC_CANDIDATES, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
4500
+ var MEANING_ONLY_NOTE, RECALL_DEPTHS, RecallOptionError, SEMANTIC_CANDIDATES, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
4084
4501
  var init_planner = __esm({
4085
4502
  "../../packages/memory-engine/src/recall/planner.ts"() {
4086
4503
  "use strict";
@@ -4089,6 +4506,7 @@ var init_planner = __esm({
4089
4506
  init_conflicts();
4090
4507
  init_chunks();
4091
4508
  init_search();
4509
+ MEANING_ONLY_NOTE = "matched by meaning only \u2014 none of the question\u2019s words are in this note";
4092
4510
  RECALL_DEPTHS = ["exact", "standard"];
4093
4511
  RecallOptionError = class extends Error {
4094
4512
  hint;
@@ -4175,18 +4593,164 @@ var init_planner = __esm({
4175
4593
  }
4176
4594
  });
4177
4595
 
4596
+ // ../../packages/memory-engine/src/note-body.ts
4597
+ function bodyOf(content) {
4598
+ const stripped = content.startsWith(BOM) ? content.slice(BOM.length) : content;
4599
+ const fence2 = FENCE.exec(stripped);
4600
+ return fence2 ? stripped.slice(fence2[0].length) : stripped;
4601
+ }
4602
+ var BOM, FENCE;
4603
+ var init_note_body = __esm({
4604
+ "../../packages/memory-engine/src/note-body.ts"() {
4605
+ "use strict";
4606
+ BOM = String.fromCharCode(65279);
4607
+ FENCE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/;
4608
+ }
4609
+ });
4610
+
4611
+ // ../../packages/memory-engine/src/base-templates.ts
4612
+ function base(id, title, summary, body) {
4613
+ return [
4614
+ "---",
4615
+ "memgineering:",
4616
+ ` id: ${id}`,
4617
+ ` title: ${title}`,
4618
+ ` summary: ${summary}`,
4619
+ " scope: base",
4620
+ "---",
4621
+ "",
4622
+ `# ${title}`,
4623
+ "",
4624
+ body,
4625
+ ""
4626
+ ].join("\n");
4627
+ }
4628
+ function ownEntry(record, key2) {
4629
+ return Object.hasOwn(record, key2) ? record[key2] : void 0;
4630
+ }
4631
+ function wordingsFor(path2) {
4632
+ const current = ownEntry(BASE_TEMPLATES, path2);
4633
+ const retired = ownEntry(RETIRED_BASE_TEMPLATES, path2) ?? [];
4634
+ return current === void 0 ? retired : [current, ...retired];
4635
+ }
4636
+ function isExactUntouchedBase(path2, content) {
4637
+ const saved = asSaved(content);
4638
+ return wordingsFor(path2).some((t) => asSaved(t) === saved);
4639
+ }
4640
+ function isTemplateBody(path2, content) {
4641
+ const lines = bodyLines(content).join("\n");
4642
+ return wordingsFor(path2).some((t) => bodyLines(t).join("\n") === lines);
4643
+ }
4644
+ function isUnwrittenBody(path2, content) {
4645
+ if (isTemplateBody(path2, content)) return true;
4646
+ const [first, ...rest] = bodyLines(content);
4647
+ if (first === void 0) return false;
4648
+ const headings = wordingsFor(path2).map((t) => bodyLines(t)[0]);
4649
+ return headings.includes(first) && rest.every((line) => /^-\s*$/.test(line));
4650
+ }
4651
+ var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES, BOM2, asSaved, bodyLines;
4652
+ var init_base_templates = __esm({
4653
+ "../../packages/memory-engine/src/base-templates.ts"() {
4654
+ "use strict";
4655
+ init_note_body();
4656
+ BASE_TEMPLATES = {
4657
+ "01_BASE/CRITICAL_FACTS.md": base(
4658
+ "critical-facts",
4659
+ "Critical facts",
4660
+ "The few things that must never be wrong.",
4661
+ [
4662
+ "Keep this short enough to read every session. Anything that can be",
4663
+ "looked up when needed belongs in a note instead.",
4664
+ "",
4665
+ "- "
4666
+ ].join("\n")
4667
+ ),
4668
+ "01_BASE/USER.md": base(
4669
+ "user",
4670
+ "About me",
4671
+ "Who I am and what context I work in.",
4672
+ [
4673
+ "Background, role, what you are working on, what an agent should assume",
4674
+ "you already know.",
4675
+ "",
4676
+ "- "
4677
+ ].join("\n")
4678
+ ),
4679
+ "01_BASE/PREFERENCES.md": base(
4680
+ "preferences",
4681
+ "Preferences",
4682
+ "How I like answers, code and decisions handled.",
4683
+ ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
4684
+ ),
4685
+ "01_BASE/BOUNDARIES.md": base(
4686
+ "boundaries",
4687
+ "Boundaries",
4688
+ "What an agent must not do without asking me first.",
4689
+ [
4690
+ "Privacy, spending, anything irreversible, anything that leaves this",
4691
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
4692
+ "that will be interpreted generously.",
4693
+ "",
4694
+ "Storage boundaries belong here too: topics you never want written into",
4695
+ "this brain \u2014 health, beliefs, relationships, anything you name. An",
4696
+ "agent checks this file before it records anything.",
4697
+ "",
4698
+ "- "
4699
+ ].join("\n")
4700
+ ),
4701
+ "01_BASE/TOOLING.md": base(
4702
+ "tooling",
4703
+ "Tooling",
4704
+ "The tools, machines and commands I actually use.",
4705
+ [
4706
+ "Editors, languages, deploy commands, where things run. Enough that an",
4707
+ "agent does not have to guess or discover it again each time.",
4708
+ "",
4709
+ "- "
4710
+ ].join("\n")
4711
+ )
4712
+ };
4713
+ RETIRED_BASE_TEMPLATES = {
4714
+ // Retired 0.11.1, when the prompt grew a storage-boundary invitation.
4715
+ "01_BASE/BOUNDARIES.md": [
4716
+ [
4717
+ "---",
4718
+ "memgineering:",
4719
+ " id: boundaries",
4720
+ " title: Boundaries",
4721
+ " summary: What an agent must not do without asking me first.",
4722
+ " scope: base",
4723
+ "---",
4724
+ "",
4725
+ "# Boundaries",
4726
+ "",
4727
+ "Privacy, spending, anything irreversible, anything that leaves this",
4728
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
4729
+ "that will be interpreted generously.",
4730
+ "",
4731
+ "- ",
4732
+ ""
4733
+ ].join("\n")
4734
+ ]
4735
+ };
4736
+ BOM2 = String.fromCharCode(65279);
4737
+ asSaved = (text) => (text.startsWith(BOM2) ? text.slice(BOM2.length) : text).replace(/\r\n/g, "\n").trimEnd();
4738
+ bodyLines = (content) => bodyOf(content).split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
4739
+ }
4740
+ });
4741
+
4178
4742
  // ../../packages/memory-engine/src/recall/semantic.ts
4179
- function withoutUntouchedBase(hits, contentById) {
4743
+ function withoutUntouchedBase(hits, contentById, isTemplate = isExactUntouchedBase) {
4180
4744
  return hits.filter((hit) => {
4181
4745
  const note = contentById.get(hit.id);
4182
- return note === void 0 ? true : !isUntouchedBase(note.path, note.content);
4746
+ return note === void 0 ? true : !isTemplate(note.path, note.content);
4183
4747
  });
4184
4748
  }
4185
4749
  var SEMANTIC_FIELDS;
4186
4750
  var init_semantic = __esm({
4187
4751
  "../../packages/memory-engine/src/recall/semantic.ts"() {
4188
4752
  "use strict";
4189
- init_markdown_mapping();
4753
+ init_base_templates();
4190
4754
  SEMANTIC_FIELDS = {
4191
4755
  /** `used` or `skipped`. Always present on a server that has this at all. */
4192
4756
  state: "semantic",
@@ -4200,6 +4764,103 @@ var init_semantic = __esm({
4200
4764
  }
4201
4765
  });
4202
4766
 
4767
+ // ../../packages/memory-engine/src/recall/unfilled.ts
4768
+ function markUnfilled(cards, contentById, slots, inConflict = /* @__PURE__ */ new Set()) {
4769
+ const kept = [];
4770
+ const withheld = [];
4771
+ let templates = 0;
4772
+ for (const card of cards) {
4773
+ const note = contentById.get(card.id);
4774
+ const unfilled = note !== void 0 && isExactUntouchedBase(note.path, note.content);
4775
+ if (unfilled && slots !== void 0 && !inConflict.has(card.id) && ++templates > slots) {
4776
+ withheld.push(card.id);
4777
+ continue;
4778
+ }
4779
+ kept.push({ ...card, unfilled });
4780
+ }
4781
+ return { cards: kept, withheld };
4782
+ }
4783
+ var UNFILLED_FIELDS;
4784
+ var init_unfilled = __esm({
4785
+ "../../packages/memory-engine/src/recall/unfilled.ts"() {
4786
+ "use strict";
4787
+ init_base_templates();
4788
+ UNFILLED_FIELDS = {
4789
+ /** On every card from a server that has this: still the starter file, nothing written in it. */
4790
+ card: "unfilled",
4791
+ /**
4792
+ * In the request: how many such cards the caller will take. Absent means no
4793
+ * cap, so a client older than the field gets the whole ranking it always got —
4794
+ * the same boundary `budget` draws: the client states the capability.
4795
+ */
4796
+ slots: "template_slots",
4797
+ /** In the response, only when the cap left something out: how many. */
4798
+ withheld: "templates_withheld"
4799
+ };
4800
+ }
4801
+ });
4802
+
4803
+ // ../../packages/memory-engine/src/setup-progress.ts
4804
+ var SETUP_PROGRESS_CODES;
4805
+ var init_setup_progress = __esm({
4806
+ "../../packages/memory-engine/src/setup-progress.ts"() {
4807
+ "use strict";
4808
+ SETUP_PROGRESS_CODES = [
4809
+ /** Installing the guidance and settings. */
4810
+ "writing_config",
4811
+ /** Uploading the chosen folder's notes. */
4812
+ "uploading",
4813
+ /** An upload chosen on the screen, held until somebody at the terminal checks the account. */
4814
+ "upload_waits_for_check",
4815
+ /** Finished. */
4816
+ "done",
4817
+ /** This machine is signed in to a different account; nothing was changed. */
4818
+ "different_account",
4819
+ /** Failed for another reason; `detail` says which. */
4820
+ "failed"
4821
+ ];
4822
+ }
4823
+ });
4824
+
4825
+ // ../../packages/memory-engine/src/recall/query-prefix.ts
4826
+ var QUERY_EMBEDDING_PREFIX;
4827
+ var init_query_prefix = __esm({
4828
+ "../../packages/memory-engine/src/recall/query-prefix.ts"() {
4829
+ "use strict";
4830
+ QUERY_EMBEDDING_PREFIX = "task: search result | query: ";
4831
+ }
4832
+ });
4833
+
4834
+ // ../../packages/memory-engine/src/recall/scope-spread.ts
4835
+ function declaredScope(memory) {
4836
+ return memory.provenance?.scopeSource === "explicit" ? memory.scope ?? null : null;
4837
+ }
4838
+ function scopeSpread(returned, brain) {
4839
+ const scoped = returned.filter((s) => typeof s === "string" && s !== "");
4840
+ const distinct = new Set(scoped);
4841
+ if (distinct.size !== 1) return null;
4842
+ const only = [...distinct][0];
4843
+ const others = /* @__PURE__ */ new Set();
4844
+ for (const scope of brain) if (scope && scope !== only) others.add(scope);
4845
+ if (others.size === 0) return null;
4846
+ const sorted = [...others].sort((a, b) => a.localeCompare(b));
4847
+ return {
4848
+ scope: only,
4849
+ cards: scoped.length,
4850
+ of: returned.length,
4851
+ elsewhere: sorted.slice(0, ELSEWHERE_SHOWN),
4852
+ elsewhere_total: sorted.length
4853
+ };
4854
+ }
4855
+ var SCOPE_SPREAD_FIELD, ELSEWHERE_SHOWN;
4856
+ var init_scope_spread = __esm({
4857
+ "../../packages/memory-engine/src/recall/scope-spread.ts"() {
4858
+ "use strict";
4859
+ SCOPE_SPREAD_FIELD = "scope_spread";
4860
+ ELSEWHERE_SHOWN = 5;
4861
+ }
4862
+ });
4863
+
4203
4864
  // ../../packages/memory-engine/src/proposal/schema.ts
4204
4865
  import { z as z24 } from "zod";
4205
4866
  function isLifecycleAction(action) {
@@ -4208,8 +4869,17 @@ function isLifecycleAction(action) {
4208
4869
  function isClassifyAction(action) {
4209
4870
  return action === "reclassify";
4210
4871
  }
4872
+ function isBindingAction(action) {
4873
+ return action === "unbind";
4874
+ }
4875
+ function isScopeAction(action) {
4876
+ return action === "rescope";
4877
+ }
4878
+ function isValidityAction(action) {
4879
+ return action === "expire";
4880
+ }
4211
4881
  function carriesNoClaim(action) {
4212
- return isLifecycleAction(action) || isClassifyAction(action);
4882
+ return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action) || isScopeAction(action) || isValidityAction(action);
4213
4883
  }
4214
4884
  function isAccepted(value) {
4215
4885
  return typeof value === "object" && value !== null && value[ACCEPTED] === true;
@@ -4238,6 +4908,47 @@ var init_schema2 = __esm({
4238
4908
  "retire",
4239
4909
  "unretire",
4240
4910
  "reclassify",
4911
+ /**
4912
+ * A memory stops binding. Nothing else about it changes.
4913
+ *
4914
+ * It exists because there was no way to do this without saying something
4915
+ * else. Taking a marking back meant `revise --claim`, which is a verb about
4916
+ * conclusions — so unbinding thirteen rules on a real hosted brain required
4917
+ * restating thirteen conclusions the author had not changed, from text the
4918
+ * CLI hands back TRUNCATED. The person doing it read the note bodies out of
4919
+ * the database to avoid corrupting them, which is not a thing a user can do.
4920
+ *
4921
+ * One direction on purpose. `unbind` that could also bind would be a name
4922
+ * that lies half the time; marking something as a rule is already reachable
4923
+ * (`remember --rule`, or a `revise` that is genuinely saying something new).
4924
+ */
4925
+ "unbind",
4926
+ /**
4927
+ * Where a memory binds changes. Nothing it SAYS changes.
4928
+ *
4929
+ * `scope` is read by `declaredScopeOf` and decides which of the two rule
4930
+ * budgets a decision is drawn from, so it is the field that answers "does
4931
+ * this bind me here". Nothing could write it. The one live consequence:
4932
+ * folding a project-scoped rule into an unscoped one required superseding
4933
+ * it, which retires the loser — so the rule would have vanished everywhere
4934
+ * except the project it was scoped to.
4935
+ *
4936
+ * The value is a scope NAME, or the reserved word `global`. Never null: see
4937
+ * the refinement below for why "absent" and "widen" must not be the same
4938
+ * value on the wire.
4939
+ */
4940
+ "rescope",
4941
+ /**
4942
+ * A memory gets an end date. It is not retired, and it still recalls.
4943
+ *
4944
+ * `freshnessOf` has always returned `expired` for a claim whose
4945
+ * `valid_until` has passed, and `CURRENCY_RANK` has always ranked that just
4946
+ * below `current` — read, ranked, and labelled, with nothing able to write
4947
+ * it. A date-stamped fact ("Phase 1 lands by the end of October") therefore
4948
+ * had two options, both wrong: retire it, which says it no longer holds, or
4949
+ * leave it, which says it holds forever.
4950
+ */
4951
+ "expire",
4241
4952
  "ignore"
4242
4953
  ]);
4243
4954
  StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
@@ -4397,6 +5108,37 @@ var init_schema2 = __esm({
4397
5108
  * reclassify is pending — approve, reject, or let it expire first.
4398
5109
  */
4399
5110
  proposed_kind: MemoryKindSchema.nullable().default(null),
5111
+ /**
5112
+ * Where `rescope` moves the memory to, and null for everything else.
5113
+ *
5114
+ * NEVER null to mean "everywhere". That was the first design and it is the
5115
+ * one thing this field must not do: "the caller sent no scope" and "the
5116
+ * caller asked to widen this rule" would be the same bytes, so the schema
5117
+ * could not require a scope on a rescope — and a request that LOST the
5118
+ * field, an older client, a hand-built body, a proposal rebuilt from disk,
5119
+ * would silently widen what a rule binds instead of failing.
5120
+ *
5121
+ * `global` is the word instead, and it is not invented here: `GLOBAL_SCOPE`
5122
+ * is already the reserved value a rule write uses to say it holds
5123
+ * everywhere, and `remember` already stores it as no scope at all. So the
5124
+ * widening is spelled, the refinement below can demand it, and the patcher
5125
+ * still writes what the thirty global rules already on a real brain look
5126
+ * like — no key.
5127
+ *
5128
+ * COMPATIBILITY BOUNDARY, as for `proposed_kind`: a proposal file carrying
5129
+ * `action: "rescope"` does not parse on a CLI built before this, and
5130
+ * `loadProposal` throws rather than silently dropping it.
5131
+ */
5132
+ proposed_scope: z24.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
5133
+ /**
5134
+ * The end date `expire` writes onto the current claim, null elsewhere.
5135
+ *
5136
+ * A date rather than a status: `freshnessOf` reads `valid_until` off the
5137
+ * claim and returns `expired`, which recall already labels and ranks below
5138
+ * `current`. Nothing here decides what expiry MEANS — that was decided
5139
+ * years of code ago and only the writer was missing.
5140
+ */
5141
+ proposed_valid_until: z24.string().min(1).nullable().default(null),
4400
5142
  relations: ProposedRelationsSchema,
4401
5143
  /** Why the AI thinks this change is right. Shown at approval time. */
4402
5144
  reason: z24.string().min(1),
@@ -4433,7 +5175,7 @@ var init_schema2 = __esm({
4433
5175
  path: ["source_excerpt"]
4434
5176
  }
4435
5177
  ).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
4436
- message: "only retire, unretire and reclassify may omit the claim",
5178
+ message: "only retire, unretire, reclassify, unbind, rescope and expire may omit the claim",
4437
5179
  path: ["claim"]
4438
5180
  }).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
4439
5181
  message: "proposed_kind belongs to reclassify, and reclassify requires one",
@@ -4441,6 +5183,18 @@ var init_schema2 = __esm({
4441
5183
  }).refine((p) => !isClassifyAction(p.action) || p.claim === null, {
4442
5184
  message: "reclassify says what sort of memory this is; it does not take a claim",
4443
5185
  path: ["claim"]
5186
+ }).refine((p) => isScopeAction(p.action) === (p.proposed_scope !== null), {
5187
+ message: "proposed_scope belongs to rescope, and rescope requires one",
5188
+ path: ["proposed_scope"]
5189
+ }).refine((p) => isValidityAction(p.action) === (p.proposed_valid_until !== null), {
5190
+ message: "proposed_valid_until belongs to expire, and expire requires one",
5191
+ path: ["proposed_valid_until"]
5192
+ }).refine((p) => !isScopeAction(p.action) || p.claim === null, {
5193
+ message: "rescope says where a memory binds; it does not take a claim",
5194
+ path: ["claim"]
5195
+ }).refine((p) => !isValidityAction(p.action) || p.claim === null, {
5196
+ message: "expire says when a memory stops being current; it does not take a claim",
5197
+ path: ["claim"]
4444
5198
  });
4445
5199
  ACCEPTED = /* @__PURE__ */ Symbol("memgineering.approved");
4446
5200
  }
@@ -4490,12 +5244,42 @@ function detectConsolidation(entries, opts = {}) {
4490
5244
  ...titles
4491
5245
  ];
4492
5246
  const out = rankCandidates(scored);
5247
+ const limit = opts.limit ?? DEFAULT_LIMIT;
4493
5248
  return {
4494
- candidates: out.slice(0, opts.limit ?? DEFAULT_LIMIT),
5249
+ candidates: shareTheList(out, limit),
5250
+ by_rule: countByRule(out),
4495
5251
  scanned: budget.scanned,
4496
5252
  truncated: budget.truncated
4497
5253
  };
4498
5254
  }
5255
+ function shareTheList(ranked, limit) {
5256
+ const lanes = /* @__PURE__ */ new Map();
5257
+ for (const candidate of ranked) {
5258
+ const lane = lanes.get(candidate.rule);
5259
+ if (lane) lane.push(candidate);
5260
+ else lanes.set(candidate.rule, [candidate]);
5261
+ }
5262
+ const out = [];
5263
+ const queues = [...lanes.values()];
5264
+ let round = 0;
5265
+ for (; ; ) {
5266
+ let added = false;
5267
+ for (const queue of queues) {
5268
+ if (out.length >= limit) return out;
5269
+ const next = queue[round];
5270
+ if (next === void 0) continue;
5271
+ out.push(next);
5272
+ added = true;
5273
+ }
5274
+ if (!added) return out;
5275
+ round += 1;
5276
+ }
5277
+ }
5278
+ function countByRule(ranked) {
5279
+ const out = {};
5280
+ for (const candidate of ranked) out[candidate.rule] = (out[candidate.rule] ?? 0) + 1;
5281
+ return out;
5282
+ }
4499
5283
  function titleTerms(title) {
4500
5284
  const out = /* @__PURE__ */ new Set();
4501
5285
  for (const term of terms(title)) {
@@ -4843,19 +5627,38 @@ function kindPromotions(entries) {
4843
5627
  const out = [];
4844
5628
  for (const entry of entries) {
4845
5629
  if (entry.memory.binding !== true) continue;
4846
- if (entry.memory.kind === "decision") continue;
5630
+ const kind = entry.memory.kind;
5631
+ if (kind !== void 0 && RULE_SHAPED_KINDS.has(kind)) continue;
5632
+ if (kind === void 0) {
5633
+ out.push({
5634
+ candidate: {
5635
+ rule: "binding_unclassified",
5636
+ memory_id: entry.memory.id,
5637
+ path: entry.path,
5638
+ title: entry.memory.title,
5639
+ other_id: null,
5640
+ other_path: null,
5641
+ other_title: null,
5642
+ evidence: "marked as binding and left unclassified \u2014 either a decision nobody classified, or something that was never a rule",
5643
+ suggested_action: "decide",
5644
+ suggested_kind: null
5645
+ },
5646
+ confidence: 4
5647
+ });
5648
+ continue;
5649
+ }
4847
5650
  out.push({
4848
5651
  candidate: {
4849
- rule: "kind_promotion",
5652
+ rule: "rule_demotion",
4850
5653
  memory_id: entry.memory.id,
4851
5654
  path: entry.path,
4852
5655
  title: entry.memory.title,
4853
5656
  other_id: null,
4854
5657
  other_path: null,
4855
5658
  other_title: null,
4856
- evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
4857
- suggested_action: "reclassify",
4858
- suggested_kind: "decision"
5659
+ evidence: `marked as binding but classified as ${kind} \u2014 the author said it is not a decision, so it may not be a rule`,
5660
+ suggested_action: "unmark_rule",
5661
+ suggested_kind: null
4859
5662
  },
4860
5663
  confidence: 4
4861
5664
  });
@@ -4962,7 +5765,7 @@ function* pairsFrom(buckets, budget) {
4962
5765
  }
4963
5766
  }
4964
5767
  }
4965
- var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, CandidateComponents;
5768
+ var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, RULE_SHAPED_KINDS, CandidateComponents;
4966
5769
  var init_detect = __esm({
4967
5770
  "../../packages/memory-engine/src/proposal/detect.ts"() {
4968
5771
  "use strict";
@@ -4973,7 +5776,9 @@ var init_detect = __esm({
4973
5776
  "semantic_similarity",
4974
5777
  "shared_context",
4975
5778
  "similar_title",
4976
- "kind_promotion"
5779
+ "kind_promotion",
5780
+ "rule_demotion",
5781
+ "binding_unclassified"
4977
5782
  ];
4978
5783
  DEFAULT_LIMIT = 50;
4979
5784
  DEFAULT_PAIR_BUDGET = 2e5;
@@ -4983,6 +5788,7 @@ var init_detect = __esm({
4983
5788
  SHARED_ENTITY_SEMANTIC_SIMILARITY = 0.75;
4984
5789
  GLOBAL_SEMANTIC_SIMILARITY = 0.72;
4985
5790
  RECIPROCAL_NEIGHBOURS = 2;
5791
+ RULE_SHAPED_KINDS = /* @__PURE__ */ new Set(["decision", "preference"]);
4986
5792
  CandidateComponents = class {
4987
5793
  parent = /* @__PURE__ */ new Map();
4988
5794
  root(id) {
@@ -5319,12 +6125,12 @@ function commit(prepared, input, opts = {}) {
5319
6125
  if (carriesNoClaim(parsed.action)) {
5320
6126
  if (parsed.claim !== null) {
5321
6127
  throw Object.assign(new Error(`${parsed.action} does not take a claim`), {
5322
- hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
6128
+ hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : isBindingAction(parsed.action) ? `${parsed.action} takes a marking back and leaves the conclusion exactly as it is \u2014 that is the whole reason it exists. To change what the note says as well, use supersede.` : isScopeAction(parsed.action) ? `${parsed.action} says where a memory binds, not what it says. To change the conclusion too, use supersede.` : isValidityAction(parsed.action) ? `${parsed.action} gives a memory an end date and leaves the conclusion exactly as it is. To change what the note says as well, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
5323
6129
  });
5324
6130
  }
5325
6131
  } else if (parsed.claim === null) {
5326
6132
  throw Object.assign(new Error(`${parsed.action} needs a claim`), {
5327
- hint: "Only retire, unretire and reclassify may omit it. They change a memory's standing or its classification rather than its content."
6133
+ hint: "Only retire, unretire, reclassify, unbind, rescope and expire may omit it. They change a memory's standing, its classification, whether it binds, where it binds or when it stops being current \u2014 rather than its content."
5328
6134
  });
5329
6135
  } else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
5330
6136
  throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
@@ -5341,6 +6147,26 @@ function commit(prepared, input, opts = {}) {
5341
6147
  hint: "Only `reclassify` moves a memory's kind, and it does nothing else. Propose it separately, with its own reason."
5342
6148
  });
5343
6149
  }
6150
+ if (isScopeAction(parsed.action) && parsed.proposed_scope === null) {
6151
+ throw Object.assign(new Error(`${parsed.action} needs the scope it is proposing`), {
6152
+ hint: `Pass a project name, or \`${GLOBAL_SCOPE}\` to say the memory holds everywhere. Widening is spelled out because it cannot be taken back by accident.`
6153
+ });
6154
+ }
6155
+ if (!isScopeAction(parsed.action) && parsed.proposed_scope !== null) {
6156
+ throw Object.assign(new Error(`${parsed.action} does not set the scope`), {
6157
+ hint: "Only `rescope` moves where a memory binds, and it does nothing else. Propose it separately, with its own reason."
6158
+ });
6159
+ }
6160
+ if (isValidityAction(parsed.action) && parsed.proposed_valid_until === null) {
6161
+ throw Object.assign(new Error(`${parsed.action} needs the date it is proposing`), {
6162
+ hint: "Pass an ISO date \u2014 the day the memory stops being current. It is not retired by this: recall still returns it, ranked below what is current and labelled."
6163
+ });
6164
+ }
6165
+ if (!isValidityAction(parsed.action) && parsed.proposed_valid_until !== null) {
6166
+ throw Object.assign(new Error(`${parsed.action} does not set an end date`), {
6167
+ hint: "Only `expire` gives a memory an end date, and it does nothing else. Propose it separately, with its own reason."
6168
+ });
6169
+ }
5344
6170
  const now = opts.now ?? /* @__PURE__ */ new Date();
5345
6171
  const at = now.toISOString();
5346
6172
  const draft = {
@@ -5350,6 +6176,8 @@ function commit(prepared, input, opts = {}) {
5350
6176
  memory_id: parsed.memory_id,
5351
6177
  claim: parsed.claim,
5352
6178
  proposed_kind: parsed.proposed_kind,
6179
+ proposed_scope: parsed.proposed_scope,
6180
+ proposed_valid_until: parsed.proposed_valid_until,
5353
6181
  relations: parsed.relations,
5354
6182
  reason: parsed.reason,
5355
6183
  source_excerpt: parsed.source_excerpt,
@@ -5380,14 +6208,22 @@ var init_commit = __esm({
5380
6208
  */
5381
6209
  memory_id: z26.string().min(1).nullable().default(null),
5382
6210
  /**
5383
- * Omitted by `retire` / `unretire` / `reclassify`, which change a memory's
5384
- * standing or its classification rather than what it says. Checked below with
5385
- * the other action rules so the error can name the action instead of pointing
5386
- * at a field.
6211
+ * Omitted by `retire` / `unretire` / `reclassify` / `unbind`, which change a
6212
+ * memory's standing, its classification or whether it binds — rather than
6213
+ * what it says. Checked below with the other action rules so the error can
6214
+ * name the action instead of pointing at a field.
5387
6215
  */
5388
6216
  claim: ProposedClaimSchema.nullable().default(null),
5389
6217
  /** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
5390
6218
  proposed_kind: MemoryKindSchema.nullable().default(null),
6219
+ /**
6220
+ * Where `rescope` moves this memory to — a scope name, or the reserved word
6221
+ * `global`. Only that action takes one, and it may not be null: see
6222
+ * `ProposalSchema.proposed_scope` for why "absent" must not spell "widen".
6223
+ */
6224
+ proposed_scope: z26.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
6225
+ /** The end date `expire` writes onto the current claim. Only that action takes one. */
6226
+ proposed_valid_until: z26.string().min(1).nullable().default(null),
5391
6227
  relations: ProposedRelationsSchema.default({}),
5392
6228
  reason: z26.string().min(1),
5393
6229
  /** The passage from the note that justifies this. Required to submit. */
@@ -5488,6 +6324,68 @@ function reclassifyInput(entry, kind, opts = {}) {
5488
6324
  target_path: entry.path
5489
6325
  };
5490
6326
  }
6327
+ function unbindInput(entry, opts = {}) {
6328
+ return {
6329
+ action: "unbind",
6330
+ memory_id: entry.memory.id,
6331
+ claim: null,
6332
+ reason: opts.reason ?? "no longer binding",
6333
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6334
+ target_path: entry.path
6335
+ };
6336
+ }
6337
+ function rescopeInput(entry, scope, opts = {}) {
6338
+ const wanted = scope.trim();
6339
+ if (wanted === "") {
6340
+ throw Object.assign(new Error("a scope cannot be empty"), {
6341
+ hint: `Name the project this memory binds in, or \`${GLOBAL_SCOPE}\` to say it holds everywhere.`
6342
+ });
6343
+ }
6344
+ if ([...wanted].length > SCOPE_LIMIT) {
6345
+ throw Object.assign(new Error(`a scope may be at most ${SCOPE_LIMIT} characters`), {
6346
+ hint: "A scope names a project. If it needs a sentence, it belongs in the note."
6347
+ });
6348
+ }
6349
+ const declared = declaredScopeOf(entry.memory);
6350
+ const same = isGlobalScope(wanted) ? declared === null : declared === wanted;
6351
+ if (same) {
6352
+ throw Object.assign(new Error(`${entry.memory.title} already binds there`), {
6353
+ hint: "Nothing was written, and nothing needed to be."
6354
+ });
6355
+ }
6356
+ return {
6357
+ action: "rescope",
6358
+ memory_id: entry.memory.id,
6359
+ claim: null,
6360
+ proposed_scope: wanted,
6361
+ reason: opts.reason ?? `binds in ${wanted}`,
6362
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6363
+ target_path: entry.path
6364
+ };
6365
+ }
6366
+ function expireInput(entry, until, opts = {}) {
6367
+ const wanted = until.trim();
6368
+ const at = Date.parse(wanted);
6369
+ if (!Number.isFinite(at)) {
6370
+ throw Object.assign(new Error(`${until} is not a date`), {
6371
+ hint: "Pass an ISO date, such as 2026-10-31 or 2026-10-31T00:00:00Z."
6372
+ });
6373
+ }
6374
+ if ((entry.claims[0]?.valid_until ?? entry.memory.valid_until ?? null) === wanted) {
6375
+ throw Object.assign(new Error(`${entry.memory.title} already ends then`), {
6376
+ hint: "Nothing was written, and nothing needed to be."
6377
+ });
6378
+ }
6379
+ return {
6380
+ action: "expire",
6381
+ memory_id: entry.memory.id,
6382
+ claim: null,
6383
+ proposed_valid_until: wanted,
6384
+ reason: opts.reason ?? `holds until ${wanted}`,
6385
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6386
+ target_path: entry.path
6387
+ };
6388
+ }
5491
6389
  function saysNothingNew(entry, input, normalize) {
5492
6390
  const norm = (v) => normalize(v ?? "").trim();
5493
6391
  const claim = input.claim;
@@ -5495,6 +6393,7 @@ function saysNothingNew(entry, input, normalize) {
5495
6393
  if (norm(claim.text) !== norm(previousClaim(entry))) return false;
5496
6394
  if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
5497
6395
  if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
6396
+ if (claim.binding != null && claim.binding !== (entry.memory.binding === true)) return false;
5498
6397
  const declared = new Set(
5499
6398
  entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
5500
6399
  );
@@ -5507,11 +6406,6 @@ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
5507
6406
  if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
5508
6407
  return bodyOf(content).includes(line) ? line : null;
5509
6408
  }
5510
- function bodyOf(content) {
5511
- const stripped = content.replace(/^/, "");
5512
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
5513
- return fence2 ? stripped.slice(fence2[0].length) : stripped;
5514
- }
5515
6409
  function previousClaim(entry) {
5516
6410
  const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
5517
6411
  return current?.text ?? null;
@@ -5527,6 +6421,9 @@ var init_curate = __esm({
5527
6421
  "../../packages/memory-engine/src/capture/curate.ts"() {
5528
6422
  "use strict";
5529
6423
  init_planner();
6424
+ init_schema();
6425
+ init_rule_budget();
6426
+ init_note_body();
5530
6427
  NO_REASON_GIVEN = "revised directly";
5531
6428
  noLifecycleReason = (action) => `${action}d directly`;
5532
6429
  }
@@ -5577,6 +6474,25 @@ function desiredFields(proposal, existing) {
5577
6474
  out["kind"] = proposal.proposed_kind;
5578
6475
  return out;
5579
6476
  }
6477
+ if (isBindingAction(proposal.action)) {
6478
+ delete out["binding"];
6479
+ return out;
6480
+ }
6481
+ if (isScopeAction(proposal.action)) {
6482
+ if (proposal.proposed_scope === null) {
6483
+ throw new Error(`${proposal.action} reached the patcher with no scope`);
6484
+ }
6485
+ if (isGlobalScope(proposal.proposed_scope)) delete out["scope"];
6486
+ else out["scope"] = proposal.proposed_scope;
6487
+ return out;
6488
+ }
6489
+ if (isValidityAction(proposal.action)) {
6490
+ if (proposal.proposed_valid_until === null) {
6491
+ throw new Error(`${proposal.action} reached the patcher with no date`);
6492
+ }
6493
+ out["valid_until"] = proposal.proposed_valid_until;
6494
+ return out;
6495
+ }
5580
6496
  const claim = proposal.claim;
5581
6497
  if (claim === null) {
5582
6498
  throw new Error(`${proposal.action} reached the patcher with no claim`);
@@ -5874,6 +6790,108 @@ function verifyPatch(patched, proposal, before) {
5874
6790
  }
5875
6791
  return;
5876
6792
  }
6793
+ if (isBindingAction(proposal.action)) {
6794
+ if (mapped.memory.binding === true) {
6795
+ throw new PatchError(
6796
+ "the patched note still reads as binding",
6797
+ "This is a defect in the patcher, not in your note. Nothing was written."
6798
+ );
6799
+ }
6800
+ if (before === void 0) {
6801
+ throw new PatchError(
6802
+ `${proposal.action} cannot be verified without the note it started from`,
6803
+ "This is a defect in the caller, not in your note. Nothing was written."
6804
+ );
6805
+ }
6806
+ const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
6807
+ if (was.claims[0]?.text !== mapped.claims[0]?.text) {
6808
+ throw new PatchError(
6809
+ `${proposal.action} changed the conclusion, which it must never do`,
6810
+ "Taking a marking back leaves what the note says exactly as it was. Nothing was written."
6811
+ );
6812
+ }
6813
+ if (was.memory.summary !== mapped.memory.summary) {
6814
+ throw new PatchError(
6815
+ `${proposal.action} changed the summary, which it must never do`,
6816
+ "Taking a marking back leaves what the note says exactly as it was \u2014 and the summary is what recall matches on. Nothing was written."
6817
+ );
6818
+ }
6819
+ if (was.memory.title !== mapped.memory.title) {
6820
+ throw new PatchError(
6821
+ `${proposal.action} changed the title, which it must never do`,
6822
+ "Taking a marking back leaves what the note says exactly as it was. Nothing was written."
6823
+ );
6824
+ }
6825
+ if (was.memory.status !== mapped.memory.status) {
6826
+ throw new PatchError(
6827
+ `${proposal.action} changed the status, which it must never do`,
6828
+ "A memory that no longer binds is still current. To take it out of the answers as well, use retire. Nothing was written."
6829
+ );
6830
+ }
6831
+ return;
6832
+ }
6833
+ if (isScopeAction(proposal.action) || isValidityAction(proposal.action)) {
6834
+ if (before === void 0) {
6835
+ throw new PatchError(
6836
+ `${proposal.action} cannot be verified without the note it started from`,
6837
+ "This is a defect in the caller, not in your note. Nothing was written."
6838
+ );
6839
+ }
6840
+ if (isScopeAction(proposal.action)) {
6841
+ const want = proposal.proposed_scope;
6842
+ const got = declaredScopeOf(mapped.memory);
6843
+ const landed = isGlobalScope(want) ? got === null : got === want;
6844
+ if (!landed) {
6845
+ throw new PatchError(
6846
+ "the patched note does not declare the scope that was asked for",
6847
+ "This is a defect in the patcher, not in your note. Nothing was written."
6848
+ );
6849
+ }
6850
+ } else if (
6851
+ // Read back the way `freshnessOf` reads it: the claim's date when the
6852
+ // note states a claim, the note's own otherwise. Asserting only on the
6853
+ // claim would reject every note `remember` wrote, since it has never
6854
+ // written a `claim:` key.
6855
+ (mapped.claims[0]?.valid_until ?? mapped.memory.valid_until ?? null) !== proposal.proposed_valid_until
6856
+ ) {
6857
+ throw new PatchError(
6858
+ "the patched note does not carry the end date that was asked for",
6859
+ "This is a defect in the patcher, not in your note. Nothing was written."
6860
+ );
6861
+ }
6862
+ const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
6863
+ if (was.claims[0]?.text !== mapped.claims[0]?.text) {
6864
+ throw new PatchError(
6865
+ `${proposal.action} changed the conclusion, which it must never do`,
6866
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
6867
+ );
6868
+ }
6869
+ if (was.memory.summary !== mapped.memory.summary) {
6870
+ throw new PatchError(
6871
+ `${proposal.action} changed the summary, which it must never do`,
6872
+ "The summary is what recall matches on, and this action does not touch what the note says. Nothing was written."
6873
+ );
6874
+ }
6875
+ if (was.memory.title !== mapped.memory.title) {
6876
+ throw new PatchError(
6877
+ `${proposal.action} changed the title, which it must never do`,
6878
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
6879
+ );
6880
+ }
6881
+ if (was.memory.status !== mapped.memory.status) {
6882
+ throw new PatchError(
6883
+ `${proposal.action} changed the status, which it must never do`,
6884
+ "Moving where a memory binds, or saying when it stops being current, does not retire it. To take it out of the answers, use retire. Nothing was written."
6885
+ );
6886
+ }
6887
+ if (was.memory.binding !== mapped.memory.binding) {
6888
+ throw new PatchError(
6889
+ `${proposal.action} changed whether the memory binds, which it must never do`,
6890
+ "To take a marking back, use `revise --no-rule`. Nothing was written."
6891
+ );
6892
+ }
6893
+ return;
6894
+ }
5877
6895
  const supersedes = new Set(
5878
6896
  mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
5879
6897
  );
@@ -5944,11 +6962,13 @@ function reversePatch(content, patch) {
5944
6962
  }
5945
6963
  return null;
5946
6964
  }
5947
- var PATCHABLE_KEYS, NEVER_WRITTEN_KEYS, STATUS_WRITING_ACTIONS, KIND_WRITING_ACTIONS, NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
6965
+ var PATCHABLE_KEYS, NEVER_WRITTEN_KEYS, STATUS_WRITING_ACTIONS, KIND_WRITING_ACTIONS, SCOPE_WRITING_ACTIONS, NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
5948
6966
  var init_patch = __esm({
5949
6967
  "../../packages/memory-engine/src/proposal/patch.ts"() {
5950
6968
  "use strict";
5951
6969
  init_markdown_mapping();
6970
+ init_rule_budget();
6971
+ init_schema();
5952
6972
  init_schema2();
5953
6973
  PATCHABLE_KEYS = [
5954
6974
  // The conclusion itself.
@@ -5972,6 +6992,7 @@ var init_patch = __esm({
5972
6992
  NEVER_WRITTEN_KEYS = ["id", "type", "scope", "aliases", "status", "kind"];
5973
6993
  STATUS_WRITING_ACTIONS = ["retire", "unretire"];
5974
6994
  KIND_WRITING_ACTIONS = ["reclassify"];
6995
+ SCOPE_WRITING_ACTIONS = ["rescope"];
5975
6996
  NAMESPACE2 = "memgineering";
5976
6997
  FENCE_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(\r?\n|$)/;
5977
6998
  LOOKS_FENCED_RE = /^---[ \t]*\r?\n/;
@@ -6013,15 +7034,15 @@ async function applyProposal(adapter, accepted) {
6013
7034
  const patch = computePatch(current.content, proposal);
6014
7035
  verifyPatch(patch.content, proposal, current.content);
6015
7036
  if (patch.content === current.content) {
6016
- const base = proposal.target.baseRevision;
6017
- if (base === null || base === void 0) {
7037
+ const base2 = proposal.target.baseRevision;
7038
+ if (base2 === null || base2 === void 0) {
6018
7039
  return {
6019
7040
  ok: false,
6020
7041
  reason: "revision_conflict",
6021
7042
  proposal: needsRebase(proposal, current.revision)
6022
7043
  };
6023
7044
  }
6024
- if (adapter.compareRevision(base, current.revision) === "diverged") {
7045
+ if (adapter.compareRevision(base2, current.revision) === "diverged") {
6025
7046
  return {
6026
7047
  ok: false,
6027
7048
  reason: "revision_conflict",
@@ -6088,23 +7109,36 @@ function rank(entries, events, opts) {
6088
7109
  const recalls = events.filter((e) => !AUTOMATIC.has(e.verb));
6089
7110
  const here = opts.here ? recalls.filter((e) => opts.here(e)) : [];
6090
7111
  const hereLabel = opts.hereLabel ?? "in this folder";
6091
- const countIn = (source) => {
6092
- const counts = /* @__PURE__ */ new Map();
7112
+ const usageIn = (source) => {
7113
+ const usage = /* @__PURE__ */ new Map();
7114
+ const at = (id) => {
7115
+ let u = usage.get(id);
7116
+ if (!u) usage.set(id, u = { opened: 0, surfaced: 0, credit: 0 });
7117
+ return u;
7118
+ };
6093
7119
  for (const event of source) {
6094
- for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
6095
- if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
7120
+ if (event.verb === "recall") {
7121
+ event.returned_ids.forEach((id, i) => {
7122
+ const u = at(id);
7123
+ u.surfaced += 1;
7124
+ if (i === 0) u.credit += SURFACED_SHARE;
7125
+ });
7126
+ } else if (DELIBERATE.has(event.verb)) {
7127
+ const ids = event.opened_id ? [event.opened_id] : event.returned_ids;
7128
+ for (const id of new Set(ids)) at(id).opened += 1;
7129
+ }
6096
7130
  }
6097
- return counts;
7131
+ return usage;
6098
7132
  };
6099
- const hereCounts = countIn(here);
6100
- const anyCounts = countIn(recalls);
7133
+ const evidence = (u) => u ? u.opened + u.credit : 0;
7134
+ const hereUsage = usageIn(here);
7135
+ const anyUsage = usageIn(recalls);
6101
7136
  const lastTouched = /* @__PURE__ */ new Map();
6102
7137
  for (const event of recalls) {
6103
7138
  const t = Date.parse(event.ts);
6104
7139
  if (!Number.isFinite(t)) continue;
6105
- for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
6106
- lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
6107
- }
7140
+ const touched = event.verb === "recall" ? event.returned_ids.slice(0, 1) : DELIBERATE.has(event.verb) ? [event.opened_id ?? "", ...event.returned_ids].filter(Boolean) : [];
7141
+ for (const id of touched) lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
6108
7142
  }
6109
7143
  const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
6110
7144
  const scored = [];
@@ -6114,16 +7148,30 @@ function rank(entries, events, opts) {
6114
7148
  if (freshness === "retired") continue;
6115
7149
  const why = [];
6116
7150
  let score = 0;
6117
- const hereCount = hereCounts.get(id) ?? 0;
6118
- if (hereCount > 0) {
6119
- score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
6120
- why.push(`recalled ${hereCount}\xD7 ${hereLabel}`);
6121
- }
6122
- const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
6123
- if (anyCount > 0) {
6124
- score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
6125
- if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
7151
+ const hereU = hereUsage.get(id);
7152
+ const anyU = anyUsage.get(id);
7153
+ const hereEvidence = evidence(hereU);
7154
+ const elsewhereEvidence = evidence(anyU) - hereEvidence;
7155
+ if (hereEvidence > 0) {
7156
+ score += WEIGHT.contextRecall * Math.log2(1 + hereEvidence);
7157
+ why.push(usageLine(hereU, hereLabel));
7158
+ }
7159
+ if (elsewhereEvidence > 1e-9) {
7160
+ score += WEIGHT.anyRecall * Math.log2(1 + elsewhereEvidence);
7161
+ if (hereEvidence === 0) {
7162
+ why.push(
7163
+ usageLine(
7164
+ {
7165
+ opened: anyU.opened - (hereU?.opened ?? 0),
7166
+ surfaced: anyU.surfaced - (hereU?.surfaced ?? 0)
7167
+ },
7168
+ "elsewhere"
7169
+ )
7170
+ );
7171
+ }
6126
7172
  }
7173
+ const hereCount = (hereU?.opened ?? 0) + (hereU?.surfaced ?? 0);
7174
+ const anyCount = (anyU?.opened ?? 0) + (anyU?.surfaced ?? 0) - hereCount;
6127
7175
  const touched = lastTouched.get(id);
6128
7176
  if (touched !== void 0) {
6129
7177
  const days = (opts.now.getTime() - touched) / 864e5;
@@ -6144,16 +7192,28 @@ function rank(entries, events, opts) {
6144
7192
  }
6145
7193
  if (score === 0 && !entry.path.startsWith("01_BASE/")) {
6146
7194
  score += WEIGHT.neverReached;
6147
- why.push("never surfaced yet");
7195
+ why.push(
7196
+ anyU && anyU.surfaced > 0 ? usageLine(
7197
+ { opened: 0, surfaced: anyU.surfaced },
7198
+ // Labelled by where it happened: all here, none here, or both —
7199
+ // a total said "in this project" would claim the whole count.
7200
+ !hereU?.surfaced ? "elsewhere" : hereU.surfaced === anyU.surfaced ? hereLabel : `(${hereU.surfaced}\xD7 ${hereLabel})`
7201
+ ) : "never surfaced yet"
7202
+ );
6148
7203
  }
6149
7204
  if (freshness !== "current") why.push(freshness);
6150
- if (score > 0) scored.push({ entry, score, why });
7205
+ if (score > 0) scored.push({ entry, score, why, used: hereCount > 0 || anyCount > 0 });
6151
7206
  }
6152
7207
  return scored.sort(
6153
7208
  (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
6154
7209
  );
6155
7210
  }
6156
- var WEIGHT, STALE_BASE_DAYS;
7211
+ function usageLine(u, where) {
7212
+ if (u.opened === 0) return `surfaced ${u.surfaced}\xD7 ${where}, never opened`;
7213
+ if (u.surfaced === 0) return `opened ${u.opened}\xD7 ${where}`;
7214
+ return `opened ${u.opened}\xD7 \xB7 surfaced ${u.surfaced}\xD7 ${where}`;
7215
+ }
7216
+ var WEIGHT, STALE_BASE_DAYS, SURFACED_SHARE, DELIBERATE;
6157
7217
  var init_rank = __esm({
6158
7218
  "../../packages/memory-engine/src/resurface/rank.ts"() {
6159
7219
  "use strict";
@@ -6206,26 +7266,35 @@ var init_rank = __esm({
6206
7266
  binding: 5
6207
7267
  };
6208
7268
  STALE_BASE_DAYS = 30;
7269
+ SURFACED_SHARE = 0.5;
7270
+ DELIBERATE = /* @__PURE__ */ new Set(["open", "publish", "adapt", "read-public"]);
6209
7271
  }
6210
7272
  });
6211
7273
 
6212
7274
  // ../../packages/memory-engine/src/index.ts
6213
7275
  var src_exports2 = {};
6214
7276
  __export(src_exports2, {
7277
+ BASE_TEMPLATES: () => BASE_TEMPLATES,
6215
7278
  CONSOLIDATION_RULES: () => CONSOLIDATION_RULES,
6216
7279
  CaptureCandidateSchema: () => CaptureCandidateSchema,
6217
7280
  ClaimConfidenceSchema: () => ClaimConfidenceSchema,
6218
7281
  ClaimSchema: () => ClaimSchema,
6219
7282
  CommitInputSchema: () => CommitInputSchema,
7283
+ DEFAULT_RULE_BUDGET: () => DEFAULT_RULE_BUDGET,
6220
7284
  DEFAULT_TTL_DAYS: () => DEFAULT_TTL_DAYS,
7285
+ DERIVED_BLUEPRINT: () => DERIVED_BLUEPRINT,
7286
+ DERIVED_DIR: () => DERIVED_DIR,
7287
+ GLOBAL_SCOPE: () => GLOBAL_SCOPE,
6221
7288
  IllegalTransitionError: () => IllegalTransitionError,
6222
7289
  KIND_WRITING_ACTIONS: () => KIND_WRITING_ACTIONS,
7290
+ MEANING_ONLY_NOTE: () => MEANING_ONLY_NOTE,
6223
7291
  MappingModeSchema: () => MappingModeSchema,
6224
7292
  MemoryKindSchema: () => MemoryKindSchema,
6225
7293
  MemorySchema: () => MemorySchema,
6226
7294
  MemoryStatusSchema: () => MemoryStatusSchema,
6227
7295
  NEVER_WRITTEN_KEYS: () => NEVER_WRITTEN_KEYS,
6228
7296
  NO_REASON_GIVEN: () => NO_REASON_GIVEN,
7297
+ OBSERVATIONS_DIR: () => OBSERVATIONS_DIR,
6229
7298
  PATCHABLE_KEYS: () => PATCHABLE_KEYS,
6230
7299
  PatchError: () => PatchError,
6231
7300
  ProposalActionSchema: () => ProposalActionSchema,
@@ -6234,14 +7303,23 @@ __export(src_exports2, {
6234
7303
  ProposedClaimSchema: () => ProposedClaimSchema,
6235
7304
  ProposedRelationsSchema: () => ProposedRelationsSchema,
6236
7305
  ProvenanceSchema: () => ProvenanceSchema,
7306
+ QUERY_EMBEDDING_PREFIX: () => QUERY_EMBEDDING_PREFIX,
6237
7307
  RECALL_DEPTHS: () => RECALL_DEPTHS,
7308
+ RETIRED_BASE_TEMPLATES: () => RETIRED_BASE_TEMPLATES,
7309
+ RULE_BLOCK_BUDGET: () => RULE_BLOCK_BUDGET,
7310
+ RULE_WIDTH_MAX: () => RULE_WIDTH_MAX,
7311
+ RULE_WIDTH_MIN: () => RULE_WIDTH_MIN,
7312
+ RULE_WIRE_WIDTH: () => RULE_WIRE_WIDTH,
6238
7313
  RecallOptionError: () => RecallOptionError,
6239
7314
  RelationSchema: () => RelationSchema,
6240
7315
  RelationSourceSchema: () => RelationSourceSchema,
6241
7316
  RelationTypeSchema: () => RelationTypeSchema,
6242
7317
  SCOPE_LIMIT: () => SCOPE_LIMIT,
7318
+ SCOPE_SPREAD_FIELD: () => SCOPE_SPREAD_FIELD,
7319
+ SCOPE_WRITING_ACTIONS: () => SCOPE_WRITING_ACTIONS,
6243
7320
  SEMANTIC_CANDIDATES: () => SEMANTIC_CANDIDATES,
6244
7321
  SEMANTIC_FIELDS: () => SEMANTIC_FIELDS,
7322
+ SETUP_PROGRESS_CODES: () => SETUP_PROGRESS_CODES,
6245
7323
  STATUS_FOR_ACTION: () => STATUS_FOR_ACTION,
6246
7324
  STATUS_WRITING_ACTIONS: () => STATUS_WRITING_ACTIONS,
6247
7325
  StorableActionSchema: () => StorableActionSchema,
@@ -6249,6 +7327,7 @@ __export(src_exports2, {
6249
7327
  TELEMETRY_MESSAGE_LIMIT: () => TELEMETRY_MESSAGE_LIMIT,
6250
7328
  TelemetryErrorPayloadSchema: () => TelemetryErrorPayloadSchema2,
6251
7329
  TransitionSchema: () => TransitionSchema,
7330
+ UNFILLED_FIELDS: () => UNFILLED_FIELDS,
6252
7331
  accept: () => accept,
6253
7332
  addedLines: () => addedLines,
6254
7333
  applyProposal: () => applyProposal,
@@ -6270,28 +7349,39 @@ __export(src_exports2, {
6270
7349
  commit: () => commit,
6271
7350
  computePatch: () => computePatch,
6272
7351
  confidenceOf: () => confidenceOf,
7352
+ declaredScope: () => declaredScope,
7353
+ declaredScopeOf: () => declaredScopeOf,
6273
7354
  defaultExpiry: () => defaultExpiry,
6274
7355
  defaultStateRoot: () => defaultStateRoot,
6275
7356
  deleteProposal: () => deleteProposal,
6276
7357
  detectConsolidation: () => detectConsolidation,
6277
7358
  expireIfDue: () => expireIfDue,
7359
+ expireInput: () => expireInput,
6278
7360
  findConflicts: () => findConflicts,
6279
7361
  freshnessOf: () => freshnessOf,
7362
+ isBindingAction: () => isBindingAction,
6280
7363
  isClassifyAction: () => isClassifyAction,
7364
+ isExactUntouchedBase: () => isExactUntouchedBase,
6281
7365
  isExpired: () => isExpired2,
7366
+ isGlobalScope: () => isGlobalScope,
6282
7367
  isLifecycleAction: () => isLifecycleAction,
6283
7368
  isRecallDepth: () => isRecallDepth,
7369
+ isScopeAction: () => isScopeAction,
7370
+ isTemplateBody: () => isTemplateBody,
6284
7371
  isTerminal: () => isTerminal,
6285
- isUntouchedBase: () => isUntouchedBase,
7372
+ isUnwrittenBody: () => isUnwrittenBody,
7373
+ isValidityAction: () => isValidityAction,
6286
7374
  lifecycleInput: () => lifecycleInput,
6287
7375
  listProposals: () => listProposals,
6288
7376
  loadProposal: () => loadProposal,
6289
7377
  mapNoteToMemory: () => mapNoteToMemory,
7378
+ markUnfilled: () => markUnfilled,
6290
7379
  matchBoundaryTerms: () => matchBoundaryTerms,
6291
7380
  missingCounterparts: () => missingCounterparts,
6292
7381
  needsRebase: () => needsRebase,
6293
7382
  newProposalId: () => newProposalId,
6294
7383
  noLifecycleReason: () => noLifecycleReason,
7384
+ ownEntry: () => ownEntry,
6295
7385
  prepare: () => prepare,
6296
7386
  prepareForMemory: () => prepareForMemory,
6297
7387
  previewProposal: () => previewProposal,
@@ -6307,15 +7397,23 @@ __export(src_exports2, {
6307
7397
  reconcile: () => reconcile,
6308
7398
  redact: () => redact,
6309
7399
  renderNewNote: () => renderNewNote,
7400
+ rescopeInput: () => rescopeInput,
6310
7401
  resolveDetail: () => resolveDetail,
6311
7402
  reversePatch: () => reversePatch,
6312
7403
  reviseInput: () => reviseInput,
7404
+ ruleLane: () => ruleLane,
7405
+ rulePassage: () => rulePassage,
7406
+ ruleTextOf: () => ruleTextOf,
7407
+ ruleWidthFor: () => ruleWidthFor,
6313
7408
  saveProposal: () => saveProposal,
6314
7409
  saysNothingNew: () => saysNothingNew,
7410
+ scopeSpread: () => scopeSpread,
6315
7411
  searchMemories: () => searchMemories,
6316
7412
  semanticSourceFromVectors: () => semanticSourceFromVectors,
7413
+ splitRuleBudget: () => splitRuleBudget,
6317
7414
  terms: () => terms,
6318
7415
  transition: () => transition,
7416
+ unbindInput: () => unbindInput,
6319
7417
  validateBoundaryTerm: () => validateBoundaryTerm,
6320
7418
  variantsOf: () => variantsOf,
6321
7419
  verifyPatch: () => verifyPatch,
@@ -6326,6 +7424,9 @@ var init_src3 = __esm({
6326
7424
  "../../packages/memory-engine/src/index.ts"() {
6327
7425
  "use strict";
6328
7426
  init_schema();
7427
+ init_layout();
7428
+ init_rule_budget();
7429
+ init_rule_text();
6329
7430
  init_boundary();
6330
7431
  init_markdown_mapping();
6331
7432
  init_build();
@@ -6333,6 +7434,11 @@ var init_src3 = __esm({
6333
7434
  init_reconcile();
6334
7435
  init_planner();
6335
7436
  init_semantic();
7437
+ init_unfilled();
7438
+ init_setup_progress();
7439
+ init_query_prefix();
7440
+ init_base_templates();
7441
+ init_scope_spread();
6336
7442
  init_conflicts();
6337
7443
  init_search();
6338
7444
  init_schema2();
@@ -6927,13 +8033,18 @@ async function useClient() {
6927
8033
  } catch (err) {
6928
8034
  const code = err.code;
6929
8035
  if (code === "authorization_pending") {
6930
- if (debug) process.stderr.write("[kordis] auto-finalize: authorization_pending (keeping pending)\n");
8036
+ if (debug)
8037
+ process.stderr.write(
8038
+ "[kordis] auto-finalize: authorization_pending (keeping pending)\n"
8039
+ );
6931
8040
  } else {
6932
8041
  await clearPending();
6933
8042
  if (debug) {
6934
8043
  const msg = err instanceof Error ? err.message : String(err);
6935
- process.stderr.write(`[kordis] auto-finalize: poll failed (code=${code ?? "none"}): ${msg}
6936
- `);
8044
+ process.stderr.write(
8045
+ `[kordis] auto-finalize: poll failed (code=${code ?? "none"}): ${msg}
8046
+ `
8047
+ );
6937
8048
  }
6938
8049
  void reportError("auto_finalize_stage1_fail", err);
6939
8050
  }
@@ -6957,7 +8068,8 @@ async function useClient() {
6957
8068
  cfg = { ...cfg, username, sessionId: pollResult.session_id };
6958
8069
  await saveCliConfig(cfg);
6959
8070
  await clearPending();
6960
- if (debug) process.stderr.write(`[kordis] auto-finalize: persisted tokens for ${username}
8071
+ if (debug)
8072
+ process.stderr.write(`[kordis] auto-finalize: persisted tokens for ${username}
6961
8073
  `);
6962
8074
  } catch (err) {
6963
8075
  await clearPending();
@@ -6986,31 +8098,51 @@ async function useClient() {
6986
8098
  }
6987
8099
  } : {}
6988
8100
  });
8101
+ const renewTokens = async (user, sent) => {
8102
+ try {
8103
+ const refreshed = await api.refreshSession(sent);
8104
+ currentTokens = {
8105
+ access_token: refreshed.access_token,
8106
+ refresh_token: refreshed.refresh_token,
8107
+ session_id: refreshed.session_id,
8108
+ expires_at_epoch: Math.floor(Date.now() / 1e3) + refreshed.expires_in
8109
+ };
8110
+ await keychain.save(user, currentTokens);
8111
+ await saveCliConfig({ ...cfg, sessionId: refreshed.session_id });
8112
+ return true;
8113
+ } catch (refreshErr) {
8114
+ if (!lostRefreshRace(refreshErr)) return false;
8115
+ const stored = await storedPairOtherThan(user, sent);
8116
+ if (stored === null) return false;
8117
+ currentTokens = stored;
8118
+ return true;
8119
+ }
8120
+ };
6989
8121
  const authedRequest = async (fn) => {
6990
8122
  try {
6991
8123
  return await fn(api);
6992
8124
  } catch (err) {
6993
- if (err instanceof KordisApiError && err.code === "not_authenticated" && currentTokens && username) {
6994
- try {
6995
- const refreshed = await api.refreshSession(currentTokens.refresh_token);
6996
- currentTokens = {
6997
- access_token: refreshed.access_token,
6998
- refresh_token: refreshed.refresh_token,
6999
- session_id: refreshed.session_id,
7000
- expires_at_epoch: Math.floor(Date.now() / 1e3) + refreshed.expires_in
7001
- };
7002
- await keychain.save(username, currentTokens);
7003
- await saveCliConfig({ ...cfg, sessionId: refreshed.session_id });
7004
- return await fn(api);
7005
- } catch {
7006
- throw err;
7007
- }
7008
- }
7009
- throw err;
8125
+ const renewed = err instanceof KordisApiError && err.code === "not_authenticated" && currentTokens !== null && username !== null && await renewTokens(username, currentTokens.refresh_token);
8126
+ if (!renewed) throw err;
7010
8127
  }
8128
+ return fn(api);
7011
8129
  };
7012
8130
  return { api, keychain, username, apiUrl: cfg.apiUrl, authedRequest };
7013
8131
  }
8132
+ function lostRefreshRace(err) {
8133
+ return err instanceof KordisApiError && err.status === 401 && isRefreshRace({ error: { code: err.code, flags: err.flags } });
8134
+ }
8135
+ var RACE_REREADS_AT_MS = [500, 1e3, 2e3];
8136
+ async function storedPairOtherThan(username, sent) {
8137
+ let waited = 0;
8138
+ for (const at of RACE_REREADS_AT_MS) {
8139
+ await new Promise((resolve6) => setTimeout(resolve6, at - waited));
8140
+ waited = at;
8141
+ const stored = await createKeychain().then((store) => store.load(username)).catch(() => null);
8142
+ if (stored !== null && stored.refresh_token !== sent) return stored;
8143
+ }
8144
+ return null;
8145
+ }
7014
8146
 
7015
8147
  // src/ui.ts
7016
8148
  import kleur from "kleur";
@@ -8332,6 +9464,7 @@ function lintForLeaks(sources) {
8332
9464
  }
8333
9465
 
8334
9466
  // src/commands/push.ts
9467
+ init_src();
8335
9468
  var collect = (val, acc) => acc.concat(val);
8336
9469
  function toLineage(raws, kind) {
8337
9470
  return (raws ?? []).map((raw) => {
@@ -8346,7 +9479,10 @@ function toLineage(raws, kind) {
8346
9479
  var ASSET_MAX_BYTES = 2 * 1024 * 1024;
8347
9480
  var BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
8348
9481
  function pushCommand() {
8349
- return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option("--slug <slug>", 'override slug; accepts "user/slug" to point at a specific package').option("--type <type>", "override type inference").option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").option(
9482
+ return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option("--slug <slug>", 'override slug; accepts "user/slug" to point at a specific package').option(
9483
+ "--type <type>",
9484
+ "type to write into a frontmatter that names none (else a SKILL.md, folder or .skill is a skill); refused if the frontmatter names another"
9485
+ ).option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").option(
8350
9486
  "--adapted-from <slug>",
8351
9487
  "source this package was adapted from (repeatable); records lineage",
8352
9488
  collect,
@@ -8361,17 +9497,15 @@ function pushCommand() {
8361
9497
  let contentLabel = "SKILL.md";
8362
9498
  if (s.isDirectory()) {
8363
9499
  ({ content, assets } = await bundleDirectory(abs));
8364
- content = ensureTypeField(content, "skill");
9500
+ content = resolveType(content, opts.type, "skill");
8365
9501
  } else if (abs.endsWith(".skill")) {
8366
9502
  ({ content, assets } = await bundleSkillZip(abs));
8367
- content = ensureTypeField(content, "skill");
9503
+ content = resolveType(content, opts.type, "skill");
8368
9504
  } else {
8369
9505
  content = await readFile6(abs, "utf8");
8370
9506
  contentLabel = basename(abs);
8371
9507
  const isSkillFilename = abs.endsWith("/SKILL.md") || abs.endsWith(".skill.md");
8372
- if (isSkillFilename) {
8373
- content = ensureTypeField(content, "skill");
8374
- }
9508
+ content = resolveType(content, opts.type, isSkillFilename ? "skill" : void 0);
8375
9509
  }
8376
9510
  let nameFromFm = null;
8377
9511
  try {
@@ -8451,7 +9585,6 @@ function pushCommand() {
8451
9585
  bump: opts.bump,
8452
9586
  slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
8453
9587
  target_full_slug: targetFullSlug,
8454
- type: opts.type,
8455
9588
  changelog: opts.changelog,
8456
9589
  lineage: lineage.length > 0 ? lineage : void 0,
8457
9590
  dry_run: !!opts.dryRun
@@ -8587,15 +9720,39 @@ function printLeakWarnings(findings) {
8587
9720
  printHuman(c.yellow(` - ${f.message}`));
8588
9721
  }
8589
9722
  }
8590
- function ensureTypeField(content, defaultType) {
9723
+ function resolveType(content, typeFlag, inferred) {
9724
+ if (typeFlag === void 0) return inferred ? ensureTypeField(content, inferred) : content;
9725
+ const declared = frontmatterType(content);
9726
+ if (declared === void 0) return ensureTypeField(content, typeFlag);
9727
+ if (declared !== typeFlag) {
9728
+ throw new KordisApiError(400, {
9729
+ error: {
9730
+ code: "usage_error",
9731
+ message: `--type "${typeFlag}" disagrees with the type in the frontmatter, "${declared}"`,
9732
+ hint: `This file's frontmatter says \`type: ${declared}\`; change it or drop \`--type\`.`
9733
+ }
9734
+ });
9735
+ }
9736
+ return content;
9737
+ }
9738
+ function frontmatterType(content) {
9739
+ try {
9740
+ const value = parseFrontmatter(content).frontmatter?.["type"];
9741
+ return typeof value === "string" ? value : void 0;
9742
+ } catch {
9743
+ return void 0;
9744
+ }
9745
+ }
9746
+ function ensureTypeField(content, type) {
8591
9747
  const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
8592
9748
  if (!fmMatch) return content;
8593
9749
  const fmBlock = fmMatch[1];
8594
9750
  if (/^type\s*:/m.test(fmBlock)) return content;
8595
- const injected = fmMatch[0].replace(/^---\r?\n/, `---
8596
- type: ${defaultType}
9751
+ const scalar = HarnessTypeSchema.safeParse(type).success ? type : JSON.stringify(type);
9752
+ const injected = fmMatch[0].replace(/^---\r?\n/, () => `---
9753
+ type: ${scalar}
8597
9754
  `);
8598
- return content.replace(fmMatch[0], injected);
9755
+ return content.replace(fmMatch[0], () => injected);
8599
9756
  }
8600
9757
  var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
8601
9758
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
@@ -8609,15 +9766,15 @@ function shouldExclude(relPath, isDir) {
8609
9766
  if (EXCLUDE_EXTS.has(ext)) return true;
8610
9767
  return false;
8611
9768
  }
8612
- async function walkDir(base, relDir, collected) {
8613
- const entries = await readdir2(join8(base, relDir), { withFileTypes: true });
9769
+ async function walkDir(base2, relDir, collected) {
9770
+ const entries = await readdir2(join8(base2, relDir), { withFileTypes: true });
8614
9771
  for (const entry of entries) {
8615
9772
  const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
8616
9773
  if (shouldExclude(rel, entry.isDirectory())) continue;
8617
9774
  if (entry.isDirectory()) {
8618
- await walkDir(base, rel, collected);
9775
+ await walkDir(base2, rel, collected);
8619
9776
  } else if (entry.isFile()) {
8620
- collected.push({ rel, abs: join8(base, rel) });
9777
+ collected.push({ rel, abs: join8(base2, rel) });
8621
9778
  }
8622
9779
  }
8623
9780
  }
@@ -9271,45 +10428,31 @@ function getTranslator(type, format) {
9271
10428
  return fn;
9272
10429
  }
9273
10430
 
9274
- // src/lockfile.ts
9275
- init_brand();
9276
- import { join as join9 } from "path";
9277
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
9278
- function kordisHome2() {
9279
- return brandHome();
9280
- }
9281
- function lockfilePath() {
9282
- return join9(kordisHome2(), "installed.json");
9283
- }
9284
- function lockfileDisplayPath() {
9285
- const path2 = lockfilePath();
9286
- const home = process.env["HOME"];
9287
- return home && path2.startsWith(home) ? "~" + path2.slice(home.length) : path2;
9288
- }
9289
- async function readLockfile() {
9290
- try {
9291
- const raw = await readFile7(lockfilePath(), "utf8");
9292
- return JSON.parse(raw);
9293
- } catch {
9294
- return { packages: {} };
9295
- }
9296
- }
9297
- async function writeLockfile(lf) {
9298
- await mkdir5(kordisHome2(), { recursive: true });
9299
- await writeFile5(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
9300
- encoding: "utf8",
9301
- mode: 384
9302
- });
9303
- }
9304
- async function recordInstalled(fullSlug, entry) {
9305
- const lf = await readLockfile();
9306
- lf.packages[fullSlug] = entry;
9307
- await writeLockfile(lf);
9308
- }
9309
-
9310
- // src/lib/merge-emitted-file.ts
10431
+ // ../../packages/translators/src/merge-emitted-file.ts
9311
10432
  import tomlLib2 from "@iarna/toml";
9312
10433
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
10434
+ var MergeRefusal = class extends Error {
10435
+ code;
10436
+ reason;
10437
+ constructor(reason) {
10438
+ super(refusalMessage(reason));
10439
+ this.name = "MergeRefusal";
10440
+ this.code = reason.kind === "prototype_key" ? "unsafe_package_config" : "merge_refused";
10441
+ this.reason = reason;
10442
+ }
10443
+ };
10444
+ function refusalMessage(reason) {
10445
+ switch (reason.kind) {
10446
+ case "prototype_key":
10447
+ return `refusing to merge config fragment \u2014 prototype-polluting key "${reason.key}" at ${reason.path}`;
10448
+ case "malformed_file":
10449
+ return `refusing to merge into "${reason.targetPath}" \u2014 existing file is not valid ${reason.format.toUpperCase()}: ${reason.parseError}`;
10450
+ case "non_object_root":
10451
+ return `refusing to merge into "${reason.targetPath}" \u2014 existing ${reason.format.toUpperCase()} root is not an object`;
10452
+ case "dangling_marker":
10453
+ return `refusing to merge \u2014 found a "kordis:${reason.fullSlug}" start marker with no matching end marker`;
10454
+ }
10455
+ }
9313
10456
  function isPlainObject(v) {
9314
10457
  return typeof v === "object" && v !== null && !Array.isArray(v);
9315
10458
  }
@@ -9334,11 +10477,7 @@ function assertNoProtoKeys(value, path2) {
9334
10477
  if (!isPlainObject(value)) return;
9335
10478
  for (const [k, v] of Object.entries(value)) {
9336
10479
  if (PROTO_KEYS.has(k)) {
9337
- const e = new Error(
9338
- `refusing to merge config fragment \u2014 prototype-polluting key "${k}" at ${[...path2, k].join(".")}`
9339
- );
9340
- e.hint = "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
9341
- throw e;
10480
+ throw new MergeRefusal({ kind: "prototype_key", key: k, path: [...path2, k].join(".") });
9342
10481
  }
9343
10482
  assertNoProtoKeys(v, [...path2, k]);
9344
10483
  }
@@ -9347,19 +10486,15 @@ function isOwnedServerLeaf(path2) {
9347
10486
  return path2.length === 2 && (path2[0] === "mcpServers" || path2[0] === "mcp_servers");
9348
10487
  }
9349
10488
  function deepMergeFragment(existing, fragment, path2 = []) {
9350
- if (path2.length === 0) assertNoProtoKeys(fragment, []);
10489
+ assertNoProtoKeys(fragment, path2);
10490
+ return mergeAt(existing, fragment, path2);
10491
+ }
10492
+ function mergeAt(existing, fragment, path2) {
9351
10493
  if (isOwnedServerLeaf(path2)) return fragment;
9352
10494
  if (isPlainObject(existing) && isPlainObject(fragment)) {
9353
10495
  const out = { ...existing };
9354
10496
  for (const [k, v] of Object.entries(fragment)) {
9355
- if (PROTO_KEYS.has(k)) {
9356
- const e = new Error(
9357
- `refusing to merge config fragment \u2014 prototype-polluting key "${k}" at ${[...path2, k].join(".")}`
9358
- );
9359
- e.hint = "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
9360
- throw e;
9361
- }
9362
- out[k] = deepMergeFragment(existing[k], v, [...path2, k]);
10497
+ out[k] = mergeAt(existing[k], v, [...path2, k]);
9363
10498
  }
9364
10499
  return out;
9365
10500
  }
@@ -9402,18 +10537,15 @@ function mergeStructuredFile(existingRaw, fragmentRaw, format, targetPath) {
9402
10537
  try {
9403
10538
  existing = parseStructured(existingRaw, format);
9404
10539
  } catch (err) {
9405
- const e = new Error(
9406
- `refusing to merge into "${targetPath}" \u2014 existing file is not valid ${format.toUpperCase()}: ${err.message}`
9407
- );
9408
- e.hint = `Fix or remove the malformed ${targetPath}, then re-run \`kordis pull\`/\`kordis update\`. Refusing to overwrite it blind \u2014 it may hold config from other tools/packages.`;
9409
- throw e;
10540
+ throw new MergeRefusal({
10541
+ kind: "malformed_file",
10542
+ targetPath,
10543
+ format,
10544
+ parseError: err.message
10545
+ });
9410
10546
  }
9411
10547
  if (!isPlainObject(existing) && isPlainObject(fragment)) {
9412
- const e = new Error(
9413
- `refusing to merge into "${targetPath}" \u2014 existing ${format.toUpperCase()} root is not an object`
9414
- );
9415
- e.hint = `${targetPath} parses but its top level is not an object, so a config fragment can't be merged in. Move the file aside (or fix its shape), then re-run \`kordis pull\`/\`kordis update\`.`;
9416
- throw e;
10548
+ throw new MergeRefusal({ kind: "non_object_root", targetPath, format });
9417
10549
  }
9418
10550
  const merged = deepMergeFragment(existing, fragment);
9419
10551
  return stringifyStructured(merged, format);
@@ -9435,15 +10567,67 @@ function mergeFencedFile(existingRaw, fullSlug, block) {
9435
10567
  return existingRaw.slice(0, startMatch.index) + trimmedBlock + existingRaw.slice(spanEnd);
9436
10568
  }
9437
10569
  if (startMatch && endIdx === -1) {
9438
- const e = new Error(
9439
- `refusing to merge \u2014 found a "kordis:${fullSlug}" start marker with no matching end marker`
9440
- );
9441
- e.hint = `The file has a dangling <!-- kordis:${fullSlug}:...:start --> marker. Delete that line (or restore its <!-- kordis:${fullSlug}:end --> line), then re-run \`kordis pull\`/\`kordis update\`.`;
9442
- throw e;
10570
+ throw new MergeRefusal({ kind: "dangling_marker", fullSlug });
9443
10571
  }
9444
10572
  return existingRaw.trimEnd() + "\n\n" + trimmedBlock + "\n";
9445
10573
  }
9446
10574
 
10575
+ // src/lockfile.ts
10576
+ init_brand();
10577
+ import { join as join9 } from "path";
10578
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
10579
+ function kordisHome2() {
10580
+ return brandHome();
10581
+ }
10582
+ function lockfilePath() {
10583
+ return join9(kordisHome2(), "installed.json");
10584
+ }
10585
+ function lockfileDisplayPath() {
10586
+ const path2 = lockfilePath();
10587
+ const home = process.env["HOME"];
10588
+ return home && path2.startsWith(home) ? "~" + path2.slice(home.length) : path2;
10589
+ }
10590
+ async function readLockfile() {
10591
+ try {
10592
+ const raw = await readFile7(lockfilePath(), "utf8");
10593
+ return JSON.parse(raw);
10594
+ } catch {
10595
+ return { packages: {} };
10596
+ }
10597
+ }
10598
+ async function writeLockfile(lf) {
10599
+ await mkdir5(kordisHome2(), { recursive: true });
10600
+ await writeFile5(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
10601
+ encoding: "utf8",
10602
+ mode: 384
10603
+ });
10604
+ }
10605
+ async function recordInstalled(fullSlug, entry) {
10606
+ const lf = await readLockfile();
10607
+ lf.packages[fullSlug] = entry;
10608
+ await writeLockfile(lf);
10609
+ }
10610
+
10611
+ // src/lib/merge-refusal.ts
10612
+ function adaptMergeRefusal(err) {
10613
+ if (!(err instanceof MergeRefusal)) return null;
10614
+ const e = new Error(err.message);
10615
+ e.hint = mergeRefusalHint(err.reason);
10616
+ return e;
10617
+ }
10618
+ function mergeRefusalHint(reason) {
10619
+ switch (reason.kind) {
10620
+ case "prototype_key":
10621
+ return "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
10622
+ case "malformed_file":
10623
+ return `Fix or remove the malformed ${reason.targetPath}, then re-run \`kordis pull\`/\`kordis update\`. Refusing to overwrite it blind \u2014 it may hold config from other tools/packages.`;
10624
+ case "non_object_root":
10625
+ return `${reason.targetPath} parses but its top level is not an object, so a config fragment can't be merged in. Move the file aside (or fix its shape), then re-run \`kordis pull\`/\`kordis update\`.`;
10626
+ case "dangling_marker":
10627
+ return `The file has a dangling <!-- kordis:${reason.fullSlug}:...:start --> marker. Delete that line (or restore its <!-- kordis:${reason.fullSlug}:end --> line), then re-run \`kordis pull\`/\`kordis update\`.`;
10628
+ }
10629
+ }
10630
+
9447
10631
  // src/commands/pull.ts
9448
10632
  async function requireExecutableApproval(pkg, fullSlug, files, approve2) {
9449
10633
  if (!pkg.capabilities.executable) return;
@@ -9841,7 +11025,12 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
9841
11025
  if (err.code !== "ENOENT") throw err;
9842
11026
  }
9843
11027
  if (f.format === "json" || f.format === "yaml" || f.format === "toml") {
9844
- const merged2 = mergeStructuredFile(existingRaw, f.content, f.format, abs);
11028
+ let merged2;
11029
+ try {
11030
+ merged2 = mergeStructuredFile(existingRaw, f.content, f.format, abs);
11031
+ } catch (err) {
11032
+ throw adaptMergeRefusal(err) ?? err;
11033
+ }
9845
11034
  if (existingRaw !== null && existingRaw.trim() !== "") {
9846
11035
  notes?.push(
9847
11036
  `merged into ${f.path} \u2014 comments/formatting in the existing file are not preserved`
@@ -9850,11 +11039,17 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
9850
11039
  await writeFile6(abs, merged2, "utf8");
9851
11040
  return;
9852
11041
  }
9853
- const merged = mergeFencedFile(existingRaw, fullSlug, f.content);
11042
+ let merged;
11043
+ try {
11044
+ merged = mergeFencedFile(existingRaw, fullSlug, f.content);
11045
+ } catch (err) {
11046
+ throw adaptMergeRefusal(err) ?? err;
11047
+ }
9854
11048
  await writeFile6(abs, merged, "utf8");
9855
11049
  }
9856
11050
 
9857
11051
  // src/commands/read.ts
11052
+ init_src2();
9858
11053
  init_sections();
9859
11054
  import { Command as Command6 } from "commander";
9860
11055
 
@@ -9874,6 +11069,15 @@ function untrustedFenceNotice(sentinel) {
9874
11069
  }
9875
11070
 
9876
11071
  // src/commands/read.ts
11072
+ var shellWord = (word) => /^[\w./@:+-]+$/.test(word) ? word : JSON.stringify(word);
11073
+ function readAgainCommand(fullSlug, parsed, opts) {
11074
+ const suffix = parsed.version ? `@${parsed.version}` : parsed.range ? `@${parsed.range}` : "";
11075
+ return [
11076
+ `kordis read ${shellWord(`${fullSlug}${suffix}`)}`,
11077
+ ...opts.pin === void 0 ? [] : [`--pin ${shellWord(opts.pin)}`],
11078
+ ...opts.allowPending ? ["--allow-pending"] : []
11079
+ ].join(" ");
11080
+ }
9877
11081
  function readCommand() {
9878
11082
  return new Command6("read").description("fetch a package body into the agent context (no install)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--pin <semver>", "pin to an exact version (overrides @suffix)").option("--outline", "print only the section map (skeleton) \u2014 cheap on context", false).option("--section <name>", "print only one section (see names with --outline)").action(
9879
11083
  async (rawSlug, opts) => {
@@ -9951,9 +11155,19 @@ Bundled files \u2014 fetch with ${c.cyan(`kordis pull ${res.full_slug}`)}:
9951
11155
  const sec = sectionOf(res.body, opts.section);
9952
11156
  if (!sec) {
9953
11157
  const names = outlineOf(res.body).sections.map((s) => s.name).join(", ");
11158
+ const readAgain = readAgainCommand(res.full_slug, parsed, opts);
11159
+ if (isJsonMode()) {
11160
+ throw new KordisApiError(400, {
11161
+ error: {
11162
+ code: "usage_error",
11163
+ message: `section "${opts.section}" not found`,
11164
+ hint: names ? `Available sections: ${names}. Read one with \`${readAgain} --section <name>\`.` : `This package has no sub-sections \u2014 read the whole body with \`${readAgain}\`.`
11165
+ }
11166
+ });
11167
+ }
9954
11168
  process.stderr.write(
9955
11169
  `${c.red("\u2717")} section "${opts.section}" not found. Available: ${names || "(none \u2014 this package has no sub-sections)"}
9956
- See the map with ${c.cyan(`kordis read ${res.full_slug} --outline`)}.
11170
+ See the map with ${c.cyan(`${readAgain} --outline`)}.
9957
11171
  `
9958
11172
  );
9959
11173
  process.exitCode = 2;
@@ -10023,6 +11237,7 @@ import { homedir as homedir4 } from "os";
10023
11237
  import { join as join11 } from "path";
10024
11238
  var PER_FILE_CAP = 16 * 1024;
10025
11239
  var TOTAL_CAP = 48 * 1024;
11240
+ var TRUNCATION_RESERVE = 64;
10026
11241
  async function pathExists3(p) {
10027
11242
  try {
10028
11243
  await access3(p);
@@ -10033,6 +11248,7 @@ async function pathExists3(p) {
10033
11248
  }
10034
11249
  function capContent(raw, cap) {
10035
11250
  if (Buffer.byteLength(raw, "utf8") <= cap) return { content: raw, truncated: false };
11251
+ const fill = cap - TRUNCATION_RESERVE;
10036
11252
  const lines = raw.split("\n");
10037
11253
  let acc = 0;
10038
11254
  let lastHeadingIdx = -1;
@@ -10041,7 +11257,7 @@ function capContent(raw, cap) {
10041
11257
  const line = lines[i] ?? "";
10042
11258
  const lineBytes = Buffer.byteLength(`${line}
10043
11259
  `, "utf8");
10044
- if (acc + lineBytes > cap) {
11260
+ if (acc + lineBytes > fill) {
10045
11261
  cutIdx = i;
10046
11262
  break;
10047
11263
  }
@@ -10049,6 +11265,7 @@ function capContent(raw, cap) {
10049
11265
  if (/^#{1,6}\s/.test(line)) lastHeadingIdx = i;
10050
11266
  }
10051
11267
  const cut = lastHeadingIdx > cutIdx / 2 ? lastHeadingIdx : cutIdx;
11268
+ if (cut === 0) return null;
10052
11269
  const kept = lines.slice(0, cut).join("\n");
10053
11270
  return {
10054
11271
  content: `${kept}
@@ -10077,7 +11294,6 @@ async function readHarnessContext(cwd = process.cwd(), home = homedir4()) {
10077
11294
  const files = [];
10078
11295
  let totalBytes = 0;
10079
11296
  for (const cand of candidates) {
10080
- if (totalBytes >= TOTAL_CAP) break;
10081
11297
  if (!await pathExists3(cand.path)) continue;
10082
11298
  let raw;
10083
11299
  try {
@@ -10086,7 +11302,9 @@ async function readHarnessContext(cwd = process.cwd(), home = homedir4()) {
10086
11302
  continue;
10087
11303
  }
10088
11304
  const cap = Math.min(PER_FILE_CAP, TOTAL_CAP - totalBytes);
10089
- const { content, truncated } = capContent(raw, cap);
11305
+ const capped2 = capContent(raw, cap);
11306
+ if (capped2 === null) break;
11307
+ const { content, truncated } = capped2;
10090
11308
  totalBytes += Buffer.byteLength(content, "utf8");
10091
11309
  files.push({
10092
11310
  path: cand.display,
@@ -12743,9 +13961,9 @@ function assertNothingLost(before, after, action) {
12743
13961
  });
12744
13962
  }
12745
13963
  }
12746
- async function commit2(root, base, next) {
13964
+ async function commit2(root, base2, next) {
12747
13965
  const current = await readIgnoreFileState(root);
12748
- if (current.hash !== base.hash) {
13966
+ if (current.hash !== base2.hash) {
12749
13967
  return { ok: false, reason: "revision_conflict", current };
12750
13968
  }
12751
13969
  if (next === current.content) return { ok: true, content: next, changed: false };
@@ -12791,15 +14009,15 @@ function planRemoveIgnoreEntry(content, relPath) {
12791
14009
  assertNothingLost(next, content, "un-excluding");
12792
14010
  return { content: next, changed: true };
12793
14011
  }
12794
- async function addIgnoreEntry(root, relPath, base) {
12795
- const edit = planAddIgnoreEntry(base.content, relPath);
12796
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
12797
- return commit2(root, base, edit.content);
14012
+ async function addIgnoreEntry(root, relPath, base2) {
14013
+ const edit = planAddIgnoreEntry(base2.content, relPath);
14014
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
14015
+ return commit2(root, base2, edit.content);
12798
14016
  }
12799
- async function removeIgnoreEntry(root, relPath, base) {
12800
- const edit = planRemoveIgnoreEntry(base.content, relPath);
12801
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
12802
- return commit2(root, base, edit.content);
14017
+ async function removeIgnoreEntry(root, relPath, base2) {
14018
+ const edit = planRemoveIgnoreEntry(base2.content, relPath);
14019
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
14020
+ return commit2(root, base2, edit.content);
12803
14021
  }
12804
14022
 
12805
14023
  // src/commands/memory/exclude.ts
@@ -12845,8 +14063,8 @@ async function openVaultOrExplain(path2) {
12845
14063
  // src/commands/memory/link.ts
12846
14064
  init_brand();
12847
14065
  function sameBasename(all, entry) {
12848
- const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
12849
- return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base);
14066
+ const base2 = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
14067
+ return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base2);
12850
14068
  }
12851
14069
  function spreadAcrossFolders(rows, max) {
12852
14070
  const seen = /* @__PURE__ */ new Set();
@@ -13110,7 +14328,7 @@ function excludeCommand() {
13110
14328
  return new Command28("exclude").description("stop reading a note entirely \u2014 the note itself is not changed").argument("<path>", "vault-relative path of the note, e.g. decisions/launch-date.md").option("--yes", "skip the confirmation prompt").option("--vault <path>", "which linked vault the note is in").action(async (path2, opts) => {
13111
14329
  const root = await resolveVaultRoot(opts.vault);
13112
14330
  const target = await mustMatchAFile(root, path2);
13113
- const base = await readIgnoreFileState(root);
14331
+ const base2 = await readIgnoreFileState(root);
13114
14332
  if (!opts.yes && isJsonMode()) {
13115
14333
  throw memoryError(
13116
14334
  "refusing to change what this vault reads without confirmation",
@@ -13134,7 +14352,7 @@ function excludeCommand() {
13134
14352
  return;
13135
14353
  }
13136
14354
  }
13137
- const result = await addIgnoreEntry(root, target, base);
14355
+ const result = await addIgnoreEntry(root, target, base2);
13138
14356
  if (!result.ok) {
13139
14357
  printDual({
13140
14358
  json: {
@@ -13172,7 +14390,7 @@ function unexcludeCommand() {
13172
14390
  return new Command28("unexclude").description("read a note again after excluding it").argument("<path>", "vault-relative path of the note").option("--yes", "skip the confirmation prompt").option("--vault <path>", "which linked vault the note is in").action(async (path2, opts) => {
13173
14391
  const root = await resolveVaultRoot(opts.vault);
13174
14392
  const target = path2.normalize("NFC");
13175
- const base = await readIgnoreFileState(root);
14393
+ const base2 = await readIgnoreFileState(root);
13176
14394
  if (!opts.yes && isJsonMode()) {
13177
14395
  throw memoryError(
13178
14396
  "refusing to change what this vault reads without confirmation",
@@ -13195,7 +14413,7 @@ function unexcludeCommand() {
13195
14413
  return;
13196
14414
  }
13197
14415
  }
13198
- const result = await removeIgnoreEntry(root, target, base);
14416
+ const result = await removeIgnoreEntry(root, target, base2);
13199
14417
  if (!result.ok) {
13200
14418
  printDual({
13201
14419
  json: {