memgineering 0.25.2 → 0.25.3

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/CHANGELOG.md CHANGED
@@ -11,6 +11,34 @@ language the reader wants. The bilingual rule the monorepo applies to
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.25.3] — 2026-10-06
15
+
16
+ ### Fixed
17
+
18
+ - **`onboard` no longer asks again for a base note you filled in with prose.**
19
+ On a hosted brain, a `USER.md` or other `01_BASE/` note whose prompt you
20
+ replaced with your own sentences still counted as unfilled, so `onboard` told
21
+ the agent to ask the same questions and write the body again — over what you
22
+ had written. A note counts as filled once its body is your words, prose or
23
+ bullets. Hosted `resurface` no longer labels such a note "not filled in yet"
24
+ either.
25
+ - **`onboard` keeps asking about a note whose body is still the prompt**, even
26
+ after a `revise` put a claim in its frontmatter. On a folder brain this used
27
+ to take the note off the list. The same goes for a note whose prompt was
28
+ deleted and nothing written in its place.
29
+ - **A starter note saved with Windows line endings (CRLF) or a byte-order mark
30
+ is still recognised as the untouched template** by `recall`, `resurface` and
31
+ `onboard`. `revise`'s "the body is still the prompt" warning now notices it
32
+ too.
33
+
34
+ ### Agent guidance
35
+
36
+ - `memgineering-writing` and `onboard`'s `how_to_write`: `onboard` lists the
37
+ base notes whose body has not been written yet, and editing one directly is
38
+ right only while that is so.
39
+ - `memgineering-recall`: a card marked `not filled in yet` is exactly what
40
+ `init` wrote, line endings aside.
41
+
14
42
  ## [0.25.2] — 2026-10-05
15
43
 
16
44
  Small fixes found by an independent review of 0.25.0 and 0.25.1. Nothing here
@@ -2,7 +2,7 @@
2
2
  name: memgineering
3
3
  description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
4
4
  type: skill
5
- version: 0.25.2
5
+ version: 0.25.3
6
6
  ---
7
7
 
8
8
  # memgineering
@@ -205,7 +205,7 @@ next brain, and three of them said it out loud. Reading another brain does not
205
205
  move the machine's pointer.
206
206
 
207
207
  **Cards that are not answers.** A card marked `not filled in yet` is one of the
208
- five `01_BASE/` files, still byte-for-byte what `init` wrote. Nobody has answered
208
+ five `01_BASE/` files, still exactly what `init` wrote (line endings aside). Nobody has answered
209
209
  it, so it is evidence of nothing — do not quote it back as what they think. When
210
210
  every card carries that mark, recall says so above them (`--json`:
211
211
  `scaffolding: true`), and that is the moment to offer to fill them in rather
@@ -359,7 +359,7 @@ so an empty one costs something every time.
359
359
  memgineering onboard
