memgineering 0.25.2 → 0.26.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
@@ -1065,22 +1065,6 @@ function mapNoteToMemory(input) {
1065
1065
  issues
1066
1066
  };
1067
1067
  }
1068
- function isUntouchedBase(path, content) {
1069
- if (!/^01_BASE\//.test(path)) return false;
1070
- const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
1071
- const lines2 = body.split(/\r?\n/).map((l) => l.trimEnd());
1072
- let seenHeading = false;
1073
- for (const line of lines2) {
1074
- if (line.trim() === "") continue;
1075
- if (!seenHeading && line.startsWith("# ")) {
1076
- seenHeading = true;
1077
- continue;
1078
- }
1079
- if (/^-\s*$/.test(line)) continue;
1080
- if (/^[-*+]\s+\S/.test(line) || /^#{1,6}\s/.test(line)) return false;
1081
- }
1082
- return seenHeading;
1083
- }
1084
1068
  var NAMESPACE, isRecord, asString, asStringArray, AUTHORABLE;
1085
1069
  var init_markdown_mapping = __esm({
1086
1070
  "../../packages/memory-engine/src/markdown-mapping.ts"() {
@@ -1341,8 +1325,8 @@ var init_exclude = __esm({
1341
1325
  [...ALWAYS_SKIP_DIRS].map((d) => fold(d))
1342
1326
  );
1343
1327
  ALWAYS_SKIP_SEGMENTS_FOLDED = ALWAYS_SKIP_SEGMENTS.map((s) => fold(s));
1344
- ALWAYS_SKIP_FILE = (basename4) => {
1345
- const b = fold(basename4);
1328
+ ALWAYS_SKIP_FILE = (basename5) => {
1329
+ const b = fold(basename5);
1346
1330
  return b.startsWith(".env") || b === ".ds_store" || b === ".memgdeny" || b === ".memgallow" || b === ".memgignore";
1347
1331
  };
1348
1332
  EMPTY_IGNORE_SET = { rules: [], unsupported: [] };
@@ -2441,6 +2425,21 @@ var init_planner = __esm({
2441
2425
  }
2442
2426
  });
2443
2427
 
2428
+ // ../../packages/memory-engine/src/note-body.ts
2429
+ function bodyOf(content) {
2430
+ const stripped = content.startsWith(BOM) ? content.slice(BOM.length) : content;
2431
+ const fence2 = FENCE.exec(stripped);
2432
+ return fence2 ? stripped.slice(fence2[0].length) : stripped;
2433
+ }
2434
+ var BOM, FENCE;
2435
+ var init_note_body = __esm({
2436
+ "../../packages/memory-engine/src/note-body.ts"() {
2437
+ "use strict";
2438
+ BOM = String.fromCharCode(65279);
2439
+ FENCE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/;
2440
+ }
2441
+ });
2442
+
2444
2443
  // ../../packages/memory-engine/src/base-templates.ts
2445
2444
  function base(id, title, summary, body) {
2446
2445
  return [
@@ -2461,10 +2460,31 @@ function base(id, title, summary, body) {
2461
2460
  function ownEntry(record, key2) {
2462
2461
  return Object.hasOwn(record, key2) ? record[key2] : void 0;
2463
2462
  }
2464
- var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES;
2463
+ function wordingsFor(path) {
2464
+ const current = ownEntry(BASE_TEMPLATES, path);
2465
+ const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
2466
+ return current === void 0 ? retired : [current, ...retired];
2467
+ }
2468
+ function isExactUntouchedBase(path, content) {
2469
+ const saved = asSaved(content);
2470
+ return wordingsFor(path).some((t) => asSaved(t) === saved);
2471
+ }
2472
+ function isTemplateBody(path, content) {
2473
+ const lines2 = bodyLines(content).join("\n");
2474
+ return wordingsFor(path).some((t) => bodyLines(t).join("\n") === lines2);
2475
+ }
2476
+ function isUnwrittenBody(path, content) {
2477
+ if (isTemplateBody(path, content)) return true;
2478
+ const [first, ...rest] = bodyLines(content);
2479
+ if (first === void 0) return false;
2480
+ const headings = wordingsFor(path).map((t) => bodyLines(t)[0]);
2481
+ return headings.includes(first) && rest.every((line) => /^-\s*$/.test(line));
2482
+ }
2483
+ var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES, BOM2, asSaved, bodyLines;
2465
2484
  var init_base_templates = __esm({
2466
2485
  "../../packages/memory-engine/src/base-templates.ts"() {
2467
2486
  "use strict";
2487
+ init_note_body();
2468
2488
  BASE_TEMPLATES = {
2469
2489
  "01_BASE/CRITICAL_FACTS.md": base(
2470
2490
  "critical-facts",
@@ -2545,6 +2565,9 @@ var init_base_templates = __esm({
2545
2565
  ].join("\n")
2546
2566
  ]
2547
2567
  };
2568
+ BOM2 = String.fromCharCode(65279);
2569
+ asSaved = (text) => (text.startsWith(BOM2) ? text.slice(BOM2.length) : text).replace(/\r\n/g, "\n").trimEnd();
2570
+ bodyLines = (content) => bodyOf(content).split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
2548
2571
  }
2549
2572
  });
2550
2573
 
@@ -4050,11 +4073,6 @@ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
4050
4073
  if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
4051
4074
  return bodyOf(content).includes(line) ? line : null;
4052
4075
  }
4053
- function bodyOf(content) {
4054
- const stripped = content.replace(/^/, "");
4055
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
4056
- return fence2 ? stripped.slice(fence2[0].length) : stripped;
4057
- }
4058
4076
  function previousClaim(entry) {
4059
4077
  const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
4060
4078
  return current?.text ?? null;
@@ -4072,6 +4090,7 @@ var init_curate = __esm({
4072
4090
  init_planner();
4073
4091
  init_schema();
4074
4092
  init_rule_budget();
4093
+ init_note_body();
4075
4094
  NO_REASON_GIVEN = "revised directly";
4076
4095
  noLifecycleReason = (action) => `${action}d directly`;
4077
4096
  }
@@ -5522,6 +5541,13 @@ var init_config = __esm({
5522
5541
  * Absent on a machine set up before it was recorded.
5523
5542
  */
5524
5543
  hook: z8.enum(["on", "off"]).optional().catch(void 0),
5544
+ /**
5545
+ * Which "left your hooks alone" notices the automatic hook refresh has
5546
+ * already given, as `<file>:<reason>` — so a symlinked or unparseable
5547
+ * settings file is mentioned once, not at the start of every session
5548
+ * (lib/hook-refresh.ts, `refreshNotice`).
5549
+ */
5550
+ hookNoticesTold: z8.array(z8.string()).optional().catch(void 0),
5525
5551
  /**
5526
5552
  * Notes `setup --web` was asked to upload, not yet sent.
5527
5553
  *
@@ -6714,7 +6740,7 @@ var init_cloud_pointer = __esm({
6714
6740
 
6715
6741
  // src/commands/link.ts
6716
6742
  import { readFile as readFile18 } from "fs/promises";
6717
- import { join as join17 } from "path";
6743
+ import { join as join18 } from "path";
6718
6744
  import { Command as Command13 } from "commander";
6719
6745
  import prompts2 from "prompts";
6720
6746
  function sameBasename(all, entry) {
@@ -6918,7 +6944,7 @@ async function recordLink(adapter, observationsDir) {
6918
6944
  }
6919
6945
  async function refuseSilentRules(path, denied, excluded, unsupported) {
6920
6946
  const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
6921
- const rawDenyLines = await readFile18(join17(path, ".memgdeny"), "utf8").then(
6947
+ const rawDenyLines = await readFile18(join18(path, ".memgdeny"), "utf8").then(
6922
6948
  (t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
6923
6949
  ).catch(() => []);
6924
6950
  const asWritten = (folded) => rawDenyLines.find(
@@ -7546,6 +7572,228 @@ var init_push = __esm({
7546
7572
  }
7547
7573
  });
7548
7574
 
7575
+ // src/lib/guard-shell.ts
7576
+ function guardHookCommand() {
7577
+ return `/bin/sh -c '${GUARD_SHELL_SCRIPT}'`;
7578
+ }
7579
+ var GUARD_SHELL_SCRIPT;
7580
+ var init_guard_shell = __esm({
7581
+ "src/lib/guard-shell.ts"() {
7582
+ "use strict";
7583
+ GUARD_SHELL_SCRIPT = [
7584
+ 'h="${MEMGINEERING_HOME-$HOME/.memgineering}"',
7585
+ 'f="$h/guard/fast"',
7586
+ 'c="$h/config.json"',
7587
+ 'k="$h/credentials.json"',
7588
+ "g() { memgineering guard; exit 0; }",
7589
+ '[ -f "$c" ] && [ -f "$k" ] && [ -d "$f" ] || g',
7590
+ "p=$(cat)",
7591
+ 'go() { printf "%s\\n" "$p" | g; exit 0; }',
7592
+ '[ -n "$p" ] || exit 0',
7593
+ 'q=\\"',
7594
+ 'case "$p" in */memory/* | *"$q"memory/* | *"\\\\/memory\\\\/"* | *"$q"memory\\\\/*) go ;; esac',
7595
+ 'w=${p%%"$q"*}',
7596
+ 'r=${p#"$w"}',
7597
+ 'case "$r" in "$q"session_id"$q"*) ;; *) go ;; esac',
7598
+ 'r=${r#"$q"session_id"$q"}',
7599
+ "w=${r%%[![:space:]]*}",
7600
+ 'r=${r#"$w"}',
7601
+ 'case "$r" in :*) ;; *) go ;; esac',
7602
+ "r=${r#:}",
7603
+ "w=${r%%[![:space:]]*}",
7604
+ 'r=${r#"$w"}',
7605
+ 'case "$r" in "$q"*) ;; *) go ;; esac',
7606
+ 'r=${r#"$q"}',
7607
+ 's=${r%%"$q"*}',
7608
+ 'case "$s" in "" | *[!A-Za-z0-9-]*) go ;; esac',
7609
+ 'm=""',
7610
+ 'if [ -f "$f/$s" ]; then m="$f/$s"; else',
7611
+ ' n=""',
7612
+ ' for e in "$f/$s".empty.*; do',
7613
+ ' [ -f "$e" ] || continue',
7614
+ " t=${e##*.}",
7615
+ ' case "$t" in "" | *[!0-9]*) continue ;; esac',
7616
+ ' [ -n "$n" ] || n=$(date +%s)',
7617
+ ' if [ "$t" -gt "$n" ]; then m="$e"; break; fi',
7618
+ " done",
7619
+ ' [ -n "$m" ] || go',
7620
+ ' [ "$f/.rules-changed" -nt "$m" ] && go',
7621
+ "fi",
7622
+ '[ "$c" -nt "$m" ] && go',
7623
+ '[ "$k" -nt "$m" ] && go',
7624
+ "exit 0"
7625
+ ].join("\n");
7626
+ }
7627
+ });
7628
+
7629
+ // src/lib/hook-refresh.ts
7630
+ import { randomUUID as randomUUID4 } from "crypto";
7631
+ import { chmod, lstat as lstat5, readdir as readdir8, readFile as readFile24, stat as stat7, unlink as unlink5, writeFile as writeFile14 } from "fs/promises";
7632
+ import { basename as basename3, dirname as dirname8, join as join24 } from "path";
7633
+ function moveShippedCommands(parsed, moves) {
7634
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7635
+ const settings = parsed;
7636
+ const hooks = settings["hooks"];
7637
+ if (hooks === void 0) return { next: settings, changed: 0 };
7638
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return null;
7639
+ let changed = 0;
7640
+ const nextHooks = { ...hooks };
7641
+ for (const move of moves) {
7642
+ const groups = nextHooks[move.event];
7643
+ if (!Array.isArray(groups)) continue;
7644
+ nextHooks[move.event] = groups.map((group) => {
7645
+ const entries = group?.hooks;
7646
+ if (!Array.isArray(entries)) return group;
7647
+ let touched = false;
7648
+ const nextEntries = entries.map((entry) => {
7649
+ const command = entry?.command;
7650
+ if (typeof command !== "string" || command === move.to || !move.from.includes(command)) {
7651
+ return entry;
7652
+ }
7653
+ touched = true;
7654
+ changed += 1;
7655
+ return { ...entry, command: move.to };
7656
+ });
7657
+ return touched ? { ...group, hooks: nextEntries } : group;
7658
+ });
7659
+ }
7660
+ return { next: changed === 0 ? settings : { ...settings, hooks: nextHooks }, changed };
7661
+ }
7662
+ async function lockHolder() {
7663
+ try {
7664
+ const [info, token] = await Promise.all([stat7(lockPath2()), readFile24(lockPath2(), "utf8")]);
7665
+ return { token, ageMs: Date.now() - info.mtimeMs };
7666
+ } catch {
7667
+ return null;
7668
+ }
7669
+ }
7670
+ async function unlinkIfStill(token) {
7671
+ const now = await readFile24(lockPath2(), "utf8").catch(() => null);
7672
+ if (now === token) await unlink5(lockPath2()).catch(() => void 0);
7673
+ }
7674
+ async function takeOver(dead) {
7675
+ try {
7676
+ await writeFile14(takeoverPath(), "", { flag: "wx", mode: 384 });
7677
+ } catch (err) {
7678
+ if (err.code === "EEXIST") {
7679
+ const age = await stat7(takeoverPath()).then(
7680
+ (info) => Date.now() - info.mtimeMs,
7681
+ () => null
7682
+ );
7683
+ if (age !== null && age > STALE_LOCK_MS) await unlink5(takeoverPath()).catch(() => void 0);
7684
+ }
7685
+ return false;
7686
+ }
7687
+ try {
7688
+ const held = await lockHolder();
7689
+ if (held !== null && held.token === dead && held.ageMs > STALE_LOCK_MS) {
7690
+ await unlink5(lockPath2()).catch(() => void 0);
7691
+ }
7692
+ return true;
7693
+ } finally {
7694
+ await unlink5(takeoverPath()).catch(() => void 0);
7695
+ }
7696
+ }
7697
+ async function withHookFileLock(fn, waitMs = 0) {
7698
+ const token = `${process.pid}-${randomUUID4()}`;
7699
+ const deadline = Date.now() + waitMs;
7700
+ for (let vanished = 0; ; ) {
7701
+ try {
7702
+ await writeFile14(lockPath2(), token, { flag: "wx", mode: 384 });
7703
+ break;
7704
+ } catch (err) {
7705
+ if (err.code !== "EEXIST") return null;
7706
+ }
7707
+ const held = await lockHolder();
7708
+ if (held === null) {
7709
+ vanished += 1;
7710
+ if (vanished > 20) return null;
7711
+ continue;
7712
+ }
7713
+ if (held.ageMs > STALE_LOCK_MS && await takeOver(held.token)) continue;
7714
+ if (Date.now() >= deadline) return null;
7715
+ await new Promise((r) => setTimeout(r, 50));
7716
+ }
7717
+ try {
7718
+ return await fn();
7719
+ } finally {
7720
+ await unlinkIfStill(token);
7721
+ }
7722
+ }
7723
+ async function clearStaleTemps(path) {
7724
+ const dir = dirname8(path);
7725
+ const prefix = `${basename3(path)}.`;
7726
+ const names = await readdir8(dir).catch(() => []);
7727
+ await Promise.all(
7728
+ names.filter((n) => n.startsWith(prefix) && /\.\d+\.tmp$/.test(n)).map(async (n) => {
7729
+ const age = await stat7(join24(dir, n)).then((s) => Date.now() - s.mtimeMs).catch(() => 0);
7730
+ if (age > STALE_LOCK_MS) await unlink5(join24(dir, n)).catch(() => void 0);
7731
+ })
7732
+ );
7733
+ }
7734
+ async function readSettings(path) {
7735
+ const raw = await readFile24(path, "utf8").catch(() => null);
7736
+ if (raw === null) return void 0;
7737
+ try {
7738
+ return JSON.parse(raw);
7739
+ } catch {
7740
+ return null;
7741
+ }
7742
+ }
7743
+ async function refreshHookFile(path, moves) {
7744
+ const info = await lstat5(path).catch(() => null);
7745
+ if (info === null) return "missing";
7746
+ if (info.isSymbolicLink()) return "symlink";
7747
+ await clearStaleTemps(path);
7748
+ const first = moveShippedCommands(await readSettings(path), moves);
7749
+ if (first === null) return "refused";
7750
+ if (first.changed === 0) return "unchanged";
7751
+ const outcome = await withHookFileLock(async () => {
7752
+ const again = moveShippedCommands(await readSettings(path), moves);
7753
+ if (again === null) return "refused";
7754
+ if (again.changed === 0) return "unchanged";
7755
+ await writeThenRename(path, `${JSON.stringify(again.next, null, 2)}
7756
+ `);
7757
+ await chmod(path, info.mode & 4095).catch(() => void 0);
7758
+ return "changed";
7759
+ });
7760
+ return outcome ?? "busy";
7761
+ }
7762
+ function refreshNotice(report3, told) {
7763
+ const lines2 = [];
7764
+ if (report3.changed.length > 0) {
7765
+ lines2.push(
7766
+ `memgineering brought its own hooks up to this version in ${report3.changed.join(", ")} \u2014 the before-edit check now answers an already-served session without starting node.`
7767
+ );
7768
+ if (report3.changed.some((p) => /[/\\]\.codex[/\\]hooks\.json$/.test(p))) {
7769
+ lines2.push(
7770
+ "Codex will ask to review the changed hook the next time it starts: trust it (press `t`) \u2014 until then neither memgineering hook runs in Codex."
7771
+ );
7772
+ }
7773
+ }
7774
+ const nowTold = [...told];
7775
+ for (const skip of report3.skipped) {
7776
+ const key2 = `${skip.path}:${skip.why}`;
7777
+ if (nowTold.includes(key2)) continue;
7778
+ nowTold.push(key2);
7779
+ lines2.push(
7780
+ skip.why === "symlink" ? `memgineering left its hooks in ${skip.path} as they are: the file is a link, and rewriting it would replace the link. \`memgineering setup\` updates them when you want it to.` : `memgineering left its hooks in ${skip.path} as they are: the file does not parse as JSON. Fix it, then \`memgineering setup\` updates them.`
7781
+ );
7782
+ }
7783
+ return { text: lines2.length === 0 ? null : lines2.join("\n"), told: nowTold };
7784
+ }
7785
+ var STALE_LOCK_MS, lockPath2, takeoverPath;
7786
+ var init_hook_refresh = __esm({
7787
+ "src/lib/hook-refresh.ts"() {
7788
+ "use strict";
7789
+ init_atomic();
7790
+ init_brand();
7791
+ STALE_LOCK_MS = 3e4;
7792
+ lockPath2 = () => join24(brandHome(), "hook-files.lock");
7793
+ takeoverPath = () => `${lockPath2()}.takeover`;
7794
+ }
7795
+ });
7796
+
7549
7797
  // src/commands/setup-brain.ts
7550
7798
  function migration(localBrains) {
7551
7799
  return ` They also keep ${localBrains} brain(s) in folders on this machine. Offer moving those notes to the account instead of starting empty: \`memgineering push --brain <name>\` uploads a folder into a hosted brain and points here (nothing local is deleted), then \`memgineering reindex\` makes the moved notes searchable by meaning. The memgineering-setup skill has the migration steps.`;
@@ -7880,38 +8128,38 @@ var init_setup_signin = __esm({
7880
8128
  });
7881
8129
 
7882
8130
  // src/lib/next-step.ts
7883
- import { mkdir as mkdir15, readFile as readFile24, writeFile as writeFile13 } from "fs/promises";
7884
- import { join as join23 } from "path";
8131
+ import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
8132
+ import { join as join25 } from "path";
7885
8133
  function isSetUp(cfg) {
7886
8134
  return Object.keys(cfg.system.installed).length > 0;
7887
8135
  }
7888
8136
  function connectedMarker(at) {
7889
- return join23(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
8137
+ return join25(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
7890
8138
  }
7891
8139
  async function connectedNoticeShown(at) {
7892
- return readFile24(connectedMarker(at), "utf8").then(
8140
+ return readFile25(connectedMarker(at), "utf8").then(
7893
8141
  () => true,
7894
8142
  () => false
7895
8143
  );
7896
8144
  }
7897
8145
  async function markConnectedNoticeShown(at) {
7898
8146
  try {
7899
- await mkdir15(brandHome(), { recursive: true });
7900
- await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
8147
+ await mkdir16(brandHome(), { recursive: true });
8148
+ await writeFile15(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
7901
8149
  `);
7902
8150
  } catch {
7903
8151
  }
7904
8152
  }
7905
8153
  async function claimFullSetupNotice() {
7906
- const marker = join23(brandHome(), "setup-notice-shown");
8154
+ const marker = join25(brandHome(), "setup-notice-shown");
7907
8155
  try {
7908
- await readFile24(marker, "utf8");
8156
+ await readFile25(marker, "utf8");
7909
8157
  return false;
7910
8158
  } catch {
7911
8159
  }
7912
8160
  try {
7913
- await mkdir15(brandHome(), { recursive: true });
7914
- await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
8161
+ await mkdir16(brandHome(), { recursive: true });
8162
+ await writeFile15(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
7915
8163
  `);
7916
8164
  } catch {
7917
8165
  }
@@ -8077,12 +8325,12 @@ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
8077
8325
  // src/lib/provenance.ts
8078
8326
  import { execFile, spawn as spawn2 } from "child_process";
8079
8327
  import { realpath as realpath3 } from "fs/promises";
8080
- import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
8328
+ import { dirname as dirname9, join as join26, resolve as resolve14 } from "path";
8081
8329
  import { fileURLToPath as fileURLToPath2 } from "url";
8082
8330
  import { promisify } from "util";
8083
8331
  async function detectProvenance(selfPath, opts = {}) {
8084
8332
  const entry = selfPath ?? fileURLToPath2(import.meta.url);
8085
- const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
8333
+ const selfDir = await realpath3(dirname9(entry)).catch(() => dirname9(entry));
8086
8334
  if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
8087
8335
  return {
8088
8336
  kind: "npx",
@@ -8200,7 +8448,7 @@ function isInside2(child, parent) {
8200
8448
  }
8201
8449
  async function unreachableByName(selfPath) {
8202
8450
  const entry = selfPath ?? fileURLToPath2(import.meta.url);
8203
- const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
8451
+ const selfDir = await realpath3(dirname9(entry)).catch(() => dirname9(entry));
8204
8452
  if (!/[\\/](node_modules|_npx|_cacache)[\\/]/.test(`${selfDir}/`)) return null;
8205
8453
  const p = await detectProvenance(entry);
8206
8454
  return p.kind === "local" || p.kind === "npx" ? p.reason : null;
@@ -8215,7 +8463,7 @@ var init_provenance = __esm({
8215
8463
  kind: "npm",
8216
8464
  bin: "npm",
8217
8465
  prefixArgs: ["prefix", "-g"],
8218
- moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
8466
+ moduleRoot: (prefix) => process.platform === "win32" ? join26(prefix, "node_modules") : join26(prefix, "lib", "node_modules"),
8219
8467
  install: "npm i -g memgineering@latest"
8220
8468
  },
8221
8469
  {
@@ -8252,9 +8500,9 @@ __export(setup_cloud_exports, {
8252
8500
  });
8253
8501
  import { spawn as spawn3 } from "child_process";
8254
8502
  import { realpathSync as realpathSync2 } from "fs";
8255
- import { readdir as readdir7, stat as stat6 } from "fs/promises";
8503
+ import { readdir as readdir9, stat as stat8 } from "fs/promises";
8256
8504
  import { homedir as homedir5 } from "os";
8257
- import { basename as basename3, dirname as dirname9, join as join25, resolve as resolve15, sep as sep4 } from "path";
8505
+ import { basename as basename4, dirname as dirname10, join as join27, resolve as resolve15, sep as sep4 } from "path";
8258
8506
  async function report2(setupToken, state, detail, code) {
8259
8507
  try {
8260
8508
  await apiRequest({
@@ -8272,16 +8520,16 @@ async function report2(setupToken, state, detail, code) {
8272
8520
  }
8273
8521
  async function folderCandidates() {
8274
8522
  const home = homedir5();
8275
- const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
8523
+ const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join27(home, d));
8276
8524
  const found = [];
8277
8525
  const countNotes = async (dir) => {
8278
8526
  let count = 0;
8279
- const entries = await readdir7(dir, { withFileTypes: true }).catch(() => []);
8527
+ const entries = await readdir9(dir, { withFileTypes: true }).catch(() => []);
8280
8528
  for (const entry of entries) {
8281
8529
  if (entry.name.startsWith(".")) continue;
8282
8530
  if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
8283
8531
  else if (entry.isDirectory()) {
8284
- const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
8532
+ const inner = await readdir9(join27(dir, entry.name)).catch(() => []);
8285
8533
  count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
8286
8534
  }
8287
8535
  if (count > 999) break;
@@ -8289,7 +8537,7 @@ async function folderCandidates() {
8289
8537
  return count;
8290
8538
  };
8291
8539
  for (const root of roots) {
8292
- const info = await stat6(root).catch(() => null);
8540
+ const info = await stat8(root).catch(() => null);
8293
8541
  if (!info?.isDirectory()) continue;
8294
8542
  const notes = await countNotes(root);
8295
8543
  if (notes > 0) found.push({ path: root, note_count: notes });
@@ -8624,9 +8872,9 @@ function insideFolder(target, folder) {
8624
8872
  realpathSync2(existing);
8625
8873
  break;
8626
8874
  } catch {
8627
- const parent = dirname9(existing);
8875
+ const parent = dirname10(existing);
8628
8876
  if (parent === existing) break;
8629
- rest.unshift(basename3(existing));
8877
+ rest.unshift(basename4(existing));
8630
8878
  existing = parent;
8631
8879
  }
8632
8880
  }
@@ -8673,14 +8921,14 @@ var init_setup_cloud = __esm({
8673
8921
 
8674
8922
  // src/commands/setup.ts
8675
8923
  import { createHash as createHash9 } from "crypto";
8676
- import { access as access2, mkdir as mkdir16, readFile as readFile25 } from "fs/promises";
8677
- import { dirname as dirname10, join as join26 } from "path";
8924
+ import { access as access2, mkdir as mkdir17, readFile as readFile26 } from "fs/promises";
8925
+ import { dirname as dirname11, join as join28 } from "path";
8678
8926
  import { fileURLToPath as fileURLToPath3 } from "url";
8679
- import { Command as Command40 } from "commander";
8927
+ import { Command as Command38 } from "commander";
8680
8928
  import prompts5 from "prompts";
8681
8929
  import { homedir as homedir6 } from "os";
8682
8930
  function setupCommand() {
8683
- 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(
8931
+ return new Command38("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(
8684
8932
  "--wait",
8685
8933
  "with --web: block until the screen is finished, even with nobody watching the terminal"
8686
8934
  ).option(
@@ -9041,7 +9289,7 @@ async function applySetup(choice, dryRun) {
9041
9289
  }
9042
9290
  async function write2() {
9043
9291
  for (const tool of choice.tools) {
9044
- const hubPath = join26(agentHome(), tool.hubFile);
9292
+ const hubPath = join28(agentHome(), tool.hubFile);
9045
9293
  const written = await injectBlock(
9046
9294
  hubPath,
9047
9295
  hub + tailFor(tool, choice.hook),
@@ -9056,8 +9304,8 @@ async function applySetup(choice, dryRun) {
9056
9304
  });
9057
9305
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
9058
9306
  for (const skill of BRAND.subSkills) {
9059
- const content = await loadAsset(join26(skill, "SKILL.md"));
9060
- const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
9307
+ const content = await loadAsset(join28(skill, "SKILL.md"));
9308
+ const path = join28(agentHome(), tool.skillsDir, skill, "SKILL.md");
9061
9309
  const out = await writeIfDifferent(path, content, dryRun);
9062
9310
  results.push({
9063
9311
  tool: tool.label,
@@ -9073,7 +9321,14 @@ async function applySetup(choice, dryRun) {
9073
9321
  results.push({
9074
9322
  tool: tool.label,
9075
9323
  what: hook.what,
9076
- result: choice.hook ? await installHook(hook, tool, dryRun) : await removeHook(hook, tool, dryRun)
9324
+ // Under the lock every writer of these files takes, so a session
9325
+ // starting meanwhile cannot refresh the same file under this write.
9326
+ // A lock nobody gives up within five seconds is a crashed holder or
9327
+ // a stuck one; the write goes ahead rather than leaving setup hung.
9328
+ result: await (async () => {
9329
+ const write3 = () => choice.hook ? installHook(hook, tool, dryRun) : removeHook(hook, tool, dryRun);
9330
+ return await withHookFileLock(write3, 5e3) ?? write3();
9331
+ })()
9077
9332
  });
9078
9333
  }
9079
9334
  }
@@ -9140,7 +9395,7 @@ async function hooksArmed(tools) {
9140
9395
  const capable = tools.filter((t) => t.hooks && t.hooksFile);
9141
9396
  if (capable.length === 0) return void 0;
9142
9397
  for (const tool of capable) {
9143
- const raw = await readFile25(join26(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
9398
+ const raw = await readFile26(join28(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
9144
9399
  () => ""
9145
9400
  );
9146
9401
  if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
@@ -9180,7 +9435,7 @@ async function detectTools() {
9180
9435
  const home = agentHome();
9181
9436
  const found = [];
9182
9437
  for (const tool of TOOLS) {
9183
- if (await exists2(join26(home, tool.dir))) found.push(tool);
9438
+ if (await exists2(join28(home, tool.dir))) found.push(tool);
9184
9439
  }
9185
9440
  return found;
9186
9441
  }
@@ -9280,10 +9535,10 @@ async function injectBlock(filePath, content, dryRun, weWroteThis) {
9280
9535
  const block = `${BRAND.hubMarkerStart}
9281
9536
  ${content.trimEnd()}
9282
9537
  ${BRAND.hubMarkerEnd}`;
9283
- const existing = await readFile25(filePath, "utf8").catch(() => null);
9538
+ const existing = await readFile26(filePath, "utf8").catch(() => null);
9284
9539
  if (existing === null) {
9285
9540
  if (!dryRun) {
9286
- await mkdir16(dirname10(filePath), { recursive: true });
9541
+ await mkdir17(dirname11(filePath), { recursive: true });
9287
9542
  await writeThenRename(filePath, `${block}
9288
9543
  `);
9289
9544
  }
@@ -9323,23 +9578,56 @@ function looksLikeOurGuide(text) {
9323
9578
  return text.includes("memgineering recall") && text.includes("memgineering remember");
9324
9579
  }
9325
9580
  async function writeIfDifferent(filePath, content, dryRun) {
9326
- const existing = await readFile25(filePath, "utf8").catch(() => null);
9581
+ const existing = await readFile26(filePath, "utf8").catch(() => null);
9327
9582
  const after = fingerprint(content);
9328
9583
  if (existing === content) return { outcome: "unchanged", before: existing, after };
9329
9584
  if (!dryRun) {
9330
- await mkdir16(dirname10(filePath), { recursive: true });
9585
+ await mkdir17(dirname11(filePath), { recursive: true });
9331
9586
  await writeThenRename(filePath, content);
9332
9587
  }
9333
9588
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
9334
9589
  }
9590
+ function guardCommandFor(platform2) {
9591
+ return platform2 === "win32" ? "memgineering guard || true" : guardHookCommand();
9592
+ }
9593
+ async function refreshOwnHookCommands() {
9594
+ const moves = HOOKS.filter((h) => h.event !== "Stop").map((h) => ({
9595
+ event: h.event,
9596
+ from: SHIPPED_HOOK_COMMANDS[h.event],
9597
+ to: h.command
9598
+ }));
9599
+ const changed = [];
9600
+ const skipped = [];
9601
+ for (const tool of TOOLS.filter((t) => t.hooks && t.hooksFile && DEFAULT_HOOK_TOOLS.includes(t.id))) {
9602
+ const path = join28(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9603
+ const outcome = await refreshHookFile(path, moves);
9604
+ if (outcome === "changed") changed.push(path);
9605
+ else if (outcome === "symlink" || outcome === "refused") skipped.push({ path, why: outcome });
9606
+ }
9607
+ return { changed, skipped };
9608
+ }
9609
+ async function refreshOwnHooksAndTell(say) {
9610
+ try {
9611
+ const report3 = await refreshOwnHookCommands();
9612
+ if (report3.changed.length === 0 && report3.skipped.length === 0) return;
9613
+ const cfg = await loadConfig();
9614
+ const told = cfg.system.hookNoticesTold ?? [];
9615
+ const { text, told: nowTold } = refreshNotice(report3, told);
9616
+ if (nowTold.length !== told.length) {
9617
+ await saveConfig({ ...cfg, system: { ...cfg.system, hookNoticesTold: nowTold } });
9618
+ }
9619
+ if (text !== null) say(text);
9620
+ } catch {
9621
+ }
9622
+ }
9335
9623
  function hooksFor(tools) {
9336
9624
  return HOOKS.filter(
9337
9625
  (h) => tools.some((t) => t.hooks && (h.tools ?? DEFAULT_HOOK_TOOLS).includes(t.id))
9338
9626
  );
9339
9627
  }
9340
9628
  async function installHook(spec, tool, dryRun) {
9341
- const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9342
- const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9629
+ const settingsPath = join28(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9630
+ const raw = await readFile26(settingsPath, "utf8").catch(() => null);
9343
9631
  let settings = {};
9344
9632
  if (raw !== null) {
9345
9633
  try {
@@ -9381,15 +9669,15 @@ async function installHook(spec, tool, dryRun) {
9381
9669
  }
9382
9670
  const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
9383
9671
  if (!dryRun) {
9384
- await mkdir16(dirname10(settingsPath), { recursive: true });
9672
+ await mkdir17(dirname11(settingsPath), { recursive: true });
9385
9673
  await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
9386
9674
  `);
9387
9675
  }
9388
9676
  return raw === null ? "injected" : "updated";
9389
9677
  }
9390
9678
  async function removeHook(spec, tool, dryRun) {
9391
- const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9392
- const raw = await readFile25(settingsPath, "utf8").catch(() => null);
9679
+ const settingsPath = join28(agentHome(), ...(tool.hooksFile ?? "").split("/"));
9680
+ const raw = await readFile26(settingsPath, "utf8").catch(() => null);
9393
9681
  if (raw === null) return "unchanged";
9394
9682
  let parsed;
9395
9683
  try {
@@ -9413,7 +9701,7 @@ async function removeHook(spec, tool, dryRun) {
9413
9701
  return "removed";
9414
9702
  }
9415
9703
  async function loadAsset(relPath) {
9416
- const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
9704
+ const raw = await readFile26(join28(await assetRoot(), relPath), "utf8");
9417
9705
  if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
9418
9706
  const end = raw.indexOf("\n---", 4);
9419
9707
  if (end !== -1) return raw.slice(end + 4).trimStart();
@@ -9421,14 +9709,14 @@ async function loadAsset(relPath) {
9421
9709
  return raw;
9422
9710
  }
9423
9711
  async function assetRoot() {
9424
- const here = dirname10(fileURLToPath3(import.meta.url));
9425
- const bundled = join26(here, "..", "assets");
9426
- if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
9712
+ const here = dirname11(fileURLToPath3(import.meta.url));
9713
+ const bundled = join28(here, "..", "assets");
9714
+ if (await exists2(join28(bundled, BRAND.hubFileName))) return bundled;
9427
9715
  let dir = here;
9428
9716
  for (; ; ) {
9429
- const candidate = join26(dir, "packages", "skills");
9430
- if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
9431
- const parent = dirname10(dir);
9717
+ const candidate = join28(dir, "packages", "skills");
9718
+ if (await exists2(join28(candidate, BRAND.hubFileName))) return candidate;
9719
+ const parent = dirname11(dir);
9432
9720
  if (parent === dir) break;
9433
9721
  dir = parent;
9434
9722
  }
@@ -9446,13 +9734,15 @@ async function exists2(path) {
9446
9734
  return false;
9447
9735
  }
9448
9736
  }
9449
- var TOOLS, HOSTED_CONNECTOR_URL, SUPPORTED_LIST, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOK_WHAT, HOOKS;
9737
+ var TOOLS, HOSTED_CONNECTOR_URL, SUPPORTED_LIST, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOK_WHAT, HOOKS, SHIPPED_HOOK_COMMANDS;
9450
9738
  var init_setup = __esm({
9451
9739
  "src/commands/setup.ts"() {
9452
9740
  "use strict";
9453
9741
  init_atomic();
9454
9742
  init_brand();
9455
9743
  init_deadline();
9744
+ init_guard_shell();
9745
+ init_hook_refresh();
9456
9746
  init_errors();
9457
9747
  init_setup_brain();
9458
9748
  init_setup_signin();
@@ -9580,7 +9870,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
9580
9870
  // here would mean a session that reads and answers never hears a rule.
9581
9871
  codex: "apply_patch|Bash"
9582
9872
  },
9583
- command: "memgineering guard || true",
9873
+ command: guardCommandFor(process.platform),
9584
9874
  signature: "memgineering guard"
9585
9875
  },
9586
9876
  {
@@ -9606,6 +9896,13 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
9606
9896
  tools: ["grok"]
9607
9897
  }
9608
9898
  ];
9899
+ SHIPPED_HOOK_COMMANDS = {
9900
+ SessionStart: [
9901
+ "memgineering resurface --limit 5 || true",
9902
+ `memgineering resurface --limit 5 --deadline ${HOOK_DEADLINE_MS} || true`
9903
+ ],
9904
+ PreToolUse: ["memgineering guard || true"]
9905
+ };
9609
9906
  }
9610
9907
  });
9611
9908
 
@@ -13372,24 +13669,11 @@ function capTemplates(cards, isTemplate, inConflict = () => false) {
13372
13669
  }
13373
13670
  return { kept, withheld };
13374
13671
  }
13375
- function bodyOf2(content) {
13376
- const stripped = content.replace(/^/, "");
13377
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
13378
- return (fence2 ? stripped.slice(fence2[0].length) : stripped).trim();
13379
- }
13380
- function templatesFor(path, current) {
13381
- const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
13382
- return current === void 0 ? retired : [current, ...retired];
13383
- }
13384
13672
  function isBaseFilePath(path) {
13385
13673
  return path.startsWith("01_BASE/");
13386
13674
  }
13387
13675
  function bodyStillTemplate(path, content) {
13388
- const template = ownEntry(files("brain"), path);
13389
- if (template === void 0) return false;
13390
- const body = bodyOf2(content);
13391
- if (body === "") return false;
13392
- return templatesFor(path, template).some((t) => body === bodyOf2(t));
13676
+ return isTemplateBody(path, content);
13393
13677
  }
13394
13678
  async function scaffoldingAmong(adapter, notes) {
13395
13679
  const basePaths = new Set(BASE_FILES.map((f) => f.path));
@@ -13404,19 +13688,18 @@ async function scaffoldingAmong(adapter, notes) {
13404
13688
  async function untouchedHub(adapter, title) {
13405
13689
  const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
13406
13690
  if (actual === null) return false;
13407
- return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
13691
+ return asSaved2(actual) === asSaved2(files(title)[HUB_PATH] ?? "");
13692
+ }
13693
+ function asSaved2(text) {
13694
+ const BOM3 = String.fromCharCode(65279);
13695
+ return (text.startsWith(BOM3) ? text.slice(BOM3.length) : text).replace(/\r\n/g, "\n").trimEnd();
13408
13696
  }
13409
13697
  async function untouchedBase(adapter, paths) {
13410
- const templates = files("brain");
13411
13698
  const out = /* @__PURE__ */ new Set();
13412
13699
  await Promise.all(
13413
13700
  paths.map(async (path) => {
13414
- const template = ownEntry(templates, path);
13415
- if (template === void 0) return;
13416
13701
  const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
13417
- if (actual === null) return;
13418
- const trimmed = actual.trimEnd();
13419
- if (templatesFor(path, template).some((t) => trimmed === t.trimEnd())) out.add(path);
13702
+ if (actual !== null && isExactUntouchedBase(path, actual)) out.add(path);
13420
13703
  })
13421
13704
  );
13422
13705
  return out;
@@ -14067,6 +14350,69 @@ init_errors();
14067
14350
  init_index_build();
14068
14351
  init_cloud_brain();
14069
14352
  init_vault();
14353
+
14354
+ // src/lib/guard-fast.ts
14355
+ init_brand();
14356
+ import { randomUUID as randomUUID2 } from "crypto";
14357
+ import { mkdir as mkdir11, readdir as readdir6, rename as rename4, stat as stat3, unlink as unlink3, utimes, writeFile as writeFile9 } from "fs/promises";
14358
+ import { join as join17 } from "path";
14359
+ var fastDir = () => join17(brandHome(), "guard", "fast");
14360
+ var EMPTY_TTL_S = 120;
14361
+ var SESSION_ID = /^[A-Za-z0-9-]{1,128}$/;
14362
+ var EMPTY_MARK = /^(.+)\.empty\.(\d+)$/;
14363
+ var RULES_CHANGED = ".rules-changed";
14364
+ var TMP_PREFIX = ".tmp-";
14365
+ async function place(name, asOfMs) {
14366
+ await mkdir11(fastDir(), { recursive: true, mode: 448 });
14367
+ const tmp = join17(fastDir(), `${TMP_PREFIX}${process.pid}-${randomUUID2()}`);
14368
+ try {
14369
+ await writeFile9(tmp, "", { mode: 384 });
14370
+ const dated = (asOfMs - 1e3) / 1e3;
14371
+ await utimes(tmp, dated, dated);
14372
+ await rename4(tmp, join17(fastDir(), name));
14373
+ } catch (err) {
14374
+ await unlink3(tmp).catch(() => void 0);
14375
+ throw err;
14376
+ }
14377
+ }
14378
+ async function markServed(sessionId, asOfMs) {
14379
+ if (!SESSION_ID.test(sessionId)) return;
14380
+ await place(sessionId, asOfMs).catch(() => void 0);
14381
+ }
14382
+ async function markEmpty(sessionId, asOfMs) {
14383
+ if (!SESSION_ID.test(sessionId)) return;
14384
+ const until = Math.floor(asOfMs / 1e3) + EMPTY_TTL_S;
14385
+ await place(`${sessionId}.empty.${until}`, asOfMs).catch(() => void 0);
14386
+ await unlink3(join17(fastDir(), sessionId)).catch(() => void 0);
14387
+ }
14388
+ async function forgetEmptyChecks() {
14389
+ await mkdir11(fastDir(), { recursive: true, mode: 448 }).then(() => writeFile9(join17(fastDir(), RULES_CHANGED), "", { mode: 384 })).catch(() => void 0);
14390
+ const names = await readdir6(fastDir()).catch(() => []);
14391
+ await Promise.all(
14392
+ names.filter((name) => EMPTY_MARK.test(name)).map((name) => unlink3(join17(fastDir(), name)).catch(() => void 0))
14393
+ );
14394
+ }
14395
+ var SERVED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14396
+ var TMP_TTL_MS = 60 * 1e3;
14397
+ async function pruneFast(nowMs = Date.now()) {
14398
+ const names = await readdir6(fastDir()).catch(() => []);
14399
+ const nowS = Math.floor(nowMs / 1e3);
14400
+ await Promise.all(
14401
+ names.map(async (name) => {
14402
+ const path = join17(fastDir(), name);
14403
+ const empty = EMPTY_MARK.exec(name);
14404
+ if (empty) {
14405
+ if (Number(empty[2]) <= nowS) await unlink3(path).catch(() => void 0);
14406
+ return;
14407
+ }
14408
+ const info = await stat3(path).catch(() => null);
14409
+ const ttl = name.startsWith(TMP_PREFIX) ? TMP_TTL_MS : SERVED_TTL_MS;
14410
+ if (info && nowMs - info.mtimeMs > ttl) await unlink3(path).catch(() => void 0);
14411
+ })
14412
+ );
14413
+ }
14414
+
14415
+ // src/commands/remember.ts
14070
14416
  init_ui();
14071
14417
  function rememberCommand() {
14072
14418
  const said = { scope: false, noScope: false };
@@ -14140,6 +14486,7 @@ function rememberCommand() {
14140
14486
  const kindLanded = kind === void 0 || written.kind === kind;
14141
14487
  const boundaryLanded = neverStore.length === 0 || Array.isArray(written.never_store) && neverStore.every((term) => written.never_store?.includes(term) === true);
14142
14488
  const overrideLanded = opts.overrideBoundary === void 0 || "boundary_override" in written;
14489
+ if (binding) await forgetEmptyChecks();
14143
14490
  printDual({
14144
14491
  json: {
14145
14492
  id: written.id,
@@ -14774,6 +15121,7 @@ async function reviseHosted(where, ref, opts, added) {
14774
15121
  }
14775
15122
  }
14776
15123
  }
15124
+ if (opts.rule === true) await forgetEmptyChecks();
14777
15125
  printDual({
14778
15126
  json: {
14779
15127
  brain: where.name,
@@ -16517,7 +16865,7 @@ async function deleteCloudBrain(args) {
16517
16865
 
16518
16866
  // src/commands/curation.ts
16519
16867
  init_src();
16520
- import { stat as stat3 } from "fs/promises";
16868
+ import { stat as stat4 } from "fs/promises";
16521
16869
  import { resolve as resolve8 } from "path";
16522
16870
  import { Command as Command14 } from "commander";
16523
16871
 
@@ -16833,7 +17181,7 @@ async function mustMatchAFile(root, adapter, path) {
16833
17181
  const wanted = path.normalize("NFC").replace(/^\/+/, "");
16834
17182
  const absolute = resolve8(root, wanted);
16835
17183
  if (isInside(absolute, root)) {
16836
- const found = await stat3(absolute).catch(() => null);
17184
+ const found = await stat4(absolute).catch(() => null);
16837
17185
  if (found?.isFile()) return wanted;
16838
17186
  }
16839
17187
  const hits = sameBasename(await adapter.list(""), path);
@@ -17407,8 +17755,8 @@ function ago(iso, now) {
17407
17755
  init_brand();
17408
17756
  init_config();
17409
17757
  import { createHash as createHash7 } from "crypto";
17410
- import { mkdir as mkdir11, readdir as readdir6, stat as stat4, unlink as unlink3, writeFile as writeFile9 } from "fs/promises";
17411
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join18, resolve as resolve9 } from "path";
17758
+ import { mkdir as mkdir12, readdir as readdir7, stat as stat5, unlink as unlink4, writeFile as writeFile10 } from "fs/promises";
17759
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join19, resolve as resolve9 } from "path";
17412
17760
  import { Command as Command17 } from "commander";
17413
17761
 
17414
17762
  // src/lib/rules.ts
@@ -17479,6 +17827,7 @@ function renderRuleGroups(rules, project) {
17479
17827
  init_src();
17480
17828
  init_deadline();
17481
17829
  init_cloud_brain();
17830
+ init_vault();
17482
17831
  function guardCommand() {
17483
17832
  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) => {
17484
17833
  try {
@@ -17543,22 +17892,35 @@ async function runStopGuard(payload, contextDirFlag) {
17543
17892
  });
17544
17893
  }
17545
17894
  async function claimRules(sessionId, contextDir) {
17895
+ const asOf = Date.now();
17546
17896
  const where = await resolveTarget({}).catch(() => null);
17547
17897
  if (where?.kind === "cloud") {
17548
- if (!await claimSession(sessionId, where.brainId)) return null;
17898
+ if (!await claimSession(sessionId, where.brainId)) {
17899
+ if (await claimExists(sessionId, where.brainId)) await markServed(sessionId, asOf);
17900
+ return null;
17901
+ }
17549
17902
  const hosted = await hostedRuleLines(where, contextDir).catch(() => null);
17550
17903
  if (hosted !== null) {
17551
- if (hosted.lines.length > 0) return { lines: hosted.lines, total: hosted.total };
17904
+ if (hosted.lines.length > 0) {
17905
+ await markServed(sessionId, asOf);
17906
+ return { lines: hosted.lines, total: hosted.total };
17907
+ }
17552
17908
  await releaseSession(sessionId, where.brainId);
17909
+ await markEmpty(sessionId, asOf);
17553
17910
  return null;
17554
17911
  }
17555
17912
  await releaseSession(sessionId, where.brainId);
17556
17913
  }
17914
+ const named = await resolveBrain(void 0, contextDir).catch(() => null);
17915
+ if (named === null) return null;
17916
+ const root = named.brain.root;
17917
+ if (!await claimSession(sessionId, root)) return null;
17557
17918
  const collected = await collectRules(void 0, contextDir).catch(() => null);
17558
- if (collected === null) return null;
17919
+ if (collected === null || collected.rules.length === 0) {
17920
+ await releaseSession(sessionId, root);
17921
+ return null;
17922
+ }
17559
17923
  const { brainRoot, rules, total } = collected;
17560
- if (rules.length === 0) return null;
17561
- if (!await claimSession(sessionId, brainRoot)) return null;
17562
17924
  await recordEvent(brainRoot, {
17563
17925
  verb: "guard",
17564
17926
  contextDir,
@@ -17614,7 +17976,7 @@ async function readPayload() {
17614
17976
  return null;
17615
17977
  }
17616
17978
  }
17617
- var guardDir = () => join18(brandHome(), "guard");
17979
+ var guardDir = () => join19(brandHome(), "guard");
17618
17980
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
17619
17981
  "Write",
17620
17982
  "Edit",
@@ -17635,7 +17997,7 @@ async function misdeliveryNotice(toolName, filePath, sessionId) {
17635
17997
  return 'This file is this harness\u2019s own memory \u2014 no other tool the user works with will ever read it. Durable facts about the user belong in their brain, which every tool shares: memgineering remember "<the fact>" --reason "<why>" (add --rule if they stated a standing rule). Write it there instead, or in addition.';
17636
17998
  }
17637
17999
  function markerFor(sessionId, brain) {
17638
- return join18(
18000
+ return join19(
17639
18001
  guardDir(),
17640
18002
  `${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
17641
18003
  );
@@ -17643,8 +18005,8 @@ function markerFor(sessionId, brain) {
17643
18005
  async function claimSession(sessionId, brain) {
17644
18006
  const marker = markerFor(sessionId, brain);
17645
18007
  try {
17646
- await mkdir11(guardDir(), { recursive: true, mode: 448 });
17647
- await writeFile9(marker, "", { flag: "wx", mode: 384 });
18008
+ await mkdir12(guardDir(), { recursive: true, mode: 448 });
18009
+ await writeFile10(marker, "", { flag: "wx", mode: 384 });
17648
18010
  } catch {
17649
18011
  return false;
17650
18012
  }
@@ -17652,19 +18014,23 @@ async function claimSession(sessionId, brain) {
17652
18014
  return true;
17653
18015
  }
17654
18016
  async function releaseSession(sessionId, brain) {
17655
- await unlink3(markerFor(sessionId, brain)).catch(() => void 0);
18017
+ await unlink4(markerFor(sessionId, brain)).catch(() => void 0);
18018
+ }
18019
+ async function claimExists(sessionId, brain) {
18020
+ return stat5(markerFor(sessionId, brain)).then(() => true).catch(() => false);
17656
18021
  }
17657
18022
  var MARKER_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
17658
18023
  async function pruneOldMarkers() {
17659
18024
  try {
17660
18025
  const dir = guardDir();
17661
18026
  const now = Date.now();
17662
- for (const name of await readdir6(dir)) {
17663
- const path = join18(dir, name);
18027
+ for (const name of await readdir7(dir)) {
18028
+ const path = join19(dir, name);
17664
18029
  if (!isAbsolute3(path)) continue;
17665
- const info = await stat4(path).catch(() => null);
17666
- if (info && now - info.mtimeMs > MARKER_TTL_MS) await unlink3(path).catch(() => void 0);
18030
+ const info = await stat5(path).catch(() => null);
18031
+ if (info && now - info.mtimeMs > MARKER_TTL_MS) await unlink4(path).catch(() => void 0);
17667
18032
  }
18033
+ await pruneFast(now);
17668
18034
  } catch {
17669
18035
  }
17670
18036
  }
@@ -18561,7 +18927,7 @@ function onboardCommand() {
18561
18927
  ...survey.missing.length === 0 ? {} : { missing: survey.missing, missing_hint: MISSING_HINT },
18562
18928
  // Spelled out in the machine-readable form too, because an agent
18563
18929
  // parsing this is the one that has to do the asking.
18564
- how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while it is still the template.'
18930
+ how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while its body has not been written yet.'
18565
18931
  },
18566
18932
  human: () => {
18567
18933
  if (survey.unfilled.length === 0 && survey.missing.length === 0) {
@@ -18575,7 +18941,7 @@ function onboardCommand() {
18575
18941
  );
18576
18942
  return;
18577
18943
  }
18578
- printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still untouched)
18944
+ printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still to write)
18579
18945
  `);
18580
18946
  if (survey.unfilled.length > 0) {
18581
18947
  printHuman(
@@ -18616,21 +18982,23 @@ function onboardCommand() {
18616
18982
  var MISSING_HINT = "A note that does not exist cannot be revised into \u2014 `revise` answers `ref_not_found`. `memgineering create <name>` writes the base notes into a brain that holds nothing but its own scaffolding. A brain that already holds other notes takes them from a folder: `memgineering init <folder>` then `memgineering push --brain <name>`, which skips every path already up there.";
18617
18983
  async function localSurvey(where) {
18618
18984
  const adapter = await openBrainOrExplain(where.brain.root);
18619
- const untouched = await untouchedBase(
18620
- adapter,
18621
- BASE_FILES.map((f) => f.path)
18985
+ const unwritten = /* @__PURE__ */ new Set();
18986
+ await Promise.all(
18987
+ BASE_FILES.map(async (f) => {
18988
+ const content = await adapter.read(f.path).then((r) => r.content).catch(() => null);
18989
+ if (content !== null && isUnwrittenBody(f.path, content)) unwritten.add(f.path);
18990
+ })
18622
18991
  );
18623
18992
  return {
18624
18993
  identity: { brain: where.brain.root },
18625
18994
  heading: "## onboard",
18626
- unfilled: BASE_FILES.filter((f) => untouched.has(f.path)),
18627
- // A file that is not there reads as written here, and that is the shipped
18628
- // behaviour rather than a decision taken now: `untouchedBase` cannot tell
18629
- // "somebody wrote in it" from "it is gone", because both fail the
18630
- // byte-equality test (`lib/base-files.ts`). `init` writes all five, so the
18631
- // state does not arise on a folder brain. The hosted half CAN arrive
18632
- // without them, which is why it answers the question separately.
18633
- filled: BASE_FILES.filter((f) => !untouched.has(f.path)).map((f) => f.path),
18995
+ unfilled: BASE_FILES.filter((f) => unwritten.has(f.path)),
18996
+ // A file that is not there reads as written here. That is a choice kept
18997
+ // from before, not something this side cannot tell: `init` writes all
18998
+ // five, so the state does not arise on a folder brain, and asking about a
18999
+ // file that is gone would send an agent to revise a ref that does not
19000
+ // exist. The hosted half CAN arrive without them, and says so (`missing`).
19001
+ filled: BASE_FILES.filter((f) => !unwritten.has(f.path)).map((f) => f.path),
18634
19002
  missing: []
18635
19003
  };
18636
19004
  }
@@ -18639,7 +19007,7 @@ async function hostedSurvey(where) {
18639
19007
  const state = (file) => {
18640
19008
  const content = contents.get(file.path);
18641
19009
  if (content === void 0) return "missing";
18642
- return isUntouchedBase(file.path, content) ? "unfilled" : "filled";
19010
+ return isUnwrittenBody(file.path, content) ? "unfilled" : "filled";
18643
19011
  };
18644
19012
  return {
18645
19013
  identity: { brain: where.name, brain_id: where.brainId },
@@ -18680,8 +19048,8 @@ init_api_client();
18680
19048
  init_config();
18681
19049
  init_cloud_brain();
18682
19050
  init_ui();
18683
- import { mkdir as mkdir12, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
18684
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join19, resolve as resolve11, sep as sep2 } from "path";
19051
+ import { mkdir as mkdir13, readFile as readFile20, writeFile as writeFile11 } from "fs/promises";
19052
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join20, resolve as resolve11, sep as sep2 } from "path";
18685
19053
  import { Command as Command24 } from "commander";
18686
19054
  function pullCommand() {
18687
19055
  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) => {
@@ -18749,8 +19117,8 @@ function pullCommand() {
18749
19117
  already.push(note.path);
18750
19118
  continue;
18751
19119
  }
18752
- await mkdir12(dirname6(file), { recursive: true });
18753
- await writeFile10(file, note.content ?? "", "utf8");
19120
+ await mkdir13(dirname6(file), { recursive: true });
19121
+ await writeFile11(file, note.content ?? "", "utf8");
18754
19122
  written.push(note.path);
18755
19123
  if (note.excluded === true) excluded.push(note.path);
18756
19124
  }
@@ -18864,7 +19232,7 @@ function safeJoin(root, relative3) {
18864
19232
  "Nothing was written for it. This should not happen \u2014 please report it."
18865
19233
  );
18866
19234
  }
18867
- const full = resolve11(join19(root, relative3));
19235
+ const full = resolve11(join20(root, relative3));
18868
19236
  if (full !== root && !full.startsWith(root + sep2)) {
18869
19237
  throw memgError(
18870
19238
  "server_refused",
@@ -19786,9 +20154,9 @@ init_errors();
19786
20154
  init_credentials();
19787
20155
 
19788
20156
  // src/lib/untrusted-fence.ts
19789
- import { randomUUID as randomUUID2 } from "crypto";
20157
+ import { randomUUID as randomUUID3 } from "crypto";
19790
20158
  function makeUntrustedSentinel() {
19791
- return `KORDIS-UNTRUSTED-${randomUUID2()}`;
20159
+ return `KORDIS-UNTRUSTED-${randomUUID3()}`;
19792
20160
  }
19793
20161
  function fenceUntrusted(sentinel, text) {
19794
20162
  const neutralized = text.replace(/KORDIS-UNTRUSTED-/g, "KORDIS-UNTRUSTED\u200B-");
@@ -20284,7 +20652,7 @@ init_errors();
20284
20652
  // src/lib/harness-context.ts
20285
20653
  init_brand();
20286
20654
  import { access, readFile as readFile21 } from "fs/promises";
20287
- import { join as join20 } from "path";
20655
+ import { join as join21 } from "path";
20288
20656
  var PER_FILE_CAP = 16 * 1024;
20289
20657
  var TOTAL_CAP = 48 * 1024;
20290
20658
  var TRUNCATION_RESERVE = 64;
@@ -20329,13 +20697,13 @@ function extractHeadings(content) {
20329
20697
  }
20330
20698
  async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
20331
20699
  const candidates = [
20332
- { path: join20(cwd, "CLAUDE.md"), display: "CLAUDE.md", scope: "project" },
20333
- { path: join20(cwd, "AGENTS.md"), display: "AGENTS.md", scope: "project" },
20334
- { path: join20(cwd, "GEMINI.md"), display: "GEMINI.md", scope: "project" },
20335
- { path: join20(cwd, ".claude", "CLAUDE.md"), display: ".claude/CLAUDE.md", scope: "project" },
20336
- { path: join20(home, ".claude", "CLAUDE.md"), display: "~/.claude/CLAUDE.md", scope: "global" },
20337
- { path: join20(home, ".codex", "AGENTS.md"), display: "~/.codex/AGENTS.md", scope: "global" },
20338
- { path: join20(home, ".gemini", "GEMINI.md"), display: "~/.gemini/GEMINI.md", scope: "global" }
20700
+ { path: join21(cwd, "CLAUDE.md"), display: "CLAUDE.md", scope: "project" },
20701
+ { path: join21(cwd, "AGENTS.md"), display: "AGENTS.md", scope: "project" },
20702
+ { path: join21(cwd, "GEMINI.md"), display: "GEMINI.md", scope: "project" },
20703
+ { path: join21(cwd, ".claude", "CLAUDE.md"), display: ".claude/CLAUDE.md", scope: "project" },
20704
+ { path: join21(home, ".claude", "CLAUDE.md"), display: "~/.claude/CLAUDE.md", scope: "global" },
20705
+ { path: join21(home, ".codex", "AGENTS.md"), display: "~/.codex/AGENTS.md", scope: "global" },
20706
+ { path: join21(home, ".gemini", "GEMINI.md"), display: "~/.gemini/GEMINI.md", scope: "global" }
20339
20707
  ];
20340
20708
  const files2 = [];
20341
20709
  let totalBytes = 0;
@@ -20740,8 +21108,8 @@ function parseLimit2(raw) {
20740
21108
  import { Command as Command30 } from "commander";
20741
21109
  import prompts3 from "prompts";
20742
21110
  import { existsSync, lstatSync, realpathSync } from "fs";
20743
- import { mkdir as mkdir14, readFile as readFile23, stat as stat5, writeFile as writeFile12 } from "fs/promises";
20744
- import { dirname as dirname7, join as join22, resolve as resolve12, sep as sep3 } from "path";
21111
+ import { mkdir as mkdir15, readFile as readFile23, stat as stat6, writeFile as writeFile13 } from "fs/promises";
21112
+ import { dirname as dirname7, join as join23, resolve as resolve12, sep as sep3 } from "path";
20745
21113
 
20746
21114
  // ../../packages/translators/src/skill/identity.ts
20747
21115
  function buildSkillFile(input, outputPath) {
@@ -21322,17 +21690,17 @@ init_ui();
21322
21690
 
21323
21691
  // src/lib/lockfile.ts
21324
21692
  init_brand();
21325
- import { copyFile, mkdir as mkdir13, readFile as readFile22, writeFile as writeFile11, constants as fsConstants } from "fs/promises";
21693
+ import { copyFile, mkdir as mkdir14, readFile as readFile22, writeFile as writeFile12, constants as fsConstants } from "fs/promises";
21326
21694
  import { homedir as homedir4 } from "os";
21327
- import { join as join21 } from "path";
21695
+ import { join as join22 } from "path";
21328
21696
  function effectiveRange(entry) {
21329
21697
  return entry.requestedRange ?? `^${entry.version}`;
21330
21698
  }
21331
21699
  function lockfilePath() {
21332
- return join21(brandHome(), "installed.json");
21700
+ return join22(brandHome(), "installed.json");
21333
21701
  }
21334
21702
  function kordisLockfilePath() {
21335
- return join21(process.env["KORDIS_HOME"] ?? join21(homedir4(), ".kordis"), "installed.json");
21703
+ return join22(process.env["KORDIS_HOME"] ?? join22(homedir4(), ".kordis"), "installed.json");
21336
21704
  }
21337
21705
  function lockfileDisplayPath() {
21338
21706
  const path = lockfilePath();
@@ -21343,7 +21711,7 @@ async function migrateFromKordis() {
21343
21711
  const source = kordisLockfilePath();
21344
21712
  const destination = lockfilePath();
21345
21713
  try {
21346
- await mkdir13(brandHome(), { recursive: true });
21714
+ await mkdir14(brandHome(), { recursive: true });
21347
21715
  await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
21348
21716
  } catch {
21349
21717
  return;
@@ -21371,15 +21739,15 @@ async function readLockfile() {
21371
21739
  }
21372
21740
  }
21373
21741
  async function writeLockfile(lf) {
21374
- await mkdir13(brandHome(), { recursive: true });
21375
- await writeFile11(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
21742
+ await mkdir14(brandHome(), { recursive: true });
21743
+ await writeFile12(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
21376
21744
  encoding: "utf8",
21377
21745
  mode: 384
21378
21746
  });
21379
21747
  }
21380
21748
  async function recordInstalled(fullSlug, entry) {
21381
21749
  const home = brandHome();
21382
- await withFileLock(join21(home, "installed.lock"), home, async () => {
21750
+ await withFileLock(join22(home, "installed.lock"), home, async () => {
21383
21751
  const lf = await readLockfile();
21384
21752
  lf.packages[fullSlug] = entry;
21385
21753
  await writeLockfile(lf);
@@ -21647,7 +22015,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
21647
22015
  );
21648
22016
  for (const f of translated.files) {
21649
22017
  const abs = safeInstallPath(installRoot, f.path);
21650
- await mkdir14(dirname7(abs), { recursive: true });
22018
+ await mkdir15(dirname7(abs), { recursive: true });
21651
22019
  await applyEmittedFile(abs, f, fullSlug, notes);
21652
22020
  written.push(abs);
21653
22021
  }
@@ -21803,7 +22171,7 @@ var TOOL_MARKERS = [
21803
22171
  async function detectTool(root) {
21804
22172
  for (const [path, tool] of TOOL_MARKERS) {
21805
22173
  try {
21806
- await stat5(join22(root, path));
22174
+ await stat6(join23(root, path));
21807
22175
  return tool;
21808
22176
  } catch {
21809
22177
  }
@@ -21819,7 +22187,7 @@ function safeInstallPath(installRoot, relPath) {
21819
22187
  "Nothing was written. The package emits an absolute file path. Tell the author to publish it with relative paths only; if you did not publish it, treat it as an attempt to write outside your project."
21820
22188
  );
21821
22189
  }
21822
- const abs = join22(installRoot, relPath);
22190
+ const abs = join23(installRoot, relPath);
21823
22191
  const resolvedRoot = resolve12(installRoot);
21824
22192
  const resolvedAbs = resolve12(abs);
21825
22193
  if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + sep3)) {
@@ -21873,9 +22241,9 @@ function safeInstallPath(installRoot, relPath) {
21873
22241
  }
21874
22242
  async function writeEmittedFile(abs, content, encoding) {
21875
22243
  if (encoding === "base64") {
21876
- await writeFile12(abs, Buffer.from(content, "base64"));
22244
+ await writeFile13(abs, Buffer.from(content, "base64"));
21877
22245
  } else {
21878
- await writeFile12(abs, content, "utf8");
22246
+ await writeFile13(abs, content, "utf8");
21879
22247
  }
21880
22248
  }
21881
22249
  async function applyEmittedFile(abs, f, fullSlug, notes) {
@@ -21896,10 +22264,10 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
21896
22264
  `merged into ${f.path} \u2014 comments/formatting in the existing file are not preserved`
21897
22265
  );
21898
22266
  }
21899
- await writeFile12(abs, merged, "utf8");
22267
+ await writeFile13(abs, merged, "utf8");
21900
22268
  return;
21901
22269
  }
21902
- await writeFile12(abs, mergeFencedFile(existingRaw, fullSlug, f.content), "utf8");
22270
+ await writeFile13(abs, mergeFencedFile(existingRaw, fullSlug, f.content), "utf8");
21903
22271
  }
21904
22272
 
21905
22273
  // src/commands/registry/update.ts
@@ -22950,15 +23318,17 @@ function isScopeSpread(value) {
22950
23318
  }
22951
23319
 
22952
23320
  // src/commands/resurface.ts
23321
+ init_setup();
23322
+ init_ui();
22953
23323
  init_deadline();
22954
23324
  init_src();
22955
- import { Command as Command38 } from "commander";
23325
+ import { Command as Command39 } from "commander";
22956
23326
  init_index_build();
22957
23327
  init_cloud_brain();
22958
23328
  init_vault();
22959
23329
  init_ui();
22960
23330
  function resurfaceCommand() {
22961
- 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(
23331
+ return new Command39("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(
22962
23332
  "--brain <name>",
22963
23333
  "read one existing hosted brain by name or id, without changing the pointer"
22964
23334
  ).option(
@@ -22969,6 +23339,11 @@ function resurfaceCommand() {
22969
23339
  async (opts) => {
22970
23340
  const bounded = (work) => opts.deadline !== void 0 && Number.isFinite(opts.deadline) ? withDeadline(opts.deadline, work) : work();
22971
23341
  await bounded(async () => {
23342
+ await refreshOwnHooksAndTell(
23343
+ (text) => (isJsonMode() ? process.stderr : process.stdout).write(`${text}
23344
+
23345
+ `)
23346
+ );
22972
23347
  const contextDir = opts.contextDir ?? process.cwd();
22973
23348
  const where = await resolveTarget({
22974
23349
  ...opts.brain === void 0 ? {} : { brain: opts.brain },
@@ -23180,11 +23555,11 @@ rank by project yet, so that is not one of the signals above.`
23180
23555
  }
23181
23556
 
23182
23557
  // src/commands/rules.ts
23183
- import { Command as Command39 } from "commander";
23558
+ import { Command as Command40 } from "commander";
23184
23559
  init_cloud_brain();
23185
23560
  init_ui();
23186
23561
  function rulesCommand() {
23187
- 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(
23562
+ return new Command40("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(
23188
23563
  "--brain <name>",
23189
23564
  "read one existing hosted brain by name or id, without changing the pointer"
23190
23565
  ).action(async (opts) => {
@@ -23351,16 +23726,16 @@ init_next_step();
23351
23726
  init_provenance();
23352
23727
  import { spawn as spawn4 } from "child_process";
23353
23728
  import { randomBytes as randomBytes2 } from "crypto";
23354
- import { mkdir as mkdir18, open, readFile as readFile27, rename as rename4, rm as rm8, stat as stat7, unlink as unlink4, writeFile as writeFile15 } from "fs/promises";
23355
- import { join as join28 } from "path";
23729
+ import { mkdir as mkdir19, open, readFile as readFile28, rename as rename5, rm as rm8, stat as stat9, unlink as unlink6, writeFile as writeFile17 } from "fs/promises";
23730
+ import { join as join30 } from "path";
23356
23731
  import { Command as Command41 } from "commander";
23357
23732
 
23358
23733
  // src/lib/update-check.ts
23359
23734
  init_atomic();
23360
23735
  init_brand();
23361
23736
  import { execFile as execFile2 } from "child_process";
23362
- import { mkdir as mkdir17, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
23363
- import { join as join27 } from "path";
23737
+ import { mkdir as mkdir18, readFile as readFile27, writeFile as writeFile16 } from "fs/promises";
23738
+ import { join as join29 } from "path";
23364
23739
  import { promisify as promisify2 } from "util";
23365
23740
  import semver4 from "semver";
23366
23741
  import { z as z33 } from "zod";
@@ -23441,11 +23816,11 @@ var UpdateStateSchema = z33.object({
23441
23816
  message: z33.string().nullable()
23442
23817
  });
23443
23818
  function statePath() {
23444
- return join27(brandHome(), "update-state.json");
23819
+ return join29(brandHome(), "update-state.json");
23445
23820
  }
23446
23821
  async function readUpdateState() {
23447
23822
  try {
23448
- const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile26(statePath(), "utf8")));
23823
+ const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile27(statePath(), "utf8")));
23449
23824
  return parsed.success ? parsed.data : null;
23450
23825
  } catch {
23451
23826
  return null;
@@ -23453,7 +23828,7 @@ async function readUpdateState() {
23453
23828
  }
23454
23829
  async function writeUpdateState(state) {
23455
23830
  try {
23456
- await mkdir17(brandHome(), { recursive: true });
23831
+ await mkdir18(brandHome(), { recursive: true });
23457
23832
  await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
23458
23833
  `);
23459
23834
  } catch {
@@ -23565,7 +23940,7 @@ async function runUpdate(opts, inherited) {
23565
23940
  }
23566
23941
  const held = await holdUpdateSlot(inherited);
23567
23942
  if (held === null && await blockedBy() === "stuck") {
23568
- const lock = join28(brandHome(), RECLAIM_LOCK);
23943
+ const lock = join30(brandHome(), RECLAIM_LOCK);
23569
23944
  const hint = `A previous update did not finish, and clearing what it left is blocked by ${lock}. If no memgineering update is running, delete that folder and run \`memgineering update\` again.`;
23570
23945
  printDual({
23571
23946
  json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
@@ -23680,14 +24055,14 @@ function newClaimToken() {
23680
24055
  }
23681
24056
  async function claimOwner() {
23682
24057
  try {
23683
- return (await readFile27(join28(brandHome(), "update-running"), "utf8")).trim() || null;
24058
+ return (await readFile28(join30(brandHome(), "update-running"), "utf8")).trim() || null;
23684
24059
  } catch {
23685
24060
  return null;
23686
24061
  }
23687
24062
  }
23688
24063
  async function releaseUpdateSlot(token) {
23689
24064
  if (await claimOwner() !== token) return;
23690
- await unlink4(join28(brandHome(), "update-running")).catch(() => void 0);
24065
+ await unlink6(join30(brandHome(), "update-running")).catch(() => void 0);
23691
24066
  }
23692
24067
  async function holdUpdateSlot(inherited) {
23693
24068
  if (inherited !== void 0 && await claimOwner() === inherited) return inherited;
@@ -23699,9 +24074,9 @@ async function holdUpdateSlot(inherited) {
23699
24074
  var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
23700
24075
  var UPDATE_CLAIM_MS = 15 * 6e4;
23701
24076
  async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
23702
- const path = join28(brandHome(), "update-running");
24077
+ const path = join30(brandHome(), "update-running");
23703
24078
  try {
23704
- await mkdir18(brandHome(), { recursive: true });
24079
+ await mkdir19(brandHome(), { recursive: true });
23705
24080
  const handle = await open(path, "wx");
23706
24081
  await handle.writeFile(`${token}
23707
24082
  `);
@@ -23711,12 +24086,12 @@ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
23711
24086
  if (err?.code !== "EEXIST") return false;
23712
24087
  }
23713
24088
  try {
23714
- if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) {
24089
+ if (Date.now() - (await stat9(path)).mtimeMs >= UPDATE_CLAIM_MS) {
23715
24090
  await hooks.afterObserve?.();
23716
24091
  await withReclaimLock(async () => {
23717
- if (Date.now() - (await stat7(path)).mtimeMs < UPDATE_CLAIM_MS) return;
24092
+ if (Date.now() - (await stat9(path)).mtimeMs < UPDATE_CLAIM_MS) return;
23718
24093
  await hooks.afterRecheck?.();
23719
- await unlink4(path);
24094
+ await unlink6(path);
23720
24095
  });
23721
24096
  }
23722
24097
  } catch {
@@ -23727,13 +24102,13 @@ var RECLAIM_LOCK = "update-running.reclaim";
23727
24102
  var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
23728
24103
  async function withReclaimLock(fn) {
23729
24104
  const home = brandHome();
23730
- const lock = join28(home, RECLAIM_LOCK);
23731
- const staging = join28(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
23732
- await mkdir18(staging, { recursive: true });
23733
- await writeFile15(join28(staging, "owner"), `${process.pid} ${Date.now()}
24105
+ const lock = join30(home, RECLAIM_LOCK);
24106
+ const staging = join30(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
24107
+ await mkdir19(staging, { recursive: true });
24108
+ await writeFile17(join30(staging, "owner"), `${process.pid} ${Date.now()}
23734
24109
  `);
23735
24110
  try {
23736
- await rename4(staging, lock);
24111
+ await rename5(staging, lock);
23737
24112
  } catch {
23738
24113
  await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
23739
24114
  await breakAbandonedLock(lock);
@@ -23743,18 +24118,18 @@ async function withReclaimLock(fn) {
23743
24118
  return await fn();
23744
24119
  } finally {
23745
24120
  const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
23746
- await rename4(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
24121
+ await rename5(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
23747
24122
  }
23748
24123
  }
23749
24124
  async function breakAbandonedLock(lock) {
23750
24125
  try {
23751
- const seen = (await readFile27(join28(lock, "owner"), "utf8")).trim();
24126
+ const seen = (await readFile28(join30(lock, "owner"), "utf8")).trim();
23752
24127
  if (!lockAbandoned(seen)) return;
23753
24128
  const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
23754
- await rename4(lock, aside);
23755
- const moved = (await readFile27(join28(aside, "owner"), "utf8").catch(() => "")).trim();
24129
+ await rename5(lock, aside);
24130
+ const moved = (await readFile28(join30(aside, "owner"), "utf8").catch(() => "")).trim();
23756
24131
  if (moved !== seen) {
23757
- await rename4(aside, lock).catch(() => void 0);
24132
+ await rename5(aside, lock).catch(() => void 0);
23758
24133
  return;
23759
24134
  }
23760
24135
  await rm8(aside, { recursive: true, force: true });
@@ -23776,9 +24151,9 @@ function lockAbandoned(owner, now = Date.now()) {
23776
24151
  }
23777
24152
  async function blockedBy() {
23778
24153
  try {
23779
- const old = Date.now() - (await stat7(join28(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
24154
+ const old = Date.now() - (await stat9(join30(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
23780
24155
  if (old) {
23781
- await stat7(join28(brandHome(), RECLAIM_LOCK));
24156
+ await stat9(join30(brandHome(), RECLAIM_LOCK));
23782
24157
  return "stuck";
23783
24158
  }
23784
24159
  } catch {
@@ -23810,8 +24185,8 @@ function reportBelowMinimum(below) {
23810
24185
 
23811
24186
  // src/commands/undo.ts
23812
24187
  init_src();
23813
- import { readFile as readFile28, rm as rm9 } from "fs/promises";
23814
- import { join as join29 } from "path";
24188
+ import { readFile as readFile29, rm as rm9 } from "fs/promises";
24189
+ import { join as join31 } from "path";
23815
24190
  import { Command as Command42 } from "commander";
23816
24191
  init_atomic();
23817
24192
  init_errors();
@@ -23908,8 +24283,8 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
23908
24283
  );
23909
24284
  }
23910
24285
  const record = await withWriteLock(brain.root, async () => {
23911
- const absolute = join29(brain.root, target.target);
23912
- const current = await readFile28(absolute, "utf8").catch(() => null);
24286
+ const absolute = join31(brain.root, target.target);
24287
+ const current = await readFile29(absolute, "utf8").catch(() => null);
23913
24288
  const actualHash = current === null ? null : hashContent(current);
23914
24289
  if (actualHash !== target.hash_after) {
23915
24290
  throw memgError(
@@ -23971,7 +24346,7 @@ function minutesSince(iso) {
23971
24346
  return Math.max(0, Math.floor((Date.now() - at) / 6e4));
23972
24347
  }
23973
24348
  async function reverse(brain, op, current) {
23974
- const absolute = join29(brain.root, op.target);
24349
+ const absolute = join31(brain.root, op.target);
23975
24350
  if (op.verb === "remember") {
23976
24351
  if (current === null) {
23977
24352
  throw memgError(
@@ -24028,7 +24403,7 @@ async function conflictHint(vaultRoot, op) {
24028
24403
  Read what it says now, then decide what should stay.`;
24029
24404
  const snapshot = await readSnapshot(vaultRoot, op.op_id);
24030
24405
  return snapshot === null ? base2 : `${base2}
24031
- The text from before that change is still on this machine: ${join29(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
24406
+ The text from before that change is still on this machine: ${join31(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
24032
24407
  }
24033
24408
 
24034
24409
  // src/commands/brains.ts
@@ -24713,6 +25088,10 @@ async function run3(argv) {
24713
25088
  });
24714
25089
  await program.parseAsync(cleaned);
24715
25090
  if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
25091
+ if (ran !== void 0 && !["guard", "resurface", "setup"].includes(ran)) {
25092
+ await refreshOwnHooksAndTell((text) => process.stderr.write(`${text}
25093
+ `));
25094
+ }
24716
25095
  }
24717
25096
  async function handleError(err) {
24718
25097
  const failure = adaptRegistryError(err) ?? err;