memgineering 0.23.1 → 0.25.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
@@ -367,6 +367,26 @@ var init_schema = __esm({
367
367
  scope: z.string().min(1).nullable(),
368
368
  /** Id of the claim that is current for this memory, when one is declared. */
369
369
  current_claim: z.string().min(1).nullable(),
370
+ /**
371
+ * The end date the NOTE declares, for the notes that state no claim.
372
+ *
373
+ * `valid_until` lived only on `Claim`, and the mapper mints a claim only for
374
+ * a note carrying a `claim:` key — which `remember` has never written. So on
375
+ * a real brain most notes could not carry an end date that anything read:
376
+ * sampled six binding rules on a production brain and four had no claim at
377
+ * all, the two that did having been through `revise`. A date in the
378
+ * frontmatter of the other four was simply ignored.
379
+ *
380
+ * Optional rather than nullable, like `kind` and `binding` above and for the
381
+ * same reason: a note written before this existed must map to exactly the
382
+ * `Memory` it always did, or the regeneration test's byte comparison rewrites
383
+ * every cache in existence on upgrade.
384
+ *
385
+ * The claim's copy WINS where both exist — it is the more specific statement,
386
+ * and a note whose conclusion carries its own dates is saying something about
387
+ * that conclusion rather than about the file.
388
+ */
389
+ valid_until: z.string().min(1).optional(),
370
390
  status: MemoryStatusSchema,
371
391
  /**
372
392
  * Whether this memory binds what an agent does, rather than informing it.
@@ -849,8 +869,8 @@ function scopeFromPath(path) {
849
869
  return parts.length >= 2 ? parts[parts.length - 2] ?? null : null;
850
870
  }
851
871
  function titleFromPath(path) {
852
- const base = path.split("/").pop() ?? path;
853
- return base.replace(/\.[^.]+$/, "") || base;
872
+ const base2 = path.split("/").pop() ?? path;
873
+ return base2.replace(/\.[^.]+$/, "") || base2;
854
874
  }
855
875
  function h1Of(intro) {
856
876
  for (const line of intro.split("\n")) {
@@ -964,8 +984,8 @@ function mapNoteToMemory(input) {
964
984
  const ns = explicit ? nsRaw : {};
965
985
  const id = asString(ns["id"]) ?? path;
966
986
  const title = asString(ns["title"]) ?? asString(frontmatter?.["title"]) ?? h1Of(outline.intro) ?? titleFromPath(path);
967
- const declaredScope = asString(ns["scope"]);
968
- const scope = declaredScope ?? scopeFromPath(path);
987
+ const declaredScope2 = asString(ns["scope"]);
988
+ const scope = declaredScope2 ?? scopeFromPath(path);
969
989
  const summary = asString(ns["summary"]) ?? outline.description ?? firstParagraph(outline.intro) ?? firstSectionParagraph(outline.sections) ?? null;
970
990
  const memory = {
971
991
  id,
@@ -1009,11 +1029,17 @@ function mapNoteToMemory(input) {
1009
1029
  aliases: asStringArray(ns["aliases"]),
1010
1030
  entities: asStringArray(ns["entities"]),
1011
1031
  updated_at: asString(ns["updated_at"]) ?? asString(frontmatter?.["updated_at"]),
1032
+ // Carried onto the memory as well as onto the claim below, because a note
1033
+ // that states no claim gets no claim object at all — and `remember` has
1034
+ // never written a `claim:` key, so that is most of a real brain. Written
1035
+ // only when present, so a note without one maps byte-identically to what
1036
+ // it always did.
1037
+ ...asString(ns["valid_until"]) === null ? {} : { valid_until: asString(ns["valid_until"]) },
1012
1038
  provenance: {
1013
1039
  sourcePath: path,
1014
1040
  sourceRevision: revision,
1015
1041
  mapping: explicit ? "explicit" : "heuristic",
1016
- scopeSource: declaredScope !== null ? "explicit" : scope === null ? "none" : "path"
1042
+ scopeSource: declaredScope2 !== null ? "explicit" : scope === null ? "none" : "path"
1017
1043
  }
1018
1044
  };
1019
1045
  const claims = [];
@@ -1144,7 +1170,7 @@ var init_cache_io = __esm({
1144
1170
  "../../packages/memory-engine/src/view/cache-io.ts"() {
1145
1171
  "use strict";
1146
1172
  init_schema();
1147
- SCHEMA_VERSION = 6;
1173
+ SCHEMA_VERSION = 7;
1148
1174
  POLICY_VERSION = 1;
1149
1175
  CacheEntrySchema = z2.object({
1150
1176
  path: z2.string().min(1),
@@ -1759,6 +1785,12 @@ function terms(text) {
1759
1785
  (match) => splitMixedScript(match[0])
1760
1786
  );
1761
1787
  }
1788
+ function contentTerms(text) {
1789
+ const all = terms(text);
1790
+ if (all.some((t) => HANGUL.test(t))) return all;
1791
+ const content = all.filter((t) => !LATIN_STOPWORDS.has(t));
1792
+ return content.length > 0 ? content : all;
1793
+ }
1762
1794
  function indexTerms(text) {
1763
1795
  return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap((match) => {
1764
1796
  const token = match[0];
@@ -1831,8 +1863,9 @@ function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false)
1831
1863
  return score;
1832
1864
  }
1833
1865
  function searchMemories(memories, query) {
1834
- const q = terms(query);
1866
+ const q = contentTerms(query);
1835
1867
  if (q.length === 0) return [];
1868
+ const scale = terms(query).length;
1836
1869
  const scored = [];
1837
1870
  for (const memory of memories) {
1838
1871
  let score = fieldScore(q, memory.title, WEIGHTS.title, true);
@@ -1840,13 +1873,13 @@ function searchMemories(memories, query) {
1840
1873
  for (const entity of memory.entities) score += fieldScore(q, entity, WEIGHTS.entity);
1841
1874
  score += fieldScore(q, memory.summary, WEIGHTS.summary);
1842
1875
  score += fieldScore(q, memory.scope, WEIGHTS.scope);
1843
- if (score > 0) scored.push({ memory, score: score / q.length });
1876
+ if (score > 0) scored.push({ memory, score: score / scale });
1844
1877
  }
1845
1878
  return scored.sort(
1846
1879
  (a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
1847
1880
  );
1848
1881
  }
1849
- var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
1882
+ var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, LATIN_STOPWORDS, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
1850
1883
  var init_search = __esm({
1851
1884
  "../../packages/memory-engine/src/recall/search.ts"() {
1852
1885
  "use strict";
@@ -1855,6 +1888,120 @@ var init_search = __esm({
1855
1888
  COMPONENT_SEPARATOR = /[._/-]+/u;
1856
1889
  HAS_COMPONENT_SEPARATOR = /[._/-]/u;
1857
1890
  LATIN_HANGUL_BOUNDARY = new RegExp("(?<=\\p{Script=Latin})(?=\\p{Script=Hangul})|(?<=\\p{Script=Hangul})(?=\\p{Script=Latin})", "u");
1891
+ LATIN_STOPWORDS = /* @__PURE__ */ new Set([
1892
+ // pronouns and possessives
1893
+ "i",
1894
+ "me",
1895
+ "my",
1896
+ "mine",
1897
+ "myself",
1898
+ "we",
1899
+ "us",
1900
+ "our",
1901
+ "ours",
1902
+ "ourselves",
1903
+ "you",
1904
+ "your",
1905
+ "yours",
1906
+ "yourself",
1907
+ "yourselves",
1908
+ "he",
1909
+ "him",
1910
+ "his",
1911
+ "himself",
1912
+ "she",
1913
+ "her",
1914
+ "hers",
1915
+ "herself",
1916
+ "it",
1917
+ "its",
1918
+ "itself",
1919
+ "they",
1920
+ "them",
1921
+ "their",
1922
+ "theirs",
1923
+ "themselves",
1924
+ // determiners
1925
+ "a",
1926
+ "an",
1927
+ "the",
1928
+ "this",
1929
+ "that",
1930
+ "these",
1931
+ "those",
1932
+ "some",
1933
+ "any",
1934
+ "each",
1935
+ "every",
1936
+ // auxiliaries and modals
1937
+ "am",
1938
+ "is",
1939
+ "are",
1940
+ "was",
1941
+ "were",
1942
+ "be",
1943
+ "been",
1944
+ "being",
1945
+ "do",
1946
+ "does",
1947
+ "did",
1948
+ "have",
1949
+ "has",
1950
+ "had",
1951
+ "will",
1952
+ "would",
1953
+ "shall",
1954
+ "should",
1955
+ "can",
1956
+ "could",
1957
+ "may",
1958
+ "might",
1959
+ "must",
1960
+ // question words
1961
+ "what",
1962
+ "which",
1963
+ "who",
1964
+ "whom",
1965
+ "whose",
1966
+ "when",
1967
+ "where",
1968
+ "why",
1969
+ "how",
1970
+ // prepositions
1971
+ "about",
1972
+ "above",
1973
+ "after",
1974
+ "at",
1975
+ "before",
1976
+ "by",
1977
+ "for",
1978
+ "from",
1979
+ "in",
1980
+ "into",
1981
+ "of",
1982
+ "off",
1983
+ "on",
1984
+ "onto",
1985
+ "over",
1986
+ "to",
1987
+ "under",
1988
+ "with",
1989
+ "without",
1990
+ "through",
1991
+ "during",
1992
+ // conjunctions and particles
1993
+ "and",
1994
+ "but",
1995
+ "or",
1996
+ "nor",
1997
+ "so",
1998
+ "if",
1999
+ "then",
2000
+ "than",
2001
+ "as",
2002
+ "there",
2003
+ "here"
2004
+ ]);
1858
2005
  SUBSTRING_MIN = 2;
1859
2006
  PARTICLES = [
1860
2007
  "\uC774\uB77C\uB3C4",
@@ -2005,7 +2152,7 @@ function freshnessOf(memory, claims, now) {
2005
2152
  if (memory.status === "superseded") return "superseded";
2006
2153
  if (memory.status === "retired") return "retired";
2007
2154
  const current = claims.find((c2) => c2.id === memory.current_claim) ?? claims[0];
2008
- const until = current?.valid_until;
2155
+ const until = current?.valid_until ?? memory.valid_until ?? null;
2009
2156
  if (until != null) {
2010
2157
  const t = Date.parse(until);
2011
2158
  if (Number.isFinite(t) && t < now.getTime()) return "expired";
@@ -2035,7 +2182,23 @@ function recall(entries, query, opts = {}) {
2035
2182
  (e) => (e.chunks ?? []).map((c2) => ({ memoryId: e.memory.id, label: c2.label, text: c2.text }))
2036
2183
  )
2037
2184
  ).search(queryShape.lexical);
2038
- const semanticHits = depth === "exact" ? [] : opts.semantic ?? [];
2185
+ const offeredSemantic = depth === "exact" ? [] : opts.semantic ?? [];
2186
+ const reachedByWords = /* @__PURE__ */ new Set([
2187
+ ...cardHits.map((hit) => hit.memory.id),
2188
+ ...chunkHits.map((hit) => hit.memoryId)
2189
+ ]);
2190
+ const floor = opts.meaningFloor;
2191
+ const bestSimilarity = offeredSemantic.reduce(
2192
+ (best, hit) => Math.max(best, Math.min(1, Math.max(0, hit.similarity))),
2193
+ 0
2194
+ );
2195
+ const semanticHits = floor === void 0 ? offeredSemantic : offeredSemantic.filter((hit) => {
2196
+ if (reachedByWords.has(hit.id)) return true;
2197
+ const s = Math.min(1, Math.max(0, hit.similarity));
2198
+ if (floor.absolute !== void 0 && s < floor.absolute) return false;
2199
+ if (floor.gap !== void 0 && bestSimilarity - s > floor.gap) return false;
2200
+ return true;
2201
+ });
2039
2202
  const scoreById = /* @__PURE__ */ new Map();
2040
2203
  for (const hit of cardHits) scoreById.set(hit.memory.id, hit.score);
2041
2204
  for (const hit of chunkHits) {
@@ -2045,6 +2208,14 @@ function recall(entries, query, opts = {}) {
2045
2208
  const similarity = Math.min(1, Math.max(0, hit.similarity));
2046
2209
  scoreById.set(hit.id, (scoreById.get(hit.id) ?? 0) + similarity * SEMANTIC_WEIGHT);
2047
2210
  }
2211
+ const wordIds = reachedByWords;
2212
+ const meaningIds = new Set(semanticHits.map((hit) => hit.id));
2213
+ const matchedOf = (id) => {
2214
+ if (opts.semantic === void 0 || depth === "exact") return void 0;
2215
+ const words = wordIds.has(id);
2216
+ const meaning = meaningIds.has(id);
2217
+ return words && meaning ? "both" : words ? "words" : meaning ? "meaning" : void 0;
2218
+ };
2048
2219
  const memoryById = new Map(allMemories.map((m) => [m.id, m]));
2049
2220
  const hits = [...scoreById.entries()].flatMap(([id, score]) => {
2050
2221
  const memory = memoryById.get(id);
@@ -2145,7 +2316,11 @@ function recall(entries, query, opts = {}) {
2145
2316
  type: memory.type,
2146
2317
  // The handle IS the progressive part. No claim text here, by
2147
2318
  // construction rather than by remembering to strip it.
2148
- detail_handle: `recall://${memory.id}`
2319
+ detail_handle: `recall://${memory.id}`,
2320
+ ...(() => {
2321
+ const matched = matchedOf(memory.id);
2322
+ return matched === void 0 ? {} : { matched };
2323
+ })()
2149
2324
  };
2150
2325
  }
2151
2326
  }
@@ -2171,7 +2346,7 @@ function depthOf(value) {
2171
2346
  function boostFor(memory) {
2172
2347
  return (memory.binding === true ? BINDING_BOOST : 0) + (memory.kind === void 0 ? 0 : KIND_BOOST[memory.kind] ?? 0);
2173
2348
  }
2174
- var RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
2349
+ var MEANING_ONLY_NOTE, RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
2175
2350
  var init_planner = __esm({
2176
2351
  "../../packages/memory-engine/src/recall/planner.ts"() {
2177
2352
  "use strict";
@@ -2180,6 +2355,7 @@ var init_planner = __esm({
2180
2355
  init_conflicts();
2181
2356
  init_chunks();
2182
2357
  init_search();
2358
+ MEANING_ONLY_NOTE = "matched by meaning only \u2014 none of the question\u2019s words are in this note";
2183
2359
  RECALL_DEPTHS = ["exact", "standard"];
2184
2360
  RecallOptionError = class extends Error {
2185
2361
  hint;
@@ -2273,6 +2449,170 @@ var init_semantic = __esm({
2273
2449
  }
2274
2450
  });
2275
2451
 
2452
+ // ../../packages/memory-engine/src/base-templates.ts
2453
+ function base(id, title, summary, body) {
2454
+ return [
2455
+ "---",
2456
+ "memgineering:",
2457
+ ` id: ${id}`,
2458
+ ` title: ${title}`,
2459
+ ` summary: ${summary}`,
2460
+ " scope: base",
2461
+ "---",
2462
+ "",
2463
+ `# ${title}`,
2464
+ "",
2465
+ body,
2466
+ ""
2467
+ ].join("\n");
2468
+ }
2469
+ function ownEntry(record, key2) {
2470
+ return Object.hasOwn(record, key2) ? record[key2] : void 0;
2471
+ }
2472
+ var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES;
2473
+ var init_base_templates = __esm({
2474
+ "../../packages/memory-engine/src/base-templates.ts"() {
2475
+ "use strict";
2476
+ BASE_TEMPLATES = {
2477
+ "01_BASE/CRITICAL_FACTS.md": base(
2478
+ "critical-facts",
2479
+ "Critical facts",
2480
+ "The few things that must never be wrong.",
2481
+ [
2482
+ "Keep this short enough to read every session. Anything that can be",
2483
+ "looked up when needed belongs in a note instead.",
2484
+ "",
2485
+ "- "
2486
+ ].join("\n")
2487
+ ),
2488
+ "01_BASE/USER.md": base(
2489
+ "user",
2490
+ "About me",
2491
+ "Who I am and what context I work in.",
2492
+ [
2493
+ "Background, role, what you are working on, what an agent should assume",
2494
+ "you already know.",
2495
+ "",
2496
+ "- "
2497
+ ].join("\n")
2498
+ ),
2499
+ "01_BASE/PREFERENCES.md": base(
2500
+ "preferences",
2501
+ "Preferences",
2502
+ "How I like answers, code and decisions handled.",
2503
+ ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
2504
+ ),
2505
+ "01_BASE/BOUNDARIES.md": base(
2506
+ "boundaries",
2507
+ "Boundaries",
2508
+ "What an agent must not do without asking me first.",
2509
+ [
2510
+ "Privacy, spending, anything irreversible, anything that leaves this",
2511
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
2512
+ "that will be interpreted generously.",
2513
+ "",
2514
+ "Storage boundaries belong here too: topics you never want written into",
2515
+ "this brain \u2014 health, beliefs, relationships, anything you name. An",
2516
+ "agent checks this file before it records anything.",
2517
+ "",
2518
+ "- "
2519
+ ].join("\n")
2520
+ ),
2521
+ "01_BASE/TOOLING.md": base(
2522
+ "tooling",
2523
+ "Tooling",
2524
+ "The tools, machines and commands I actually use.",
2525
+ [
2526
+ "Editors, languages, deploy commands, where things run. Enough that an",
2527
+ "agent does not have to guess or discover it again each time.",
2528
+ "",
2529
+ "- "
2530
+ ].join("\n")
2531
+ )
2532
+ };
2533
+ RETIRED_BASE_TEMPLATES = {
2534
+ // Retired 0.11.1, when the prompt grew a storage-boundary invitation.
2535
+ "01_BASE/BOUNDARIES.md": [
2536
+ [
2537
+ "---",
2538
+ "memgineering:",
2539
+ " id: boundaries",
2540
+ " title: Boundaries",
2541
+ " summary: What an agent must not do without asking me first.",
2542
+ " scope: base",
2543
+ "---",
2544
+ "",
2545
+ "# Boundaries",
2546
+ "",
2547
+ "Privacy, spending, anything irreversible, anything that leaves this",
2548
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
2549
+ "that will be interpreted generously.",
2550
+ "",
2551
+ "- ",
2552
+ ""
2553
+ ].join("\n")
2554
+ ]
2555
+ };
2556
+ }
2557
+ });
2558
+
2559
+ // ../../packages/memory-engine/src/recall/unfilled.ts
2560
+ var UNFILLED_FIELDS;
2561
+ var init_unfilled = __esm({
2562
+ "../../packages/memory-engine/src/recall/unfilled.ts"() {
2563
+ "use strict";
2564
+ init_base_templates();
2565
+ UNFILLED_FIELDS = {
2566
+ /** On every card from a server that has this: still the starter file, nothing written in it. */
2567
+ card: "unfilled",
2568
+ /**
2569
+ * In the request: how many such cards the caller will take. Absent means no
2570
+ * cap, so a client older than the field gets the whole ranking it always got —
2571
+ * the same boundary `budget` draws: the client states the capability.
2572
+ */
2573
+ slots: "template_slots",
2574
+ /** In the response, only when the cap left something out: how many. */
2575
+ withheld: "templates_withheld"
2576
+ };
2577
+ }
2578
+ });
2579
+
2580
+ // ../../packages/memory-engine/src/recall/query-prefix.ts
2581
+ var init_query_prefix = __esm({
2582
+ "../../packages/memory-engine/src/recall/query-prefix.ts"() {
2583
+ "use strict";
2584
+ }
2585
+ });
2586
+
2587
+ // ../../packages/memory-engine/src/recall/scope-spread.ts
2588
+ function declaredScope(memory) {
2589
+ return memory.provenance?.scopeSource === "explicit" ? memory.scope ?? null : null;
2590
+ }
2591
+ function scopeSpread(returned, brain) {
2592
+ const scoped = returned.filter((s) => typeof s === "string" && s !== "");
2593
+ const distinct = new Set(scoped);
2594
+ if (distinct.size !== 1) return null;
2595
+ const only = [...distinct][0];
2596
+ const others = /* @__PURE__ */ new Set();
2597
+ for (const scope of brain) if (scope && scope !== only) others.add(scope);
2598
+ if (others.size === 0) return null;
2599
+ const sorted = [...others].sort((a, b) => a.localeCompare(b));
2600
+ return {
2601
+ scope: only,
2602
+ cards: scoped.length,
2603
+ of: returned.length,
2604
+ elsewhere: sorted.slice(0, ELSEWHERE_SHOWN),
2605
+ elsewhere_total: sorted.length
2606
+ };
2607
+ }
2608
+ var ELSEWHERE_SHOWN;
2609
+ var init_scope_spread = __esm({
2610
+ "../../packages/memory-engine/src/recall/scope-spread.ts"() {
2611
+ "use strict";
2612
+ ELSEWHERE_SHOWN = 5;
2613
+ }
2614
+ });
2615
+
2276
2616
  // ../../packages/memory-engine/src/proposal/schema.ts
2277
2617
  import { z as z3 } from "zod";
2278
2618
  function isLifecycleAction(action) {
@@ -2284,8 +2624,14 @@ function isClassifyAction(action) {
2284
2624
  function isBindingAction(action) {
2285
2625
  return action === "unbind";
2286
2626
  }
2627
+ function isScopeAction(action) {
2628
+ return action === "rescope";
2629
+ }
2630
+ function isValidityAction(action) {
2631
+ return action === "expire";
2632
+ }
2287
2633
  function carriesNoClaim(action) {
2288
- return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action);
2634
+ return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action) || isScopeAction(action) || isValidityAction(action);
2289
2635
  }
2290
2636
  function isAccepted(value) {
2291
2637
  return typeof value === "object" && value !== null && value[ACCEPTED] === true;
@@ -2329,6 +2675,32 @@ var init_schema2 = __esm({
2329
2675
  * (`remember --rule`, or a `revise` that is genuinely saying something new).
2330
2676
  */
2331
2677
  "unbind",
2678
+ /**
2679
+ * Where a memory binds changes. Nothing it SAYS changes.
2680
+ *
2681
+ * `scope` is read by `declaredScopeOf` and decides which of the two rule
2682
+ * budgets a decision is drawn from, so it is the field that answers "does
2683
+ * this bind me here". Nothing could write it. The one live consequence:
2684
+ * folding a project-scoped rule into an unscoped one required superseding
2685
+ * it, which retires the loser — so the rule would have vanished everywhere
2686
+ * except the project it was scoped to.
2687
+ *
2688
+ * The value is a scope NAME, or the reserved word `global`. Never null: see
2689
+ * the refinement below for why "absent" and "widen" must not be the same
2690
+ * value on the wire.
2691
+ */
2692
+ "rescope",
2693
+ /**
2694
+ * A memory gets an end date. It is not retired, and it still recalls.
2695
+ *
2696
+ * `freshnessOf` has always returned `expired` for a claim whose
2697
+ * `valid_until` has passed, and `CURRENCY_RANK` has always ranked that just
2698
+ * below `current` — read, ranked, and labelled, with nothing able to write
2699
+ * it. A date-stamped fact ("Phase 1 lands by the end of October") therefore
2700
+ * had two options, both wrong: retire it, which says it no longer holds, or
2701
+ * leave it, which says it holds forever.
2702
+ */
2703
+ "expire",
2332
2704
  "ignore"
2333
2705
  ]);
2334
2706
  StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
@@ -2488,6 +2860,37 @@ var init_schema2 = __esm({
2488
2860
  * reclassify is pending — approve, reject, or let it expire first.
2489
2861
  */
2490
2862
  proposed_kind: MemoryKindSchema.nullable().default(null),
2863
+ /**
2864
+ * Where `rescope` moves the memory to, and null for everything else.
2865
+ *
2866
+ * NEVER null to mean "everywhere". That was the first design and it is the
2867
+ * one thing this field must not do: "the caller sent no scope" and "the
2868
+ * caller asked to widen this rule" would be the same bytes, so the schema
2869
+ * could not require a scope on a rescope — and a request that LOST the
2870
+ * field, an older client, a hand-built body, a proposal rebuilt from disk,
2871
+ * would silently widen what a rule binds instead of failing.
2872
+ *
2873
+ * `global` is the word instead, and it is not invented here: `GLOBAL_SCOPE`
2874
+ * is already the reserved value a rule write uses to say it holds
2875
+ * everywhere, and `remember` already stores it as no scope at all. So the
2876
+ * widening is spelled, the refinement below can demand it, and the patcher
2877
+ * still writes what the thirty global rules already on a real brain look
2878
+ * like — no key.
2879
+ *
2880
+ * COMPATIBILITY BOUNDARY, as for `proposed_kind`: a proposal file carrying
2881
+ * `action: "rescope"` does not parse on a CLI built before this, and
2882
+ * `loadProposal` throws rather than silently dropping it.
2883
+ */
2884
+ proposed_scope: z3.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
2885
+ /**
2886
+ * The end date `expire` writes onto the current claim, null elsewhere.
2887
+ *
2888
+ * A date rather than a status: `freshnessOf` reads `valid_until` off the
2889
+ * claim and returns `expired`, which recall already labels and ranks below
2890
+ * `current`. Nothing here decides what expiry MEANS — that was decided
2891
+ * years of code ago and only the writer was missing.
2892
+ */
2893
+ proposed_valid_until: z3.string().min(1).nullable().default(null),
2491
2894
  relations: ProposedRelationsSchema,
2492
2895
  /** Why the AI thinks this change is right. Shown at approval time. */
2493
2896
  reason: z3.string().min(1),
@@ -2524,7 +2927,7 @@ var init_schema2 = __esm({
2524
2927
  path: ["source_excerpt"]
2525
2928
  }
2526
2929
  ).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
2527
- message: "only retire, unretire, reclassify and unbind may omit the claim",
2930
+ message: "only retire, unretire, reclassify, unbind, rescope and expire may omit the claim",
2528
2931
  path: ["claim"]
2529
2932
  }).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
2530
2933
  message: "proposed_kind belongs to reclassify, and reclassify requires one",
@@ -2532,6 +2935,18 @@ var init_schema2 = __esm({
2532
2935
  }).refine((p) => !isClassifyAction(p.action) || p.claim === null, {
2533
2936
  message: "reclassify says what sort of memory this is; it does not take a claim",
2534
2937
  path: ["claim"]
2938
+ }).refine((p) => isScopeAction(p.action) === (p.proposed_scope !== null), {
2939
+ message: "proposed_scope belongs to rescope, and rescope requires one",
2940
+ path: ["proposed_scope"]
2941
+ }).refine((p) => isValidityAction(p.action) === (p.proposed_valid_until !== null), {
2942
+ message: "proposed_valid_until belongs to expire, and expire requires one",
2943
+ path: ["proposed_valid_until"]
2944
+ }).refine((p) => !isScopeAction(p.action) || p.claim === null, {
2945
+ message: "rescope says where a memory binds; it does not take a claim",
2946
+ path: ["claim"]
2947
+ }).refine((p) => !isValidityAction(p.action) || p.claim === null, {
2948
+ message: "expire says when a memory stops being current; it does not take a claim",
2949
+ path: ["claim"]
2535
2950
  });
2536
2951
  ACCEPTED = /* @__PURE__ */ Symbol("memgineering.approved");
2537
2952
  }
@@ -3347,12 +3762,12 @@ function commit(prepared, input, opts = {}) {
3347
3762
  if (carriesNoClaim(parsed.action)) {
3348
3763
  if (parsed.claim !== null) {
3349
3764
  throw Object.assign(new Error(`${parsed.action} does not take a claim`), {
3350
- 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.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
3765
+ 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.`
3351
3766
  });
3352
3767
  }
3353
3768
  } else if (parsed.claim === null) {
3354
3769
  throw Object.assign(new Error(`${parsed.action} needs a claim`), {
3355
- hint: "Only retire, unretire, reclassify and unbind may omit it. They change a memory's standing, its classification or whether it binds, rather than its content."
3770
+ 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."
3356
3771
  });
3357
3772
  } else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
3358
3773
  throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
@@ -3369,6 +3784,26 @@ function commit(prepared, input, opts = {}) {
3369
3784
  hint: "Only `reclassify` moves a memory's kind, and it does nothing else. Propose it separately, with its own reason."
3370
3785
  });
3371
3786
  }
3787
+ if (isScopeAction(parsed.action) && parsed.proposed_scope === null) {
3788
+ throw Object.assign(new Error(`${parsed.action} needs the scope it is proposing`), {
3789
+ 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.`
3790
+ });
3791
+ }
3792
+ if (!isScopeAction(parsed.action) && parsed.proposed_scope !== null) {
3793
+ throw Object.assign(new Error(`${parsed.action} does not set the scope`), {
3794
+ hint: "Only `rescope` moves where a memory binds, and it does nothing else. Propose it separately, with its own reason."
3795
+ });
3796
+ }
3797
+ if (isValidityAction(parsed.action) && parsed.proposed_valid_until === null) {
3798
+ throw Object.assign(new Error(`${parsed.action} needs the date it is proposing`), {
3799
+ 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."
3800
+ });
3801
+ }
3802
+ if (!isValidityAction(parsed.action) && parsed.proposed_valid_until !== null) {
3803
+ throw Object.assign(new Error(`${parsed.action} does not set an end date`), {
3804
+ hint: "Only `expire` gives a memory an end date, and it does nothing else. Propose it separately, with its own reason."
3805
+ });
3806
+ }
3372
3807
  const now = opts.now ?? /* @__PURE__ */ new Date();
3373
3808
  const at = now.toISOString();
3374
3809
  const draft = {
@@ -3378,6 +3813,8 @@ function commit(prepared, input, opts = {}) {
3378
3813
  memory_id: parsed.memory_id,
3379
3814
  claim: parsed.claim,
3380
3815
  proposed_kind: parsed.proposed_kind,
3816
+ proposed_scope: parsed.proposed_scope,
3817
+ proposed_valid_until: parsed.proposed_valid_until,
3381
3818
  relations: parsed.relations,
3382
3819
  reason: parsed.reason,
3383
3820
  source_excerpt: parsed.source_excerpt,
@@ -3416,6 +3853,14 @@ var init_commit = __esm({
3416
3853
  claim: ProposedClaimSchema.nullable().default(null),
3417
3854
  /** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
3418
3855
  proposed_kind: MemoryKindSchema.nullable().default(null),
3856
+ /**
3857
+ * Where `rescope` moves this memory to — a scope name, or the reserved word
3858
+ * `global`. Only that action takes one, and it may not be null: see
3859
+ * `ProposalSchema.proposed_scope` for why "absent" must not spell "widen".
3860
+ */
3861
+ proposed_scope: z5.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
3862
+ /** The end date `expire` writes onto the current claim. Only that action takes one. */
3863
+ proposed_valid_until: z5.string().min(1).nullable().default(null),
3419
3864
  relations: ProposedRelationsSchema.default({}),
3420
3865
  reason: z5.string().min(1),
3421
3866
  /** The passage from the note that justifies this. Required to submit. */
@@ -3526,6 +3971,58 @@ function unbindInput(entry, opts = {}) {
3526
3971
  target_path: entry.path
3527
3972
  };
3528
3973
  }
3974
+ function rescopeInput(entry, scope, opts = {}) {
3975
+ const wanted = scope.trim();
3976
+ if (wanted === "") {
3977
+ throw Object.assign(new Error("a scope cannot be empty"), {
3978
+ hint: `Name the project this memory binds in, or \`${GLOBAL_SCOPE}\` to say it holds everywhere.`
3979
+ });
3980
+ }
3981
+ if ([...wanted].length > SCOPE_LIMIT) {
3982
+ throw Object.assign(new Error(`a scope may be at most ${SCOPE_LIMIT} characters`), {
3983
+ hint: "A scope names a project. If it needs a sentence, it belongs in the note."
3984
+ });
3985
+ }
3986
+ const declared = declaredScopeOf(entry.memory);
3987
+ const same = isGlobalScope(wanted) ? declared === null : declared === wanted;
3988
+ if (same) {
3989
+ throw Object.assign(new Error(`${entry.memory.title} already binds there`), {
3990
+ hint: "Nothing was written, and nothing needed to be."
3991
+ });
3992
+ }
3993
+ return {
3994
+ action: "rescope",
3995
+ memory_id: entry.memory.id,
3996
+ claim: null,
3997
+ proposed_scope: wanted,
3998
+ reason: opts.reason ?? `binds in ${wanted}`,
3999
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
4000
+ target_path: entry.path
4001
+ };
4002
+ }
4003
+ function expireInput(entry, until, opts = {}) {
4004
+ const wanted = until.trim();
4005
+ const at = Date.parse(wanted);
4006
+ if (!Number.isFinite(at)) {
4007
+ throw Object.assign(new Error(`${until} is not a date`), {
4008
+ hint: "Pass an ISO date, such as 2026-10-31 or 2026-10-31T00:00:00Z."
4009
+ });
4010
+ }
4011
+ if ((entry.claims[0]?.valid_until ?? entry.memory.valid_until ?? null) === wanted) {
4012
+ throw Object.assign(new Error(`${entry.memory.title} already ends then`), {
4013
+ hint: "Nothing was written, and nothing needed to be."
4014
+ });
4015
+ }
4016
+ return {
4017
+ action: "expire",
4018
+ memory_id: entry.memory.id,
4019
+ claim: null,
4020
+ proposed_valid_until: wanted,
4021
+ reason: opts.reason ?? `holds until ${wanted}`,
4022
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
4023
+ target_path: entry.path
4024
+ };
4025
+ }
3529
4026
  function saysNothingNew(entry, input, normalize) {
3530
4027
  const norm = (v) => normalize(v ?? "").trim();
3531
4028
  const claim = input.claim;
@@ -3565,6 +4062,8 @@ var init_curate = __esm({
3565
4062
  "../../packages/memory-engine/src/capture/curate.ts"() {
3566
4063
  "use strict";
3567
4064
  init_planner();
4065
+ init_schema();
4066
+ init_rule_budget();
3568
4067
  NO_REASON_GIVEN = "revised directly";
3569
4068
  noLifecycleReason = (action) => `${action}d directly`;
3570
4069
  }
@@ -3619,6 +4118,21 @@ function desiredFields(proposal, existing) {
3619
4118
  delete out["binding"];
3620
4119
  return out;
3621
4120
  }
4121
+ if (isScopeAction(proposal.action)) {
4122
+ if (proposal.proposed_scope === null) {
4123
+ throw new Error(`${proposal.action} reached the patcher with no scope`);
4124
+ }
4125
+ if (isGlobalScope(proposal.proposed_scope)) delete out["scope"];
4126
+ else out["scope"] = proposal.proposed_scope;
4127
+ return out;
4128
+ }
4129
+ if (isValidityAction(proposal.action)) {
4130
+ if (proposal.proposed_valid_until === null) {
4131
+ throw new Error(`${proposal.action} reached the patcher with no date`);
4132
+ }
4133
+ out["valid_until"] = proposal.proposed_valid_until;
4134
+ return out;
4135
+ }
3622
4136
  const claim = proposal.claim;
3623
4137
  if (claim === null) {
3624
4138
  throw new Error(`${proposal.action} reached the patcher with no claim`);
@@ -3956,6 +4470,68 @@ function verifyPatch(patched, proposal, before) {
3956
4470
  }
3957
4471
  return;
3958
4472
  }
4473
+ if (isScopeAction(proposal.action) || isValidityAction(proposal.action)) {
4474
+ if (before === void 0) {
4475
+ throw new PatchError(
4476
+ `${proposal.action} cannot be verified without the note it started from`,
4477
+ "This is a defect in the caller, not in your note. Nothing was written."
4478
+ );
4479
+ }
4480
+ if (isScopeAction(proposal.action)) {
4481
+ const want = proposal.proposed_scope;
4482
+ const got = declaredScopeOf(mapped.memory);
4483
+ const landed = isGlobalScope(want) ? got === null : got === want;
4484
+ if (!landed) {
4485
+ throw new PatchError(
4486
+ "the patched note does not declare the scope that was asked for",
4487
+ "This is a defect in the patcher, not in your note. Nothing was written."
4488
+ );
4489
+ }
4490
+ } else if (
4491
+ // Read back the way `freshnessOf` reads it: the claim's date when the
4492
+ // note states a claim, the note's own otherwise. Asserting only on the
4493
+ // claim would reject every note `remember` wrote, since it has never
4494
+ // written a `claim:` key.
4495
+ (mapped.claims[0]?.valid_until ?? mapped.memory.valid_until ?? null) !== proposal.proposed_valid_until
4496
+ ) {
4497
+ throw new PatchError(
4498
+ "the patched note does not carry the end date that was asked for",
4499
+ "This is a defect in the patcher, not in your note. Nothing was written."
4500
+ );
4501
+ }
4502
+ const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
4503
+ if (was.claims[0]?.text !== mapped.claims[0]?.text) {
4504
+ throw new PatchError(
4505
+ `${proposal.action} changed the conclusion, which it must never do`,
4506
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
4507
+ );
4508
+ }
4509
+ if (was.memory.summary !== mapped.memory.summary) {
4510
+ throw new PatchError(
4511
+ `${proposal.action} changed the summary, which it must never do`,
4512
+ "The summary is what recall matches on, and this action does not touch what the note says. Nothing was written."
4513
+ );
4514
+ }
4515
+ if (was.memory.title !== mapped.memory.title) {
4516
+ throw new PatchError(
4517
+ `${proposal.action} changed the title, which it must never do`,
4518
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
4519
+ );
4520
+ }
4521
+ if (was.memory.status !== mapped.memory.status) {
4522
+ throw new PatchError(
4523
+ `${proposal.action} changed the status, which it must never do`,
4524
+ "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."
4525
+ );
4526
+ }
4527
+ if (was.memory.binding !== mapped.memory.binding) {
4528
+ throw new PatchError(
4529
+ `${proposal.action} changed whether the memory binds, which it must never do`,
4530
+ "To take a marking back, use `revise --no-rule`. Nothing was written."
4531
+ );
4532
+ }
4533
+ return;
4534
+ }
3959
4535
  const supersedes = new Set(
3960
4536
  mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
3961
4537
  );
@@ -4031,6 +4607,8 @@ var init_patch = __esm({
4031
4607
  "../../packages/memory-engine/src/proposal/patch.ts"() {
4032
4608
  "use strict";
4033
4609
  init_markdown_mapping();
4610
+ init_rule_budget();
4611
+ init_schema();
4034
4612
  init_schema2();
4035
4613
  NAMESPACE2 = "memgineering";
4036
4614
  FENCE_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(\r?\n|$)/;
@@ -4073,15 +4651,15 @@ async function applyProposal(adapter, accepted) {
4073
4651
  const patch = computePatch(current.content, proposal);
4074
4652
  verifyPatch(patch.content, proposal, current.content);
4075
4653
  if (patch.content === current.content) {
4076
- const base = proposal.target.baseRevision;
4077
- if (base === null || base === void 0) {
4654
+ const base2 = proposal.target.baseRevision;
4655
+ if (base2 === null || base2 === void 0) {
4078
4656
  return {
4079
4657
  ok: false,
4080
4658
  reason: "revision_conflict",
4081
4659
  proposal: needsRebase(proposal, current.revision)
4082
4660
  };
4083
4661
  }
4084
- if (adapter.compareRevision(base, current.revision) === "diverged") {
4662
+ if (adapter.compareRevision(base2, current.revision) === "diverged") {
4085
4663
  return {
4086
4664
  ok: false,
4087
4665
  reason: "revision_conflict",
@@ -4148,23 +4726,36 @@ function rank(entries, events, opts) {
4148
4726
  const recalls = events.filter((e) => !AUTOMATIC.has(e.verb));
4149
4727
  const here = opts.here ? recalls.filter((e) => opts.here(e)) : [];
4150
4728
  const hereLabel = opts.hereLabel ?? "in this folder";
4151
- const countIn = (source) => {
4152
- const counts = /* @__PURE__ */ new Map();
4729
+ const usageIn = (source) => {
4730
+ const usage = /* @__PURE__ */ new Map();
4731
+ const at = (id) => {
4732
+ let u = usage.get(id);
4733
+ if (!u) usage.set(id, u = { opened: 0, surfaced: 0, credit: 0 });
4734
+ return u;
4735
+ };
4153
4736
  for (const event of source) {
4154
- for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
4155
- if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
4737
+ if (event.verb === "recall") {
4738
+ event.returned_ids.forEach((id, i) => {
4739
+ const u = at(id);
4740
+ u.surfaced += 1;
4741
+ if (i === 0) u.credit += SURFACED_SHARE;
4742
+ });
4743
+ } else if (DELIBERATE.has(event.verb)) {
4744
+ const ids = event.opened_id ? [event.opened_id] : event.returned_ids;
4745
+ for (const id of new Set(ids)) at(id).opened += 1;
4746
+ }
4156
4747
  }
4157
- return counts;
4748
+ return usage;
4158
4749
  };
4159
- const hereCounts = countIn(here);
4160
- const anyCounts = countIn(recalls);
4750
+ const evidence = (u) => u ? u.opened + u.credit : 0;
4751
+ const hereUsage = usageIn(here);
4752
+ const anyUsage = usageIn(recalls);
4161
4753
  const lastTouched = /* @__PURE__ */ new Map();
4162
4754
  for (const event of recalls) {
4163
4755
  const t = Date.parse(event.ts);
4164
4756
  if (!Number.isFinite(t)) continue;
4165
- for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
4166
- lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
4167
- }
4757
+ const touched = event.verb === "recall" ? event.returned_ids.slice(0, 1) : DELIBERATE.has(event.verb) ? [event.opened_id ?? "", ...event.returned_ids].filter(Boolean) : [];
4758
+ for (const id of touched) lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
4168
4759
  }
4169
4760
  const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
4170
4761
  const scored = [];
@@ -4174,16 +4765,30 @@ function rank(entries, events, opts) {
4174
4765
  if (freshness === "retired") continue;
4175
4766
  const why = [];
4176
4767
  let score = 0;
4177
- const hereCount = hereCounts.get(id) ?? 0;
4178
- if (hereCount > 0) {
4179
- score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
4180
- why.push(`recalled ${hereCount}\xD7 ${hereLabel}`);
4181
- }
4182
- const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
4183
- if (anyCount > 0) {
4184
- score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
4185
- if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
4768
+ const hereU = hereUsage.get(id);
4769
+ const anyU = anyUsage.get(id);
4770
+ const hereEvidence = evidence(hereU);
4771
+ const elsewhereEvidence = evidence(anyU) - hereEvidence;
4772
+ if (hereEvidence > 0) {
4773
+ score += WEIGHT.contextRecall * Math.log2(1 + hereEvidence);
4774
+ why.push(usageLine(hereU, hereLabel));
4775
+ }
4776
+ if (elsewhereEvidence > 1e-9) {
4777
+ score += WEIGHT.anyRecall * Math.log2(1 + elsewhereEvidence);
4778
+ if (hereEvidence === 0) {
4779
+ why.push(
4780
+ usageLine(
4781
+ {
4782
+ opened: anyU.opened - (hereU?.opened ?? 0),
4783
+ surfaced: anyU.surfaced - (hereU?.surfaced ?? 0)
4784
+ },
4785
+ "elsewhere"
4786
+ )
4787
+ );
4788
+ }
4186
4789
  }
4790
+ const hereCount = (hereU?.opened ?? 0) + (hereU?.surfaced ?? 0);
4791
+ const anyCount = (anyU?.opened ?? 0) + (anyU?.surfaced ?? 0) - hereCount;
4187
4792
  const touched = lastTouched.get(id);
4188
4793
  if (touched !== void 0) {
4189
4794
  const days = (opts.now.getTime() - touched) / 864e5;
@@ -4204,16 +4809,28 @@ function rank(entries, events, opts) {
4204
4809
  }
4205
4810
  if (score === 0 && !entry.path.startsWith("01_BASE/")) {
4206
4811
  score += WEIGHT.neverReached;
4207
- why.push("never surfaced yet");
4812
+ why.push(
4813
+ anyU && anyU.surfaced > 0 ? usageLine(
4814
+ { opened: 0, surfaced: anyU.surfaced },
4815
+ // Labelled by where it happened: all here, none here, or both —
4816
+ // a total said "in this project" would claim the whole count.
4817
+ !hereU?.surfaced ? "elsewhere" : hereU.surfaced === anyU.surfaced ? hereLabel : `(${hereU.surfaced}\xD7 ${hereLabel})`
4818
+ ) : "never surfaced yet"
4819
+ );
4208
4820
  }
4209
4821
  if (freshness !== "current") why.push(freshness);
4210
- if (score > 0) scored.push({ entry, score, why });
4822
+ if (score > 0) scored.push({ entry, score, why, used: hereCount > 0 || anyCount > 0 });
4211
4823
  }
4212
4824
  return scored.sort(
4213
4825
  (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
4214
4826
  );
4215
4827
  }
4216
- var WEIGHT, STALE_BASE_DAYS;
4828
+ function usageLine(u, where) {
4829
+ if (u.opened === 0) return `surfaced ${u.surfaced}\xD7 ${where}, never opened`;
4830
+ if (u.surfaced === 0) return `opened ${u.opened}\xD7 ${where}`;
4831
+ return `opened ${u.opened}\xD7 \xB7 surfaced ${u.surfaced}\xD7 ${where}`;
4832
+ }
4833
+ var WEIGHT, STALE_BASE_DAYS, SURFACED_SHARE, DELIBERATE;
4217
4834
  var init_rank = __esm({
4218
4835
  "../../packages/memory-engine/src/resurface/rank.ts"() {
4219
4836
  "use strict";
@@ -4266,6 +4883,8 @@ var init_rank = __esm({
4266
4883
  binding: 5
4267
4884
  };
4268
4885
  STALE_BASE_DAYS = 30;
4886
+ SURFACED_SHARE = 0.5;
4887
+ DELIBERATE = /* @__PURE__ */ new Set(["open", "publish", "adapt", "read-public"]);
4269
4888
  }
4270
4889
  });
4271
4890
 
@@ -4284,6 +4903,10 @@ var init_src = __esm({
4284
4903
  init_reconcile();
4285
4904
  init_planner();
4286
4905
  init_semantic();
4906
+ init_unfilled();
4907
+ init_query_prefix();
4908
+ init_base_templates();
4909
+ init_scope_spread();
4287
4910
  init_conflicts();
4288
4911
  init_search();
4289
4912
  init_schema2();
@@ -4299,6 +4922,27 @@ var init_src = __esm({
4299
4922
  }
4300
4923
  });
4301
4924
 
4925
+ // src/version.ts
4926
+ import { readFileSync } from "fs";
4927
+ import { fileURLToPath } from "url";
4928
+ import { dirname, join as join4 } from "path";
4929
+ var __dir, pkgPath, VERSION;
4930
+ var init_version = __esm({
4931
+ "src/version.ts"() {
4932
+ "use strict";
4933
+ __dir = dirname(fileURLToPath(import.meta.url));
4934
+ pkgPath = join4(__dir, "..", "package.json");
4935
+ VERSION = (() => {
4936
+ try {
4937
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
4938
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown";
4939
+ } catch {
4940
+ return "0.0.0-unknown";
4941
+ }
4942
+ })();
4943
+ }
4944
+ });
4945
+
4302
4946
  // src/lib/credentials.ts
4303
4947
  import { mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile2 } from "fs/promises";
4304
4948
  import { join as join5 } from "path";
@@ -4473,8 +5117,8 @@ function readCause(err) {
4473
5117
  return { detail, retryable, nothingSent: retryable || !vetoed && preFlight };
4474
5118
  }
4475
5119
  async function apiRequest(args) {
4476
- const base = args.baseUrl ?? apiUrl();
4477
- const url = `${base}${args.path}`;
5120
+ const base2 = args.baseUrl ?? apiUrl();
5121
+ const url = `${base2}${args.path}`;
4478
5122
  let res;
4479
5123
  try {
4480
5124
  res = await fetch(url, {
@@ -4498,7 +5142,7 @@ async function apiRequest(args) {
4498
5142
  }
4499
5143
  throw memgError(
4500
5144
  "server_unreachable",
4501
- `could not reach ${base}`,
5145
+ `could not reach ${base2}`,
4502
5146
  `${cause.nothingSent ? NOTHING_SENT : MAY_HAVE_ARRIVED} Underlying cause: ${cause.detail}`
4503
5147
  );
4504
5148
  }
@@ -4530,7 +5174,7 @@ async function apiRequest(args) {
4530
5174
  }
4531
5175
  throw memgError(
4532
5176
  "server_refused",
4533
- `${base} answered ${res.status} with no readable error`,
5177
+ `${base2} answered ${res.status} with no readable error`,
4534
5178
  `This is not a shape this CLI knows. If MEMGINEERING_API_URL points at something other than a memgineering brain server, that would explain it. First 200 characters of the response: ${text.slice(0, 200)}`
4535
5179
  );
4536
5180
  }
@@ -4819,7 +5463,30 @@ var init_config = __esm({
4819
5463
  * their own words are about to be replaced. It records no content, only
4820
5464
  * whether the bytes are still ours.
4821
5465
  */
4822
- installed: z8.record(z8.string(), z8.string()).default({})
5466
+ installed: z8.record(z8.string(), z8.string()).default({}),
5467
+ /**
5468
+ * Which release wrote the guidance that is sitting in the agent tools.
5469
+ *
5470
+ * The upgrade path used to be a `postinstall` script that told the agent
5471
+ * to run `setup`. npm 12 blocks install scripts by default — measured on
5472
+ * 12.0.2 installing this package: the script never ran and npm printed
5473
+ * only `1 package had install scripts blocked`, which says nothing about
5474
+ * what the script would have said. So the one channel that carried
5475
+ * "your guidance is now out of date" is gone for every user on that npm,
5476
+ * and nothing else noticed: the CLI upgrades itself, the hub and skills
5477
+ * in `~/.claude`, `~/.codex` and `~/.gemini` stay at whatever release
5478
+ * last ran setup, and both halves report success.
5479
+ *
5480
+ * A version rather than a digest, because this is read by a check that
5481
+ * runs before EVERY command and may not pay for reading eight bundled
5482
+ * assets to find out. The cost of the cheap answer is that a release
5483
+ * which did not change any guidance still asks for one `setup` run; the
5484
+ * cost of the expensive one is paid by every recall, forever.
5485
+ *
5486
+ * Absent means "written before this field existed", which is the same
5487
+ * answer as "older than this CLI" and wants the same one command.
5488
+ */
5489
+ guidanceVersion: z8.string().optional()
4823
5490
  }).default({ autoUpdate: "on" })
4824
5491
  });
4825
5492
  EMPTY_CONFIG = ConfigSchema.parse({});
@@ -4859,7 +5526,7 @@ async function createCloudBrain(token, baseUrl, name) {
4859
5526
  });
4860
5527
  return res.body;
4861
5528
  }
4862
- var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudUnbind, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRules, cloudReindex;
5529
+ var call, askedFromField, cloudRecall, cloudOpen, cloudEvidence, cloudRemember, cloudUndo, cloudLog, cloudPutNote, cloudListNotes, cloudRevise, cloudBaseWrite, cloudLifecycle, cloudUnbind, cloudExclude, RANKED_BY_PROJECT, rankedOnProject, cloudResurface, cloudRescope, cloudExpire, cloudRules, cloudReindex;
4863
5530
  var init_cloud_brain = __esm({
4864
5531
  "src/lib/cloud-brain.ts"() {
4865
5532
  "use strict";
@@ -4884,7 +5551,8 @@ var init_cloud_brain = __esm({
4884
5551
  // the honest answer — a client that pre-validated against its own list
4885
5552
  // would report a depth as accepted and then have it silently ignored.
4886
5553
  ...args.depth === void 0 ? {} : { depth: args.depth },
4887
- ...args.budget === void 0 ? {} : { budget: args.budget }
5554
+ ...args.budget === void 0 ? {} : { budget: args.budget },
5555
+ ...args.templateSlots === void 0 ? {} : { [UNFILLED_FIELDS.slots]: args.templateSlots }
4888
5556
  });
4889
5557
  cloudOpen = (brain, ref, askedFrom) => call(brain, "POST", "/open", { ref, ...askedFromField(askedFrom) });
4890
5558
  cloudEvidence = (brain, ref) => call(brain, "POST", "/evidence", { ref });
@@ -4963,6 +5631,16 @@ var init_cloud_brain = __esm({
4963
5631
  ...args.limit === void 0 ? {} : { limit: args.limit },
4964
5632
  ...askedFromField(args.askedFrom)
4965
5633
  });
5634
+ cloudRescope = (brain, args) => call(brain, "POST", "/rescope", {
5635
+ ref: args.ref,
5636
+ scope: args.scope,
5637
+ ...args.reason === void 0 ? {} : { reason: args.reason }
5638
+ });
5639
+ cloudExpire = (brain, args) => call(brain, "POST", "/expire", {
5640
+ ref: args.ref,
5641
+ until: args.until,
5642
+ ...args.reason === void 0 ? {} : { reason: args.reason }
5643
+ });
4966
5644
  cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
4967
5645
  ...args.limit === void 0 ? {} : { limit: args.limit },
4968
5646
  // The server cannot see this machine's folders, so the project it is
@@ -5834,20 +6512,6 @@ async function createBrain(path, name) {
5834
6512
  return { root: canonicalRoot2, indexed: built.entries.length };
5835
6513
  }
5836
6514
  function files(name) {
5837
- const base = (id, title, summary, body) => [
5838
- "---",
5839
- "memgineering:",
5840
- ` id: ${id}`,
5841
- ` title: ${title}`,
5842
- ` summary: ${summary}`,
5843
- " scope: base",
5844
- "---",
5845
- "",
5846
- `# ${title}`,
5847
- "",
5848
- body,
5849
- ""
5850
- ].join("\n");
5851
6515
  return {
5852
6516
  "00_HUB/HUB.md": [
5853
6517
  "---",
@@ -5887,61 +6551,9 @@ function files(name) {
5887
6551
  "and rewritten without asking. A number in front means it is yours.",
5888
6552
  ""
5889
6553
  ].join("\n"),
5890
- "01_BASE/CRITICAL_FACTS.md": base(
5891
- "critical-facts",
5892
- "Critical facts",
5893
- "The few things that must never be wrong.",
5894
- [
5895
- "Keep this short enough to read every session. Anything that can be",
5896
- "looked up when needed belongs in a note instead.",
5897
- "",
5898
- "- "
5899
- ].join("\n")
5900
- ),
5901
- "01_BASE/USER.md": base(
5902
- "user",
5903
- "About me",
5904
- "Who I am and what context I work in.",
5905
- [
5906
- "Background, role, what you are working on, what an agent should assume",
5907
- "you already know.",
5908
- "",
5909
- "- "
5910
- ].join("\n")
5911
- ),
5912
- "01_BASE/PREFERENCES.md": base(
5913
- "preferences",
5914
- "Preferences",
5915
- "How I like answers, code and decisions handled.",
5916
- ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
5917
- ),
5918
- "01_BASE/BOUNDARIES.md": base(
5919
- "boundaries",
5920
- "Boundaries",
5921
- "What an agent must not do without asking me first.",
5922
- [
5923
- "Privacy, spending, anything irreversible, anything that leaves this",
5924
- "machine. Be specific \u2014 a boundary that has to be interpreted is one",
5925
- "that will be interpreted generously.",
5926
- "",
5927
- "Storage boundaries belong here too: topics you never want written into",
5928
- "this brain \u2014 health, beliefs, relationships, anything you name. An",
5929
- "agent checks this file before it records anything.",
5930
- "",
5931
- "- "
5932
- ].join("\n")
5933
- ),
5934
- "01_BASE/TOOLING.md": base(
5935
- "tooling",
5936
- "Tooling",
5937
- "The tools, machines and commands I actually use.",
5938
- [
5939
- "Editors, languages, deploy commands, where things run. Enough that an",
5940
- "agent does not have to guess or discover it again each time.",
5941
- "",
5942
- "- "
5943
- ].join("\n")
5944
- )
6554
+ // The five base files come from the engine, which the hosted server reads
6555
+ // too — one copy of what a starter template is (`base-templates.ts`).
6556
+ ...BASE_TEMPLATES
5945
6557
  };
5946
6558
  }
5947
6559
  var OBSERVATIONS_DIR2, FOLDERS;
@@ -6043,8 +6655,8 @@ import { join as join17 } from "path";
6043
6655
  import { Command as Command13 } from "commander";
6044
6656
  import prompts2 from "prompts";
6045
6657
  function sameBasename(all, entry) {
6046
- const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
6047
- return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base);
6658
+ const base2 = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
6659
+ return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base2);
6048
6660
  }
6049
6661
  function spreadAcrossFolders(rows, max) {
6050
6662
  const seen = /* @__PURE__ */ new Set();
@@ -6470,23 +7082,23 @@ var init_open_browser = __esm({
6470
7082
  });
6471
7083
 
6472
7084
  // src/commands/login.ts
6473
- import { Command as Command20 } from "commander";
7085
+ import { Command as Command21 } from "commander";
6474
7086
  function loginCommand() {
6475
- return new Command20("login").description("sign in to a hosted brain").option(
7087
+ return new Command21("login").description("sign in to a hosted brain").option(
6476
7088
  "--emit-only",
6477
7089
  "start the sign-in, open the browser, and return immediately \u2014 THE path an agent should use on a user\u2019s behalf; the next command claims the token automatically. Prefer this over the plain form, whose wait for a human click will outlast an agent\u2019s command timeout"
6478
7090
  ).option("--no-browser", "do not try to open a browser \u2014 just print the link").option("--print-url", "print the approval link even when a browser opened").addHelpText(
6479
7091
  "after",
6480
7092
  "\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to check it against what you showed them,\nand they cannot if you never showed it. Do not ask the user to run a\nlogin command themselves."
6481
7093
  ).action(async (opts) => {
6482
- const base = apiUrl();
6483
- const already = await currentCredentials(base);
7094
+ const base2 = apiUrl();
7095
+ const already = await currentCredentials(base2);
6484
7096
  if (already) {
6485
7097
  printDual({
6486
7098
  json: {
6487
7099
  already_signed_in: true,
6488
7100
  account: { id: already.account_id, display_name: already.display_name },
6489
- api_url: base
7101
+ api_url: base2
6490
7102
  },
6491
7103
  human: () => {
6492
7104
  printHuman(
@@ -6500,11 +7112,11 @@ function loginCommand() {
6500
7112
  const { pending, opened } = await beginSignIn({ browser: opts.browser !== false });
6501
7113
  const showUrl = !opened || opts.printUrl === true;
6502
7114
  if (opts.emitOnly) {
6503
- emit(pending, { base, opened, showUrl, blocking: false });
7115
+ emit(pending, { base: base2, opened, showUrl, blocking: false });
6504
7116
  return;
6505
7117
  }
6506
- emit(pending, { base, opened, showUrl, blocking: true });
6507
- await waitForApproval(pending, base);
7118
+ emit(pending, { base: base2, opened, showUrl, blocking: true });
7119
+ await waitForApproval(pending, base2);
6508
7120
  });
6509
7121
  }
6510
7122
  async function beginSignIn(opts) {
@@ -6514,32 +7126,32 @@ async function beginSignIn(opts) {
6514
7126
  return { pending, opened };
6515
7127
  }
6516
7128
  async function signInSummary() {
6517
- const base = apiUrl();
7129
+ const base2 = apiUrl();
6518
7130
  const stored = await readCredentials();
6519
7131
  const pending = await pendingOrNull();
6520
7132
  return {
6521
- signedIn: stored !== null && stored.api_url === base,
6522
- displayName: stored?.api_url === base ? stored?.display_name ?? null : null,
6523
- pendingUserCode: pending?.api_url === base ? pending.user_code : null,
6524
- apiUrl: base
7133
+ signedIn: stored !== null && stored.api_url === base2,
7134
+ displayName: stored?.api_url === base2 ? stored?.display_name ?? null : null,
7135
+ pendingUserCode: pending?.api_url === base2 ? pending.user_code : null,
7136
+ apiUrl: base2
6525
7137
  };
6526
7138
  }
6527
- async function startPairing(base) {
7139
+ async function startPairing(base2) {
6528
7140
  const res = await apiRequest({
6529
7141
  method: "POST",
6530
7142
  path: "/v1/auth/device/code",
6531
- baseUrl: base
7143
+ baseUrl: base2
6532
7144
  });
6533
7145
  const { device_code, user_code, verification_url, expires_in } = res.body ?? {};
6534
7146
  if (!device_code || !user_code || !verification_url) {
6535
7147
  throw memgError(
6536
7148
  "server_refused",
6537
7149
  "the brain server did not return a usable sign-in code",
6538
- `Nothing is stored. Check that MEMGINEERING_API_URL points at a memgineering brain server \u2014 it is currently ${base}.`
7150
+ `Nothing is stored. Check that MEMGINEERING_API_URL points at a memgineering brain server \u2014 it is currently ${base2}.`
6539
7151
  );
6540
7152
  }
6541
7153
  return {
6542
- api_url: base,
7154
+ api_url: base2,
6543
7155
  device_code,
6544
7156
  user_code,
6545
7157
  verification_url,
@@ -6590,7 +7202,7 @@ function emit(pending, ctx) {
6590
7202
  }
6591
7203
  });
6592
7204
  }
6593
- async function waitForApproval(pending, base) {
7205
+ async function waitForApproval(pending, base2) {
6594
7206
  const deadline = Date.now() + BLOCKING_TIMEOUT_MS;
6595
7207
  progress("Waiting for approval");
6596
7208
  for (; ; ) {
@@ -6604,7 +7216,7 @@ async function waitForApproval(pending, base) {
6604
7216
  await sleep(5e3);
6605
7217
  let claimed;
6606
7218
  try {
6607
- claimed = await claimOnce(pending, base);
7219
+ claimed = await claimOnce(pending, base2);
6608
7220
  } catch (err) {
6609
7221
  if (err instanceof MemgError && err.code === "server_unreachable") continue;
6610
7222
  throw err;
@@ -6614,7 +7226,7 @@ async function waitForApproval(pending, base) {
6614
7226
  json: {
6615
7227
  signed_in: true,
6616
7228
  account: { id: claimed.account_id, display_name: claimed.display_name },
6617
- api_url: base,
7229
+ api_url: base2,
6618
7230
  registry: claimed.registry ? { username: claimed.registry.username, api_url: claimed.registry.api_url } : null
6619
7231
  },
6620
7232
  human: () => {
@@ -6667,9 +7279,9 @@ var push_exports = {};
6667
7279
  __export(push_exports, {
6668
7280
  pushCommand: () => pushCommand
6669
7281
  });
6670
- import { Command as Command35 } from "commander";
7282
+ import { Command as Command36 } from "commander";
6671
7283
  function pushCommand() {
6672
- return new Command35("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
7284
+ return new Command36("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
6673
7285
  const { brain: local } = await resolveBrain(opts.vault);
6674
7286
  const adapter = await openBrainOrExplain(local.root);
6675
7287
  const scan = await adapter.scan();
@@ -6822,28 +7434,28 @@ Nothing was deleted from \`${local.root}\` \u2014
6822
7434
  });
6823
7435
  }
6824
7436
  async function resolveDestination(named, root) {
6825
- const base = apiUrl();
6826
- const credentials = await requireCredentials(base);
7437
+ const base2 = apiUrl();
7438
+ const credentials = await requireCredentials(base2);
6827
7439
  const config = await loadConfig();
6828
- const brains = await listCloudBrains(credentials.token, base);
7440
+ const brains = await listCloudBrains(credentials.token, base2);
6829
7441
  const pick = (summary) => ({
6830
7442
  brainId: summary.id,
6831
7443
  name: summary.name,
6832
7444
  token: credentials.token,
6833
- apiUrl: base
7445
+ apiUrl: base2
6834
7446
  });
6835
7447
  if (named) {
6836
7448
  const found = brains.find((b) => b.name === named || b.id === named);
6837
- return pick(found ?? await createCloudBrain(credentials.token, base, named));
7449
+ return pick(found ?? await createCloudBrain(credentials.token, base2, named));
6838
7450
  }
6839
7451
  const current = config.brain.cloud;
6840
- if (current && current.api_url === base) {
7452
+ if (current && current.api_url === base2) {
6841
7453
  const found = brains.find((b) => b.id === current.id);
6842
7454
  if (found) return pick(found);
6843
7455
  }
6844
7456
  const fallbackName = root.split("/").filter(Boolean).pop() ?? "brain";
6845
7457
  const existing = brains.find((b) => b.name === fallbackName);
6846
- return pick(existing ?? await createCloudBrain(credentials.token, base, fallbackName));
7458
+ return pick(existing ?? await createCloudBrain(credentials.token, base2, fallbackName));
6847
7459
  }
6848
7460
  var HELD_BACK_WORDING, printHeldBack2;
6849
7461
  var init_push = __esm({
@@ -7198,33 +7810,129 @@ var init_setup_signin = __esm({
7198
7810
  }
7199
7811
  });
7200
7812
 
7201
- // src/commands/setup-cloud.ts
7202
- var setup_cloud_exports = {};
7203
- __export(setup_cloud_exports, {
7204
- SETUP_CHILD_ENV: () => SETUP_CHILD_ENV,
7205
- applyAnswers: () => applyAnswers,
7206
- readChildToken: () => readChildToken,
7207
- runSetupCloud: () => runSetupCloud,
7208
- runSetupCloudChild: () => runSetupCloudChild,
7209
- waitAndApply: () => waitAndApply
7210
- });
7211
- import { spawn as spawn2 } from "child_process";
7212
- import { readdir as readdir7, stat as stat6 } from "fs/promises";
7213
- import { homedir as homedir5 } from "os";
7214
- import { join as join23 } from "path";
7215
- async function report2(setupToken, state, detail) {
7216
- try {
7217
- await apiRequest({
7218
- method: "POST",
7219
- path: "/v1/setup/session/report",
7220
- body: { setup_token: setupToken, state, ...detail === void 0 ? {} : { detail } }
7221
- });
7222
- } catch {
7223
- }
7224
- }
7225
- async function folderCandidates() {
7813
+ // src/lib/provenance.ts
7814
+ import { execFile } from "child_process";
7815
+ import { realpath as realpath3 } from "fs/promises";
7816
+ import { dirname as dirname8, join as join23, resolve as resolve14 } from "path";
7817
+ import { fileURLToPath as fileURLToPath2 } from "url";
7818
+ import { promisify } from "util";
7819
+ async function detectProvenance(selfPath) {
7820
+ const entry = selfPath ?? fileURLToPath2(import.meta.url);
7821
+ const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
7822
+ if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
7823
+ return {
7824
+ kind: "npx",
7825
+ selfDir,
7826
+ prefix: null,
7827
+ command: "npm i -g memgineering@latest",
7828
+ reason: "this is a one-off npx run, not an installed copy"
7829
+ };
7830
+ }
7831
+ for (const manager of MANAGERS) {
7832
+ const prefix = await prefixOf(manager.bin, manager.prefixArgs);
7833
+ if (prefix === null) continue;
7834
+ if (isInside2(selfDir, manager.moduleRoot(prefix))) {
7835
+ return {
7836
+ kind: manager.kind,
7837
+ selfDir,
7838
+ prefix,
7839
+ command: manager.install,
7840
+ reason: null
7841
+ };
7842
+ }
7843
+ }
7844
+ const local = selfDir.includes(`${"node_modules"}`);
7845
+ return {
7846
+ kind: local ? "local" : "unknown",
7847
+ selfDir,
7848
+ prefix: null,
7849
+ command: "npm i -g memgineering@latest",
7850
+ reason: local ? "this copy is a project dependency, not a global install" : "no package manager on PATH owns the directory this copy is in \u2014 under nvm, asdf, Volta or mise that usually means a different Node version is active than the one it was installed with"
7851
+ };
7852
+ }
7853
+ function canSelfUpdate(p) {
7854
+ return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
7855
+ }
7856
+ async function prefixOf(bin, args) {
7857
+ try {
7858
+ const { stdout } = await run(bin, args, { timeout: 5e3 });
7859
+ const value = stdout.trim();
7860
+ return value === "" ? null : await realpath3(value).catch(() => value);
7861
+ } catch {
7862
+ return null;
7863
+ }
7864
+ }
7865
+ function isInside2(child, parent) {
7866
+ const c2 = resolve14(child);
7867
+ const p = resolve14(parent);
7868
+ return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
7869
+ }
7870
+ async function unreachableByName(selfPath) {
7871
+ const entry = selfPath ?? fileURLToPath2(import.meta.url);
7872
+ const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
7873
+ if (!/[\\/](node_modules|_npx|_cacache)[\\/]/.test(`${selfDir}/`)) return null;
7874
+ const p = await detectProvenance(entry);
7875
+ return p.kind === "local" || p.kind === "npx" ? p.reason : null;
7876
+ }
7877
+ var run, MANAGERS;
7878
+ var init_provenance = __esm({
7879
+ "src/lib/provenance.ts"() {
7880
+ "use strict";
7881
+ run = promisify(execFile);
7882
+ MANAGERS = [
7883
+ {
7884
+ kind: "npm",
7885
+ bin: "npm",
7886
+ prefixArgs: ["prefix", "-g"],
7887
+ moduleRoot: (prefix) => process.platform === "win32" ? join23(prefix, "node_modules") : join23(prefix, "lib", "node_modules"),
7888
+ install: "npm i -g memgineering@latest"
7889
+ },
7890
+ {
7891
+ kind: "pnpm",
7892
+ bin: "pnpm",
7893
+ // `pnpm root -g` already prints the global node_modules.
7894
+ prefixArgs: ["root", "-g"],
7895
+ moduleRoot: (root) => root,
7896
+ install: "pnpm add -g memgineering@latest"
7897
+ },
7898
+ {
7899
+ kind: "bun",
7900
+ bin: "bun",
7901
+ prefixArgs: ["pm", "bin", "-g"],
7902
+ moduleRoot: (bin) => bin,
7903
+ install: "bun add -g memgineering"
7904
+ }
7905
+ ];
7906
+ }
7907
+ });
7908
+
7909
+ // src/commands/setup-cloud.ts
7910
+ var setup_cloud_exports = {};
7911
+ __export(setup_cloud_exports, {
7912
+ SETUP_CHILD_ENV: () => SETUP_CHILD_ENV,
7913
+ applyAnswers: () => applyAnswers,
7914
+ readChildToken: () => readChildToken,
7915
+ runSetupCloud: () => runSetupCloud,
7916
+ runSetupCloudChild: () => runSetupCloudChild,
7917
+ waitAndApply: () => waitAndApply
7918
+ });
7919
+ import { spawn as spawn2 } from "child_process";
7920
+ import { readdir as readdir7, stat as stat6 } from "fs/promises";
7921
+ import { homedir as homedir5 } from "os";
7922
+ import { join as join24 } from "path";
7923
+ async function report2(setupToken, state, detail) {
7924
+ try {
7925
+ await apiRequest({
7926
+ method: "POST",
7927
+ path: "/v1/setup/session/report",
7928
+ body: { setup_token: setupToken, state, ...detail === void 0 ? {} : { detail } }
7929
+ });
7930
+ } catch {
7931
+ }
7932
+ }
7933
+ async function folderCandidates() {
7226
7934
  const home = homedir5();
7227
- const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join23(home, d));
7935
+ const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join24(home, d));
7228
7936
  const found = [];
7229
7937
  const countNotes = async (dir) => {
7230
7938
  let count = 0;
@@ -7233,7 +7941,7 @@ async function folderCandidates() {
7233
7941
  if (entry.name.startsWith(".")) continue;
7234
7942
  if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
7235
7943
  else if (entry.isDirectory()) {
7236
- const inner = await readdir7(join23(dir, entry.name)).catch(() => []);
7944
+ const inner = await readdir7(join24(dir, entry.name)).catch(() => []);
7237
7945
  count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
7238
7946
  }
7239
7947
  if (count > 999) break;
@@ -7488,18 +8196,18 @@ var init_setup_cloud = __esm({
7488
8196
  // src/commands/setup.ts
7489
8197
  import { createHash as createHash9 } from "crypto";
7490
8198
  import { access as access2, mkdir as mkdir15, readFile as readFile24 } from "fs/promises";
7491
- import { dirname as dirname8, join as join24 } from "path";
7492
- import { fileURLToPath as fileURLToPath2 } from "url";
7493
- import { Command as Command39 } from "commander";
8199
+ import { dirname as dirname9, join as join25 } from "path";
8200
+ import { fileURLToPath as fileURLToPath3 } from "url";
8201
+ import { Command as Command40 } from "commander";
7494
8202
  import prompts5 from "prompts";
7495
8203
  import { homedir as homedir6 } from "os";
7496
8204
  function setupCommand() {
7497
- return new Command39("setup").description("install into your agents, and point at a brain (--dry-run to only report)").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option(
8205
+ return new Command40("setup").description("install into your agents, and point at a brain (--dry-run to only report)").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option(
7498
8206
  "--wait",
7499
8207
  "with --web: block until the screen is finished, even with nobody watching the terminal"
7500
8208
  ).option(
7501
8209
  "--tools <list>",
7502
- "comma-separated: claude, codex, grok, gemini (default: all detected)"
8210
+ "comma-separated: claude, codex, grok, gemini (default: all detected) \u2014 chat apps: see below"
7503
8211
  ).option("--auto-update <choice>", "on | off (default: on, recommended)").option(
7504
8212
  "--error-telemetry <choice>",
7505
8213
  "on | off \u2014 anonymized crash reports; unset leaves it undecided (off) so a person can be asked later"
@@ -7509,13 +8217,22 @@ function setupCommand() {
7509
8217
  ).option("--login", "start a hosted-account sign-in as the first step").option("--no-browser", "with --login: print the approval link instead of opening a browser").option(
7510
8218
  "--no-login",
7511
8219
  "skip the sign-in step \u2014 everything installed here works without an account"
7512
- ).option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").action(async (opts) => {
8220
+ ).option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").addHelpText(
8221
+ "after",
8222
+ [
8223
+ "",
8224
+ "Chat apps with no shell \u2014 ChatGPT, Claude.ai, Claude Desktop, Cowork \u2014 need no",
8225
+ `setup: add ${HOSTED_CONNECTOR_URL} as a custom connector in the app and`,
8226
+ "sign in. It reads and writes the brain on your account (not a folder on this",
8227
+ "machine)."
8228
+ ].join("\n")
8229
+ ).action(async (opts) => {
7513
8230
  const detected = await detectTools();
7514
8231
  if (detected.length === 0) {
7515
8232
  throw memgError(
7516
8233
  "invalid_input",
7517
8234
  "no supported agent tools found on this machine",
7518
- "memgineering installs into Claude Code (~/.claude), Codex (~/.codex), Grok (~/.grok) or Antigravity (~/.gemini). Install one of those first, then run setup again."
8235
+ `memgineering installs into ${SUPPORTED_LIST}. Each is found by the folder it creates the first time it runs, so a tool installed but never opened is not found yet \u2014 open it once, then run setup again. If none is installed, install one first.`
7519
8236
  );
7520
8237
  }
7521
8238
  if (opts.web) {
@@ -7545,11 +8262,13 @@ function setupCommand() {
7545
8262
  const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
7546
8263
  const defaultsTaken = !mode.chosen ? [
7547
8264
  ...opts.autoUpdate === void 0 ? ["auto-update: on"] : [],
7548
- // Both of them, named. This one flag arms two callbacks — note
7549
- // summaries at session start, and the user's own rules before an
7550
- // edit — and saying only the first would be reporting half of what
7551
- // was decided on their behalf.
7552
- ...opts.hook === void 0 && choice?.hook ? ["session-start hook: on", "before-edit hook: on"] : []
8265
+ // Every callback this flag arms, named — on Claude Code and Codex
8266
+ // that is two (note summaries at session start, the user's own
8267
+ // rules before an edit), and saying only the first would be
8268
+ // reporting half of what was decided on their behalf. On Grok it
8269
+ // is one, at the end of a turn, and naming the other two there
8270
+ // reported callbacks that were never installed.
8271
+ ...opts.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
7553
8272
  ] : [];
7554
8273
  if (choice === null) {
7555
8274
  printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
@@ -7562,9 +8281,14 @@ function setupCommand() {
7562
8281
  const changed = results.filter((r) => r.result !== "unchanged" && r.result !== "refused");
7563
8282
  const brains = (await loadConfig()).brain.brains;
7564
8283
  const brainStep = await runBrainStep(signIn, opts.dryRun === true);
8284
+ const notByName = opts.dryRun !== true ? await unreachableByName() : null;
8285
+ const hooksLanded = choice.hook && results.some(
8286
+ (r) => Object.values(HOOK_WHAT).includes(r.what) && r.result !== "refused" && r.result !== "removed"
8287
+ );
7565
8288
  const hints = [
7566
8289
  ...refusals.length > 0 ? [REFUSAL_HINT] : [],
7567
8290
  ...duplicated.length > 0 ? [DUPLICATE_HINT] : [],
8291
+ ...notByName !== null ? [globalInstallHint(notByName, hooksLanded)] : [],
7568
8292
  /*
7569
8293
  * Local mode only, and that condition is the point of it.
7570
8294
  *
@@ -7603,6 +8327,10 @@ function setupCommand() {
7603
8327
  */
7604
8328
  ...agentHome() !== homedir6() ? { agent_home: agentHome() } : {},
7605
8329
  tools: choice.tools.map((t) => t.id),
8330
+ // `tools` is what was asked for; this is where the guide actually
8331
+ // stands after the run. They differ exactly when a guide was refused.
8332
+ registered: registeredTools(choice.tools, results).map((t) => t.id),
8333
+ ...notByName !== null ? { global_install_needed: notByName } : {},
7606
8334
  auto_update: choice.autoUpdate,
7607
8335
  error_telemetry: choice.errorTelemetry,
7608
8336
  hook: choice.hook,
@@ -7652,7 +8380,7 @@ function setupCommand() {
7652
8380
  }
7653
8381
  printSignInStep(signIn);
7654
8382
  printBrainStep(brainStep);
7655
- printHuman(`Registered with ${choice.tools.map((t) => t.label).join(", ")}.
8383
+ printHuman(`${registrationLine(choice.tools, results)}
7656
8384
  `);
7657
8385
  const edited = results.filter((r) => r.edited === true);
7658
8386
  if (edited.length > 0) {
@@ -7669,14 +8397,35 @@ function setupCommand() {
7669
8397
  if (choice.errorTelemetry !== "undecided") {
7670
8398
  printHuman(c.gray(` error-telemetry: ${choice.errorTelemetry}`));
7671
8399
  }
7672
- if (choice.hook) {
7673
- printHuman(c.gray(" resurface runs at session start"));
8400
+ const armed = (what) => choice.tools.filter(
8401
+ (t) => results.some(
8402
+ (r) => r.tool === t.label && r.what === what && r.result !== "refused" && r.result !== "removed"
8403
+ )
8404
+ );
8405
+ const atStart = choice.hook ? armed(HOOK_WHAT.sessionStart) : [];
8406
+ const atStop = choice.hook ? armed(HOOK_WHAT.endOfTurn) : [];
8407
+ const once = notByName !== null ? ", once memgineering is installed globally (below)" : "";
8408
+ if (atStop.length > 0) {
8409
+ printHuman(
8410
+ c.gray(
8411
+ ` your rules are shown at the end of each turn on ${atStop.map((t) => t.label).join(", ")}${once} \u2014
8412
+ nothing there reaches the model earlier, so a turn that breaks one is corrected
8413
+ in the next.`
8414
+ )
8415
+ );
8416
+ }
8417
+ if (atStart.length > 0) {
8418
+ printHuman(
8419
+ c.gray(
8420
+ ` resurface runs at session start on ${atStart.map((t) => t.label).join(", ")}${once}`
8421
+ )
8422
+ );
7674
8423
  printHuman(
7675
8424
  c.gray(
7676
- " \u2192 this puts a few note summaries into every new session, unasked.\n Anything you would not want surfacing there: `memgineering exclude <path>`.\n Turn it back off with `memgineering setup --agent --hook off`."
8425
+ " \u2192 this puts a few note summaries into every new session, unasked.\n Anything you would not want surfacing there: `memgineering exclude <path>`."
7677
8426
  )
7678
8427
  );
7679
- if (choice.tools.some((t) => t.id === "codex")) {
8428
+ if (atStart.some((t) => t.id === "codex")) {
7680
8429
  printHuman(
7681
8430
  c.gray(
7682
8431
  " \u2192 Codex will ask you to review these hooks once, next time it\n starts: press `t` to trust them. Until you do, neither one runs,\n and an upgrade that changes a hook asks again."
@@ -7684,6 +8433,19 @@ function setupCommand() {
7684
8433
  );
7685
8434
  }
7686
8435
  }
8436
+ if (atStart.length > 0 || atStop.length > 0) {
8437
+ printHuman(
8438
+ c.gray(" Turn the hooks back off with `memgineering setup --agent --hook off`.")
8439
+ );
8440
+ }
8441
+ if (notByName !== null) {
8442
+ printHuman(
8443
+ c.yellow(
8444
+ `
8445
+ ${globalInstallHint(notByName, hooksLanded).split("\n").join("\n ")}`
8446
+ )
8447
+ );
8448
+ }
7687
8449
  const removed = results.filter((r) => r.result === "removed");
7688
8450
  if (!choice.hook && removed.length > 0) {
7689
8451
  printHuman(c.gray(` removed: ${label(removed)}`));
@@ -7716,7 +8478,7 @@ function setupCommand() {
7716
8478
  const codexHooksPending = choice.hook && choice.tools.some((t) => t.id === "codex");
7717
8479
  printHuman(
7718
8480
  c.bold(
7719
- codexHooksPending ? "\n Ready to use now \u2014 recall and remember work; the hooks start once\n Codex has asked you about them." : "\n Ready to use now \u2014 recall, remember and the hooks all work."
8481
+ notByName !== null ? hooksLanded ? "\n Installed \u2014 the hooks, and an agent following the guide, start working\n once memgineering is installed globally." : "\n Installed \u2014 an agent following the guide starts working once\n memgineering is installed globally." : codexHooksPending ? "\n Ready to use now \u2014 recall and remember work; the hooks start once\n Codex has asked you about them." : "\n Ready to use now \u2014 recall, remember and the hooks all work."
7720
8482
  )
7721
8483
  );
7722
8484
  printHuman(
@@ -7773,6 +8535,11 @@ async function applySetup(choice, dryRun) {
7773
8535
  ...cfg.system,
7774
8536
  autoUpdate: choice.autoUpdate,
7775
8537
  installed,
8538
+ // Stamped whether or not anything was rewritten. A run that finds every
8539
+ // file already current is exactly the run that must clear the notice —
8540
+ // recording only on a change would leave `refresh_guidance` firing
8541
+ // forever on the release where the guidance happened not to move.
8542
+ guidanceVersion: VERSION,
7776
8543
  // 'undecided' means nobody answered THIS run — it must not erase an
7777
8544
  // answer somebody gave in an earlier one.
7778
8545
  ...choice.errorTelemetry !== "undecided" ? { errorTelemetry: choice.errorTelemetry } : {}
@@ -7786,13 +8553,13 @@ async function applySetup(choice, dryRun) {
7786
8553
  };
7787
8554
  const keyOf = (path) => path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7788
8555
  try {
7789
- return await write();
8556
+ return await write2();
7790
8557
  } finally {
7791
8558
  await persist();
7792
8559
  }
7793
- async function write() {
8560
+ async function write2() {
7794
8561
  for (const tool of choice.tools) {
7795
- const hubPath = join24(agentHome(), tool.hubFile);
8562
+ const hubPath = join25(agentHome(), tool.hubFile);
7796
8563
  const written = await injectBlock(
7797
8564
  hubPath,
7798
8565
  hub + tailFor(tool, choice.hook),
@@ -7807,8 +8574,8 @@ async function applySetup(choice, dryRun) {
7807
8574
  });
7808
8575
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
7809
8576
  for (const skill of BRAND.subSkills) {
7810
- const content = await loadAsset(join24(skill, "SKILL.md"));
7811
- const path = join24(agentHome(), tool.skillsDir, skill, "SKILL.md");
8577
+ const content = await loadAsset(join25(skill, "SKILL.md"));
8578
+ const path = join25(agentHome(), tool.skillsDir, skill, "SKILL.md");
7812
8579
  const out = await writeIfDifferent(path, content, dryRun);
7813
8580
  results.push({
7814
8581
  tool: tool.label,
@@ -7853,6 +8620,34 @@ ${surfacedForYou && rulesForYou ? `Two moments matter here, and a hook covers ea
7853
8620
  The skills named above are files at \`${tool.skillsDir}/memgineering-<name>/SKILL.md\`. Your host may list a skill by name with its description trimmed away, in which case the name is all you get \u2014 read the file rather than assuming there is nothing there.${skills}
7854
8621
  `;
7855
8622
  }
8623
+ function registeredTools(tools, results) {
8624
+ return tools.filter(
8625
+ (t) => results.some((r) => r.tool === t.label && r.what === "guide" && r.result !== "refused")
8626
+ );
8627
+ }
8628
+ function registrationLine(tools, results) {
8629
+ const registered = registeredTools(tools, results);
8630
+ const notRegistered = tools.filter((t) => !registered.includes(t));
8631
+ const skipped = (t) => results.filter((r) => r.tool === t.label && r.result === "refused").length;
8632
+ const names = (ts) => ts.map((t) => t.label).join(", ");
8633
+ const parts = [];
8634
+ if (registered.length > 0) {
8635
+ const partial = registered.filter((t) => skipped(t) > 0);
8636
+ parts.push(
8637
+ `Registered with ${names(registered)}` + (partial.length > 0 ? ` \u2014 but ${partial.map((t) => `${skipped(t)} file(s) for ${t.label}`).join(", ")} could not be written, listed below` : "")
8638
+ );
8639
+ }
8640
+ if (notRegistered.length > 0) {
8641
+ parts.push(
8642
+ `Not registered with ${names(notRegistered)} \u2014 its guide could not be written, listed below`
8643
+ );
8644
+ }
8645
+ return `${parts.join(". ")}.`;
8646
+ }
8647
+ function globalInstallHint(reason, hooks) {
8648
+ return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
8649
+ so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
8650
+ }
7856
8651
  async function resolveMode(opts) {
7857
8652
  if (opts.agent) return { kind: "agent", chosen: true };
7858
8653
  if (opts.human) {
@@ -7886,21 +8681,28 @@ async function detectTools() {
7886
8681
  const home = agentHome();
7887
8682
  const found = [];
7888
8683
  for (const tool of TOOLS) {
7889
- if (await exists2(join24(home, tool.dir))) found.push(tool);
8684
+ if (await exists2(join25(home, tool.dir))) found.push(tool);
7890
8685
  }
7891
8686
  return found;
7892
8687
  }
7893
8688
  function fromFlags(detected, opts) {
7894
8689
  const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
7895
8690
  const tool = detected.find((t) => t.id === id);
7896
- if (!tool) {
8691
+ if (tool) return tool;
8692
+ const detectedList = `Detected: ${detected.map((t) => t.id).join(", ") || "none"}. Omit --tools to use all of them.`;
8693
+ const known = TOOLS.find((t) => t.id === id);
8694
+ if (!known) {
7897
8695
  throw memgError(
7898
8696
  "invalid_input",
7899
- `not installed on this machine: ${id}`,
7900
- `Detected: ${detected.map((t) => t.id).join(", ") || "none"}. Omit --tools to use all of them.`
8697
+ `unknown tool: ${id}`,
8698
+ `Supported: ${TOOLS.map((t) => t.id).join(", ")}. ${detectedList}`
7901
8699
  );
7902
8700
  }
7903
- return tool;
8701
+ throw memgError(
8702
+ "invalid_input",
8703
+ `${known.label} not found on this machine: no ~/${known.dir} yet`,
8704
+ `${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
8705
+ );
7904
8706
  }) : detected;
7905
8707
  return {
7906
8708
  tools,
@@ -7978,7 +8780,7 @@ ${BRAND.hubMarkerEnd}`;
7978
8780
  const existing = await readFile24(filePath, "utf8").catch(() => null);
7979
8781
  if (existing === null) {
7980
8782
  if (!dryRun) {
7981
- await mkdir15(dirname8(filePath), { recursive: true });
8783
+ await mkdir15(dirname9(filePath), { recursive: true });
7982
8784
  await writeThenRename(filePath, `${block}
7983
8785
  `);
7984
8786
  }
@@ -8022,13 +8824,18 @@ async function writeIfDifferent(filePath, content, dryRun) {
8022
8824
  const after = fingerprint(content);
8023
8825
  if (existing === content) return { outcome: "unchanged", before: existing, after };
8024
8826
  if (!dryRun) {
8025
- await mkdir15(dirname8(filePath), { recursive: true });
8827
+ await mkdir15(dirname9(filePath), { recursive: true });
8026
8828
  await writeThenRename(filePath, content);
8027
8829
  }
8028
8830
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
8029
8831
  }
8832
+ function hooksFor(tools) {
8833
+ return HOOKS.filter(
8834
+ (h) => tools.some((t) => t.hooks && (h.tools ?? DEFAULT_HOOK_TOOLS).includes(t.id))
8835
+ );
8836
+ }
8030
8837
  async function installHook(spec, tool, dryRun) {
8031
- const settingsPath = join24(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8838
+ const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8032
8839
  const raw = await readFile24(settingsPath, "utf8").catch(() => null);
8033
8840
  let settings = {};
8034
8841
  if (raw !== null) {
@@ -8071,14 +8878,14 @@ async function installHook(spec, tool, dryRun) {
8071
8878
  }
8072
8879
  const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
8073
8880
  if (!dryRun) {
8074
- await mkdir15(dirname8(settingsPath), { recursive: true });
8881
+ await mkdir15(dirname9(settingsPath), { recursive: true });
8075
8882
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
8076
8883
  `);
8077
8884
  }
8078
8885
  return raw === null ? "injected" : "updated";
8079
8886
  }
8080
8887
  async function removeHook(spec, tool, dryRun) {
8081
- const settingsPath = join24(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8888
+ const settingsPath = join25(agentHome(), ...(tool.hooksFile ?? "").split("/"));
8082
8889
  const raw = await readFile24(settingsPath, "utf8").catch(() => null);
8083
8890
  if (raw === null) return "unchanged";
8084
8891
  let parsed;
@@ -8103,7 +8910,7 @@ async function removeHook(spec, tool, dryRun) {
8103
8910
  return "removed";
8104
8911
  }
8105
8912
  async function loadAsset(relPath) {
8106
- const raw = await readFile24(join24(await assetRoot(), relPath), "utf8");
8913
+ const raw = await readFile24(join25(await assetRoot(), relPath), "utf8");
8107
8914
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
8108
8915
  const end = raw.indexOf("\n---", 4);
8109
8916
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -8111,14 +8918,14 @@ async function loadAsset(relPath) {
8111
8918
  return raw;
8112
8919
  }
8113
8920
  async function assetRoot() {
8114
- const here = dirname8(fileURLToPath2(import.meta.url));
8115
- const bundled = join24(here, "..", "assets");
8116
- if (await exists2(join24(bundled, BRAND.hubFileName))) return bundled;
8921
+ const here = dirname9(fileURLToPath3(import.meta.url));
8922
+ const bundled = join25(here, "..", "assets");
8923
+ if (await exists2(join25(bundled, BRAND.hubFileName))) return bundled;
8117
8924
  let dir = here;
8118
8925
  for (; ; ) {
8119
- const candidate = join24(dir, "packages", "skills");
8120
- if (await exists2(join24(candidate, BRAND.hubFileName))) return candidate;
8121
- const parent = dirname8(dir);
8926
+ const candidate = join25(dir, "packages", "skills");
8927
+ if (await exists2(join25(candidate, BRAND.hubFileName))) return candidate;
8928
+ const parent = dirname9(dir);
8122
8929
  if (parent === dir) break;
8123
8930
  dir = parent;
8124
8931
  }
@@ -8136,7 +8943,7 @@ async function exists2(path) {
8136
8943
  return false;
8137
8944
  }
8138
8945
  }
8139
- var TOOLS, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOKS;
8946
+ var TOOLS, HOSTED_CONNECTOR_URL, SUPPORTED_LIST, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOK_WHAT, HOOKS;
8140
8947
  var init_setup = __esm({
8141
8948
  "src/commands/setup.ts"() {
8142
8949
  "use strict";
@@ -8147,7 +8954,9 @@ var init_setup = __esm({
8147
8954
  init_setup_brain();
8148
8955
  init_setup_signin();
8149
8956
  init_config();
8957
+ init_provenance();
8150
8958
  init_ui();
8959
+ init_version();
8151
8960
  TOOLS = [
8152
8961
  {
8153
8962
  id: "claude",
@@ -8215,20 +9024,27 @@ var init_setup = __esm({
8215
9024
  hooksFile: ".grok/hooks/memgineering.json"
8216
9025
  }
8217
9026
  ];
9027
+ HOSTED_CONNECTOR_URL = "https://mcp.memgineering.com/mcp";
9028
+ SUPPORTED_LIST = TOOLS.map((t) => `${t.label} (\`${t.id}\`, ~/${t.dir})`).join(", ");
8218
9029
  REFUSAL_HINT = `Either a guide setup wrote is still there with its markers removed, or a settings file is not valid JSON. Everything else in this run was applied.
8219
9030
  For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around that block, or delete the block, then run setup again.`;
8220
9031
  DUPLICATE_HINT = "That file already looked like it held a memgineering guide with no markers around it, so the new one was appended below rather than replacing it.\nIf an older copy is now sitting above it, delete that copy \u2014 two guides means both are in every session. If those lines are your own writing, nothing is wrong.";
8221
9032
  HOOK_TIMEOUT_S = Math.ceil(HOOK_DEADLINE_MS / 1e3) + 3;
8222
9033
  DEFAULT_HOOK_TOOLS = ["claude", "codex"];
9034
+ HOOK_WHAT = {
9035
+ sessionStart: "session-start hook",
9036
+ beforeEdit: "before-edit hook",
9037
+ endOfTurn: "end-of-turn hook"
9038
+ };
8223
9039
  HOOKS = [
8224
9040
  {
8225
- what: "session-start hook",
9041
+ what: HOOK_WHAT.sessionStart,
8226
9042
  event: "SessionStart",
8227
9043
  command: `memgineering resurface --limit 5 --deadline ${HOOK_DEADLINE_MS} || true`,
8228
9044
  signature: "memgineering resurface"
8229
9045
  },
8230
9046
  {
8231
- what: "before-edit hook",
9047
+ what: HOOK_WHAT.beforeEdit,
8232
9048
  event: "PreToolUse",
8233
9049
  // Both lists are subsets of what `guard` filters on, so the process does
8234
9050
  // not start at all for a call it would ignore.
@@ -8279,7 +9095,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
8279
9095
  * because speaking here keeps the agent working — which is why the report
8280
9096
  * says so rather than leaving somebody to notice.
8281
9097
  */
8282
- what: "end-of-turn hook",
9098
+ what: HOOK_WHAT.endOfTurn,
8283
9099
  event: "Stop",
8284
9100
  command: "memgineering guard --at stop || true",
8285
9101
  signature: "memgineering guard --at stop",
@@ -8293,7 +9109,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
8293
9109
  import net from "net";
8294
9110
 
8295
9111
  // src/program.ts
8296
- import { Command as Command45 } from "commander";
9112
+ import { Command as Command46 } from "commander";
8297
9113
 
8298
9114
  // src/help.ts
8299
9115
  init_ui();
@@ -8337,7 +9153,18 @@ var COMMAND_GROUPS = [
8337
9153
  {
8338
9154
  title: "CURATION",
8339
9155
  blurb: "occasional \u2014 correct what is remembered and what is read",
8340
- commands: ["retire", "unretire", "retract", "exclude", "unexclude", "consolidate", "propose", "proposals"]
9156
+ commands: [
9157
+ "retire",
9158
+ "unretire",
9159
+ "rescope",
9160
+ "expire",
9161
+ "retract",
9162
+ "exclude",
9163
+ "unexclude",
9164
+ "consolidate",
9165
+ "propose",
9166
+ "proposals"
9167
+ ]
8341
9168
  },
8342
9169
  {
8343
9170
  title: "ACCOUNT",
@@ -8429,26 +9256,10 @@ init_errors();
8429
9256
  // src/lib/error-reporter.ts
8430
9257
  init_src();
8431
9258
  init_errors();
8432
- import { homedir as homedir3, platform } from "os";
8433
-
8434
- // src/version.ts
8435
- import { readFileSync } from "fs";
8436
- import { fileURLToPath } from "url";
8437
- import { dirname, join as join4 } from "path";
8438
- var __dir = dirname(fileURLToPath(import.meta.url));
8439
- var pkgPath = join4(__dir, "..", "package.json");
8440
- var VERSION = (() => {
8441
- try {
8442
- const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
8443
- return typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown";
8444
- } catch {
8445
- return "0.0.0-unknown";
8446
- }
8447
- })();
8448
-
8449
- // src/lib/error-reporter.ts
9259
+ init_version();
8450
9260
  init_api_client();
8451
9261
  init_config();
9262
+ import { homedir as homedir3, platform } from "os";
8452
9263
  var HOME = homedir3();
8453
9264
  var REPORT_TIMEOUT_MS = 3e3;
8454
9265
  function classifyError(err) {
@@ -8494,9 +9305,9 @@ async function reportError(kind, err) {
8494
9305
  ...code ? { error_code: code } : {},
8495
9306
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8496
9307
  });
8497
- const base = process.env["MEMGINEERING_TELEMETRY_URL"]?.trim() || apiUrl();
9308
+ const base2 = process.env["MEMGINEERING_TELEMETRY_URL"]?.trim() || apiUrl();
8498
9309
  await Promise.race([
8499
- fetch(`${base.replace(/\/+$/, "")}/v1/telemetry/error`, {
9310
+ fetch(`${base2.replace(/\/+$/, "")}/v1/telemetry/error`, {
8500
9311
  method: "POST",
8501
9312
  headers: { "content-type": "application/json" },
8502
9313
  body: JSON.stringify(payload)
@@ -10587,10 +11398,10 @@ function adaptRegistryError(err) {
10587
11398
  }
10588
11399
  if (err instanceof KordisApiError) {
10589
11400
  const code = CODE_MAP[err.code] ?? "server_refused";
10590
- const base = err.hint ?? FALLBACK_HINTS[code] ?? "Nothing was changed. The registry refused this request; running it again is reasonable.";
11401
+ const base2 = err.hint ?? FALLBACK_HINTS[code] ?? "Nothing was changed. The registry refused this request; running it again is reasonable.";
10591
11402
  const retry = err.retryAfter !== void 0 ? `
10592
11403
  Wait ${err.retryAfter}s before trying again.` : "";
10593
- return memgError(code, err.message, `${withFlags(base, err)}${retry}`);
11404
+ return memgError(code, err.message, `${withFlags(base2, err)}${retry}`);
10594
11405
  }
10595
11406
  return null;
10596
11407
  }
@@ -10743,7 +11554,24 @@ var OpVerbSchema = z23.enum([
10743
11554
  * same thing. A verb the server can emit and this cannot name is a line this
10744
11555
  * CLI would silently count as corruption.
10745
11556
  */
10746
- "write"
11557
+ "write",
11558
+ /**
11559
+ * Where a memory binds, moved — and nothing it says changed.
11560
+ *
11561
+ * Its own verb rather than a `revise` for the reason `reclassify` has one: a
11562
+ * ledger is read by people asking what happened to their notes, and "revise"
11563
+ * for a change that touched no conclusion tells them something untrue. This
11564
+ * one has a second reason — a rescope is the only claimless write that can
11565
+ * change WHICH decisions an agent is handed before it edits a file, so it is
11566
+ * the row somebody goes looking for after a rule started or stopped applying
11567
+ * to them.
11568
+ *
11569
+ * Note `unbind` is absent here on purpose: local `revise --no-rule` records
11570
+ * `revise`, because it IS that command. These two are their own commands.
11571
+ */
11572
+ "rescope",
11573
+ /** A memory was given an end date. It was NOT retired — see `expire`. */
11574
+ "expire"
10747
11575
  ]);
10748
11576
  var OpRecordSchema = z23.object({
10749
11577
  op_id: z23.string().min(1),
@@ -10980,9 +11808,9 @@ async function namedCloudTarget(opts) {
10980
11808
  "`--brain <name>` reads an existing brain on the account. `--local` and `--vault <path>` read a folder on this machine. Nothing was read or changed; choose one target kind."
10981
11809
  );
10982
11810
  }
10983
- const base = apiUrl();
10984
- const credentials = await requireCredentials(base);
10985
- const brains = await listCloudBrains(credentials.token, base);
11811
+ const base2 = apiUrl();
11812
+ const credentials = await requireCredentials(base2);
11813
+ const brains = await listCloudBrains(credentials.token, base2);
10986
11814
  if (brains.length === 0) {
10987
11815
  throw memgError(
10988
11816
  "invalid_input",
@@ -11001,23 +11829,23 @@ ${available}
11001
11829
  Use one of those names or ids. No brain was created.`
11002
11830
  );
11003
11831
  }
11004
- return { kind: "cloud", brainId: found.id, name: found.name, apiUrl: base };
11832
+ return { kind: "cloud", brainId: found.id, name: found.name, apiUrl: base2 };
11005
11833
  }
11006
11834
  var isNoVaultLinked = (err) => typeof err === "object" && err !== null && err.code === "no_vault_linked";
11007
11835
  async function accountAwareRefusal() {
11008
11836
  try {
11009
- const base = apiUrl();
11837
+ const base2 = apiUrl();
11010
11838
  const credentials = await readCredentials();
11011
- if (!credentials || credentials.api_url !== base) {
11839
+ if (!credentials || credentials.api_url !== base2) {
11012
11840
  const cloud = (await loadConfig()).brain.cloud;
11013
- if (!cloud || cloud.api_url !== base) return null;
11841
+ if (!cloud || cloud.api_url !== base2) return null;
11014
11842
  return memgError(
11015
11843
  "not_signed_in",
11016
11844
  `this machine is signed out of \`${oneLine(cloud.name, WIDTH.title)}\``,
11017
11845
  "Your notes are on that account \u2014 nothing is lost, and nothing here needs creating.\n\nSign back in: `memgineering login`\n\nDo NOT run `init` or `link` to fix this. They make a NEW empty brain on this\nmachine and leave the notes where they are."
11018
11846
  );
11019
11847
  }
11020
- const brains = await listCloudBrains(credentials.token, base, { retries: 0 });
11848
+ const brains = await listCloudBrains(credentials.token, base2, { retries: 0 });
11021
11849
  if (brains.length === 0) return null;
11022
11850
  const names = brains.map((b) => ` ${oneLine(b.name, WIDTH.title)}`).join("\n");
11023
11851
  return memgError(
@@ -11052,29 +11880,29 @@ machine and leave the notes where they are.`
11052
11880
  }
11053
11881
  }
11054
11882
  async function hostedPointer() {
11055
- const base = apiUrl();
11883
+ const base2 = apiUrl();
11056
11884
  const config = await loadConfig();
11057
11885
  const cloud = config.brain.cloud;
11058
- if (!cloud || cloud.api_url !== base) return null;
11886
+ if (!cloud || cloud.api_url !== base2) return null;
11059
11887
  const stored = await readCredentials().catch(() => null);
11060
- const signedIn = stored !== null && stored.api_url === base;
11888
+ const signedIn = stored !== null && stored.api_url === base2;
11061
11889
  const pending = signedIn ? null : await readPendingLogin().catch(() => null);
11062
11890
  return {
11063
11891
  name: cloud.name,
11064
11892
  id: cloud.id,
11065
- apiUrl: base,
11893
+ apiUrl: base2,
11066
11894
  signedIn,
11067
- signInPending: pending !== null && pending.api_url === base && !pendingExpired(pending)
11895
+ signInPending: pending !== null && pending.api_url === base2 && !pendingExpired(pending)
11068
11896
  };
11069
11897
  }
11070
11898
  async function cloudTarget() {
11071
- const base = apiUrl();
11899
+ const base2 = apiUrl();
11072
11900
  const config = await loadConfig();
11073
11901
  const cloud = config.brain.cloud;
11074
- if (!cloud || cloud.api_url !== base) return null;
11075
- const credentials = await currentCredentials(base);
11902
+ if (!cloud || cloud.api_url !== base2) return null;
11903
+ const credentials = await currentCredentials(base2);
11076
11904
  if (!credentials) return null;
11077
- return { kind: "cloud", brainId: cloud.id, name: cloud.name, apiUrl: base };
11905
+ return { kind: "cloud", brainId: cloud.id, name: cloud.name, apiUrl: base2 };
11078
11906
  }
11079
11907
 
11080
11908
  // src/commands/base-write.ts
@@ -11979,6 +12807,7 @@ async function readEventsAt(stateDir) {
11979
12807
  init_index_build();
11980
12808
 
11981
12809
  // src/lib/base-files.ts
12810
+ init_src();
11982
12811
  init_init();
11983
12812
  var BASE_FILES = [
11984
12813
  {
@@ -12028,43 +12857,31 @@ var BASE_FILES = [
12028
12857
  }
12029
12858
  ];
12030
12859
  var HUB_PATH = "00_HUB/HUB.md";
12860
+ var TEMPLATE_SLOTS = 2;
12861
+ function capTemplates(cards, isTemplate) {
12862
+ const kept = [];
12863
+ const withheld = [];
12864
+ let templates = 0;
12865
+ for (const card of cards) {
12866
+ if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
12867
+ else kept.push(card);
12868
+ }
12869
+ return { kept, withheld };
12870
+ }
12031
12871
  function bodyOf2(content) {
12032
12872
  const stripped = content.replace(/^/, "");
12033
12873
  const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
12034
12874
  return (fence2 ? stripped.slice(fence2[0].length) : stripped).trim();
12035
12875
  }
12036
- var RETIRED_BASE_TEMPLATES = {
12037
- // Retired 0.11.1, when the prompt grew a storage-boundary invitation.
12038
- "01_BASE/BOUNDARIES.md": [
12039
- [
12040
- "---",
12041
- "memgineering:",
12042
- " id: boundaries",
12043
- " title: Boundaries",
12044
- " summary: What an agent must not do without asking me first.",
12045
- " scope: base",
12046
- "---",
12047
- "",
12048
- "# Boundaries",
12049
- "",
12050
- "Privacy, spending, anything irreversible, anything that leaves this",
12051
- "machine. Be specific \u2014 a boundary that has to be interpreted is one",
12052
- "that will be interpreted generously.",
12053
- "",
12054
- "- ",
12055
- ""
12056
- ].join("\n")
12057
- ]
12058
- };
12059
12876
  function templatesFor(path, current) {
12060
- const retired = RETIRED_BASE_TEMPLATES[path] ?? [];
12877
+ const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
12061
12878
  return current === void 0 ? retired : [current, ...retired];
12062
12879
  }
12063
12880
  function isBaseFilePath(path) {
12064
12881
  return path.startsWith("01_BASE/");
12065
12882
  }
12066
12883
  function bodyStillTemplate(path, content) {
12067
- const template = files("brain")[path];
12884
+ const template = ownEntry(files("brain"), path);
12068
12885
  if (template === void 0) return false;
12069
12886
  const body = bodyOf2(content);
12070
12887
  if (body === "") return false;
@@ -12090,7 +12907,7 @@ async function untouchedBase(adapter, paths) {
12090
12907
  const out = /* @__PURE__ */ new Set();
12091
12908
  await Promise.all(
12092
12909
  paths.map(async (path) => {
12093
- const template = templates[path];
12910
+ const template = ownEntry(templates, path);
12094
12911
  if (template === void 0) return;
12095
12912
  const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
12096
12913
  if (actual === null) return;
@@ -12173,6 +12990,9 @@ function openCommand() {
12173
12990
  claims: found.claims,
12174
12991
  relations: found.relations,
12175
12992
  ...showBody ? { body: found.body } : {},
12993
+ // Only when the server said; an older one cannot, and false
12994
+ // would be a claim it never made.
12995
+ ...found.unfilled === void 0 ? {} : { scaffolding: found.unfilled },
12176
12996
  brain: { hosted: true, name: target.name }
12177
12997
  },
12178
12998
  human: () => {
@@ -12180,6 +13000,11 @@ function openCommand() {
12180
13000
  `## ${oneLine(String(found.memory["title"] ?? found.path), WIDTH.title)}
12181
13001
  `
12182
13002
  );
13003
+ if (found.unfilled === true) {
13004
+ printHuman(
13005
+ c.yellow("not filled in yet") + c.gray(" \u2014 still the template it started as")
13006
+ );
13007
+ }
12183
13008
  const summary = found.memory["summary"];
12184
13009
  if (typeof summary === "string" && summary !== "") {
12185
13010
  printHuman(`> ${oneLine(summary, WIDTH.text)}
@@ -12784,7 +13609,7 @@ function rememberCommand() {
12784
13609
  const kind = parseKind(opts.kind);
12785
13610
  const neverStore = parseNeverStore(opts.neverStore);
12786
13611
  const binding = opts.rule === true || neverStore.length > 0;
12787
- const scope = await scopeToWrite(opts.scope);
13612
+ const { scope, via: scopeVia } = await scopeToWrite(opts.scope);
12788
13613
  if (binding && scope === void 0 && !said.noScope) {
12789
13614
  throw memgError(
12790
13615
  "invalid_input",
@@ -12817,6 +13642,7 @@ function rememberCommand() {
12817
13642
  path: written.path,
12818
13643
  op_id: written.op_id,
12819
13644
  scope: written.scope,
13645
+ scope_via: scopeVia,
12820
13646
  brain: { hosted: true, name: target.name },
12821
13647
  ...binding ? { binding: true } : {},
12822
13648
  ...neverStore.length > 0 ? { never_store: neverStore } : {},
@@ -12857,6 +13683,7 @@ function rememberCommand() {
12857
13683
  printHuman(
12858
13684
  `note: \`${oneLine(written.id, WIDTH.handle)}\` \xB7 \`${oneLine(written.path, WIDTH.path)}\``
12859
13685
  );
13686
+ printHuman(c.gray(scopeLine(written.scope, scopeVia, binding)));
12860
13687
  if (binding) {
12861
13688
  printHuman("rule: shown before you write a file here, once a session");
12862
13689
  }
@@ -12979,6 +13806,7 @@ function rememberCommand() {
12979
13806
  path,
12980
13807
  op_id: record.op_id,
12981
13808
  scope: scope ?? null,
13809
+ scope_via: scopeVia,
12982
13810
  binding,
12983
13811
  ...neverStore.length > 0 ? { never_store: neverStore } : {},
12984
13812
  // Null on nearly every write, and present exactly when the ledger
@@ -12992,6 +13820,7 @@ function rememberCommand() {
12992
13820
  ` : `Remembered.
12993
13821
  `);
12994
13822
  printHuman(`note: \`${id}\` \xB7 \`${path}\``);
13823
+ printHuman(c.gray(scopeLine(scope, scopeVia, binding)));
12995
13824
  if (binding) {
12996
13825
  printHuman(c.gray(`rule: shown before you write a file here, once a session`));
12997
13826
  }
@@ -13039,14 +13868,27 @@ function renderObservation(args) {
13039
13868
  return lines2.join("\n");
13040
13869
  }
13041
13870
  async function scopeToWrite(flag2) {
13042
- if (flag2 === false) return void 0;
13043
- if (flag2 !== void 0) return flag2;
13871
+ if (flag2 === false) return { scope: void 0, via: "flag" };
13872
+ if (flag2 !== void 0) return { scope: flag2, via: "flag" };
13044
13873
  try {
13045
- const derived = (await projectOf()).name;
13046
- return derived !== null && stampable(derived) ? derived : void 0;
13874
+ const derived = await projectOf();
13875
+ return derived.name !== null && stampable(derived.name) ? { scope: derived.name, via: derived.via } : { scope: void 0, via: "none" };
13047
13876
  } catch {
13048
- return void 0;
13877
+ return { scope: void 0, via: "none" };
13878
+ }
13879
+ }
13880
+ function scopeLine(scope, via, binding) {
13881
+ if (!scope) {
13882
+ return binding ? "scope: none \u2014 this rule holds in every project" : "scope: none \u2014 recall finds it from any project";
13049
13883
  }
13884
+ const from = {
13885
+ flag: "as given with --scope",
13886
+ override: "the name set for this folder (`memgineering project`)",
13887
+ remote: "from this repository's git remote",
13888
+ dirname: "from this folder's name \u2014 the repository has no git remote",
13889
+ none: "as given"
13890
+ };
13891
+ return `scope: \`${oneLine(scope, WIDTH.title)}\` \u2014 ${from[via]}`;
13050
13892
  }
13051
13893
  function stampable(name) {
13052
13894
  if (name.length > SCOPE_LIMIT) return false;
@@ -14512,11 +15354,11 @@ init_init();
14512
15354
  var USER_PATH = "01_BASE/USER.md";
14513
15355
  var PREFERENCES_PATH = "01_BASE/PREFERENCES.md";
14514
15356
  var SCAFFOLD_PATHS = Object.keys(files("any"));
14515
- function baseFilledPaths(base) {
14516
- return Object.keys(bulletsFor(base));
15357
+ function baseFilledPaths(base2) {
15358
+ return Object.keys(bulletsFor(base2));
14517
15359
  }
14518
- function scaffoldNotes(name, base = {}) {
14519
- const bullets = bulletsFor(base);
15360
+ function scaffoldNotes(name, base2 = {}) {
15361
+ const bullets = bulletsFor(base2);
14520
15362
  return Object.entries(files(name)).map(([path, content]) => {
14521
15363
  const lines2 = bullets[path];
14522
15364
  return { path, content: lines2 ? fillPlaceholder(content, lines2) : content };
@@ -14526,10 +15368,10 @@ function oneLine2(value) {
14526
15368
  const trimmed = value?.replace(/\s+/g, " ").trim();
14527
15369
  return trimmed ? trimmed : void 0;
14528
15370
  }
14529
- function bulletsFor(base) {
14530
- const user = oneLine2(base.user);
14531
- const language = oneLine2(base.language);
14532
- const about = oneLine2(base.about);
15371
+ function bulletsFor(base2) {
15372
+ const user = oneLine2(base2.user);
15373
+ const language = oneLine2(base2.language);
15374
+ const about = oneLine2(base2.about);
14533
15375
  const out = {};
14534
15376
  const userBullets = [
14535
15377
  ...user === void 0 ? [] : [`Call them: ${user}`],
@@ -14580,9 +15422,9 @@ function createCommand() {
14580
15422
  "Nothing was created. Agree one with the user \u2014 it is what they will see in\n`memgineering brains` and how they tell two brains apart \u2014 then run\n`memgineering create <name>`."
14581
15423
  );
14582
15424
  }
14583
- const base = { user: opts.user, language: opts.language, about: opts.about };
14584
- const notes = scaffoldNotes(name, base);
14585
- const filled = baseFilledPaths(base);
15425
+ const base2 = { user: opts.user, language: opts.language, about: opts.about };
15426
+ const notes = scaffoldNotes(name, base2);
15427
+ const filled = baseFilledPaths(base2);
14586
15428
  if (opts.dryRun === true) {
14587
15429
  printDual({
14588
15430
  json: {
@@ -15208,9 +16050,9 @@ function assertNothingLost(before, after, action) {
15208
16050
  });
15209
16051
  }
15210
16052
  }
15211
- async function commit2(root, base, next) {
16053
+ async function commit2(root, base2, next) {
15212
16054
  const current = await readIgnoreFileState(root);
15213
- if (current.hash !== base.hash) {
16055
+ if (current.hash !== base2.hash) {
15214
16056
  return { ok: false, reason: "revision_conflict", current };
15215
16057
  }
15216
16058
  if (next === current.content) return { ok: true, content: next, changed: false };
@@ -15256,15 +16098,15 @@ function planRemoveIgnoreEntry(content, relPath) {
15256
16098
  assertNothingLost(next, content, "un-excluding");
15257
16099
  return { content: next, changed: true };
15258
16100
  }
15259
- async function addIgnoreEntry(root, relPath, base) {
15260
- const edit = planAddIgnoreEntry(base.content, relPath);
15261
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
15262
- return commit2(root, base, edit.content);
16101
+ async function addIgnoreEntry(root, relPath, base2) {
16102
+ const edit = planAddIgnoreEntry(base2.content, relPath);
16103
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
16104
+ return commit2(root, base2, edit.content);
15263
16105
  }
15264
- async function removeIgnoreEntry(root, relPath, base) {
15265
- const edit = planRemoveIgnoreEntry(base.content, relPath);
15266
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
15267
- return commit2(root, base, edit.content);
16106
+ async function removeIgnoreEntry(root, relPath, base2) {
16107
+ const edit = planRemoveIgnoreEntry(base2.content, relPath);
16108
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
16109
+ return commit2(root, base2, edit.content);
15268
16110
  }
15269
16111
 
15270
16112
  // src/commands/curation.ts
@@ -15379,9 +16221,9 @@ function ignoreCommand(mode) {
15379
16221
  createPrefix: brain.observationsDir
15380
16222
  });
15381
16223
  const target = mode === "exclude" ? await mustMatchAFile(brain.root, adapter, path) : path;
15382
- const base = await readIgnoreFileState(brain.root);
16224
+ const base2 = await readIgnoreFileState(brain.root);
15383
16225
  const record = await withWriteLock(brain.root, async () => {
15384
- const result = mode === "exclude" ? await addIgnoreEntry(brain.root, target, base) : await removeIgnoreEntry(brain.root, target, base);
16226
+ const result = mode === "exclude" ? await addIgnoreEntry(brain.root, target, base2) : await removeIgnoreEntry(brain.root, target, base2);
15385
16227
  if (!result.ok) {
15386
16228
  throw memgError(
15387
16229
  "undo_conflict",
@@ -15394,11 +16236,11 @@ function ignoreCommand(mode) {
15394
16236
  verb: mode,
15395
16237
  target: ".memgignore",
15396
16238
  memoryId: null,
15397
- hashBefore: hashContent(base.content),
16239
+ hashBefore: hashContent(base2.content),
15398
16240
  hashAfter: hashContent(result.content),
15399
- patch: { before: base.content, after: result.content },
16241
+ patch: { before: base2.content, after: result.content },
15400
16242
  rationale,
15401
- snapshotBefore: base.content
16243
+ snapshotBefore: base2.content
15402
16244
  });
15403
16245
  });
15404
16246
  printDual({
@@ -15502,59 +16344,321 @@ var unretireCommand = () => lifecycleCommand("unretire");
15502
16344
  var excludeCommand = () => ignoreCommand("exclude");
15503
16345
  var unexcludeCommand = () => ignoreCommand("unexclude");
15504
16346
 
15505
- // src/commands/evidence.ts
16347
+ // src/commands/scope-validity.ts
15506
16348
  init_src();
15507
16349
  import { Command as Command15 } from "commander";
16350
+ init_errors();
15508
16351
  init_index_build();
15509
16352
  init_cloud_brain();
15510
16353
  init_vault();
15511
16354
  init_ui();
15512
- function evidenceCommand() {
15513
- return new Command15("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
15514
- "--brain <name>",
15515
- "read from one existing hosted brain by name or id, without changing the pointer"
15516
- ).action(async (ref, opts) => {
15517
- const target = await resolveTarget({
15518
- ...opts.brain === void 0 ? {} : { brain: opts.brain },
15519
- ...opts.local === void 0 ? {} : { local: opts.local },
15520
- ...opts.vault === void 0 ? {} : { vault: opts.vault }
15521
- });
15522
- if (target.kind === "cloud") {
15523
- const cloud = await openCloudBrain(target);
15524
- const report4 = await cloudEvidence(cloud, ref);
16355
+ function rescopeCommand() {
16356
+ return new Command15("rescope").description("change which project a memory binds in \u2014 nothing it says changes").argument("<ref>", "handle, id, path, or exact title").option("--scope <name>", "the project it binds in from now on").option("--global", "it binds everywhere \u2014 say this rather than leaving the scope off").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
16357
+ async (ref, opts) => {
16358
+ const scope = scopeAskedFor(opts);
16359
+ const where = await resolveTarget({
16360
+ ...opts.local === void 0 ? {} : { local: opts.local },
16361
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
16362
+ });
16363
+ if (where.kind === "cloud") {
16364
+ await rescopeHosted(where, ref, scope, opts.reason);
16365
+ return;
16366
+ }
16367
+ const { entry, record, was } = await rescopeLocal(opts.vault, ref, scope, opts.reason);
16368
+ const widened = isGlobalScope(scope);
15525
16369
  printDual({
15526
- json: { ...report4, brain: { hosted: true, name: target.name } },
15527
- human: () => renderEvidence(viewFromCloud(report4), {
15528
- now: /* @__PURE__ */ new Date(),
15529
- hosted: true,
15530
- brain: target.name
15531
- })
16370
+ json: {
16371
+ action: "rescope",
16372
+ memory_id: entry.memory.id,
16373
+ target: entry.path,
16374
+ scope: widened ? null : scope,
16375
+ op_id: record.op_id
16376
+ },
16377
+ human: () => {
16378
+ printHuman(
16379
+ widened ? `**${oneLine(entry.memory.title, WIDTH.title)}** now binds everywhere.
16380
+ ` : `**${oneLine(entry.memory.title, WIDTH.title)}** now binds in **${oneLine(scope, WIDTH.title)}**.
16381
+ `
16382
+ );
16383
+ printHuman(c.gray(reachOf(entry.memory.binding === true, widened, scope, was)));
16384
+ printHuman(c.gray(`
16385
+ Undo: \`${undoHint(record.op_id, opts)}\``));
16386
+ }
15532
16387
  });
15533
- return;
15534
16388
  }
15535
- const brain = target.brain;
15536
- const adapter = await openBrainOrExplain(brain.root);
15537
- const built = await openIndex(adapter, brain.root);
15538
- const entry = resolveRef(built.entries, ref);
15539
- const [events, ledger] = await Promise.all([readEvents(brain.root), readOps(brain.root)]);
15540
- const now = /* @__PURE__ */ new Date();
15541
- const report3 = assembleEvidence(entry, events, ledger.ops, {
15542
- contextDir: process.cwd(),
15543
- now,
15544
- brain: built.entries
15545
- });
15546
- printDual({
15547
- json: report3,
15548
- human: () => renderEvidence(report3, { now, hosted: false })
15549
- });
15550
- });
16389
+ );
15551
16390
  }
15552
- function renderEvidence(report3, opts) {
15553
- const brainHeading = opts.brain === void 0 ? "" : ` \u2014 ${oneLine(opts.brain, WIDTH.title)}`;
15554
- printHuman(`## evidence: ${oneLine(report3.title, WIDTH.title)}${brainHeading}
15555
- `);
15556
- printHuman(
15557
- `\`id: ${oneLine(report3.id, WIDTH.handle)}\` \xB7 \`${oneLine(report3.path, WIDTH.path)}\`
16391
+ function expireCommand() {
16392
+ return new Command15("expire").description("give a memory an end date \u2014 it is NOT retired, and recall still returns it").argument("<ref>", "handle, id, path, or exact title").requiredOption("--until <date>", "the day it stops being current, e.g. 2026-10-31").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
16393
+ async (ref, opts) => {
16394
+ const where = await resolveTarget({
16395
+ ...opts.local === void 0 ? {} : { local: opts.local },
16396
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
16397
+ });
16398
+ if (where.kind === "cloud") {
16399
+ await expireHosted(where, ref, opts.until, opts.reason);
16400
+ return;
16401
+ }
16402
+ const { entry, record } = await expireLocal(opts.vault, ref, opts.until, opts.reason);
16403
+ printDual({
16404
+ json: {
16405
+ action: "expire",
16406
+ memory_id: entry.memory.id,
16407
+ target: entry.path,
16408
+ valid_until: opts.until.trim(),
16409
+ op_id: record.op_id
16410
+ },
16411
+ human: () => {
16412
+ printHuman(
16413
+ `**${oneLine(entry.memory.title, WIDTH.title)}** holds until **${oneLine(opts.until.trim(), WIDTH.title)}**.
16414
+ `
16415
+ );
16416
+ printHuman(
16417
+ c.gray(
16418
+ "It is NOT retired. After that date recall still returns it \u2014 ranked below what is current, and labelled.\nTo take it out of the answers: `memgineering retire " + oneLine(entry.path, WIDTH.path) + "`"
16419
+ )
16420
+ );
16421
+ printHuman(c.gray(`
16422
+ Undo: \`${undoHint(record.op_id, opts)}\``));
16423
+ }
16424
+ });
16425
+ }
16426
+ );
16427
+ }
16428
+ function scopeAskedFor(opts) {
16429
+ const named = opts.scope !== void 0;
16430
+ if (named && opts.global === true) {
16431
+ throw memgError(
16432
+ "invalid_input",
16433
+ "`--scope` and `--global` say opposite things",
16434
+ "Pass one. `--scope <name>` binds the memory in that project; `--global` binds it everywhere."
16435
+ );
16436
+ }
16437
+ if (opts.global === true) return GLOBAL_SCOPE;
16438
+ if (!named || oneLineWhole(opts.scope ?? "") === "") {
16439
+ throw memgError(
16440
+ "invalid_input",
16441
+ "rescope needs to know where the memory binds",
16442
+ "Pass `--scope <project>`, or `--global` if it holds everywhere. Widening is spelled out because it is the change that is hardest to notice afterwards."
16443
+ );
16444
+ }
16445
+ const scope = oneLineWhole(opts.scope ?? "");
16446
+ if (isGlobalScope(scope)) {
16447
+ throw memgError(
16448
+ "invalid_input",
16449
+ "`global` is a reserved scope",
16450
+ "Use `--global` to say a memory binds everywhere. Keeping one spelling of it means nothing downstream has two to match on."
16451
+ );
16452
+ }
16453
+ return scope;
16454
+ }
16455
+ function reachOf(binding, widened, scope, was) {
16456
+ if (!binding) {
16457
+ return "Where a memory is scoped decides which project raises it. Nothing else about it changed.";
16458
+ }
16459
+ if (widened) {
16460
+ return `It is a rule, and it binds everywhere now${was === null ? "" : ` rather than only in ${was}`}. Agents in every project will be shown it before they edit a file.`;
16461
+ }
16462
+ return `It is a rule, and it binds only in ${scope} now. Agents working anywhere else will no longer be shown it.`;
16463
+ }
16464
+ async function rescopeLocal(vault, ref, scope, reason) {
16465
+ const { adapter, brain, built, entry } = await openFor(vault, ref);
16466
+ assertLocalBoundary(built.entries, { texts: [scope] });
16467
+ const was = declaredScopeOf(entry.memory);
16468
+ if (isGlobalScope(scope) ? was === null : was === scope) {
16469
+ throw memgError(
16470
+ "nothing_to_change",
16471
+ `${entry.memory.title} already binds there`,
16472
+ `Nothing was written, and nothing needed to be. \`memgineering open ${ref}\` shows the scope it declares; a note that declares none binds everywhere.`
16473
+ );
16474
+ }
16475
+ const record = await write(
16476
+ adapter,
16477
+ brain.root,
16478
+ entry,
16479
+ ref,
16480
+ "rescope",
16481
+ () => rescopeInput(entry, scope, { reason: reason ?? null }),
16482
+ reason
16483
+ );
16484
+ return { entry, record, was };
16485
+ }
16486
+ async function expireLocal(vault, ref, until, reason) {
16487
+ const { adapter, brain, entry } = await openFor(vault, ref);
16488
+ const wanted = until.trim();
16489
+ if (!Number.isFinite(Date.parse(wanted))) {
16490
+ throw memgError(
16491
+ "invalid_input",
16492
+ `${until} is not a date`,
16493
+ "Pass an ISO date, such as 2026-10-31 or 2026-10-31T00:00:00Z."
16494
+ );
16495
+ }
16496
+ if ((entry.claims[0]?.valid_until ?? entry.memory.valid_until ?? null) === wanted) {
16497
+ throw memgError(
16498
+ "nothing_to_change",
16499
+ `${entry.memory.title} already ends then`,
16500
+ `Nothing was written, and nothing needed to be. \`memgineering open ${ref}\` shows the date it carries.`
16501
+ );
16502
+ }
16503
+ const record = await write(
16504
+ adapter,
16505
+ brain.root,
16506
+ entry,
16507
+ ref,
16508
+ "expire",
16509
+ () => expireInput(entry, wanted, { reason: reason ?? null }),
16510
+ reason
16511
+ );
16512
+ return { entry, record };
16513
+ }
16514
+ async function openFor(vault, ref) {
16515
+ const { brain } = await resolveBrain(vault);
16516
+ const adapter = await LocalFileCasAdapter.openVault(brain.root, {
16517
+ createPrefix: brain.observationsDir
16518
+ });
16519
+ const built = await openIndex(adapter, brain.root);
16520
+ return { adapter, brain, built, entry: resolveRef(built.entries, ref) };
16521
+ }
16522
+ async function write(adapter, root, entry, ref, verb, input, reason) {
16523
+ const rationale = sanitizeRationale(reason);
16524
+ const result = commit(asSingleMatch(entry), input());
16525
+ if (result.action === "ignore") {
16526
+ throw memgError("invalid_input", `${verb} produced nothing`, "This is a defect \u2014 report it.");
16527
+ }
16528
+ const preview = await previewProposal(adapter, result.proposal);
16529
+ if (preview.refusal) throw preview.refusal;
16530
+ const before = await adapter.read(entry.path);
16531
+ if (changesNothing(result.proposal, preview, before.content)) {
16532
+ throw memgError(
16533
+ "nothing_to_change",
16534
+ `${entry.path} already says exactly this`,
16535
+ `Nothing was written. \`memgineering open ${ref}\` shows what it says now.`
16536
+ );
16537
+ }
16538
+ return withWriteLock(root, async () => {
16539
+ const outcome = await applyProposal(
16540
+ adapter,
16541
+ approve(result.proposal, {
16542
+ actor: "agent",
16543
+ note: `${verb}d autonomously; reversible from the operation ledger`
16544
+ })
16545
+ );
16546
+ if (!outcome.ok) {
16547
+ throw memgError(
16548
+ "undo_conflict",
16549
+ `${entry.path} changed while this was being prepared`,
16550
+ "Run the command again \u2014 it will re-read the note first."
16551
+ );
16552
+ }
16553
+ return appendOp(root, {
16554
+ verb,
16555
+ target: entry.path,
16556
+ memoryId: entry.memory.id,
16557
+ hashBefore: hashContent(before.content),
16558
+ hashAfter: hashContent(outcome.content),
16559
+ patch: { before: preview.before, after: preview.after },
16560
+ rationale,
16561
+ snapshotBefore: before.content
16562
+ });
16563
+ });
16564
+ }
16565
+ async function rescopeHosted(where, ref, scope, reason) {
16566
+ const brain = await openCloudBrain(where);
16567
+ const result = await cloudRescope(brain, {
16568
+ ref,
16569
+ scope,
16570
+ ...reason === void 0 ? {} : { reason }
16571
+ });
16572
+ printDual({
16573
+ json: { brain: where.name, ...result },
16574
+ human: () => {
16575
+ printHuman(
16576
+ result.scope === null ? `Binds everywhere in **${oneLine(where.name, WIDTH.title)}**.
16577
+ ` : `Binds in **${oneLine(result.scope, WIDTH.title)}**.
16578
+ `
16579
+ );
16580
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
16581
+ `);
16582
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
16583
+ printHuman(c.gray(`
16584
+ Undo: \`memgineering undo ${result.op_id}\``));
16585
+ }
16586
+ });
16587
+ }
16588
+ async function expireHosted(where, ref, until, reason) {
16589
+ const brain = await openCloudBrain(where);
16590
+ const result = await cloudExpire(brain, {
16591
+ ref,
16592
+ until,
16593
+ ...reason === void 0 ? {} : { reason }
16594
+ });
16595
+ printDual({
16596
+ json: { brain: where.name, ...result },
16597
+ human: () => {
16598
+ printHuman(`Holds until **${oneLine(result.valid_until, WIDTH.title)}**.
16599
+ `);
16600
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
16601
+ `);
16602
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
16603
+ printHuman(c.gray(`
16604
+ Undo: \`memgineering undo ${result.op_id}\``));
16605
+ }
16606
+ });
16607
+ }
16608
+
16609
+ // src/commands/evidence.ts
16610
+ init_src();
16611
+ import { Command as Command16 } from "commander";
16612
+ init_index_build();
16613
+ init_cloud_brain();
16614
+ init_vault();
16615
+ init_ui();
16616
+ function evidenceCommand() {
16617
+ return new Command16("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
16618
+ "--brain <name>",
16619
+ "read from one existing hosted brain by name or id, without changing the pointer"
16620
+ ).action(async (ref, opts) => {
16621
+ const target = await resolveTarget({
16622
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
16623
+ ...opts.local === void 0 ? {} : { local: opts.local },
16624
+ ...opts.vault === void 0 ? {} : { vault: opts.vault }
16625
+ });
16626
+ if (target.kind === "cloud") {
16627
+ const cloud = await openCloudBrain(target);
16628
+ const report4 = await cloudEvidence(cloud, ref);
16629
+ printDual({
16630
+ json: { ...report4, brain: { hosted: true, name: target.name } },
16631
+ human: () => renderEvidence(viewFromCloud(report4), {
16632
+ now: /* @__PURE__ */ new Date(),
16633
+ hosted: true,
16634
+ brain: target.name
16635
+ })
16636
+ });
16637
+ return;
16638
+ }
16639
+ const brain = target.brain;
16640
+ const adapter = await openBrainOrExplain(brain.root);
16641
+ const built = await openIndex(adapter, brain.root);
16642
+ const entry = resolveRef(built.entries, ref);
16643
+ const [events, ledger] = await Promise.all([readEvents(brain.root), readOps(brain.root)]);
16644
+ const now = /* @__PURE__ */ new Date();
16645
+ const report3 = assembleEvidence(entry, events, ledger.ops, {
16646
+ contextDir: process.cwd(),
16647
+ now,
16648
+ brain: built.entries
16649
+ });
16650
+ printDual({
16651
+ json: report3,
16652
+ human: () => renderEvidence(report3, { now, hosted: false })
16653
+ });
16654
+ });
16655
+ }
16656
+ function renderEvidence(report3, opts) {
16657
+ const brainHeading = opts.brain === void 0 ? "" : ` \u2014 ${oneLine(opts.brain, WIDTH.title)}`;
16658
+ printHuman(`## evidence: ${oneLine(report3.title, WIDTH.title)}${brainHeading}
16659
+ `);
16660
+ printHuman(
16661
+ `\`id: ${oneLine(report3.id, WIDTH.handle)}\` \xB7 \`${oneLine(report3.path, WIDTH.path)}\`
15558
16662
  `
15559
16663
  );
15560
16664
  if (report3.critical) {
@@ -15801,7 +16905,7 @@ init_config();
15801
16905
  import { createHash as createHash7 } from "crypto";
15802
16906
  import { mkdir as mkdir11, readdir as readdir6, stat as stat4, unlink as unlink3, writeFile as writeFile9 } from "fs/promises";
15803
16907
  import { dirname as dirname5, isAbsolute as isAbsolute3, join as join18, resolve as resolve9 } from "path";
15804
- import { Command as Command16 } from "commander";
16908
+ import { Command as Command17 } from "commander";
15805
16909
 
15806
16910
  // src/lib/rules.ts
15807
16911
  init_src();
@@ -15872,7 +16976,7 @@ init_src();
15872
16976
  init_deadline();
15873
16977
  init_cloud_brain();
15874
16978
  function guardCommand() {
15875
- return new Command16("guard").description("hook entry \u2014 show the user\u2019s rules before the first edit of a session").option("--context-dir <path>", "where the work is happening, when stdin does not say").option("--at <event>", "pre-tool-use | stop", "pre-tool-use").action(async (opts) => {
16979
+ return new Command17("guard").description("hook entry \u2014 show the user\u2019s rules before the first edit of a session").option("--context-dir <path>", "where the work is happening, when stdin does not say").option("--at <event>", "pre-tool-use | stop", "pre-tool-use").action(async (opts) => {
15876
16980
  try {
15877
16981
  const out = await withDeadline(HOOK_DEADLINE_MS, async () => {
15878
16982
  const payload = await readPayload();
@@ -16068,7 +17172,11 @@ async function hostedRuleLines(where, contextDir) {
16068
17172
  }
16069
17173
  function groupHostedLines(rules, project) {
16070
17174
  const width = ruleWidthFor(rules.length);
16071
- const line = (r) => renderRuleLine(r.text ?? r.summary ?? r.title, width);
17175
+ const line = (r) => {
17176
+ const quoted = renderRuleLine(r.text ?? r.summary ?? r.title, width);
17177
+ return r.freshness === "current" || r.freshness === void 0 ? quoted : `${quoted}
17178
+ (${r.freshness === "expired" ? "past its end date" : "superseded"} \u2014 worth checking)`;
17179
+ };
16072
17180
  const globals = rules.filter((r) => r.lane === "global");
16073
17181
  const scoped = rules.filter((r) => r.lane === "project");
16074
17182
  if (globals.length === 0 || scoped.length === 0) return rules.map(line);
@@ -16089,14 +17197,14 @@ init_link();
16089
17197
  init_src();
16090
17198
  init_walk();
16091
17199
  import { lstat as lstat4, readFile as readFile19 } from "fs/promises";
16092
- import { Command as Command18 } from "commander";
17200
+ import { Command as Command19 } from "commander";
16093
17201
  init_errors();
16094
17202
  init_cloud_brain();
16095
17203
 
16096
17204
  // src/commands/retract.ts
16097
17205
  init_errors();
16098
17206
  init_cloud_brain();
16099
- import { Command as Command17 } from "commander";
17207
+ import { Command as Command18 } from "commander";
16100
17208
  init_vault();
16101
17209
  init_ui();
16102
17210
  var CONTENT_VERBS = /* @__PURE__ */ new Set([
@@ -16167,7 +17275,7 @@ function parseInstant(flag2, raw) {
16167
17275
  return { ms, iso: new Date(ms).toISOString() };
16168
17276
  }
16169
17277
  function retractCommand() {
16170
- return new Command17("retract").description(
17278
+ return new Command18("retract").description(
16171
17279
  "retire every memory one writer or one stretch of time wrote \u2014 shows the set first; --yes applies it"
16172
17280
  ).option("--actor <actor>", 'writes recorded under this actor, e.g. "mcp:ChatGPT" or "agent"').option(
16173
17281
  "--token <id>",
@@ -16382,7 +17490,7 @@ ${failed.length} could not be retired:`);
16382
17490
  init_vault();
16383
17491
  init_ui();
16384
17492
  function logCommand() {
16385
- return new Command18("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
17493
+ return new Command19("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
16386
17494
  "--brain <name>",
16387
17495
  "read one existing hosted brain by name or id, without changing the pointer"
16388
17496
  ).option("--actor <actor>", 'only operations recorded under this actor, e.g. "mcp:ChatGPT" or "agent"').option("--token <id>", "only operations made with this token (id or 5+ char prefix) \u2014 hosted brains").option("--since <when>", "only operations at or after this date / ISO instant").option("--until <when>", "only operations before this date / ISO instant").action(async (opts) => {
@@ -16563,9 +17671,19 @@ var REVERSIBLE_LOCAL = /* @__PURE__ */ new Set([
16563
17671
  "retire",
16564
17672
  "unretire",
16565
17673
  "exclude",
16566
- "unexclude"
17674
+ "unexclude",
17675
+ "rescope",
17676
+ "expire"
17677
+ ]);
17678
+ var REVERSIBLE_HOSTED = /* @__PURE__ */ new Set([
17679
+ "remember",
17680
+ "revise",
17681
+ "retire",
17682
+ "unretire",
17683
+ "write",
17684
+ "rescope",
17685
+ "expire"
16567
17686
  ]);
16568
- var REVERSIBLE_HOSTED = /* @__PURE__ */ new Set(["remember", "revise", "retire", "unretire", "write"]);
16569
17687
  function undoable(op, hosted) {
16570
17688
  if (op.undone || op.diverged || op.superseded) return false;
16571
17689
  return (hosted ? REVERSIBLE_HOSTED : REVERSIBLE_LOCAL).has(op.verb);
@@ -16650,10 +17768,10 @@ async function readOrNull(root, path) {
16650
17768
  // src/commands/project.ts
16651
17769
  init_errors();
16652
17770
  import { resolve as resolve10 } from "path";
16653
- import { Command as Command19 } from "commander";
17771
+ import { Command as Command20 } from "commander";
16654
17772
  init_ui();
16655
17773
  function projectCommand() {
16656
- return new Command19("project").description("show or set what the work in this folder is called").argument("[name]", "what to call it \u2014 omit to show what this folder answers to").option("--here <dir>", "act on this directory instead of the current one").option("--unset", "forget the name set here and go back to what the repository says").addHelpText(
17774
+ return new Command20("project").description("show or set what the work in this folder is called").argument("[name]", "what to call it \u2014 omit to show what this folder answers to").option("--here <dir>", "act on this directory instead of the current one").option("--unset", "forget the name set here and go back to what the repository says").addHelpText(
16657
17775
  "after",
16658
17776
  [
16659
17777
  "",
@@ -16780,18 +17898,18 @@ init_api_client();
16780
17898
  init_credentials();
16781
17899
  init_pending_login();
16782
17900
  init_ui();
16783
- import { Command as Command21 } from "commander";
17901
+ import { Command as Command22 } from "commander";
16784
17902
  function logoutCommand() {
16785
- return new Command21("logout").description("sign out of your account \u2014 the pointer and your notes stay").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
17903
+ return new Command22("logout").description("sign out of your account \u2014 the pointer and your notes stay").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
16786
17904
  "--local",
16787
17905
  "forget the token here without telling the server \u2014 only for a server you cannot reach"
16788
17906
  ).action(async (opts) => {
16789
- const base = apiUrl();
17907
+ const base2 = apiUrl();
16790
17908
  const credentials = await readCredentials();
16791
17909
  await clearPendingLogin();
16792
17910
  if (!credentials) {
16793
17911
  printDual({
16794
- json: { signed_out: true, was_signed_in: false, api_url: base },
17912
+ json: { signed_out: true, was_signed_in: false, api_url: base2 },
16795
17913
  human: () => printHuman("Not signed in \u2014 nothing to do.")
16796
17914
  });
16797
17915
  return;
@@ -16901,12 +18019,12 @@ async function revokeRegistrySession(reg) {
16901
18019
 
16902
18020
  // src/commands/onboard.ts
16903
18021
  init_src();
16904
- import { Command as Command22 } from "commander";
18022
+ import { Command as Command23 } from "commander";
16905
18023
  init_cloud_brain();
16906
18024
  init_vault();
16907
18025
  init_ui();
16908
18026
  function onboardCommand() {
16909
- return new Command22("onboard").description("what to ask so the brain knows who it belongs to").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
18027
+ return new Command23("onboard").description("what to ask so the brain knows who it belongs to").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
16910
18028
  "--brain <name>",
16911
18029
  "read one existing hosted brain by name or id, without changing the pointer"
16912
18030
  ).action(async (opts) => {
@@ -17052,9 +18170,9 @@ init_cloud_brain();
17052
18170
  init_ui();
17053
18171
  import { mkdir as mkdir12, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
17054
18172
  import { dirname as dirname6, isAbsolute as isAbsolute4, join as join19, resolve as resolve11, sep as sep2 } from "path";
17055
- import { Command as Command23 } from "commander";
18173
+ import { Command as Command24 } from "commander";
17056
18174
  function pullCommand() {
17057
- return new Command23("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
18175
+ return new Command24("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
17058
18176
  const { brain: source, others } = await resolveSource(opts.brain);
17059
18177
  const target = resolve11(destination ?? `./${oneLine(source.name, WIDTH.title)}`);
17060
18178
  const first = await cloudListNotes(source, { limit: 1 });
@@ -17181,9 +18299,9 @@ async function collectPaths(source) {
17181
18299
  return all;
17182
18300
  }
17183
18301
  async function resolveSource(named) {
17184
- const base = apiUrl();
17185
- const credentials = await requireCredentials(base);
17186
- const brains = await listCloudBrains(credentials.token, base);
18302
+ const base2 = apiUrl();
18303
+ const credentials = await requireCredentials(base2);
18304
+ const brains = await listCloudBrains(credentials.token, base2);
17187
18305
  if (brains.length === 0) {
17188
18306
  throw memgError(
17189
18307
  "invalid_input",
@@ -17192,7 +18310,7 @@ async function resolveSource(named) {
17192
18310
  );
17193
18311
  }
17194
18312
  const pointedAt = named ? null : (await loadConfig()).brain.cloud;
17195
- const usable = pointedAt && pointedAt.api_url === base ? pointedAt.id : null;
18313
+ const usable = pointedAt && pointedAt.api_url === base2 ? pointedAt.id : null;
17196
18314
  const found = named ? brains.find((b) => b.name === named || b.id === named) : usable ? brains.find((b) => b.id === usable) ?? (brains.length === 1 ? brains[0] : void 0) : brains.length === 1 ? brains[0] : void 0;
17197
18315
  if (!found) {
17198
18316
  throw memgError(
@@ -17202,7 +18320,7 @@ async function resolveSource(named) {
17202
18320
  );
17203
18321
  }
17204
18322
  return {
17205
- brain: { brainId: found.id, name: found.name, token: credentials.token, apiUrl: base },
18323
+ brain: { brainId: found.id, name: found.name, token: credentials.token, apiUrl: base2 },
17206
18324
  // The ones this run is NOT downloading.
17207
18325
  //
17208
18326
  // A tester pulled a brain, was told 21 notes had arrived, and reasonably
@@ -17254,7 +18372,7 @@ async function exists(file) {
17254
18372
  }
17255
18373
 
17256
18374
  // src/commands/publish.ts
17257
- import { Command as Command25 } from "commander";
18375
+ import { Command as Command26 } from "commander";
17258
18376
  import { z as z32 } from "zod";
17259
18377
  init_src();
17260
18378
  init_errors();
@@ -17398,6 +18516,7 @@ init_index_build();
17398
18516
  // src/lib/registry-session.ts
17399
18517
  init_errors();
17400
18518
  init_credentials();
18519
+ init_version();
17401
18520
  function registryApiUrl(stored) {
17402
18521
  const override = process.env["KORDIS_API_URL"];
17403
18522
  return override !== void 0 && override !== "" ? override : stored;
@@ -18150,7 +19269,7 @@ function isSameBrainNote(publishedOrigin, memoryId) {
18150
19269
  }
18151
19270
 
18152
19271
  // src/commands/registry/read.ts
18153
- import { Command as Command24 } from "commander";
19272
+ import { Command as Command25 } from "commander";
18154
19273
  init_errors();
18155
19274
  init_credentials();
18156
19275
 
@@ -18171,8 +19290,9 @@ function untrustedFenceNotice(sentinel) {
18171
19290
 
18172
19291
  // src/commands/registry/read.ts
18173
19292
  init_ui();
19293
+ init_version();
18174
19294
  function readCommand() {
18175
- return new Command24("read").description("read a judgment from the public layer into your context (nothing is installed)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was read").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
19295
+ return new Command25("read").description("read a judgment from the public layer into your context (nothing is installed)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was read").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
18176
19296
  "after",
18177
19297
  "\nEnvironment:\n KORDIS_API_URL=<url> read from this registry instead of the linked one\n"
18178
19298
  ).action(async (rawSlug, opts) => {
@@ -18320,7 +19440,7 @@ async function recordUsage(verb, detail, where = {}) {
18320
19440
  init_vault();
18321
19441
  init_ui();
18322
19442
  function publishCommand() {
18323
- return new Command25("publish").description("publish one decision from this brain to the public layer").argument("<ref>", "handle, id, path, or exact title of the note to publish").option("--slug <name>", "the package name (default: derived from the note title)").option("--yes", "actually send it (without this, nothing leaves this machine)").option("--reason <text>", "why this is being published \u2014 the user's own words").option("--no-lineage", "do not propose lineage from what this machine has adapted").option(
19443
+ return new Command26("publish").description("publish one decision from this brain to the public layer").argument("<ref>", "handle, id, path, or exact title of the note to publish").option("--slug <name>", "the package name (default: derived from the note title)").option("--yes", "actually send it (without this, nothing leaves this machine)").option("--reason <text>", "why this is being published \u2014 the user's own words").option("--no-lineage", "do not propose lineage from what this machine has adapted").option(
18324
19444
  "--bump <kind>",
18325
19445
  "patch | minor | major \u2014 publish the next version of a package this note already produced"
18326
19446
  ).option("--wait <ms>", "how long to wait for the AI review before answering (default 3000)").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").addHelpText(
@@ -18393,8 +19513,8 @@ function publishCommand() {
18393
19513
  });
18394
19514
  return;
18395
19515
  }
18396
- const session = await openRegistrySession();
18397
19516
  const waitForReviewMs = parseWait(opts.wait);
19517
+ const session = await openRegistrySession();
18398
19518
  const response = await publishOrExplain(
18399
19519
  session.client,
18400
19520
  ref,
@@ -18646,7 +19766,7 @@ function parseWait(raw) {
18646
19766
  }
18647
19767
 
18648
19768
  // src/commands/registry/adapt.ts
18649
- import { Command as Command26 } from "commander";
19769
+ import { Command as Command27 } from "commander";
18650
19770
  init_errors();
18651
19771
 
18652
19772
  // src/lib/harness-context.ts
@@ -18655,6 +19775,7 @@ import { access, readFile as readFile21 } from "fs/promises";
18655
19775
  import { join as join20 } from "path";
18656
19776
  var PER_FILE_CAP = 16 * 1024;
18657
19777
  var TOTAL_CAP = 48 * 1024;
19778
+ var TRUNCATION_RESERVE = 64;
18658
19779
  async function pathExists(p) {
18659
19780
  try {
18660
19781
  await access(p);
@@ -18665,6 +19786,7 @@ async function pathExists(p) {
18665
19786
  }
18666
19787
  function capContent(raw, cap) {
18667
19788
  if (Buffer.byteLength(raw, "utf8") <= cap) return { content: raw, truncated: false };
19789
+ const fill = cap - TRUNCATION_RESERVE;
18668
19790
  const lines2 = raw.split("\n");
18669
19791
  let acc = 0;
18670
19792
  let lastHeadingIdx = -1;
@@ -18673,7 +19795,7 @@ function capContent(raw, cap) {
18673
19795
  const line = lines2[i] ?? "";
18674
19796
  const lineBytes = Buffer.byteLength(`${line}
18675
19797
  `, "utf8");
18676
- if (acc + lineBytes > cap) {
19798
+ if (acc + lineBytes > fill) {
18677
19799
  cutIdx = i;
18678
19800
  break;
18679
19801
  }
@@ -18681,6 +19803,7 @@ function capContent(raw, cap) {
18681
19803
  if (/^#{1,6}\s/.test(line)) lastHeadingIdx = i;
18682
19804
  }
18683
19805
  const cut2 = lastHeadingIdx > cutIdx / 2 ? lastHeadingIdx : cutIdx;
19806
+ if (cut2 === 0) return null;
18684
19807
  const kept = lines2.slice(0, cut2).join("\n");
18685
19808
  return {
18686
19809
  content: `${kept}
@@ -18705,7 +19828,6 @@ async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
18705
19828
  const files2 = [];
18706
19829
  let totalBytes = 0;
18707
19830
  for (const cand of candidates) {
18708
- if (totalBytes >= TOTAL_CAP) break;
18709
19831
  if (!await pathExists(cand.path)) continue;
18710
19832
  let raw;
18711
19833
  try {
@@ -18714,7 +19836,9 @@ async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
18714
19836
  continue;
18715
19837
  }
18716
19838
  const cap = Math.min(PER_FILE_CAP, TOTAL_CAP - totalBytes);
18717
- const { content, truncated } = capContent(raw, cap);
19839
+ const capped2 = capContent(raw, cap);
19840
+ if (capped2 === null) break;
19841
+ const { content, truncated } = capped2;
18718
19842
  totalBytes += Buffer.byteLength(content, "utf8");
18719
19843
  files2.push({
18720
19844
  path: cand.display,
@@ -18778,7 +19902,7 @@ function renderBrief(res, harness) {
18778
19902
  return out.join("\n");
18779
19903
  }
18780
19904
  function adaptCommand() {
18781
- return new Command26("adapt").description("fit a public judgment to your existing harness (prints a brief; writes nothing)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was adapted").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
19905
+ return new Command27("adapt").description("fit a public judgment to your existing harness (prints a brief; writes nothing)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was adapted").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
18782
19906
  "after",
18783
19907
  "\nEnvironment:\n KORDIS_API_URL=<url> read from this registry instead of the linked one\n"
18784
19908
  ).action(async (rawSlug, opts) => {
@@ -18826,10 +19950,10 @@ function adaptCommand() {
18826
19950
  }
18827
19951
 
18828
19952
  // src/commands/registry/info.ts
18829
- import { Command as Command27 } from "commander";
19953
+ import { Command as Command28 } from "commander";
18830
19954
  init_ui();
18831
19955
  function infoCommand() {
18832
- return new Command27("info").description("what a package in the public layer is, without reading it").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").addHelpText(
19956
+ return new Command28("info").description("what a package in the public layer is, without reading it").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").addHelpText(
18833
19957
  "after",
18834
19958
  "\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
18835
19959
  ).action(async (rawSlug) => {
@@ -18915,7 +20039,7 @@ function renderCanonical(component) {
18915
20039
  }
18916
20040
 
18917
20041
  // src/commands/registry/search.ts
18918
- import { Command as Command28 } from "commander";
20042
+ import { Command as Command29 } from "commander";
18919
20043
  init_errors();
18920
20044
  init_ui();
18921
20045
  var SEARCHABLE_TYPES = PHASE_1_TYPES;
@@ -18923,7 +20047,7 @@ var LIMIT_MIN = 1;
18923
20047
  var LIMIT_MAX = 100;
18924
20048
  var LIMIT_DEFAULT = 20;
18925
20049
  function searchCommand() {
18926
- return new Command28("search").description("find a judgment in the public layer (titles only; nothing is read or installed)").argument("[query...]", "words to match against title and description").option("--type <type>", `narrow by kind: ${SEARCHABLE_TYPES.join("|")}`).option("--limit <n>", `how many results (${LIMIT_MIN}-${LIMIT_MAX})`, String(LIMIT_DEFAULT)).addHelpText(
20050
+ return new Command29("search").description("find a judgment in the public layer (titles only; nothing is read or installed)").argument("[query...]", "words to match against title and description").option("--type <type>", `narrow by kind: ${SEARCHABLE_TYPES.join("|")}`).option("--limit <n>", `how many results (${LIMIT_MIN}-${LIMIT_MAX})`, String(LIMIT_DEFAULT)).addHelpText(
18927
20051
  "after",
18928
20052
  "\nEnvironment:\n KORDIS_API_URL=<url> search this registry instead of the linked one\n"
18929
20053
  ).action(async (query, opts) => {
@@ -19101,7 +20225,7 @@ function parseLimit2(raw) {
19101
20225
  }
19102
20226
 
19103
20227
  // src/commands/registry/pull.ts
19104
- import { Command as Command29 } from "commander";
20228
+ import { Command as Command30 } from "commander";
19105
20229
  import prompts3 from "prompts";
19106
20230
  import { existsSync, lstatSync, realpathSync } from "fs";
19107
20231
  import { mkdir as mkdir14, readFile as readFile23, stat as stat5, writeFile as writeFile12 } from "fs/promises";
@@ -20040,7 +21164,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
20040
21164
  };
20041
21165
  }
20042
21166
  function registryPullCommand() {
20043
- return new Command29("pull").description("install a package from the public layer into this project").argument(
21167
+ return new Command30("pull").description("install a package from the public layer into this project").argument(
20044
21168
  "<full_slug>",
20045
21169
  "package identifier {username}/{slug}, optionally @{version} or @{range}"
20046
21170
  ).option("--tool <name>", "target tool format (default: work it out from this directory)").option("--target <path>", "install into this directory instead of the current one").option("--with-recommends", "also install recommended (depth-0) dependencies", false).option(
@@ -20269,10 +21393,10 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
20269
21393
  // src/commands/registry/update.ts
20270
21394
  init_errors();
20271
21395
  init_ui();
20272
- import { Command as Command30 } from "commander";
21396
+ import { Command as Command31 } from "commander";
20273
21397
  import semver2 from "semver";
20274
21398
  function registryUpdateCommand() {
20275
- return new Command30("update").description("update installed packages to their latest in-range versions").argument("[full_slug]", "package identifier {username}/{slug} (omit to update all)").option("--dry-run", "show what would be updated without doing it", false).addHelpText(
21399
+ return new Command31("update").description("update installed packages to their latest in-range versions").argument("[full_slug]", "package identifier {username}/{slug} (omit to update all)").option("--dry-run", "show what would be updated without doing it", false).addHelpText(
20276
21400
  "after",
20277
21401
  "\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
20278
21402
  ).action(async (fullSlugArg, opts) => {
@@ -20406,10 +21530,10 @@ function registryUpdateCommand() {
20406
21530
 
20407
21531
  // src/commands/registry/outdated.ts
20408
21532
  init_ui();
20409
- import { Command as Command31 } from "commander";
21533
+ import { Command as Command32 } from "commander";
20410
21534
  import semver3 from "semver";
20411
21535
  function outdatedCommand() {
20412
- return new Command31("outdated").description("check installed packages for available updates").addHelpText(
21536
+ return new Command32("outdated").description("check installed packages for available updates").addHelpText(
20413
21537
  "after",
20414
21538
  "\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
20415
21539
  ).action(async () => {
@@ -20501,9 +21625,9 @@ function outdatedCommand() {
20501
21625
  // src/commands/registry/versions.ts
20502
21626
  init_errors();
20503
21627
  init_ui();
20504
- import { Command as Command32 } from "commander";
21628
+ import { Command as Command33 } from "commander";
20505
21629
  function versionsCommand() {
20506
- return new Command32("versions").description("list every published version of a package").argument("<full_slug>", "package identifier {username}/{slug}").addHelpText(
21630
+ return new Command33("versions").description("list every published version of a package").argument("<full_slug>", "package identifier {username}/{slug}").addHelpText(
20507
21631
  "after",
20508
21632
  "\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
20509
21633
  ).action(async (fullSlug) => {
@@ -20567,9 +21691,9 @@ function relativeTime(isoString) {
20567
21691
  // src/commands/registry/diff.ts
20568
21692
  init_errors();
20569
21693
  init_ui();
20570
- import { Command as Command33 } from "commander";
21694
+ import { Command as Command34 } from "commander";
20571
21695
  function diffCommand() {
20572
- return new Command33("diff").description("show what changed between two versions of a package").argument(
21696
+ return new Command34("diff").description("show what changed between two versions of a package").argument(
20573
21697
  "<full_slug>",
20574
21698
  "package identifier {username}/{slug}, or {username}/{slug}@{from}..{to}"
20575
21699
  ).argument("[from]", "older version (omit to use the second-latest)").argument("[to]", "newer version (omit to use the latest)").addHelpText(
@@ -20646,7 +21770,7 @@ function diffCommand() {
20646
21770
  }
20647
21771
 
20648
21772
  // src/commands/registry/list.ts
20649
- import { Command as Command34 } from "commander";
21773
+ import { Command as Command35 } from "commander";
20650
21774
  init_errors();
20651
21775
  init_ui();
20652
21776
  var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
@@ -20669,7 +21793,7 @@ function parseUsername(raw) {
20669
21793
  return parsed.data;
20670
21794
  }
20671
21795
  function listCommand() {
20672
- return new Command34("list").description("list packages \u2014 your own with no argument, or a public user\u2019s").argument("[username]", "whose packages to list (omit for your own)").option("--limit <n>", "maximum results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "page number for your own packages (1-based)", (v) => parseInt(v, 10), 1).option("--cursor <token>", "continue a public listing from a previous result").option(
21796
+ return new Command35("list").description("list packages \u2014 your own with no argument, or a public user\u2019s").argument("[username]", "whose packages to list (omit for your own)").option("--limit <n>", "maximum results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "page number for your own packages (1-based)", (v) => parseInt(v, 10), 1).option("--cursor <token>", "continue a public listing from a previous result").option(
20673
21797
  "--sort <mode>",
20674
21798
  "latest|popular for your own; stars|downloads|recent|updated for a public listing",
20675
21799
  "latest"
@@ -20767,7 +21891,7 @@ init_push();
20767
21891
  // src/commands/recall.ts
20768
21892
  init_src();
20769
21893
  init_config();
20770
- import { Command as Command36 } from "commander";
21894
+ import { Command as Command37 } from "commander";
20771
21895
  import { basename as basename2, resolve as resolve13 } from "path";
20772
21896
  init_index_build();
20773
21897
  init_cloud_brain();
@@ -20775,7 +21899,7 @@ init_errors();
20775
21899
  init_vault();
20776
21900
  init_ui();
20777
21901
  function recallCommand() {
20778
- return new Command36("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option(
21902
+ return new Command37("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option(
20779
21903
  "--brain <name>",
20780
21904
  "search one existing hosted brain by name or id, without changing the pointer"
20781
21905
  ).option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
@@ -20797,6 +21921,8 @@ function recallCommand() {
20797
21921
  let unsearchedLocal = [];
20798
21922
  let searchedLocalName = null;
20799
21923
  let isScaffolding;
21924
+ let templatesWithheld = 0;
21925
+ let spread = null;
20800
21926
  let reasons;
20801
21927
  let pathOf;
20802
21928
  let entries;
@@ -20825,13 +21951,25 @@ function recallCommand() {
20825
21951
  // older clients: the route applies a ceiling only when one arrives,
20826
21952
  // so a CLI that predates this field gets the whole ranking rather
20827
21953
  // than a trimmed packet it cannot describe.
20828
- budget
21954
+ budget,
21955
+ // Two, like a folder brain. The server holds the content the check
21956
+ // needs; a server older than the field ignores it and caps nothing.
21957
+ templateSlots: TEMPLATE_SLOTS
20829
21958
  });
20830
- rows = answered.memory_cards.map((card) => ({ card, handle: card.handle, stage: {} }));
21959
+ const unfilledIds = new Set(
21960
+ answered.memory_cards.filter((card) => card.unfilled === true).map((card) => card.id)
21961
+ );
21962
+ rows = answered.memory_cards.map(({ unfilled: _unfilled, ...card }) => ({
21963
+ card,
21964
+ handle: card.handle,
21965
+ stage: {}
21966
+ }));
21967
+ spread = isScopeSpread(answered.scope_spread) ? answered.scope_spread : null;
21968
+ templatesWithheld = Number.isInteger(answered.templates_withheld) && (answered.templates_withheld ?? 0) > 0 ? answered.templates_withheld : 0;
20831
21969
  if (rows.length === 0) {
20832
21970
  unsearchedBrains = await listCloudBrains(cloud.token, cloud.apiUrl, { retries: 0 }).then((all) => all.filter((b) => b.id !== cloud.brainId)).catch(() => []);
20833
21971
  }
20834
- isScaffolding = () => false;
21972
+ isScaffolding = (card) => unfilledIds.has(card.id);
20835
21973
  reasons = /* @__PURE__ */ new Map();
20836
21974
  pathOf = () => null;
20837
21975
  entries = null;
@@ -20861,8 +21999,28 @@ function recallCommand() {
20861
21999
  ...depth === void 0 ? {} : { depth },
20862
22000
  tokenBudget: budget
20863
22001
  });
20864
- result = local;
22002
+ const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
22003
+ const scaffolding = await scaffoldingAmong(
22004
+ adapter,
22005
+ local.memory_cards.map((card) => {
22006
+ const entry = byId.get(card.id);
22007
+ return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
22008
+ })
22009
+ );
22010
+ isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
22011
+ const capped2 = capTemplates(local.memory_cards, isScaffolding);
22012
+ templatesWithheld = capped2.withheld.length;
22013
+ result = { ...local, memory_cards: capped2.kept };
20865
22014
  entries = built.entries;
22015
+ if (!opts.scope) {
22016
+ spread = scopeSpread(
22017
+ result.memory_cards.map((card) => {
22018
+ const memory = byId.get(card.id)?.memory;
22019
+ return memory === void 0 ? null : declaredScope(memory);
22020
+ }),
22021
+ built.entries.map((e) => declaredScope(e.memory))
22022
+ );
22023
+ }
20866
22024
  if (result.memory_cards.length === 0) {
20867
22025
  searchedLocalName = basename2(resolve13(brain.root));
20868
22026
  unsearchedLocal = await loadConfig().then(
@@ -20882,7 +22040,6 @@ function recallCommand() {
20882
22040
  returnedIds: result.memory_cards.map((card) => card.id)
20883
22041
  });
20884
22042
  const allIds = built.entries.map((e) => e.memory.id);
20885
- const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
20886
22043
  pathOf = (card) => byId.get(card.id)?.path ?? null;
20887
22044
  rows = await Promise.all(
20888
22045
  result.memory_cards.map(async (card) => ({
@@ -20891,14 +22048,6 @@ function recallCommand() {
20891
22048
  stage: await stageFor(adapter, byId.get(card.id), detail)
20892
22049
  }))
20893
22050
  );
20894
- const scaffolding = await scaffoldingAmong(
20895
- adapter,
20896
- rows.map(({ card }) => {
20897
- const entry = byId.get(card.id);
20898
- return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
20899
- })
20900
- );
20901
- isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
20902
22051
  reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
20903
22052
  }
20904
22053
  printDual({
@@ -20935,7 +22084,11 @@ function recallCommand() {
20935
22084
  // Same order as `between`, so a parser can zip the three arrays.
20936
22085
  reason: conflict.between.map((id) => reasons.get(id) ?? null)
20937
22086
  })),
20938
- ...result.truncated === void 0 ? {} : { truncated: result.truncated }
22087
+ ...result.truncated === void 0 ? {} : { truncated: result.truncated },
22088
+ // Counted, never silent: these matched and were left off because
22089
+ // they are still exactly what `init` wrote.
22090
+ ...templatesWithheld > 0 ? { templates_withheld: templatesWithheld } : {},
22091
+ ...spread === null ? {} : { scope_spread: spread }
20939
22092
  },
20940
22093
  human: () => {
20941
22094
  const brainHeading = answeredBy === null ? "" : ` \u2014 ${oneLine(answeredBy, WIDTH.title)}`;
@@ -21027,20 +22180,48 @@ ${unsearchedLocal.map(
21027
22180
  )
21028
22181
  );
21029
22182
  }
21030
- if (otherScript) sayWhy();
21031
- const glossOn = rows.findIndex(
21032
- ({ card }) => card.confidence === "stated" && !isScaffolding(card)
21033
- );
21034
- for (const [i, { card, handle, stage }] of rows.entries()) {
21035
- const mark = reachedSomethingWritten && isScaffolding(card);
22183
+ const templateCount = rows.filter(({ card }) => isScaffolding(card)).length;
22184
+ if (reachedSomethingWritten && templateCount > 0) {
21036
22185
  printHuman(
21037
- renderCard(
21038
- i + 1,
21039
- card,
21040
- handle,
21041
- detail,
21042
- stage,
21043
- i === glossOn,
22186
+ c.gray(
22187
+ ` ${templateCount} of ${rows.length} cards ${templateCount === 1 ? "is" : "are"} still the untouched starter template \u2014 marked "not filled in yet" below.
22188
+ `
22189
+ )
22190
+ );
22191
+ }
22192
+ if (spread !== null) {
22193
+ const others = spread.elsewhere.map((s) => `\`${oneLine(s, WIDTH.title)}\``).join(", ");
22194
+ const more = spread.elsewhere_total - spread.elsewhere.length;
22195
+ printHuman(
22196
+ c.gray(
22197
+ (spread.cards === spread.of ? ` ${spread.cards === 1 ? "The one card is" : `All ${spread.cards} cards are`} from \`${oneLine(spread.scope, WIDTH.title)}\`.` : ` Every card with a project is from \`${oneLine(spread.scope, WIDTH.title)}\` (${spread.cards} of ${spread.of}).`) + `
22198
+ Also in this brain: ${others}${more > 0 ? ` and ${more} more` : ""} \u2014 add \`--scope <name>\` to ask one of them.
22199
+ `
22200
+ )
22201
+ );
22202
+ }
22203
+ if (templatesWithheld > 0) {
22204
+ printHuman(
22205
+ c.gray(
22206
+ ` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer.
22207
+ `
22208
+ )
22209
+ );
22210
+ }
22211
+ if (otherScript) sayWhy();
22212
+ const glossOn = rows.findIndex(
22213
+ ({ card }) => card.confidence === "stated" && !isScaffolding(card)
22214
+ );
22215
+ for (const [i, { card, handle, stage }] of rows.entries()) {
22216
+ const mark = reachedSomethingWritten && isScaffolding(card);
22217
+ printHuman(
22218
+ renderCard(
22219
+ i + 1,
22220
+ card,
22221
+ handle,
22222
+ detail,
22223
+ stage,
22224
+ i === glossOn,
21044
22225
  mark,
21045
22226
  // Only when `--brain` aimed this elsewhere. A recall of the
21046
22227
  // brain this machine already points at mints handles `open`
@@ -21167,6 +22348,7 @@ function renderCard(n, card, handle, detail, stage, glossHere = false, scaffoldi
21167
22348
  ].filter(Boolean);
21168
22349
  out.push(oneLine(marks.join(c.gray(" \xB7 ")), WIDTH.text));
21169
22350
  }
22351
+ if (card.matched === "meaning") out.push(c.gray(MEANING_ONLY_NOTE));
21170
22352
  if (glossHere && !scaffolding) {
21171
22353
  out.push(c.gray("stated = these fields came from the note, not from inference"));
21172
22354
  }
@@ -21249,17 +22431,21 @@ function dominantScriptOf(text) {
21249
22431
  function scriptsIn(text) {
21250
22432
  return new Set(scriptCounts(text).keys());
21251
22433
  }
22434
+ function isScopeSpread(value) {
22435
+ const v = value;
22436
+ return typeof v === "object" && v !== null && typeof v.scope === "string" && Number.isInteger(v.cards) && Number.isInteger(v.of) && Number.isInteger(v.elsewhere_total) && Array.isArray(v.elsewhere) && v.elsewhere.every((s) => typeof s === "string");
22437
+ }
21252
22438
 
21253
22439
  // src/commands/resurface.ts
21254
22440
  init_deadline();
21255
22441
  init_src();
21256
- import { Command as Command37 } from "commander";
22442
+ import { Command as Command38 } from "commander";
21257
22443
  init_index_build();
21258
22444
  init_cloud_brain();
21259
22445
  init_vault();
21260
22446
  init_ui();
21261
22447
  function resurfaceCommand() {
21262
- return new Command37("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
22448
+ return new Command38("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
21263
22449
  "--brain <name>",
21264
22450
  "read one existing hosted brain by name or id, without changing the pointer"
21265
22451
  ).option(
@@ -21301,7 +22487,6 @@ function resurfaceCommand() {
21301
22487
  // so it is the only side that can answer this.
21302
22488
  here: (event) => isInside(event.context_dir, contextDir)
21303
22489
  });
21304
- const TEMPLATE_SLOTS = 2;
21305
22490
  const untouchedPaths = await untouchedBase(
21306
22491
  adapter,
21307
22492
  ranked.map((r) => r.entry.path)
@@ -21373,9 +22558,7 @@ function resurfaceCommand() {
21373
22558
  )
21374
22559
  );
21375
22560
  }
21376
- const noEvidenceYet = !rankedShown.some(
21377
- ({ why }) => why.some((w) => w.startsWith("recalled"))
21378
- );
22561
+ const noEvidenceYet = !rankedShown.some(({ used }) => used);
21379
22562
  for (const [i, { entry, why }] of rankedShown.entries()) {
21380
22563
  const blank = untouched.has(entry.path);
21381
22564
  printHuman(`### ${i + 1}. ${oneLine(entry.memory.title, WIDTH.title)}`);
@@ -21484,11 +22667,11 @@ rank by project yet, so that is not one of the signals above.`
21484
22667
  }
21485
22668
 
21486
22669
  // src/commands/rules.ts
21487
- import { Command as Command38 } from "commander";
22670
+ import { Command as Command39 } from "commander";
21488
22671
  init_cloud_brain();
21489
22672
  init_ui();
21490
22673
  function rulesCommand() {
21491
- return new Command38("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
22674
+ return new Command39("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
21492
22675
  "--brain <name>",
21493
22676
  "read one existing hosted brain by name or id, without changing the pointer"
21494
22677
  ).action(async (opts) => {
@@ -21648,89 +22831,160 @@ If the rest are decisions that no longer decide anything: \`memgineering retire
21648
22831
  init_setup();
21649
22832
 
21650
22833
  // src/commands/update.ts
21651
- import { Command as Command40 } from "commander";
22834
+ init_brand();
22835
+ init_version();
21652
22836
  init_config();
22837
+ import { spawn as spawn3 } from "child_process";
22838
+ import { randomBytes as randomBytes2 } from "crypto";
22839
+ import { mkdir as mkdir18, open, readFile as readFile27, stat as stat7, unlink as unlink4 } from "fs/promises";
22840
+ import { join as join28 } from "path";
22841
+ import { Command as Command41 } from "commander";
21653
22842
 
21654
- // src/lib/provenance.ts
21655
- import { execFile } from "child_process";
21656
- import { realpath as realpath3 } from "fs/promises";
21657
- import { dirname as dirname9, join as join25, resolve as resolve14 } from "path";
21658
- import { fileURLToPath as fileURLToPath3 } from "url";
21659
- import { promisify } from "util";
21660
- var run = promisify(execFile);
21661
- var MANAGERS = [
21662
- {
21663
- kind: "npm",
21664
- bin: "npm",
21665
- prefixArgs: ["prefix", "-g"],
21666
- install: "npm i -g memgineering@latest"
21667
- },
21668
- {
21669
- kind: "pnpm",
21670
- bin: "pnpm",
21671
- prefixArgs: ["root", "-g"],
21672
- install: "pnpm add -g memgineering@latest"
21673
- },
21674
- { kind: "bun", bin: "bun", prefixArgs: ["pm", "bin", "-g"], install: "bun add -g memgineering" }
21675
- ];
21676
- async function detectProvenance(selfPath) {
21677
- const entry = selfPath ?? fileURLToPath3(import.meta.url);
21678
- const selfDir = await realpath3(dirname9(entry)).catch(() => dirname9(entry));
21679
- if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
21680
- return {
21681
- kind: "npx",
21682
- selfDir,
21683
- prefix: null,
21684
- command: "npm i -g memgineering@latest",
21685
- reason: "this is a one-off npx run, not an installed copy"
21686
- };
22843
+ // src/lib/next-step.ts
22844
+ init_brand();
22845
+ init_api_client();
22846
+ init_credentials();
22847
+ init_config();
22848
+ init_version();
22849
+ import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
22850
+ import { join as join26 } from "path";
22851
+ function isSetUp(cfg) {
22852
+ return Object.keys(cfg.system.installed).length > 0;
22853
+ }
22854
+ var FINISH_SETUP = `## memgineering is installed, but not set up yet.
22855
+
22856
+ Nothing is connected: there is no brain to read, and the agent tools on this
22857
+ machine have no memgineering guidance yet. Setup is one command:
22858
+
22859
+ \`\`\`bash
22860
+ memgineering setup --web
22861
+ \`\`\`
22862
+
22863
+ It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
22864
+ a setup screen open in their browser at memgineering.com, where they choose
22865
+ everything: signing in, where the memory lives, which notes. Nothing is
22866
+ installed until they finish there, and this machine picks it up on its own.
22867
+
22868
+ If they have not already asked you to set it up, ask them first \u2014 it opens a
22869
+ browser window on their computer. Once you run it, tell them in their language
22870
+ that the setup screen is open. It prints a short code first: show it to them,
22871
+ because the screen asks them to compare it.
22872
+
22873
+ No browser on this machine (a server, a container, a sandbox)? Run
22874
+ \`memgineering setup --web --print-url\` and give them the link to open on the
22875
+ computer they are actually sitting at.
22876
+
22877
+ \`setup --agent\` answers every one of those questions on their behalf, and
22878
+ \`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
22879
+ them to another machine. Use it only when they have said they would rather you
22880
+ decided, or when this machine cannot reach a browser and \`--print-url\` is no
22881
+ use to them either.
22882
+
22883
+ Setup's output then says how to connect their brain \u2014 create one, pick one, or
22884
+ move existing notes up.
22885
+
22886
+ The \`memgineering-setup\` skill covers what comes after.`;
22887
+ var SETUP_NUDGE = "memgineering is installed, but not set up yet. `memgineering setup --web` opens a setup screen in their browser and does NOT wait for them \u2014 ask them first unless they asked you to set it up.\nThe full notes: run `memgineering` on its own.";
22888
+ async function claimFullSetupNotice() {
22889
+ const marker = join26(brandHome(), "setup-notice-shown");
22890
+ try {
22891
+ await readFile25(marker, "utf8");
22892
+ return false;
22893
+ } catch {
21687
22894
  }
21688
- for (const manager of MANAGERS) {
21689
- const prefix = await prefixOf(manager.bin, manager.prefixArgs);
21690
- if (prefix === null) continue;
21691
- if (isInside2(selfDir, prefix)) {
21692
- return {
21693
- kind: manager.kind,
21694
- selfDir,
21695
- prefix,
21696
- command: manager.install,
21697
- reason: null
21698
- };
21699
- }
22895
+ try {
22896
+ await mkdir16(brandHome(), { recursive: true });
22897
+ await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
22898
+ `);
22899
+ } catch {
21700
22900
  }
21701
- const local = selfDir.includes(`${"node_modules"}`);
21702
- return {
21703
- kind: local ? "local" : "unknown",
21704
- selfDir,
21705
- prefix: null,
21706
- command: "npm i -g memgineering@latest",
21707
- reason: local ? "this copy is a project dependency, not a global install" : "no package manager on PATH owns the directory this copy is in \u2014 under nvm, asdf, Volta or mise that usually means a different Node version is active than the one it was installed with"
21708
- };
22901
+ return true;
21709
22902
  }
21710
- function canSelfUpdate(p) {
21711
- return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
22903
+ var REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
22904
+
22905
+ The hub and skills this CLI installs into your tool's home were written by an
22906
+ earlier release. One command rewrites them:
22907
+
22908
+ \`\`\`bash
22909
+ memgineering setup
22910
+ \`\`\`
22911
+
22912
+ It does not change which brain this machine reads, or any note. Two things it
22913
+ does change, both worth knowing before it runs:
22914
+
22915
+ - A setup-owned file your user edited is overwritten; setup names it afterwards.
22916
+ - Run with no flags where there is no terminal (an agent's shell), it applies
22917
+ setup's defaults over whatever they chose before: auto-update on, its hooks
22918
+ on, every agent tool it finds \u2014 and on a machine not signed in, it starts a
22919
+ sign-in and prints a code. To keep what they chose, pass it:
22920
+ \`--auto-update off\`, \`--hook off\`, \`--no-login\` for a machine they keep
22921
+ local, and \`--tools\` naming every tool they use (a tool left out keeps its
22922
+ old guidance, and this notice stops appearing anyway).
22923
+
22924
+ If you are not sure what they chose, ask them. If you run it,
22925
+ mention it to your user in one line: it is upkeep for a tool they already set
22926
+ up, and they should not have to discover it later.
22927
+
22928
+ Guidance files are read at the start of a session, so what changes today
22929
+ arrives in the next one.`;
22930
+ var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
22931
+
22932
+ A brain is a folder of markdown notes. Until one is connected there is nothing
22933
+ to recall and nowhere to remember. Which folder is your user's choice; the
22934
+ command is one you can run for them:
22935
+
22936
+ \`\`\`bash
22937
+ memgineering link <their-notes-folder> # they already keep notes
22938
+ memgineering init <path> # they do not \u2014 creates one, laid out
22939
+ \`\`\`
22940
+
22941
+ \`link\` reports what would be stored before anything is indexed. An existing
22942
+ folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
22943
+
22944
+ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
22945
+ it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
22946
+
22947
+ Read \`memgineering-setup\` for accounts and which of their tools this reaches.
22948
+
22949
+ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
22950
+ var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
22951
+
22952
+ \`memgineering brains\` lists what is on the account.
22953
+ \`memgineering use --brain <name>\` points this machine at one.
22954
+ \`memgineering create <name>\` makes one and points here, when the account has none yet.
22955
+ `;
22956
+ function nextStepFor(cfg, signedIn = false) {
22957
+ if (signedIn && cfg.brain.cloud === void 0) {
22958
+ return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
22959
+ }
22960
+ if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
22961
+ return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
22962
+ }
22963
+ if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
22964
+ if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
22965
+ if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
22966
+ return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
22967
+ }
22968
+ return null;
21712
22969
  }
21713
- async function prefixOf(bin, args) {
22970
+ async function currentNextStep() {
21714
22971
  try {
21715
- const { stdout } = await run(bin, args, { timeout: 5e3 });
21716
- const value = stdout.trim();
21717
- return value === "" ? null : await realpath3(value).catch(() => value);
22972
+ const stored = await readCredentials().catch(() => null);
22973
+ return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
21718
22974
  } catch {
21719
22975
  return null;
21720
22976
  }
21721
22977
  }
21722
- function isInside2(child, parent) {
21723
- const c2 = resolve14(child);
21724
- const p = resolve14(parent);
21725
- return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
21726
- }
22978
+
22979
+ // src/commands/update.ts
22980
+ init_provenance();
21727
22981
 
21728
22982
  // src/lib/update-check.ts
21729
22983
  init_atomic();
21730
22984
  init_brand();
21731
22985
  import { execFile as execFile2 } from "child_process";
21732
- import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
21733
- import { join as join26 } from "path";
22986
+ import { mkdir as mkdir17, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
22987
+ import { join as join27 } from "path";
21734
22988
  import { promisify as promisify2 } from "util";
21735
22989
  import semver4 from "semver";
21736
22990
  import { z as z33 } from "zod";
@@ -21811,11 +23065,11 @@ var UpdateStateSchema = z33.object({
21811
23065
  message: z33.string().nullable()
21812
23066
  });
21813
23067
  function statePath() {
21814
- return join26(brandHome(), "update-state.json");
23068
+ return join27(brandHome(), "update-state.json");
21815
23069
  }
21816
23070
  async function readUpdateState() {
21817
23071
  try {
21818
- const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile25(statePath(), "utf8")));
23072
+ const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile26(statePath(), "utf8")));
21819
23073
  return parsed.success ? parsed.data : null;
21820
23074
  } catch {
21821
23075
  return null;
@@ -21823,7 +23077,7 @@ async function readUpdateState() {
21823
23077
  }
21824
23078
  async function writeUpdateState(state) {
21825
23079
  try {
21826
- await mkdir16(brandHome(), { recursive: true });
23080
+ await mkdir17(brandHome(), { recursive: true });
21827
23081
  await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
21828
23082
  `);
21829
23083
  } catch {
@@ -21879,70 +23133,106 @@ function shortMessage(err) {
21879
23133
  // src/commands/update.ts
21880
23134
  init_ui();
21881
23135
  function updateCommand() {
21882
- return new Command40("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
21883
- const status = await checkForUpdate(VERSION);
21884
- const provenance = await detectProvenance();
21885
- const previous = await readUpdateState();
21886
- const cfg = await loadConfig();
21887
- await saveConfig({
21888
- ...cfg,
21889
- system: { ...cfg.system, lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString() }
21890
- });
21891
- if (!status.updateAvailable) {
21892
- printDual({
21893
- json: { ...status, provenance: provenance.kind, updated: false, previous },
21894
- human: () => {
21895
- printHuman(
21896
- status.latest === null ? `memgineering ${VERSION} \u2014 could not reach npm to check for a newer version.` : `memgineering ${VERSION} is current.`
21897
- );
21898
- reportNotice(status.notice);
21899
- reportBelowMinimum(status.belowMinimum);
21900
- }
21901
- });
21902
- return;
23136
+ return new Command41("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
23137
+ const inherited = process.env[UPDATE_CLAIM_ENV] || void 0;
23138
+ try {
23139
+ await runUpdate(opts, inherited);
23140
+ } finally {
23141
+ if (inherited !== void 0) await releaseUpdateSlot(inherited);
21903
23142
  }
21904
- if (opts.check || !canSelfUpdate(provenance)) {
21905
- printDual({
21906
- json: {
21907
- ...status,
21908
- provenance: provenance.kind,
21909
- reason: provenance.reason,
21910
- command: provenance.command,
21911
- updated: false
21912
- },
21913
- human: () => {
21914
- printHuman(`memgineering ${status.latest} is available (you have ${VERSION}).
23143
+ });
23144
+ }
23145
+ async function runUpdate(opts, inherited) {
23146
+ const status = await checkForUpdate(VERSION);
23147
+ const provenance = await detectProvenance();
23148
+ const previous = await readUpdateState();
23149
+ const cfg = await loadConfig();
23150
+ await saveConfig({
23151
+ ...cfg,
23152
+ system: { ...cfg.system, lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString() }
23153
+ });
23154
+ if (!status.updateAvailable) {
23155
+ printDual({
23156
+ json: { ...status, provenance: provenance.kind, updated: false, previous },
23157
+ human: () => {
23158
+ printHuman(
23159
+ status.latest === null ? `memgineering ${VERSION} \u2014 could not reach npm to check for a newer version.` : `memgineering ${VERSION} is current.`
23160
+ );
23161
+ reportNotice(status.notice);
23162
+ reportBelowMinimum(status.belowMinimum);
23163
+ }
23164
+ });
23165
+ return;
23166
+ }
23167
+ if (opts.check || !canSelfUpdate(provenance)) {
23168
+ printDual({
23169
+ json: {
23170
+ ...status,
23171
+ provenance: provenance.kind,
23172
+ reason: provenance.reason,
23173
+ command: provenance.command,
23174
+ updated: false
23175
+ },
23176
+ human: () => {
23177
+ printHuman(`memgineering ${status.latest} is available (you have ${VERSION}).
21915
23178
  `);
21916
- if (!opts.check && provenance.reason) {
21917
- printHuman(c.yellow(`Not updating automatically \u2014 ${provenance.reason}.
23179
+ if (!opts.check && provenance.reason) {
23180
+ printHuman(c.yellow(`Not updating automatically \u2014 ${provenance.reason}.
21918
23181
  `));
21919
- }
21920
- printHuman(`Run: ${provenance.command}`);
21921
- reportNotice(status.notice);
21922
- reportBelowMinimum(status.belowMinimum);
21923
23182
  }
21924
- });
21925
- return;
21926
- }
21927
- printHuman(c.gray(`Updating ${VERSION} \u2192 ${status.latest}\u2026`));
21928
- const result = await performUpdate({ command: provenance.command, from: VERSION });
21929
- await writeUpdateState(result);
23183
+ printHuman(`Run: ${provenance.command}`);
23184
+ reportNotice(status.notice);
23185
+ reportBelowMinimum(status.belowMinimum);
23186
+ }
23187
+ });
23188
+ return;
23189
+ }
23190
+ const held = await holdUpdateSlot(inherited);
23191
+ if (held === null) {
23192
+ const started = (await claimOwner())?.split(" ")[0];
23193
+ const hint = "Another memgineering update is installing right now. Run `memgineering update` again in a few minutes \u2014 it reports whether that install worked.";
21930
23194
  printDual({
21931
- json: { ...status, provenance: provenance.kind, updated: result.ok, result },
23195
+ json: {
23196
+ ...status,
23197
+ provenance: provenance.kind,
23198
+ updated: false,
23199
+ reason: "update_in_progress",
23200
+ ...started ? { started_at: started } : {},
23201
+ hint
23202
+ },
21932
23203
  human: () => {
21933
- if (result.ok) {
21934
- printHuman(c.green(`Updated to ${result.to}.`));
21935
- printHuman(c.gray("Restart your agent session to pick up any changed guidance."));
21936
- return;
21937
- }
21938
- printHuman(c.yellow(`Update did not complete: ${result.message}`));
21939
- printHuman(c.gray(`Run it yourself: ${provenance.command}`));
23204
+ printHuman(
23205
+ `memgineering ${status.latest} is available, and an update is already installing${started ? ` (started ${started})` : ""} \u2014 not starting a second one.`
23206
+ );
23207
+ printHuman(c.gray(hint));
21940
23208
  }
21941
23209
  });
23210
+ return;
23211
+ }
23212
+ printHuman(c.gray(`Updating ${VERSION} \u2192 ${status.latest}\u2026`));
23213
+ let result;
23214
+ try {
23215
+ result = await performUpdate({ command: provenance.command, from: VERSION });
23216
+ await writeUpdateState(result);
23217
+ } finally {
23218
+ await releaseUpdateSlot(held);
23219
+ }
23220
+ printDual({
23221
+ json: { ...status, provenance: provenance.kind, updated: result.ok, result },
23222
+ human: () => {
23223
+ if (result.ok) {
23224
+ printHuman(c.green(`Updated to ${result.to}.`));
23225
+ printHuman(c.gray("Restart your agent session to pick up any changed guidance."));
23226
+ return;
23227
+ }
23228
+ printHuman(c.yellow(`Update did not complete: ${result.message}`));
23229
+ printHuman(c.gray(`Run it yourself: ${provenance.command}`));
23230
+ }
21942
23231
  });
21943
23232
  }
21944
- async function backgroundUpdateNotice() {
23233
+ async function backgroundUpdateNotice(deps = {}) {
21945
23234
  try {
23235
+ if (deps.verb !== void 0 && HOOK_VERBS.has(deps.verb)) return;
21946
23236
  const cfg = await loadConfig();
21947
23237
  const now = /* @__PURE__ */ new Date();
21948
23238
  if (!shouldCheck(cfg.system.lastUpdateCheck, now)) {
@@ -21950,8 +23240,22 @@ async function backgroundUpdateNotice() {
21950
23240
  return;
21951
23241
  }
21952
23242
  await saveConfig({ ...cfg, system: { ...cfg.system, lastUpdateCheck: now.toISOString() } });
21953
- const status = await checkForUpdate(VERSION);
23243
+ const status = await (deps.check ?? checkForUpdate)(VERSION);
21954
23244
  await reportPreviousAttempt();
23245
+ if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
23246
+ const provenance = await (deps.provenance ?? detectProvenance)();
23247
+ const token = newClaimToken();
23248
+ if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
23249
+ (deps.startUpdate ?? ((t) => startDetachedUpdate(spawn3, t)))(token);
23250
+ notice(
23251
+ c.gray(
23252
+ `memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
23253
+ )
23254
+ );
23255
+ if (status.notice) notice(c.gray(`note from memgineering.com: "${status.notice}"`));
23256
+ return;
23257
+ }
23258
+ }
21955
23259
  if (status.belowMinimum) {
21956
23260
  notice(
21957
23261
  c.yellow(
@@ -21965,10 +23269,73 @@ async function backgroundUpdateNotice() {
21965
23269
  } catch {
21966
23270
  }
21967
23271
  }
23272
+ function startDetachedUpdate(spawnImpl = spawn3, token) {
23273
+ const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
23274
+ detached: true,
23275
+ stdio: "ignore",
23276
+ env: {
23277
+ ...process.env,
23278
+ MEMGINEERING_NO_UPDATE: "1",
23279
+ // The claim this run took, so the child installs under it and gives it
23280
+ // back, instead of finding it held and standing down.
23281
+ ...token === void 0 ? {} : { [UPDATE_CLAIM_ENV]: token }
23282
+ }
23283
+ });
23284
+ child.on("error", () => {
23285
+ if (token !== void 0) void releaseUpdateSlot(token);
23286
+ });
23287
+ child.unref();
23288
+ }
23289
+ var UPDATE_CLAIM_ENV = "MEMGINEERING_UPDATE_CLAIM";
23290
+ function newClaimToken() {
23291
+ return `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${randomBytes2(6).toString("hex")}`;
23292
+ }
23293
+ async function claimOwner() {
23294
+ try {
23295
+ return (await readFile27(join28(brandHome(), "update-running"), "utf8")).trim() || null;
23296
+ } catch {
23297
+ return null;
23298
+ }
23299
+ }
23300
+ async function releaseUpdateSlot(token) {
23301
+ if (await claimOwner() !== token) return;
23302
+ await unlink4(join28(brandHome(), "update-running")).catch(() => void 0);
23303
+ }
23304
+ async function holdUpdateSlot(inherited) {
23305
+ if (inherited !== void 0 && await claimOwner() === inherited) return inherited;
23306
+ const token = newClaimToken();
23307
+ if (await claimUpdateSlot(token)) return token;
23308
+ if (await claimOwner() === null && await claimUpdateSlot(token)) return token;
23309
+ return null;
23310
+ }
23311
+ var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
23312
+ var UPDATE_CLAIM_MS = 15 * 6e4;
23313
+ async function claimUpdateSlot(token = newClaimToken()) {
23314
+ const path = join28(brandHome(), "update-running");
23315
+ try {
23316
+ await mkdir18(brandHome(), { recursive: true });
23317
+ const handle = await open(path, "wx");
23318
+ await handle.writeFile(`${token}
23319
+ `);
23320
+ await handle.close();
23321
+ return true;
23322
+ } catch (err) {
23323
+ if (err?.code !== "EEXIST") return false;
23324
+ }
23325
+ try {
23326
+ if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) await unlink4(path);
23327
+ } catch {
23328
+ }
23329
+ return false;
23330
+ }
21968
23331
  async function reportPreviousAttempt() {
21969
23332
  const previous = await readUpdateState();
21970
23333
  if (!previous || previous.ok) return;
21971
- notice(c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`));
23334
+ notice(
23335
+ c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`) + c.gray(
23336
+ "\n `memgineering update` shows why. `memgineering setup --agent --auto-update off --hook <on|off>` stops\n these attempts \u2014 name the hook setting you want kept, because `--agent` sets that too."
23337
+ )
23338
+ );
21972
23339
  }
21973
23340
  function reportNotice(text) {
21974
23341
  if (!text) return;
@@ -21986,16 +23353,16 @@ function reportBelowMinimum(below) {
21986
23353
 
21987
23354
  // src/commands/undo.ts
21988
23355
  init_src();
21989
- import { readFile as readFile26, rm as rm8 } from "fs/promises";
21990
- import { join as join27 } from "path";
21991
- import { Command as Command41 } from "commander";
23356
+ import { readFile as readFile28, rm as rm8 } from "fs/promises";
23357
+ import { join as join29 } from "path";
23358
+ import { Command as Command42 } from "commander";
21992
23359
  init_atomic();
21993
23360
  init_errors();
21994
23361
  init_cloud_brain();
21995
23362
  init_vault();
21996
23363
  init_ui();
21997
23364
  function undoCommand() {
21998
- return new Command41("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").option("--local", "act on the brain on this machine, even when signed in to a hosted one").action(
23365
+ return new Command42("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").option("--local", "act on the brain on this machine, even when signed in to a hosted one").action(
21999
23366
  async (opId, opts) => {
22000
23367
  const rationale = sanitizeRationale(opts.reason);
22001
23368
  const where = await resolveTarget({
@@ -22084,8 +23451,8 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
22084
23451
  );
22085
23452
  }
22086
23453
  const record = await withWriteLock(brain.root, async () => {
22087
- const absolute = join27(brain.root, target.target);
22088
- const current = await readFile26(absolute, "utf8").catch(() => null);
23454
+ const absolute = join29(brain.root, target.target);
23455
+ const current = await readFile28(absolute, "utf8").catch(() => null);
22089
23456
  const actualHash = current === null ? null : hashContent(current);
22090
23457
  if (actualHash !== target.hash_after) {
22091
23458
  throw memgError(
@@ -22147,7 +23514,7 @@ function minutesSince(iso) {
22147
23514
  return Math.max(0, Math.floor((Date.now() - at) / 6e4));
22148
23515
  }
22149
23516
  async function reverse(brain, op, current) {
22150
- const absolute = join27(brain.root, op.target);
23517
+ const absolute = join29(brain.root, op.target);
22151
23518
  if (op.verb === "remember") {
22152
23519
  if (current === null) {
22153
23520
  throw memgError(
@@ -22200,11 +23567,11 @@ async function reverse(brain, op, current) {
22200
23567
  return reverted;
22201
23568
  }
22202
23569
  async function conflictHint(vaultRoot, op) {
22203
- const base = `Nothing was changed. Someone \u2014 you, an editor, or a sync \u2014 has written to that file since.
23570
+ const base2 = `Nothing was changed. Someone \u2014 you, an editor, or a sync \u2014 has written to that file since.
22204
23571
  Read what it says now, then decide what should stay.`;
22205
23572
  const snapshot = await readSnapshot(vaultRoot, op.op_id);
22206
- return snapshot === null ? base : `${base}
22207
- The text from before that change is still on this machine: ${join27(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
23573
+ return snapshot === null ? base2 : `${base2}
23574
+ The text from before that change is still on this machine: ${join29(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
22208
23575
  }
22209
23576
 
22210
23577
  // src/commands/brains.ts
@@ -22213,19 +23580,19 @@ init_api_client();
22213
23580
  init_cloud_brain();
22214
23581
  init_config();
22215
23582
  init_ui();
22216
- import { Command as Command42 } from "commander";
23583
+ import { Command as Command43 } from "commander";
22217
23584
  import stringWidth from "string-width";
22218
23585
  var NAME_WIDTH2 = 48;
22219
23586
  function brainsCommand() {
22220
- return new Command42("brains").description("list the hosted brains on your account, and which one this machine reads").addHelpText(
23587
+ return new Command43("brains").description("list the hosted brains on your account, and which one this machine reads").addHelpText(
22221
23588
  "after",
22222
23589
  "\nReads only. `memgineering use --brain <name>` points this machine at one of\nthem. `memgineering create <name>` makes a new empty one, and\n`push --brain <name>` makes one from a folder on this machine."
22223
23590
  ).action(async () => {
22224
- const base = apiUrl();
22225
- const credentials = await requireCredentials(base);
22226
- const brains = await listCloudBrains(credentials.token, base);
23591
+ const base2 = apiUrl();
23592
+ const credentials = await requireCredentials(base2);
23593
+ const brains = await listCloudBrains(credentials.token, base2);
22227
23594
  const pointer = (await loadConfig()).brain.cloud;
22228
- const currentId = pointer?.api_url === base ? pointer.id : null;
23595
+ const currentId = pointer?.api_url === base2 ? pointer.id : null;
22229
23596
  if (brains.length === 0) {
22230
23597
  throw memgError(
22231
23598
  "invalid_input",
@@ -22253,7 +23620,7 @@ function brainsCommand() {
22253
23620
  // that answered none of the rest. Two round-test agents ran all three.
22254
23621
  // The name is free here; it came back with the token.
22255
23622
  account: { id: credentials.account_id, name: credentials.display_name },
22256
- server: base,
23623
+ server: base2,
22257
23624
  brains: rows,
22258
23625
  count: rows.length,
22259
23626
  // Named at the top level too, so a caller does not have to scan the
@@ -22297,7 +23664,7 @@ init_credentials();
22297
23664
  init_cloud_brain();
22298
23665
  init_config();
22299
23666
  import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve15 } from "path";
22300
- import { Command as Command43 } from "commander";
23667
+ import { Command as Command44 } from "commander";
22301
23668
  init_vault();
22302
23669
  init_ui();
22303
23670
  function lostDefaultLines(missing) {
@@ -22316,7 +23683,7 @@ var VIA_MEANS = {
22316
23683
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
22317
23684
  };
22318
23685
  function useCommand() {
22319
- return new Command43("use").description(
23686
+ return new Command44("use").description(
22320
23687
  "say which brain answers here \u2014 `--brain <name>` for the whole machine, or a path to bind this folder"
22321
23688
  ).argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--brain <name>", "point this machine at a hosted brain, by its name or id").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
22322
23689
  "--default",
@@ -22542,8 +23909,8 @@ async function pointAtHosted(named, opts, path) {
22542
23909
  "They point different things. `--brain <name>` moves the one hosted brain this machine reads, which is a single setting for the whole machine; a path, `--default` and `--here` all bind a FOLDER on this disk. Run them separately."
22543
23910
  );
22544
23911
  }
22545
- const base = apiUrl();
22546
- const credentials = await requireCredentials(base).catch((err) => {
23912
+ const base2 = apiUrl();
23913
+ const credentials = await requireCredentials(base2).catch((err) => {
22547
23914
  if (err instanceof MemgError && err.code === "not_signed_in") {
22548
23915
  throw memgError(
22549
23916
  "not_signed_in",
@@ -22553,7 +23920,7 @@ async function pointAtHosted(named, opts, path) {
22553
23920
  }
22554
23921
  throw err;
22555
23922
  });
22556
- const brains = await listCloudBrains(credentials.token, base);
23923
+ const brains = await listCloudBrains(credentials.token, base2);
22557
23924
  if (brains.length === 0) {
22558
23925
  throw memgError(
22559
23926
  "invalid_input",
@@ -22571,11 +23938,11 @@ async function pointAtHosted(named, opts, path) {
22571
23938
  }
22572
23939
  const config = await loadConfig();
22573
23940
  const previous = config.brain.cloud ?? null;
22574
- const unchanged = previous?.id === found.id && previous.api_url === base && previous.name === found.name;
23941
+ const unchanged = previous?.id === found.id && previous.api_url === base2 && previous.name === found.name;
22575
23942
  if (!unchanged) {
22576
23943
  await saveConfig({
22577
23944
  ...config,
22578
- brain: { ...config.brain, cloud: { id: found.id, name: found.name, api_url: base } }
23945
+ brain: { ...config.brain, cloud: { id: found.id, name: found.name, api_url: base2 } }
22579
23946
  });
22580
23947
  }
22581
23948
  printDual({
@@ -22583,7 +23950,7 @@ async function pointAtHosted(named, opts, path) {
22583
23950
  brain: {
22584
23951
  id: found.id,
22585
23952
  name: found.name,
22586
- api_url: base,
23953
+ api_url: base2,
22587
23954
  // Carried because the human line says it. A caller that has to ask a
22588
23955
  // second time to learn the destination is empty is a caller that will
22589
23956
  // not ask.
@@ -22624,16 +23991,16 @@ async function pointAtHosted(named, opts, path) {
22624
23991
  // src/commands/whoami.ts
22625
23992
  init_api_client();
22626
23993
  init_ui();
22627
- import { Command as Command44 } from "commander";
23994
+ import { Command as Command45 } from "commander";
22628
23995
  function whoamiCommand() {
22629
- return new Command44("whoami").description("show which account this machine is signed in as").action(async () => {
22630
- const base = apiUrl();
22631
- const credentials = await requireCredentials(base);
23996
+ return new Command45("whoami").description("show which account this machine is signed in as").action(async () => {
23997
+ const base2 = apiUrl();
23998
+ const credentials = await requireCredentials(base2);
22632
23999
  const res = await apiRequest({
22633
24000
  method: "GET",
22634
24001
  path: "/v1/me",
22635
24002
  token: credentials.token,
22636
- baseUrl: base
24003
+ baseUrl: base2
22637
24004
  });
22638
24005
  const account = res.body?.account ?? {};
22639
24006
  const live = res.body?.tokens?.live ?? null;
@@ -22647,7 +24014,7 @@ function whoamiCommand() {
22647
24014
  created_at: account.created_at ?? null
22648
24015
  },
22649
24016
  tokens: { live },
22650
- api_url: base,
24017
+ api_url: base2,
22651
24018
  signed_in_at: credentials.signed_in_at,
22652
24019
  registry: credentials.registry ? {
22653
24020
  username: credentials.registry.username,
@@ -22662,7 +24029,7 @@ function whoamiCommand() {
22662
24029
  )
22663
24030
  );
22664
24031
  if (account.email) printHuman(c.gray(` ${oneLine(account.email, WIDTH.title)}`));
22665
- printHuman(c.gray(` on ${base}`));
24032
+ printHuman(c.gray(` on ${base2}`));
22666
24033
  if (credentials.registry) {
22667
24034
  printHuman(
22668
24035
  c.gray(
@@ -22688,94 +24055,6 @@ function whoamiCommand() {
22688
24055
  });
22689
24056
  }
22690
24057
 
22691
- // src/lib/next-step.ts
22692
- init_api_client();
22693
- init_credentials();
22694
- init_config();
22695
- function isSetUp(cfg) {
22696
- return Object.keys(cfg.system.installed).length > 0;
22697
- }
22698
- var FINISH_SETUP = `## memgineering is installed. It is NOT set up yet.
22699
-
22700
- **TO THE AGENT READING THIS** \u2014 finish it now, in this same turn. Do not print
22701
- this for your user and stop: the command is yours to run.
22702
-
22703
- \`\`\`bash
22704
- memgineering setup --web
22705
- \`\`\`
22706
-
22707
- **It does NOT wait for them.** It returns in a few seconds \u2014 measured \u2014 and
22708
- leaves a setup screen open in their browser at memgineering.com, where they
22709
- choose everything: signing in, where the memory lives, which notes. Nothing is
22710
- installed until they finish there, and this machine picks it up on its own when
22711
- they do, after your turn has ended.
22712
-
22713
- So: run it, then tell them **in their language** that you opened the setup
22714
- screen in their browser and they can finish it there. It prints a short code
22715
- first \u2014 show them that code, because the screen asks whether they started this
22716
- and expects them to compare it.
22717
-
22718
- No browser on this machine (a server, a container, a sandbox)? Run
22719
- \`memgineering setup --web --print-url\` and give them the URL to open on the
22720
- computer they are actually sitting at.
22721
-
22722
- \`setup --agent\` is NOT the safe fallback. It answers every one of those
22723
- questions on the user's behalf, and \`--no-login\` answers the biggest one \u2014
22724
- no account, so the memory cannot follow them to another machine, which they
22725
- find out weeks later. Run it only when they have said they would rather you
22726
- decided, or when this machine genuinely cannot reach a browser and
22727
- \`--print-url\` is no use to them either.
22728
-
22729
- Setup's output then says how to connect their brain \u2014 create one, pick one, or
22730
- move existing notes up.
22731
-
22732
- Then read \`memgineering-setup\` for what to do with the brain it points at.`;
22733
- var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
22734
-
22735
- **TO THE AGENT READING THIS** \u2014 a brain is a folder of markdown notes. Until one
22736
- is connected there is nothing to recall and nowhere to remember. Pick with your
22737
- user, then run it yourself:
22738
-
22739
- \`\`\`bash
22740
- memgineering link <their-notes-folder> # they already keep notes
22741
- memgineering init <path> # they do not \u2014 creates one, laid out
22742
- \`\`\`
22743
-
22744
- \`link\` reports what would be stored before anything is indexed. An existing
22745
- folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
22746
-
22747
- Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
22748
- it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
22749
-
22750
- Read \`memgineering-setup\` for accounts and which of their tools this reaches.
22751
-
22752
- Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
22753
- var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
22754
-
22755
- \`memgineering brains\` lists what is on the account.
22756
- \`memgineering use --brain <name>\` points this machine at one.
22757
- \`memgineering create <name>\` makes one and points here, when the account has none yet.
22758
- `;
22759
- function nextStepFor(cfg, signedIn = false) {
22760
- if (signedIn && cfg.brain.cloud === void 0) {
22761
- return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
22762
- }
22763
- if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
22764
- if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
22765
- if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
22766
- return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
22767
- }
22768
- return null;
22769
- }
22770
- async function currentNextStep() {
22771
- try {
22772
- const stored = await readCredentials().catch(() => null);
22773
- return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
22774
- } catch {
22775
- return null;
22776
- }
22777
- }
22778
-
22779
24058
  // src/program.ts
22780
24059
  init_ui();
22781
24060
  init_brand();
@@ -22785,11 +24064,12 @@ var HOMONYM_VERBS = ["pull", "push", "update"];
22785
24064
  var NO_FLAT_ALIAS = new Set(HOMONYM_VERBS);
22786
24065
 
22787
24066
  // src/program.ts
24067
+ init_version();
22788
24068
  function buildProgram() {
22789
- const program = new Command45("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", async (thisCommand, actionCommand) => {
24069
+ const program = new Command46("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", async (thisCommand, actionCommand) => {
22790
24070
  const opts = thisCommand.optsWithGlobals();
22791
24071
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
22792
- await announceNextStep(commandPath(actionCommand));
24072
+ await announceNextStep(commandPath(actionCommand), "verb");
22793
24073
  });
22794
24074
  configureGroupedHelp(program);
22795
24075
  program.addCommand(recallCommand());
@@ -22815,7 +24095,7 @@ function buildProgram() {
22815
24095
  diffCommand,
22816
24096
  listCommand
22817
24097
  ];
22818
- const registry = new Command45("registry").description(
24098
+ const registry = new Command46("registry").description(
22819
24099
  "the public layer: find, read, adapt, publish, install and keep packages current"
22820
24100
  );
22821
24101
  for (const factory of publicLayer) registry.addCommand(factory());
@@ -22842,6 +24122,8 @@ function buildProgram() {
22842
24122
  program.addCommand(baseWriteCommand());
22843
24123
  program.addCommand(retireCommand());
22844
24124
  program.addCommand(unretireCommand());
24125
+ program.addCommand(rescopeCommand());
24126
+ program.addCommand(expireCommand());
22845
24127
  program.addCommand(retractCommand());
22846
24128
  program.addCommand(excludeCommand());
22847
24129
  program.addCommand(unexcludeCommand());
@@ -22890,15 +24172,16 @@ function commandPath(cmd) {
22890
24172
  const parent = cmd.parent;
22891
24173
  return parent && parent.parent ? `${parent.name()} ${cmd.name()}` : cmd.name();
22892
24174
  }
22893
- async function announceNextStep(commandName, forPerson = false) {
24175
+ async function announceNextStep(commandName, reader) {
22894
24176
  if (announced) return;
22895
24177
  if (WITHOUT_NEXT_STEP.has(commandName)) return;
22896
24178
  const step = await currentNextStep();
22897
24179
  if (step === null) return;
22898
24180
  announced = true;
22899
- if (forPerson && step.id === "finish_setup") {
24181
+ if (step.id === "finish_setup" && reader !== "full") {
24182
+ const text = reader === "person" ? SHORT_SETUP_NOTICE : reader === "verb" && await claimFullSetupNotice() ? step.markdown : SETUP_NUDGE;
22900
24183
  process.stderr.write(`
22901
- ${SHORT_SETUP_NOTICE}
24184
+ ${text}
22902
24185
 
22903
24186
  `);
22904
24187
  return;
@@ -22912,8 +24195,8 @@ function makeParseErrorsActionable(program) {
22912
24195
  const decorate = (cmd) => {
22913
24196
  cmd.showSuggestionAfterError(true);
22914
24197
  cmd.configureOutput({
22915
- outputError: (str, write) => {
22916
- write(str);
24198
+ outputError: (str, write2) => {
24199
+ write2(str);
22917
24200
  if (!/unknown option|unknown command|too many arguments/i.test(str)) return;
22918
24201
  const names = [];
22919
24202
  for (let node = cmd; node !== null; node = node.parent) {
@@ -22922,12 +24205,12 @@ function makeParseErrorsActionable(program) {
22922
24205
  const path = names.join(" ");
22923
24206
  const runnable = cmd.commands.filter((sub) => sub._hidden !== true).map((sub) => sub.name()).sort();
22924
24207
  if (/unknown command/i.test(str) && runnable.length > 0) {
22925
- write(`hint: ${path} has ${runnable.join(" ")}
24208
+ write2(`hint: ${path} has ${runnable.join(" ")}
22926
24209
  `);
22927
24210
  return;
22928
24211
  }
22929
24212
  const flags = cmd.options.map((o) => o.long ?? o.short).filter(Boolean);
22930
- write(
24213
+ write2(
22931
24214
  flags.length > 0 ? `hint: ${path} takes ${flags.join(" ")}
22932
24215
  ` : `hint: \`${path} --help\` lists what this takes.
22933
24216
  `
@@ -22954,10 +24237,13 @@ async function run3(argv) {
22954
24237
  return;
22955
24238
  }
22956
24239
  }
22957
- await announceNextStep("", process.stderr.isTTY === true);
24240
+ await announceNextStep(
24241
+ "",
24242
+ process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
24243
+ );
22958
24244
  }
22959
24245
  await program.parseAsync(cleaned);
22960
- if (argv[2] !== "update") await backgroundUpdateNotice();
24246
+ if (argv[2] !== "update") await backgroundUpdateNotice(argv[2] === void 0 ? {} : { verb: argv[2] });
22961
24247
  }
22962
24248
  async function handleError(err) {
22963
24249
  const failure = adaptRegistryError(err) ?? err;