360
360
  ```
361
361
 
362
- Says which are still untouched and what to ask for each. Run it first rather
362
+ Says which still have no body written and what to ask for each. Run it first rather
363
363
  than working from a script — asking somebody a question they already answered is
364
364
  its own kind of forgetting. They may also ask outright ("set up my memory", "fill in my profile"); same job.
365
365
 
@@ -404,7 +404,7 @@ always true and you have only just been told them, so dating them today is wrong
404
404
  in the one field a memory store exists to get right.
405
405
 
406
406
  **This is the ONE place you edit a brain file directly** — `01_BASE/` only, and
407
- only while it is still the template. After that first pass these are theirs like
407
+ only while its body has not been written yet. After that first pass these are theirs like
408
408
  any other note.
409
409
 
410
410
  **Do not invent an answer to move on, and do not fill a file because it is
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"() {
@@ -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
  }
@@ -13372,24 +13391,11 @@ function capTemplates(cards, isTemplate, inConflict = () => false) {
13372
13391
  }
13373
13392
  return { kept, withheld };
13374
13393
  }
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
13394
  function isBaseFilePath(path) {
13385
13395
  return path.startsWith("01_BASE/");
13386
13396
  }
13387
13397
  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));
13398
+ return isTemplateBody(path, content);
13393
13399
  }
13394
13400
  async function scaffoldingAmong(adapter, notes) {
13395
13401
  const basePaths = new Set(BASE_FILES.map((f) => f.path));
@@ -13404,19 +13410,18 @@ async function scaffoldingAmong(adapter, notes) {
13404
13410
  async function untouchedHub(adapter, title) {
13405
13411
  const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
13406
13412
  if (actual === null) return false;
13407
- return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
13413
+ return asSaved2(actual) === asSaved2(files(title)[HUB_PATH] ?? "");
13414
+ }
13415
+ function asSaved2(text) {
13416
+ const BOM3 = String.fromCharCode(65279);
13417
+ return (text.startsWith(BOM3) ? text.slice(BOM3.length) : text).replace(/\r\n/g, "\n").trimEnd();
13408
13418
  }
13409
13419
  async function untouchedBase(adapter, paths) {
13410
- const templates = files("brain");
13411
13420
  const out = /* @__PURE__ */ new Set();
13412
13421
  await Promise.all(
13413
13422
  paths.map(async (path) => {
13414
- const template = ownEntry(templates, path);
13415
- if (template === void 0) return;
13416
13423
  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);
13424
+ if (actual !== null && isExactUntouchedBase(path, actual)) out.add(path);
13420
13425
  })
13421
13426
  );
13422
13427
  return out;
@@ -18561,7 +18566,7 @@ function onboardCommand() {
18561
18566
  ...survey.missing.length === 0 ? {} : { missing: survey.missing, missing_hint: MISSING_HINT },
18562
18567
  // Spelled out in the machine-readable form too, because an agent
18563
18568
  // 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.'
18569
+ 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
18570
  },
18566
18571
  human: () => {
18567
18572
  if (survey.unfilled.length === 0 && survey.missing.length === 0) {
@@ -18575,7 +18580,7 @@ function onboardCommand() {
18575
18580
  );
18576
18581
  return;
18577
18582
  }
18578
- printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still untouched)
18583
+ printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still to write)
18579
18584
  `);
18580
18585
  if (survey.unfilled.length > 0) {
18581
18586
  printHuman(
@@ -18616,21 +18621,23 @@ function onboardCommand() {
18616
18621
  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
18622
  async function localSurvey(where) {
18618
18623
  const adapter = await openBrainOrExplain(where.brain.root);
18619
- const untouched = await untouchedBase(
18620
- adapter,
18621
- BASE_FILES.map((f) => f.path)
18624
+ const unwritten = /* @__PURE__ */ new Set();
18625
+ await Promise.all(
18626
+ BASE_FILES.map(async (f) => {
18627
+ const content = await adapter.read(f.path).then((r) => r.content).catch(() => null);
18628
+ if (content !== null && isUnwrittenBody(f.path, content)) unwritten.add(f.path);
18629
+ })
18622
18630
  );
18623
18631
  return {
18624
18632
  identity: { brain: where.brain.root },
18625
18633
  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),
18634
+ unfilled: BASE_FILES.filter((f) => unwritten.has(f.path)),
18635
+ // A file that is not there reads as written here. That is a choice kept
18636
+ // from before, not something this side cannot tell: `init` writes all
18637
+ // five, so the state does not arise on a folder brain, and asking about a
18638
+ // file that is gone would send an agent to revise a ref that does not
18639
+ // exist. The hosted half CAN arrive without them, and says so (`missing`).
18640
+ filled: BASE_FILES.filter((f) => !unwritten.has(f.path)).map((f) => f.path),
18634
18641
  missing: []
18635
18642
  };
18636
18643
  }
@@ -18639,7 +18646,7 @@ async function hostedSurvey(where) {
18639
18646
  const state = (file) => {
18640
18647
  const content = contents.get(file.path);
18641
18648
  if (content === void 0) return "missing";
18642
- return isUntouchedBase(file.path, content) ? "unfilled" : "filled";
18649
+ return isUnwrittenBody(file.path, content) ? "unfilled" : "filled";
18643
18650
  };
18644
18651
  return {
18645
18652
  identity: { brain: where.name, brain_id: where.brainId },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memgineering",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "private": false,
5
5
  "description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share — stored in your own folder.",
6
6
  "license": "Apache-2.0",