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
|
package/assets/MEMGINEERING.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
18620
|
-
|
|
18621
|
-
BASE_FILES.map((f) =>
|
|
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) =>
|
|
18627
|
-
// A file that is not there reads as written here
|
|
18628
|
-
//
|
|
18629
|
-
//
|
|
18630
|
-
//
|
|
18631
|
-
//
|
|
18632
|
-
|
|
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
|
|
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