memgineering 0.3.0 → 0.4.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/CHANGELOG.md +68 -0
- package/assets/memgineering-memory/SKILL.md +55 -6
- package/assets/memgineering-setup/SKILL.md +15 -1
- package/dist/index.js +513 -334
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,74 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.4.0] — 2026-08-10
|
|
15
|
+
|
|
16
|
+
A template is not an answer, a revision that revises nothing is not written,
|
|
17
|
+
and linking a second brain no longer breaks the first one everywhere.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`recall` tells a template apart from an answer.** A card that is one of the
|
|
22
|
+
five `01_BASE/` files and is still byte-for-byte what `init` wrote is marked
|
|
23
|
+
`not filled in yet`; when every card in a result carries that mark, one line
|
|
24
|
+
above them says nothing you have written matched, and points at `onboard`.
|
|
25
|
+
`--json` carries it as `scaffolding`. Nothing is removed from the result —
|
|
26
|
+
the label is a fact about the file, so an unfilled base note padding out a
|
|
27
|
+
weak answer is visible as padding instead of reading like a conclusion. The
|
|
28
|
+
mark disappears the moment the file is edited, which is also the moment a base
|
|
29
|
+
note starts being the best answer in the brain.
|
|
30
|
+
|
|
31
|
+
- **`revise` refuses a revision that revises nothing.** When the claim, summary
|
|
32
|
+
and title all match what the note already holds, nothing is written and the
|
|
33
|
+
reason is given — a user repeating an answer they had already given used to
|
|
34
|
+
become ledger entries recording no change. Saying `--reason` makes it a real
|
|
35
|
+
record again, which is what `reinforce` is for.
|
|
36
|
+
- **`revise` says when a conclusion landed above `init`'s instructions.** These
|
|
37
|
+
files take two steps — write the body, then record the claim — and doing only
|
|
38
|
+
the second leaves a note whose frontmatter concludes something while the prose
|
|
39
|
+
under it is still the form telling you what to write.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`revise --help` says which `--action` to pick and what it costs to pick
|
|
44
|
+
wrong.** Only `supersede` moves `valid_from`, so using it on a fact that was
|
|
45
|
+
true all along dates it from today. Filling in a blank `01_BASE/` file for the
|
|
46
|
+
first time is `reinforce`.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Linking a second brain no longer breaks the first one everywhere.** With
|
|
51
|
+
more than one brain linked and no pointer in the current folder, every command
|
|
52
|
+
refused as ambiguous — so a scratch brain linked for an afternoon stopped
|
|
53
|
+
`recall` and `remember` working in every other directory, for every tool
|
|
54
|
+
sharing the config. The first brain linked is now the default and is used when
|
|
55
|
+
nothing more specific applies; `memgineering use --default <path>` changes it,
|
|
56
|
+
and `--vault`, a `.memgineering` pointer, and standing inside a brain all
|
|
57
|
+
still win. The ambiguity refusal remains for the case it was written for —
|
|
58
|
+
several brains and nothing ever said which — and now names the one-time fix.
|
|
59
|
+
Upgrading picks the brain you already had, not the next one you happen to
|
|
60
|
+
link.
|
|
61
|
+
- **The cross-language warning no longer fires over a base file the user wrote.**
|
|
62
|
+
It decided "did this query reach anything the person wrote" from the path — a
|
|
63
|
+
match under `01_BASE/` did not count — so somebody who had answered those five
|
|
64
|
+
questions could ask about their own preferences, get their own note, and be
|
|
65
|
+
told the query could not reach their brain. It now asks whether the file has
|
|
66
|
+
been touched, which is what it meant all along.
|
|
67
|
+
|
|
68
|
+
### Agent guidance
|
|
69
|
+
|
|
70
|
+
- `memgineering-memory` covers the `not filled in yet` mark and the header above
|
|
71
|
+
a result made only of templates: what they mean, and that a card carrying one
|
|
72
|
+
is not evidence of anything.
|
|
73
|
+
- `memgineering-memory` spells out that filling in a base file is two steps —
|
|
74
|
+
write the body, then record the claim — why it is `--action reinforce` rather
|
|
75
|
+
than the default, and that re-recording something unchanged is refused.
|
|
76
|
+
- `memgineering-setup` covers the default brain, and says not to abandon a write
|
|
77
|
+
because the brain was ambiguous: a refused `remember` means the thing the user
|
|
78
|
+
asked you to keep was not kept.
|
|
79
|
+
|
|
80
|
+
**Restart your agent session after upgrading** so the updated skills load.
|
|
81
|
+
|
|
14
82
|
## [0.3.0] — 2026-08-10
|
|
15
83
|
|
|
16
84
|
Recall says how old a memory is and why two of them disagree; a screen for
|
|
@@ -74,6 +74,29 @@ Say so. Only titles, aliases and summaries are searched, so suggest broader
|
|
|
74
74
|
words — but do not go read their folder yourself to compensate. A brain that
|
|
75
75
|
answers "nothing" is giving you real information.
|
|
76
76
|
|
|
77
|
+
### Cards that are not answers
|
|
78
|
+
|
|
79
|
+
A card marked `not filled in yet` is one of the five `01_BASE/` files, still
|
|
80
|
+
byte-for-byte what `init` wrote. Nobody has answered it, so it is evidence of
|
|
81
|
+
nothing — do not quote it back as if it were what they think.
|
|
82
|
+
|
|
83
|
+
When every card carries that mark, recall says so above them:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
⚠ Nothing you have written matched this.
|
|
87
|
+
Every card below is still the template `init` wrote
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
That is the moment to offer to fill them in rather than to answer from them —
|
|
91
|
+
`memgineering onboard` prints the questions. The cards are still listed, because
|
|
92
|
+
the label is a claim about the file, not a decision about what you may see;
|
|
93
|
+
`--json` carries the same fact as `scaffolding: true`.
|
|
94
|
+
|
|
95
|
+
The mark disappears the moment the user writes in the file. A base note they
|
|
96
|
+
have filled in is an ordinary note of theirs and often the best answer in the
|
|
97
|
+
brain — `who am I`, `what am I not allowed to do`, `where does everything live`
|
|
98
|
+
are all answered from `01_BASE/`.
|
|
99
|
+
|
|
77
100
|
## Filling in the base files, which only you can do
|
|
78
101
|
|
|
79
102
|
`init` creates five files in `01_BASE/` and leaves them empty, because what
|
|
@@ -114,17 +137,30 @@ answers thin out — three good lines beat a filled-in template. Take what they
|
|
|
114
137
|
say in passing during ordinary work too; most of `TOOLING.md` gets written by
|
|
115
138
|
noticing, not by asking.
|
|
116
139
|
|
|
117
|
-
**How to write it
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
**How to write it — both halves.** `revise` only touches frontmatter, so it
|
|
141
|
+
takes two steps, and skipping the first is the common failure: write the body
|
|
142
|
+
with your normal file tools FIRST, replacing `init`'s instructions with what
|
|
143
|
+
they actually said, then record the conclusion.
|
|
120
144
|
|
|
121
145
|
```
|
|
146
|
+
# 1. replace the body — their words, not the template's prompt
|
|
147
|
+
# 2. then:
|
|
122
148
|
memgineering revise 01_BASE/USER.md \
|
|
123
149
|
--action reinforce \
|
|
124
150
|
--claim "<one line: who they are>" \
|
|
125
151
|
--summary "<the line recall should show>"
|
|
126
152
|
```
|
|
127
153
|
|
|
154
|
+
A claim written above the untouched template leaves the file reading as a form
|
|
155
|
+
somebody half-filled — the conclusion in the frontmatter, the instructions for
|
|
156
|
+
writing one still underneath. `revise` warns when you do this; the warning means
|
|
157
|
+
go back and write the body.
|
|
158
|
+
|
|
159
|
+
`--action reinforce`, not the default, and the reason is the date. `supersede`
|
|
160
|
+
stamps `valid_from` with now because a replaced conclusion starts now — but
|
|
161
|
+
these facts were always true and you have only just been told them. Dating them
|
|
162
|
+
today is wrong in the one field a memory store exists to get right.
|
|
163
|
+
|
|
128
164
|
This is the ONE place you edit a brain file directly, and the boundary is
|
|
129
165
|
narrow: `01_BASE/` only, and only while the file is still the template `init`
|
|
130
166
|
wrote. Nothing is lost — there is no history to break and nothing to undo — and
|
|
@@ -221,11 +257,24 @@ memgineering revise deploy-manual \
|
|
|
221
257
|
--summary "manual only, no CD"
|
|
222
258
|
```
|
|
223
259
|
|
|
224
|
-
- `--action supersede` (default)
|
|
225
|
-
|
|
226
|
-
- `--action
|
|
260
|
+
- `--action supersede` (default) — the conclusion CHANGED. Stamps `valid_from`
|
|
261
|
+
with now, because a replaced conclusion starts now.
|
|
262
|
+
- `--action reinforce` — the SAME conclusion, new support. Leaves `valid_from`
|
|
263
|
+
alone. Use it when nothing about the fact changed, only your evidence.
|
|
264
|
+
- `--action conflict` — two notes DISAGREE. Records the edge, leaves both
|
|
265
|
+
standing, and deliberately does not write the claim you passed.
|
|
227
266
|
- `--dry-run` shows the diff and writes nothing
|
|
228
267
|
|
|
268
|
+
Pick by what changed, not by habit: `supersede` on a fact that was true all
|
|
269
|
+
along dates it from today, and the date is the field this is all for.
|
|
270
|
+
|
|
271
|
+
**Re-recording something unchanged is refused.** If the claim, summary and title
|
|
272
|
+
all match what the note already holds, `revise` writes nothing and says so —
|
|
273
|
+
otherwise a user repeating themselves turns into ledger entries that record no
|
|
274
|
+
change. When you do mean "this still holds", say what confirms it:
|
|
275
|
+
`--action reinforce --reason "<what confirms it>"`. Check with `open <ref>`
|
|
276
|
+
before rewriting something you may already have.
|
|
277
|
+
|
|
229
278
|
This only ever rewrites the memory block in a note's frontmatter. The prose is
|
|
230
279
|
the user's; neither this command nor you should rewrite it uninvited.
|
|
231
280
|
|
|
@@ -137,8 +137,9 @@ one answers is decided by where you are, in this order:
|
|
|
137
137
|
2. a `.memgineering` pointer file, found by walking up from the cwd
|
|
138
138
|
3. the brain the current directory is inside
|
|
139
139
|
4. the only one linked
|
|
140
|
+
5. their default brain — the first one they linked, unless they changed it
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
Bind a directory to one brain when that directory belongs to it:
|
|
142
143
|
|
|
143
144
|
```
|
|
144
145
|
memgineering use ~/brains/work
|
|
@@ -147,6 +148,19 @@ memgineering use ~/brains/work
|
|
|
147
148
|
Inside a repository this writes a relative path, so it can be committed and
|
|
148
149
|
will resolve for a teammate who links the same brain.
|
|
149
150
|
|
|
151
|
+
If it still says the choice is ambiguous — several brains, and nothing has ever
|
|
152
|
+
said which is theirs — that is a question for them, not a guess for you. Show
|
|
153
|
+
the list it printed and offer the one-time fix:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
memgineering use --default ~/brains/work
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Do not give up on a write because of this.** A refused `remember` means the
|
|
160
|
+
thing they asked you to keep was not kept, and "I could not tell which brain"
|
|
161
|
+
is a solvable problem — ask which one, or use `--vault` for the write and raise
|
|
162
|
+
the default with them afterwards.
|
|
163
|
+
|
|
150
164
|
## Checking it worked
|
|
151
165
|
|
|
152
166
|
```
|
package/dist/index.js
CHANGED
|
@@ -2624,6 +2624,11 @@ var init_atomic = __esm({
|
|
|
2624
2624
|
import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
|
|
2625
2625
|
import { join as join5, resolve as resolve2 } from "path";
|
|
2626
2626
|
import { z as z6 } from "zod";
|
|
2627
|
+
function withDefaultBrain(brain) {
|
|
2628
|
+
if (brain.default) return brain;
|
|
2629
|
+
const oldest = [...brain.brains].sort((a, b) => a.linkedAt.localeCompare(b.linkedAt))[0];
|
|
2630
|
+
return oldest ? { ...brain, default: oldest.root } : brain;
|
|
2631
|
+
}
|
|
2627
2632
|
function configPath() {
|
|
2628
2633
|
return join5(brandHome(), "config.json");
|
|
2629
2634
|
}
|
|
@@ -2666,7 +2671,22 @@ var init_config = __esm({
|
|
|
2666
2671
|
});
|
|
2667
2672
|
ConfigSchema = z6.object({
|
|
2668
2673
|
brain: z6.object({
|
|
2669
|
-
brains: z6.array(BrainSchema).default([])
|
|
2674
|
+
brains: z6.array(BrainSchema).default([]),
|
|
2675
|
+
/**
|
|
2676
|
+
* The brain to use when nothing else says which — root path, or absent.
|
|
2677
|
+
*
|
|
2678
|
+
* Exists because linking a second brain used to break the first one
|
|
2679
|
+
* everywhere. `resolveBrain` answered "1 brain linked" with that brain and
|
|
2680
|
+
* "more than 1" with a refusal, so a scratch folder linked for an
|
|
2681
|
+
* afternoon made every command in every directory ambiguous — including
|
|
2682
|
+
* for other tools on the same machine, which is how a Gemini session lost
|
|
2683
|
+
* a preference its user had just asked it to remember.
|
|
2684
|
+
*
|
|
2685
|
+
* Set by the first `link` or `init` and never moved on its own. A default
|
|
2686
|
+
* that followed the newest link would have pointed at the scratch folder,
|
|
2687
|
+
* which is the failure with the sign flipped.
|
|
2688
|
+
*/
|
|
2689
|
+
default: z6.string().min(1).optional()
|
|
2670
2690
|
}).default({ brains: [] }),
|
|
2671
2691
|
system: z6.object({
|
|
2672
2692
|
/** Self-update: `on` by default, decided at setup. */
|
|
@@ -2712,6 +2732,13 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
|
|
|
2712
2732
|
const containing = brains.filter((b) => isInside(cwd, b.root)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
2713
2733
|
if (containing) return { brain: containing, via: "cwd" };
|
|
2714
2734
|
if (brains.length === 1) return { brain: brains[0], via: "only" };
|
|
2735
|
+
const preferred = cfg.brain.default;
|
|
2736
|
+
if (preferred) {
|
|
2737
|
+
const known = brains.find((b) => b.root === preferred);
|
|
2738
|
+
if (known && (await liveBrains([known])).length === 1) {
|
|
2739
|
+
return { brain: known, via: "default" };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2715
2742
|
const present = await liveBrains(brains);
|
|
2716
2743
|
if (present.length === 1) return { brain: present[0], via: "only" };
|
|
2717
2744
|
if (brains.length === 0) {
|
|
@@ -2726,7 +2753,10 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
|
|
|
2726
2753
|
throw memgError(
|
|
2727
2754
|
"ambiguous_vault",
|
|
2728
2755
|
`${present.length} of ${brains.length} linked brains are reachable, so this is ambiguous`,
|
|
2729
|
-
`
|
|
2756
|
+
`Set the one you actually use, once, and this stops happening everywhere:
|
|
2757
|
+
memgineering use --default ${present[0]?.root ?? "<path>"}
|
|
2758
|
+
|
|
2759
|
+
Or answer just for this run with --vault <path>, or bind this folder with \`memgineering use <path>\`.
|
|
2730
2760
|
${listed}${missing.length > 0 ? `
|
|
2731
2761
|
A folder marked (missing) is not on disk right now. If it is gone for good, \`memgineering unlink ${missing[0]?.root}\` stops it getting in the way.` : ""}`
|
|
2732
2762
|
);
|
|
@@ -2820,10 +2850,205 @@ var init_index_build = __esm({
|
|
|
2820
2850
|
}
|
|
2821
2851
|
});
|
|
2822
2852
|
|
|
2853
|
+
// src/commands/init.ts
|
|
2854
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
2855
|
+
import { join as join12, resolve as resolve5 } from "path";
|
|
2856
|
+
import { Command as Command3 } from "commander";
|
|
2857
|
+
function initCommand() {
|
|
2858
|
+
return new Command3("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
|
|
2859
|
+
const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
|
|
2860
|
+
printDual({
|
|
2861
|
+
json: {
|
|
2862
|
+
brain: canonicalRoot2,
|
|
2863
|
+
folders: FOLDERS,
|
|
2864
|
+
files: Object.keys(files(opts.name)),
|
|
2865
|
+
observations_dir: OBSERVATIONS_DIR,
|
|
2866
|
+
indexed
|
|
2867
|
+
},
|
|
2868
|
+
human: () => {
|
|
2869
|
+
printHuman(`Created a brain at \`${canonicalRoot2}\`.
|
|
2870
|
+
`);
|
|
2871
|
+
printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
|
|
2872
|
+
printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
|
|
2873
|
+
printHuman(`- nine more folders, ready when you need them
|
|
2874
|
+
`);
|
|
2875
|
+
printHuman(c.gray("The five base files are empty templates.\n"));
|
|
2876
|
+
printHuman(
|
|
2877
|
+
c.gray(
|
|
2878
|
+
"AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
|
|
2879
|
+
)
|
|
2880
|
+
);
|
|
2881
|
+
printHuman(`If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`);
|
|
2882
|
+
printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
|
|
2883
|
+
printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
async function createBrain(path, name) {
|
|
2889
|
+
const root = resolve5(path);
|
|
2890
|
+
const existing = await readdir5(root).catch(() => null);
|
|
2891
|
+
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
2892
|
+
throw memgError(
|
|
2893
|
+
"already_initialized",
|
|
2894
|
+
`${root} already has files in it`,
|
|
2895
|
+
`\`init\` is for a fresh folder. To use notes you already have, read them as they are:
|
|
2896
|
+
memgineering link ${path}`
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
await mkdir8(root, { recursive: true });
|
|
2900
|
+
for (const dir of FOLDERS) await mkdir8(join12(root, dir), { recursive: true });
|
|
2901
|
+
for (const [file, content] of Object.entries(files(name))) {
|
|
2902
|
+
await writeFile6(join12(root, file), content, "utf8");
|
|
2903
|
+
}
|
|
2904
|
+
const canonicalRoot2 = await canonicalize(root);
|
|
2905
|
+
const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
|
|
2906
|
+
const built = await openIndex(adapter, canonicalRoot2);
|
|
2907
|
+
const cfg = await loadConfig();
|
|
2908
|
+
const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
|
|
2909
|
+
brains.push({
|
|
2910
|
+
root: canonicalRoot2,
|
|
2911
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2912
|
+
observationsDir: OBSERVATIONS_DIR
|
|
2913
|
+
});
|
|
2914
|
+
await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
|
|
2915
|
+
return { root: canonicalRoot2, indexed: built.entries.length };
|
|
2916
|
+
}
|
|
2917
|
+
function files(name) {
|
|
2918
|
+
const base = (id, title, summary, body) => [
|
|
2919
|
+
"---",
|
|
2920
|
+
"memgineering:",
|
|
2921
|
+
` id: ${id}`,
|
|
2922
|
+
` title: ${title}`,
|
|
2923
|
+
` summary: ${summary}`,
|
|
2924
|
+
" scope: base",
|
|
2925
|
+
"---",
|
|
2926
|
+
"",
|
|
2927
|
+
`# ${title}`,
|
|
2928
|
+
"",
|
|
2929
|
+
body,
|
|
2930
|
+
""
|
|
2931
|
+
].join("\n");
|
|
2932
|
+
return {
|
|
2933
|
+
"00_HUB/HUB.md": [
|
|
2934
|
+
"---",
|
|
2935
|
+
"memgineering:",
|
|
2936
|
+
" id: hub",
|
|
2937
|
+
` title: ${name}`,
|
|
2938
|
+
" summary: Where to look for what, in this brain.",
|
|
2939
|
+
"---",
|
|
2940
|
+
"",
|
|
2941
|
+
`# ${name}`,
|
|
2942
|
+
"",
|
|
2943
|
+
"This file says where things are. It holds no content of its own \u2014",
|
|
2944
|
+
"when it starts explaining something instead of pointing at it, that",
|
|
2945
|
+
"explanation belongs in a note.",
|
|
2946
|
+
"",
|
|
2947
|
+
"## Read first",
|
|
2948
|
+
"",
|
|
2949
|
+
"- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
|
|
2950
|
+
"- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
|
|
2951
|
+
"",
|
|
2952
|
+
"## Where things go",
|
|
2953
|
+
"",
|
|
2954
|
+
"| Folder | What lives here |",
|
|
2955
|
+
"| --- | --- |",
|
|
2956
|
+
"| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
|
|
2957
|
+
"| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
|
|
2958
|
+
"| `30_ACTIONS/` | Open work and decisions waiting on something |",
|
|
2959
|
+
"| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
|
|
2960
|
+
"| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
|
|
2961
|
+
"| `60_TEMPLATES/` | Shapes for notes you write often |",
|
|
2962
|
+
"| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
|
|
2963
|
+
"| `80_SHAREABLE/` | Notes you would be willing to publish |",
|
|
2964
|
+
"| `90_ARCHIVE/` | Kept, but no longer current |",
|
|
2965
|
+
""
|
|
2966
|
+
].join("\n"),
|
|
2967
|
+
"01_BASE/CRITICAL_FACTS.md": base(
|
|
2968
|
+
"critical-facts",
|
|
2969
|
+
"Critical facts",
|
|
2970
|
+
"The few things that must never be wrong.",
|
|
2971
|
+
[
|
|
2972
|
+
"Keep this short enough to read every session. Anything that can be",
|
|
2973
|
+
"looked up when needed belongs in a note instead.",
|
|
2974
|
+
"",
|
|
2975
|
+
"- "
|
|
2976
|
+
].join("\n")
|
|
2977
|
+
),
|
|
2978
|
+
"01_BASE/USER.md": base(
|
|
2979
|
+
"user",
|
|
2980
|
+
"About me",
|
|
2981
|
+
"Who I am and what context I work in.",
|
|
2982
|
+
[
|
|
2983
|
+
"Background, role, what you are working on, what an agent should assume",
|
|
2984
|
+
"you already know.",
|
|
2985
|
+
"",
|
|
2986
|
+
"- "
|
|
2987
|
+
].join("\n")
|
|
2988
|
+
),
|
|
2989
|
+
"01_BASE/PREFERENCES.md": base(
|
|
2990
|
+
"preferences",
|
|
2991
|
+
"Preferences",
|
|
2992
|
+
"How I like answers, code and decisions handled.",
|
|
2993
|
+
["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
|
|
2994
|
+
),
|
|
2995
|
+
"01_BASE/BOUNDARIES.md": base(
|
|
2996
|
+
"boundaries",
|
|
2997
|
+
"Boundaries",
|
|
2998
|
+
"What an agent must not do without asking me first.",
|
|
2999
|
+
[
|
|
3000
|
+
"Privacy, spending, anything irreversible, anything that leaves this",
|
|
3001
|
+
"machine. Be specific \u2014 a boundary that has to be interpreted is one",
|
|
3002
|
+
"that will be interpreted generously.",
|
|
3003
|
+
"",
|
|
3004
|
+
"- "
|
|
3005
|
+
].join("\n")
|
|
3006
|
+
),
|
|
3007
|
+
"01_BASE/TOOLING.md": base(
|
|
3008
|
+
"tooling",
|
|
3009
|
+
"Tooling",
|
|
3010
|
+
"The tools, machines and commands I actually use.",
|
|
3011
|
+
[
|
|
3012
|
+
"Editors, languages, deploy commands, where things run. Enough that an",
|
|
3013
|
+
"agent does not have to guess or discover it again each time.",
|
|
3014
|
+
"",
|
|
3015
|
+
"- "
|
|
3016
|
+
].join("\n")
|
|
3017
|
+
)
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
var OBSERVATIONS_DIR, FOLDERS;
|
|
3021
|
+
var init_init = __esm({
|
|
3022
|
+
"src/commands/init.ts"() {
|
|
3023
|
+
"use strict";
|
|
3024
|
+
init_adapter();
|
|
3025
|
+
init_errors();
|
|
3026
|
+
init_config();
|
|
3027
|
+
init_index_build();
|
|
3028
|
+
init_vault();
|
|
3029
|
+
init_ui();
|
|
3030
|
+
OBSERVATIONS_DIR = "20_TIMELINE/observations";
|
|
3031
|
+
FOLDERS = [
|
|
3032
|
+
"00_HUB",
|
|
3033
|
+
"01_BASE",
|
|
3034
|
+
"10_ENTITIES",
|
|
3035
|
+
"20_TIMELINE",
|
|
3036
|
+
"20_TIMELINE/observations",
|
|
3037
|
+
"30_ACTIONS",
|
|
3038
|
+
"40_KNOWLEDGE",
|
|
3039
|
+
"50_SKILLS",
|
|
3040
|
+
"60_TEMPLATES",
|
|
3041
|
+
"70_SOURCES",
|
|
3042
|
+
"80_SHAREABLE",
|
|
3043
|
+
"90_ARCHIVE"
|
|
3044
|
+
];
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
|
|
2823
3048
|
// src/commands/link.ts
|
|
2824
3049
|
import { readFile as readFile13 } from "fs/promises";
|
|
2825
|
-
import { join as
|
|
2826
|
-
import { Command as
|
|
3050
|
+
import { join as join13 } from "path";
|
|
3051
|
+
import { Command as Command5 } from "commander";
|
|
2827
3052
|
import prompts from "prompts";
|
|
2828
3053
|
function sameBasename(all, entry) {
|
|
2829
3054
|
const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
|
|
@@ -2846,7 +3071,7 @@ function spreadAcrossFolders(rows, max) {
|
|
|
2846
3071
|
return picked;
|
|
2847
3072
|
}
|
|
2848
3073
|
function linkCommand() {
|
|
2849
|
-
return new
|
|
3074
|
+
return new Command5("link").description("read a notes folder as a brain \u2014 audits its rules first").argument("<path>", "path to the notes folder").option("--yes", "skip the confirmation").option("--dry-run", "show exactly what would be stored, then stop").option(
|
|
2850
3075
|
"--observations <dir>",
|
|
2851
3076
|
"where `remember` writes, relative to the folder",
|
|
2852
3077
|
DEFAULT_OBSERVATIONS_DIR
|
|
@@ -3017,12 +3242,12 @@ async function recordLink(adapter, observationsDir) {
|
|
|
3017
3242
|
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3018
3243
|
observationsDir: observationsDir.replace(/^\/+|\/+$/g, "")
|
|
3019
3244
|
});
|
|
3020
|
-
await saveConfig({ ...cfg, brain: { brains } });
|
|
3245
|
+
await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
|
|
3021
3246
|
return built;
|
|
3022
3247
|
}
|
|
3023
3248
|
async function refuseSilentRules(path, denied, excluded, unsupported) {
|
|
3024
3249
|
const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
|
|
3025
|
-
const rawDenyLines = await readFile13(
|
|
3250
|
+
const rawDenyLines = await readFile13(join13(path, ".memgdeny"), "utf8").then(
|
|
3026
3251
|
(t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
|
|
3027
3252
|
).catch(() => []);
|
|
3028
3253
|
const asWritten = (folded) => rawDenyLines.find(
|
|
@@ -3145,247 +3370,50 @@ Brain: ${root}`));
|
|
|
3145
3370
|
for (const d of refused.slice(0, 10)) printHuman(c.gray(` ${d.path} (${d.detail})`));
|
|
3146
3371
|
printHuman(c.gray(" Those are not read, not indexed, and not recalled."));
|
|
3147
3372
|
}
|
|
3148
|
-
if (samples.length > 0) {
|
|
3149
|
-
printHuman(c.gray("\n For example, these exact lines would be stored:"));
|
|
3150
|
-
for (const r of samples) {
|
|
3151
|
-
printHuman(c.gray(` ${r.title}`));
|
|
3152
|
-
printHuman(c.gray(` ${r.summary ?? "(no summary \u2014 title only)"}`));
|
|
3153
|
-
}
|
|
3154
|
-
if (notes.length > samples.length) {
|
|
3155
|
-
printHuman(
|
|
3156
|
-
c.gray(` \u2026and ${notes.length - samples.length} more \u2014 see them all with --dry-run.`)
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
printHuman(c.bold("\n A name list cannot protect a file with an ordinary name."));
|
|
3161
|
-
printHuman(
|
|
3162
|
-
c.gray(
|
|
3163
|
-
" Notes whose CONTENT looks like a credential are refused automatically,\n but a diagnosis or an address written as plain prose matches nothing.\n List those by exact path in a .memgdeny file at the top of your notes\n folder \u2014 one path per line, relative to that folder, no wildcards:\n 30_personal/medical.md\n 99_archive/ (a trailing slash covers a whole folder)"
|
|
3164
|
-
)
|
|
3165
|
-
);
|
|
3166
|
-
printHuman(
|
|
3167
|
-
c.gray(
|
|
3168
|
-
"\n This tool writes into your notes without asking each time \u2014 new\n observations, and changes to the memory block of a note. Every one is\n recorded and reversible: memgineering log \xB7 memgineering undo"
|
|
3169
|
-
)
|
|
3170
|
-
);
|
|
3171
|
-
const { ok } = await prompts({
|
|
3172
|
-
type: "confirm",
|
|
3173
|
-
name: "ok",
|
|
3174
|
-
message: "Build the index for this folder?",
|
|
3175
|
-
initial: false
|
|
3176
|
-
});
|
|
3177
|
-
return ok === true;
|
|
3178
|
-
}
|
|
3179
|
-
var DEFAULT_OBSERVATIONS_DIR;
|
|
3180
|
-
var init_link = __esm({
|
|
3181
|
-
"src/commands/link.ts"() {
|
|
3182
|
-
"use strict";
|
|
3183
|
-
init_adapter();
|
|
3184
|
-
init_rules_file();
|
|
3185
|
-
init_src();
|
|
3186
|
-
init_errors();
|
|
3187
|
-
init_config();
|
|
3188
|
-
init_index_build();
|
|
3189
|
-
init_vault();
|
|
3190
|
-
init_ui();
|
|
3191
|
-
DEFAULT_OBSERVATIONS_DIR = "_observations";
|
|
3192
|
-
}
|
|
3193
|
-
});
|
|
3194
|
-
|
|
3195
|
-
// src/commands/init.ts
|
|
3196
|
-
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
3197
|
-
import { join as join13, resolve as resolve6 } from "path";
|
|
3198
|
-
import { Command as Command7 } from "commander";
|
|
3199
|
-
function initCommand() {
|
|
3200
|
-
return new Command7("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
|
|
3201
|
-
const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
|
|
3202
|
-
printDual({
|
|
3203
|
-
json: {
|
|
3204
|
-
brain: canonicalRoot2,
|
|
3205
|
-
folders: FOLDERS,
|
|
3206
|
-
files: Object.keys(files(opts.name)),
|
|
3207
|
-
observations_dir: OBSERVATIONS_DIR,
|
|
3208
|
-
indexed
|
|
3209
|
-
},
|
|
3210
|
-
human: () => {
|
|
3211
|
-
printHuman(`Created a brain at \`${canonicalRoot2}\`.
|
|
3212
|
-
`);
|
|
3213
|
-
printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
|
|
3214
|
-
printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
|
|
3215
|
-
printHuman(`- nine more folders, ready when you need them
|
|
3216
|
-
`);
|
|
3217
|
-
printHuman(c.gray("The five base files are empty templates.\n"));
|
|
3218
|
-
printHuman(
|
|
3219
|
-
c.gray(
|
|
3220
|
-
"AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
|
|
3221
|
-
)
|
|
3222
|
-
);
|
|
3223
|
-
printHuman(
|
|
3224
|
-
`If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`
|
|
3225
|
-
);
|
|
3226
|
-
printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
|
|
3227
|
-
printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
|
|
3228
|
-
}
|
|
3229
|
-
});
|
|
3230
|
-
});
|
|
3231
|
-
}
|
|
3232
|
-
async function createBrain(path, name) {
|
|
3233
|
-
const root = resolve6(path);
|
|
3234
|
-
const existing = await readdir5(root).catch(() => null);
|
|
3235
|
-
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
3236
|
-
throw memgError(
|
|
3237
|
-
"already_initialized",
|
|
3238
|
-
`${root} already has files in it`,
|
|
3239
|
-
`\`init\` is for a fresh folder. To use notes you already have, read them as they are:
|
|
3240
|
-
memgineering link ${path}`
|
|
3241
|
-
);
|
|
3242
|
-
}
|
|
3243
|
-
await mkdir8(root, { recursive: true });
|
|
3244
|
-
for (const dir of FOLDERS) await mkdir8(join13(root, dir), { recursive: true });
|
|
3245
|
-
for (const [file, content] of Object.entries(files(name))) {
|
|
3246
|
-
await writeFile6(join13(root, file), content, "utf8");
|
|
3247
|
-
}
|
|
3248
|
-
const canonicalRoot2 = await canonicalize(root);
|
|
3249
|
-
const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
|
|
3250
|
-
const built = await openIndex(adapter, canonicalRoot2);
|
|
3251
|
-
const cfg = await loadConfig();
|
|
3252
|
-
const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
|
|
3253
|
-
brains.push({
|
|
3254
|
-
root: canonicalRoot2,
|
|
3255
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3256
|
-
observationsDir: OBSERVATIONS_DIR
|
|
3257
|
-
});
|
|
3258
|
-
await saveConfig({ ...cfg, brain: { brains } });
|
|
3259
|
-
return { root: canonicalRoot2, indexed: built.entries.length };
|
|
3260
|
-
}
|
|
3261
|
-
function files(name) {
|
|
3262
|
-
const base = (id, title, summary, body) => [
|
|
3263
|
-
"---",
|
|
3264
|
-
"memgineering:",
|
|
3265
|
-
` id: ${id}`,
|
|
3266
|
-
` title: ${title}`,
|
|
3267
|
-
` summary: ${summary}`,
|
|
3268
|
-
" scope: base",
|
|
3269
|
-
"---",
|
|
3270
|
-
"",
|
|
3271
|
-
`# ${title}`,
|
|
3272
|
-
"",
|
|
3273
|
-
body,
|
|
3274
|
-
""
|
|
3275
|
-
].join("\n");
|
|
3276
|
-
return {
|
|
3277
|
-
"00_HUB/HUB.md": [
|
|
3278
|
-
"---",
|
|
3279
|
-
"memgineering:",
|
|
3280
|
-
" id: hub",
|
|
3281
|
-
` title: ${name}`,
|
|
3282
|
-
" summary: Where to look for what, in this brain.",
|
|
3283
|
-
"---",
|
|
3284
|
-
"",
|
|
3285
|
-
`# ${name}`,
|
|
3286
|
-
"",
|
|
3287
|
-
"This file says where things are. It holds no content of its own \u2014",
|
|
3288
|
-
"when it starts explaining something instead of pointing at it, that",
|
|
3289
|
-
"explanation belongs in a note.",
|
|
3290
|
-
"",
|
|
3291
|
-
"## Read first",
|
|
3292
|
-
"",
|
|
3293
|
-
"- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
|
|
3294
|
-
"- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
|
|
3295
|
-
"",
|
|
3296
|
-
"## Where things go",
|
|
3297
|
-
"",
|
|
3298
|
-
"| Folder | What lives here |",
|
|
3299
|
-
"| --- | --- |",
|
|
3300
|
-
"| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
|
|
3301
|
-
"| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
|
|
3302
|
-
"| `30_ACTIONS/` | Open work and decisions waiting on something |",
|
|
3303
|
-
"| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
|
|
3304
|
-
"| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
|
|
3305
|
-
"| `60_TEMPLATES/` | Shapes for notes you write often |",
|
|
3306
|
-
"| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
|
|
3307
|
-
"| `80_SHAREABLE/` | Notes you would be willing to publish |",
|
|
3308
|
-
"| `90_ARCHIVE/` | Kept, but no longer current |",
|
|
3309
|
-
""
|
|
3310
|
-
].join("\n"),
|
|
3311
|
-
"01_BASE/CRITICAL_FACTS.md": base(
|
|
3312
|
-
"critical-facts",
|
|
3313
|
-
"Critical facts",
|
|
3314
|
-
"The few things that must never be wrong.",
|
|
3315
|
-
[
|
|
3316
|
-
"Keep this short enough to read every session. Anything that can be",
|
|
3317
|
-
"looked up when needed belongs in a note instead.",
|
|
3318
|
-
"",
|
|
3319
|
-
"- "
|
|
3320
|
-
].join("\n")
|
|
3321
|
-
),
|
|
3322
|
-
"01_BASE/USER.md": base(
|
|
3323
|
-
"user",
|
|
3324
|
-
"About me",
|
|
3325
|
-
"Who I am and what context I work in.",
|
|
3326
|
-
[
|
|
3327
|
-
"Background, role, what you are working on, what an agent should assume",
|
|
3328
|
-
"you already know.",
|
|
3329
|
-
"",
|
|
3330
|
-
"- "
|
|
3331
|
-
].join("\n")
|
|
3332
|
-
),
|
|
3333
|
-
"01_BASE/PREFERENCES.md": base(
|
|
3334
|
-
"preferences",
|
|
3335
|
-
"Preferences",
|
|
3336
|
-
"How I like answers, code and decisions handled.",
|
|
3337
|
-
["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
|
|
3338
|
-
),
|
|
3339
|
-
"01_BASE/BOUNDARIES.md": base(
|
|
3340
|
-
"boundaries",
|
|
3341
|
-
"Boundaries",
|
|
3342
|
-
"What an agent must not do without asking me first.",
|
|
3343
|
-
[
|
|
3344
|
-
"Privacy, spending, anything irreversible, anything that leaves this",
|
|
3345
|
-
"machine. Be specific \u2014 a boundary that has to be interpreted is one",
|
|
3346
|
-
"that will be interpreted generously.",
|
|
3347
|
-
"",
|
|
3348
|
-
"- "
|
|
3349
|
-
].join("\n")
|
|
3350
|
-
),
|
|
3351
|
-
"01_BASE/TOOLING.md": base(
|
|
3352
|
-
"tooling",
|
|
3353
|
-
"Tooling",
|
|
3354
|
-
"The tools, machines and commands I actually use.",
|
|
3355
|
-
[
|
|
3356
|
-
"Editors, languages, deploy commands, where things run. Enough that an",
|
|
3357
|
-
"agent does not have to guess or discover it again each time.",
|
|
3358
|
-
"",
|
|
3359
|
-
"- "
|
|
3360
|
-
].join("\n")
|
|
3373
|
+
if (samples.length > 0) {
|
|
3374
|
+
printHuman(c.gray("\n For example, these exact lines would be stored:"));
|
|
3375
|
+
for (const r of samples) {
|
|
3376
|
+
printHuman(c.gray(` ${r.title}`));
|
|
3377
|
+
printHuman(c.gray(` ${r.summary ?? "(no summary \u2014 title only)"}`));
|
|
3378
|
+
}
|
|
3379
|
+
if (notes.length > samples.length) {
|
|
3380
|
+
printHuman(
|
|
3381
|
+
c.gray(` \u2026and ${notes.length - samples.length} more \u2014 see them all with --dry-run.`)
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
printHuman(c.bold("\n A name list cannot protect a file with an ordinary name."));
|
|
3386
|
+
printHuman(
|
|
3387
|
+
c.gray(
|
|
3388
|
+
" Notes whose CONTENT looks like a credential are refused automatically,\n but a diagnosis or an address written as plain prose matches nothing.\n List those by exact path in a .memgdeny file at the top of your notes\n folder \u2014 one path per line, relative to that folder, no wildcards:\n 30_personal/medical.md\n 99_archive/ (a trailing slash covers a whole folder)"
|
|
3361
3389
|
)
|
|
3362
|
-
|
|
3390
|
+
);
|
|
3391
|
+
printHuman(
|
|
3392
|
+
c.gray(
|
|
3393
|
+
"\n This tool writes into your notes without asking each time \u2014 new\n observations, and changes to the memory block of a note. Every one is\n recorded and reversible: memgineering log \xB7 memgineering undo"
|
|
3394
|
+
)
|
|
3395
|
+
);
|
|
3396
|
+
const { ok } = await prompts({
|
|
3397
|
+
type: "confirm",
|
|
3398
|
+
name: "ok",
|
|
3399
|
+
message: "Build the index for this folder?",
|
|
3400
|
+
initial: false
|
|
3401
|
+
});
|
|
3402
|
+
return ok === true;
|
|
3363
3403
|
}
|
|
3364
|
-
var
|
|
3365
|
-
var
|
|
3366
|
-
"src/commands/
|
|
3404
|
+
var DEFAULT_OBSERVATIONS_DIR;
|
|
3405
|
+
var init_link = __esm({
|
|
3406
|
+
"src/commands/link.ts"() {
|
|
3367
3407
|
"use strict";
|
|
3368
3408
|
init_adapter();
|
|
3409
|
+
init_rules_file();
|
|
3410
|
+
init_src();
|
|
3369
3411
|
init_errors();
|
|
3370
3412
|
init_config();
|
|
3371
3413
|
init_index_build();
|
|
3372
3414
|
init_vault();
|
|
3373
3415
|
init_ui();
|
|
3374
|
-
|
|
3375
|
-
FOLDERS = [
|
|
3376
|
-
"00_HUB",
|
|
3377
|
-
"01_BASE",
|
|
3378
|
-
"10_ENTITIES",
|
|
3379
|
-
"20_TIMELINE",
|
|
3380
|
-
"20_TIMELINE/observations",
|
|
3381
|
-
"30_ACTIONS",
|
|
3382
|
-
"40_KNOWLEDGE",
|
|
3383
|
-
"50_SKILLS",
|
|
3384
|
-
"60_TEMPLATES",
|
|
3385
|
-
"70_SOURCES",
|
|
3386
|
-
"80_SHAREABLE",
|
|
3387
|
-
"90_ARCHIVE"
|
|
3388
|
-
];
|
|
3416
|
+
DEFAULT_OBSERVATIONS_DIR = "_observations";
|
|
3389
3417
|
}
|
|
3390
3418
|
});
|
|
3391
3419
|
|
|
@@ -5424,7 +5452,11 @@ function unlinkCommand() {
|
|
|
5424
5452
|
const { brain } = await resolveBrain(path);
|
|
5425
5453
|
const cfg = await loadConfig();
|
|
5426
5454
|
const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
|
|
5427
|
-
|
|
5455
|
+
const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
|
|
5456
|
+
await saveConfig({
|
|
5457
|
+
...cfg,
|
|
5458
|
+
brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
|
|
5459
|
+
});
|
|
5428
5460
|
if (opts.purge) {
|
|
5429
5461
|
await clearCache(cacheDirFor(brain.root, brandHome()));
|
|
5430
5462
|
await rm5(stateDirFor(brain.root), { recursive: true, force: true });
|
|
@@ -5515,8 +5547,8 @@ async function readLines(root, name) {
|
|
|
5515
5547
|
// src/commands/curation.ts
|
|
5516
5548
|
init_src();
|
|
5517
5549
|
import { stat as stat2 } from "fs/promises";
|
|
5518
|
-
import { resolve as
|
|
5519
|
-
import { Command as
|
|
5550
|
+
import { resolve as resolve6 } from "path";
|
|
5551
|
+
import { Command as Command6 } from "commander";
|
|
5520
5552
|
|
|
5521
5553
|
// ../../packages/storage-adapter/src/local-file/ignore-file.ts
|
|
5522
5554
|
init_atomic();
|
|
@@ -5964,7 +5996,7 @@ init_ui();
|
|
|
5964
5996
|
// src/commands/open.ts
|
|
5965
5997
|
init_src();
|
|
5966
5998
|
init_errors();
|
|
5967
|
-
import { Command as
|
|
5999
|
+
import { Command as Command4 } from "commander";
|
|
5968
6000
|
|
|
5969
6001
|
// src/lib/events.ts
|
|
5970
6002
|
import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile12, readdir as readdir4 } from "fs/promises";
|
|
@@ -6037,6 +6069,96 @@ async function readEvents(vaultRoot) {
|
|
|
6037
6069
|
// src/commands/open.ts
|
|
6038
6070
|
init_index_build();
|
|
6039
6071
|
|
|
6072
|
+
// src/lib/base-files.ts
|
|
6073
|
+
init_init();
|
|
6074
|
+
var BASE_FILES = [
|
|
6075
|
+
{
|
|
6076
|
+
path: "01_BASE/USER.md",
|
|
6077
|
+
title: "Who they are",
|
|
6078
|
+
asks: "who they are, what they work on, and what they are trying to get done",
|
|
6079
|
+
openers: [
|
|
6080
|
+
"What should I know about you before we work together?",
|
|
6081
|
+
"What are you building, or what do you spend most of your time on?"
|
|
6082
|
+
]
|
|
6083
|
+
},
|
|
6084
|
+
{
|
|
6085
|
+
path: "01_BASE/PREFERENCES.md",
|
|
6086
|
+
title: "How they want you to work",
|
|
6087
|
+
asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
|
|
6088
|
+
openers: [
|
|
6089
|
+
"When I answer you, do you want the short version or the reasoning too?",
|
|
6090
|
+
"How much should I check in before doing something, versus just doing it?"
|
|
6091
|
+
]
|
|
6092
|
+
},
|
|
6093
|
+
{
|
|
6094
|
+
path: "01_BASE/BOUNDARIES.md",
|
|
6095
|
+
title: "What to ask before doing",
|
|
6096
|
+
asks: "what you must never do without asking first",
|
|
6097
|
+
openers: [
|
|
6098
|
+
"What should I never do without asking you first?",
|
|
6099
|
+
"Has an assistant ever done something for you that you wish it had asked about?"
|
|
6100
|
+
]
|
|
6101
|
+
},
|
|
6102
|
+
{
|
|
6103
|
+
path: "01_BASE/CRITICAL_FACTS.md",
|
|
6104
|
+
title: "What is expensive to get wrong",
|
|
6105
|
+
asks: "the facts where being wrong costs real money, data, or trust",
|
|
6106
|
+
openers: [
|
|
6107
|
+
"What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
|
|
6108
|
+
"Is there anything that looks routine but is actually irreversible?"
|
|
6109
|
+
]
|
|
6110
|
+
},
|
|
6111
|
+
{
|
|
6112
|
+
path: "01_BASE/TOOLING.md",
|
|
6113
|
+
title: "What they actually run",
|
|
6114
|
+
asks: "the editors, machines, languages and commands they really use",
|
|
6115
|
+
openers: [
|
|
6116
|
+
"What do you actually run this on \u2014 editor, machine, language, deploy command?",
|
|
6117
|
+
"Anything I would guess wrong about your setup if I did not ask?"
|
|
6118
|
+
]
|
|
6119
|
+
}
|
|
6120
|
+
];
|
|
6121
|
+
var HUB_PATH = "00_HUB/HUB.md";
|
|
6122
|
+
function bodyOf(content) {
|
|
6123
|
+
const stripped = content.replace(/^/, "");
|
|
6124
|
+
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
6125
|
+
return (fence ? stripped.slice(fence[0].length) : stripped).trim();
|
|
6126
|
+
}
|
|
6127
|
+
function bodyStillTemplate(path, content) {
|
|
6128
|
+
const template = files("brain")[path];
|
|
6129
|
+
if (template === void 0) return false;
|
|
6130
|
+
const body = bodyOf(content);
|
|
6131
|
+
return body !== "" && body === bodyOf(template);
|
|
6132
|
+
}
|
|
6133
|
+
async function scaffoldingAmong(adapter, notes) {
|
|
6134
|
+
const basePaths = new Set(BASE_FILES.map((f) => f.path));
|
|
6135
|
+
const found = await untouchedBase(
|
|
6136
|
+
adapter,
|
|
6137
|
+
notes.map((n) => n.path).filter((path) => basePaths.has(path))
|
|
6138
|
+
);
|
|
6139
|
+
const hub = notes.find((n) => n.path === HUB_PATH);
|
|
6140
|
+
if (hub && await untouchedHub(adapter, hub.title)) found.add(HUB_PATH);
|
|
6141
|
+
return found;
|
|
6142
|
+
}
|
|
6143
|
+
async function untouchedHub(adapter, title) {
|
|
6144
|
+
const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
|
|
6145
|
+
if (actual === null) return false;
|
|
6146
|
+
return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
|
|
6147
|
+
}
|
|
6148
|
+
async function untouchedBase(adapter, paths) {
|
|
6149
|
+
const templates = files("brain");
|
|
6150
|
+
const out = /* @__PURE__ */ new Set();
|
|
6151
|
+
await Promise.all(
|
|
6152
|
+
paths.map(async (path) => {
|
|
6153
|
+
const template = templates[path];
|
|
6154
|
+
if (template === void 0) return;
|
|
6155
|
+
const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
|
|
6156
|
+
if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
|
|
6157
|
+
})
|
|
6158
|
+
);
|
|
6159
|
+
return out;
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6040
6162
|
// src/lib/stages.ts
|
|
6041
6163
|
init_sections();
|
|
6042
6164
|
init_errors();
|
|
@@ -6082,7 +6204,7 @@ async function assembleStage(adapter, entry, detail, sectionName) {
|
|
|
6082
6204
|
init_vault();
|
|
6083
6205
|
init_ui();
|
|
6084
6206
|
function openCommand() {
|
|
6085
|
-
return new
|
|
6207
|
+
return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(async (ref, opts) => {
|
|
6086
6208
|
const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
|
|
6087
6209
|
const { brain } = await resolveBrain(opts.vault);
|
|
6088
6210
|
const adapter = await openBrainOrExplain(brain.root);
|
|
@@ -6101,12 +6223,14 @@ function openCommand() {
|
|
|
6101
6223
|
entry.memory.id,
|
|
6102
6224
|
built.entries.map((e) => e.memory.id)
|
|
6103
6225
|
);
|
|
6226
|
+
const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
|
|
6104
6227
|
printDual({
|
|
6105
6228
|
json: {
|
|
6106
6229
|
id: entry.memory.id,
|
|
6107
6230
|
handle: handle2,
|
|
6108
6231
|
path: entry.path,
|
|
6109
6232
|
detail,
|
|
6233
|
+
scaffolding,
|
|
6110
6234
|
memory: entry.memory,
|
|
6111
6235
|
claims,
|
|
6112
6236
|
relations: entry.relations,
|
|
@@ -6115,9 +6239,15 @@ function openCommand() {
|
|
|
6115
6239
|
human: () => {
|
|
6116
6240
|
printHuman(`## ${entry.memory.title}
|
|
6117
6241
|
`);
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6242
|
+
if (scaffolding) {
|
|
6243
|
+
printHuman(
|
|
6244
|
+
c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
|
|
6245
|
+
);
|
|
6246
|
+
} else {
|
|
6247
|
+
printHuman(
|
|
6248
|
+
`${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
|
|
6249
|
+
);
|
|
6250
|
+
}
|
|
6121
6251
|
printHuman(`
|
|
6122
6252
|
\`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
|
|
6123
6253
|
`);
|
|
@@ -6126,7 +6256,19 @@ function openCommand() {
|
|
|
6126
6256
|
printHuman(`- ${claim.text}`);
|
|
6127
6257
|
printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
|
|
6128
6258
|
}
|
|
6129
|
-
if (claims.length === 0) {
|
|
6259
|
+
if (claims.length === 0 && scaffolding) {
|
|
6260
|
+
printHuman(
|
|
6261
|
+
c.gray(
|
|
6262
|
+
`_Nobody has written this yet. The line above is the prompt \`init\` left, and the
|
|
6263
|
+
body is its instructions \u2014 neither is something the user said, so do not answer
|
|
6264
|
+
from them._
|
|
6265
|
+
|
|
6266
|
+
This one is filled in by asking. \`memgineering onboard\` prints the questions;
|
|
6267
|
+
write the body, then record the conclusion:
|
|
6268
|
+
memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
|
|
6269
|
+
)
|
|
6270
|
+
);
|
|
6271
|
+
} else if (claims.length === 0) {
|
|
6130
6272
|
printHuman(
|
|
6131
6273
|
c.gray(
|
|
6132
6274
|
entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
|
|
@@ -6223,7 +6365,7 @@ function fold2(value) {
|
|
|
6223
6365
|
// src/commands/curation.ts
|
|
6224
6366
|
init_link();
|
|
6225
6367
|
function lifecycleCommand(action) {
|
|
6226
|
-
return new
|
|
6368
|
+
return new Command6(action).description(
|
|
6227
6369
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
6228
6370
|
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").action(async (ref, opts) => {
|
|
6229
6371
|
const rationale = sanitizeRationale(opts.reason);
|
|
@@ -6327,7 +6469,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6327
6469
|
});
|
|
6328
6470
|
}
|
|
6329
6471
|
function ignoreCommand(mode) {
|
|
6330
|
-
return new
|
|
6472
|
+
return new Command6(mode).description(
|
|
6331
6473
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
6332
6474
|
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
|
|
6333
6475
|
const rationale = sanitizeRationale(opts.reason);
|
|
@@ -6385,7 +6527,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6385
6527
|
}
|
|
6386
6528
|
async function mustMatchAFile(root, adapter, path) {
|
|
6387
6529
|
const wanted = path.normalize("NFC").replace(/^\/+/, "");
|
|
6388
|
-
const absolute =
|
|
6530
|
+
const absolute = resolve6(root, wanted);
|
|
6389
6531
|
if (isInside(absolute, root)) {
|
|
6390
6532
|
const found = await stat2(absolute).catch(() => null);
|
|
6391
6533
|
if (found?.isFile()) return wanted;
|
|
@@ -6403,12 +6545,12 @@ var excludeCommand = () => ignoreCommand("exclude");
|
|
|
6403
6545
|
var unexcludeCommand = () => ignoreCommand("unexclude");
|
|
6404
6546
|
|
|
6405
6547
|
// src/commands/evidence.ts
|
|
6406
|
-
import { Command as
|
|
6548
|
+
import { Command as Command7 } from "commander";
|
|
6407
6549
|
init_index_build();
|
|
6408
6550
|
init_vault();
|
|
6409
6551
|
init_ui();
|
|
6410
6552
|
function evidenceCommand() {
|
|
6411
|
-
return new
|
|
6553
|
+
return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
|
|
6412
6554
|
const { brain } = await resolveBrain(opts.vault);
|
|
6413
6555
|
const adapter = await openBrainOrExplain(brain.root);
|
|
6414
6556
|
const built = await openIndex(adapter, brain.root);
|
|
@@ -6673,71 +6815,6 @@ async function readOrNull(adapter, path) {
|
|
|
6673
6815
|
|
|
6674
6816
|
// src/commands/onboard.ts
|
|
6675
6817
|
import { Command as Command9 } from "commander";
|
|
6676
|
-
|
|
6677
|
-
// src/lib/base-files.ts
|
|
6678
|
-
init_init();
|
|
6679
|
-
var BASE_FILES = [
|
|
6680
|
-
{
|
|
6681
|
-
path: "01_BASE/USER.md",
|
|
6682
|
-
title: "Who they are",
|
|
6683
|
-
asks: "who they are, what they work on, and what they are trying to get done",
|
|
6684
|
-
openers: [
|
|
6685
|
-
"What should I know about you before we work together?",
|
|
6686
|
-
"What are you building, or what do you spend most of your time on?"
|
|
6687
|
-
]
|
|
6688
|
-
},
|
|
6689
|
-
{
|
|
6690
|
-
path: "01_BASE/PREFERENCES.md",
|
|
6691
|
-
title: "How they want you to work",
|
|
6692
|
-
asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
|
|
6693
|
-
openers: [
|
|
6694
|
-
"When I answer you, do you want the short version or the reasoning too?",
|
|
6695
|
-
"How much should I check in before doing something, versus just doing it?"
|
|
6696
|
-
]
|
|
6697
|
-
},
|
|
6698
|
-
{
|
|
6699
|
-
path: "01_BASE/BOUNDARIES.md",
|
|
6700
|
-
title: "What to ask before doing",
|
|
6701
|
-
asks: "what you must never do without asking first",
|
|
6702
|
-
openers: [
|
|
6703
|
-
"What should I never do without asking you first?",
|
|
6704
|
-
"Has an assistant ever done something for you that you wish it had asked about?"
|
|
6705
|
-
]
|
|
6706
|
-
},
|
|
6707
|
-
{
|
|
6708
|
-
path: "01_BASE/CRITICAL_FACTS.md",
|
|
6709
|
-
title: "What is expensive to get wrong",
|
|
6710
|
-
asks: "the facts where being wrong costs real money, data, or trust",
|
|
6711
|
-
openers: [
|
|
6712
|
-
"What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
|
|
6713
|
-
"Is there anything that looks routine but is actually irreversible?"
|
|
6714
|
-
]
|
|
6715
|
-
},
|
|
6716
|
-
{
|
|
6717
|
-
path: "01_BASE/TOOLING.md",
|
|
6718
|
-
title: "What they actually run",
|
|
6719
|
-
asks: "the editors, machines, languages and commands they really use",
|
|
6720
|
-
openers: [
|
|
6721
|
-
"What do you actually run this on \u2014 editor, machine, language, deploy command?",
|
|
6722
|
-
"Anything I would guess wrong about your setup if I did not ask?"
|
|
6723
|
-
]
|
|
6724
|
-
}
|
|
6725
|
-
];
|
|
6726
|
-
async function untouchedBase(adapter, paths) {
|
|
6727
|
-
const templates = files("brain");
|
|
6728
|
-
const out = /* @__PURE__ */ new Set();
|
|
6729
|
-
await Promise.all(
|
|
6730
|
-
paths.map(async (path) => {
|
|
6731
|
-
const template = templates[path];
|
|
6732
|
-
if (template === void 0) return;
|
|
6733
|
-
const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
|
|
6734
|
-
if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
|
|
6735
|
-
})
|
|
6736
|
-
);
|
|
6737
|
-
return out;
|
|
6738
|
-
}
|
|
6739
|
-
|
|
6740
|
-
// src/commands/onboard.ts
|
|
6741
6818
|
init_vault();
|
|
6742
6819
|
init_ui();
|
|
6743
6820
|
function onboardCommand() {
|
|
@@ -6836,6 +6913,14 @@ function recallCommand() {
|
|
|
6836
6913
|
stage: await stageFor(adapter, byId.get(card.id), detail)
|
|
6837
6914
|
}))
|
|
6838
6915
|
);
|
|
6916
|
+
const scaffolding = await scaffoldingAmong(
|
|
6917
|
+
adapter,
|
|
6918
|
+
rows.map(({ card }) => {
|
|
6919
|
+
const entry = byId.get(card.id);
|
|
6920
|
+
return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
|
|
6921
|
+
})
|
|
6922
|
+
);
|
|
6923
|
+
const isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
6839
6924
|
const reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
|
|
6840
6925
|
printDual({
|
|
6841
6926
|
json: {
|
|
@@ -6845,6 +6930,10 @@ function recallCommand() {
|
|
|
6845
6930
|
...card,
|
|
6846
6931
|
handle: handle2,
|
|
6847
6932
|
path: byId.get(card.id)?.path ?? null,
|
|
6933
|
+
// A fact about the file, not a judgement about the match: this
|
|
6934
|
+
// note is still exactly what `init` wrote, so nobody has answered
|
|
6935
|
+
// it yet.
|
|
6936
|
+
scaffolding: isScaffolding(card),
|
|
6848
6937
|
...stage
|
|
6849
6938
|
})),
|
|
6850
6939
|
related: result.related,
|
|
@@ -6855,9 +6944,7 @@ function recallCommand() {
|
|
|
6855
6944
|
}))
|
|
6856
6945
|
},
|
|
6857
6946
|
human: () => {
|
|
6858
|
-
const reachedSomethingWritten = rows.some(
|
|
6859
|
-
({ card }) => !(byId.get(card.id)?.path ?? "").startsWith("01_BASE/")
|
|
6860
|
-
);
|
|
6947
|
+
const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
|
|
6861
6948
|
const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
|
|
6862
6949
|
const sayWhy = () => {
|
|
6863
6950
|
printHuman(
|
|
@@ -6888,10 +6975,20 @@ function recallCommand() {
|
|
|
6888
6975
|
}
|
|
6889
6976
|
printHuman(`## recall: ${query} (${rows.length} cards)
|
|
6890
6977
|
`);
|
|
6978
|
+
if (!reachedSomethingWritten) {
|
|
6979
|
+
printHuman(
|
|
6980
|
+
c.yellow("\u26A0 Nothing you have written matched this.") + c.gray(
|
|
6981
|
+
"\n Every card below is still the template `init` wrote \u2014 nobody has answered\n these yet, so none of them is evidence of anything.\n `memgineering onboard` prints what to ask to fill them in.\n"
|
|
6982
|
+
)
|
|
6983
|
+
);
|
|
6984
|
+
}
|
|
6891
6985
|
if (otherScript) sayWhy();
|
|
6892
|
-
const glossOn = rows.findIndex(
|
|
6986
|
+
const glossOn = rows.findIndex(
|
|
6987
|
+
({ card }) => card.confidence === "stated" && !isScaffolding(card)
|
|
6988
|
+
);
|
|
6893
6989
|
for (const [i, { card, handle: handle2, stage }] of rows.entries()) {
|
|
6894
|
-
|
|
6990
|
+
const mark = reachedSomethingWritten && isScaffolding(card);
|
|
6991
|
+
printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn, mark));
|
|
6895
6992
|
}
|
|
6896
6993
|
if (result.conflicts.length > 0) {
|
|
6897
6994
|
printHuman(`### \u26A0 contradictions (${result.conflicts.length})
|
|
@@ -6947,13 +7044,19 @@ async function stageFor(adapter, entry, detail) {
|
|
|
6947
7044
|
if (!entry) return {};
|
|
6948
7045
|
return assembleStage(adapter, entry, detail);
|
|
6949
7046
|
}
|
|
6950
|
-
function renderCard(n, card, handle2, detail, stage, glossHere = false) {
|
|
7047
|
+
function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false) {
|
|
6951
7048
|
const out = [];
|
|
6952
7049
|
const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
|
|
6953
7050
|
const age = card.age_days === null ? "" : ` \xB7 ${ago2(card.age_days)}`;
|
|
6954
7051
|
out.push(`### ${n}. ${card.title}`);
|
|
6955
|
-
|
|
6956
|
-
|
|
7052
|
+
if (scaffolding) {
|
|
7053
|
+
out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
|
|
7054
|
+
} else {
|
|
7055
|
+
out.push(`${card.confidence}${standing}${age}`);
|
|
7056
|
+
}
|
|
7057
|
+
if (glossHere && !scaffolding) {
|
|
7058
|
+
out.push(c.gray("stated = these fields came from the note, not from inference"));
|
|
7059
|
+
}
|
|
6957
7060
|
if (detail !== "title") {
|
|
6958
7061
|
if (card.summary) out.push("", quoteBlock(card.summary));
|
|
6959
7062
|
out.push("", `\`open: ${handle2}\``);
|
|
@@ -7280,7 +7383,22 @@ function reviseCommand() {
|
|
|
7280
7383
|
return new Command13("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
|
|
7281
7384
|
"--valid-from <iso>",
|
|
7282
7385
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
7283
|
-
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").
|
|
7386
|
+
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").addHelpText(
|
|
7387
|
+
"after",
|
|
7388
|
+
[
|
|
7389
|
+
"",
|
|
7390
|
+
"Which --action:",
|
|
7391
|
+
" supersede the conclusion CHANGED. Stamps valid_from with now, because a",
|
|
7392
|
+
" replaced conclusion starts now. The default.",
|
|
7393
|
+
" reinforce the SAME conclusion, with new support. Leaves valid_from alone \u2014",
|
|
7394
|
+
" use this when nothing about the fact changed, only your evidence.",
|
|
7395
|
+
" conflict two memories DISAGREE. Records the edge and leaves both standing;",
|
|
7396
|
+
" the claim you pass is deliberately not written.",
|
|
7397
|
+
"",
|
|
7398
|
+
"Filling in a blank 01_BASE file for the first time is `reinforce`: the fact was",
|
|
7399
|
+
"always true, you have only just been told it. `supersede` would date it today."
|
|
7400
|
+
].join("\n")
|
|
7401
|
+
).action(async (ref, opts) => {
|
|
7284
7402
|
const { brain } = await resolveBrain(opts.vault);
|
|
7285
7403
|
await openBrainOrExplain(brain.root);
|
|
7286
7404
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
@@ -7294,9 +7412,23 @@ function reviseCommand() {
|
|
|
7294
7412
|
);
|
|
7295
7413
|
const result = commit(asSingleMatch(entry), input);
|
|
7296
7414
|
if (result.action === "ignore") {
|
|
7297
|
-
throw memgError(
|
|
7415
|
+
throw memgError(
|
|
7416
|
+
"invalid_input",
|
|
7417
|
+
"nothing to revise",
|
|
7418
|
+
"Pass --claim, or --input with one."
|
|
7419
|
+
);
|
|
7298
7420
|
}
|
|
7299
7421
|
const proposal = result.proposal;
|
|
7422
|
+
if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
|
|
7423
|
+
throw memgError(
|
|
7424
|
+
"invalid_input",
|
|
7425
|
+
`${entry.memory.title} already says this \u2014 nothing was written`,
|
|
7426
|
+
`The claim, summary and title all match what the note already holds.
|
|
7427
|
+
If you meant to record that it still holds, say why:
|
|
7428
|
+
memgineering revise ${ref} --action reinforce --claim "\u2026" --reason "<what confirms it>"
|
|
7429
|
+
To see what is there now: \`memgineering open ` + ref + "`"
|
|
7430
|
+
);
|
|
7431
|
+
}
|
|
7300
7432
|
const preview = await previewProposal(adapter, proposal);
|
|
7301
7433
|
if (preview.refusal) {
|
|
7302
7434
|
throw Object.assign(preview.refusal, {
|
|
@@ -7305,7 +7437,12 @@ function reviseCommand() {
|
|
|
7305
7437
|
}
|
|
7306
7438
|
if (opts.dryRun) {
|
|
7307
7439
|
printDual({
|
|
7308
|
-
json: {
|
|
7440
|
+
json: {
|
|
7441
|
+
dryRun: true,
|
|
7442
|
+
target: entry.path,
|
|
7443
|
+
before: preview.before,
|
|
7444
|
+
after: preview.after
|
|
7445
|
+
},
|
|
7309
7446
|
human: () => {
|
|
7310
7447
|
printHuman(`## revise (dry run): ${entry.memory.title}
|
|
7311
7448
|
`);
|
|
@@ -7353,6 +7490,7 @@ function reviseCommand() {
|
|
|
7353
7490
|
return { record, content: outcome.content };
|
|
7354
7491
|
});
|
|
7355
7492
|
const drift = proposal.action === "supersede" ? bodyStillAsserts(applied.content, previousClaim(entry), proposal.claim?.text ?? null) : null;
|
|
7493
|
+
const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
|
|
7356
7494
|
printDual({
|
|
7357
7495
|
json: {
|
|
7358
7496
|
target: entry.path,
|
|
@@ -7360,7 +7498,8 @@ function reviseCommand() {
|
|
|
7360
7498
|
op_id: applied.record.op_id,
|
|
7361
7499
|
before: preview.before,
|
|
7362
7500
|
after: preview.after,
|
|
7363
|
-
body_drift: drift
|
|
7501
|
+
body_drift: drift,
|
|
7502
|
+
body_unwritten: unwrittenBody
|
|
7364
7503
|
},
|
|
7365
7504
|
human: () => {
|
|
7366
7505
|
if (proposal.action === "conflict") {
|
|
@@ -7388,6 +7527,14 @@ function reviseCommand() {
|
|
|
7388
7527
|
)
|
|
7389
7528
|
);
|
|
7390
7529
|
}
|
|
7530
|
+
if (unwrittenBody) {
|
|
7531
|
+
printHuman(
|
|
7532
|
+
c.yellow(`
|
|
7533
|
+
\u26A0 the body of this note is still the template \`init\` wrote`) + c.gray(
|
|
7534
|
+
"\n \u2014 the conclusion above it is theirs, the prose under it is instructions for\n writing one. Anyone opening the file reads the form, not the answer.\n Replace the body with what they actually said, in their words."
|
|
7535
|
+
)
|
|
7536
|
+
);
|
|
7537
|
+
}
|
|
7391
7538
|
if (isCritical(entry)) {
|
|
7392
7539
|
printHuman(
|
|
7393
7540
|
c.yellow(
|
|
@@ -7497,14 +7644,26 @@ function previousClaim(entry) {
|
|
|
7497
7644
|
const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
|
|
7498
7645
|
return current?.text ?? null;
|
|
7499
7646
|
}
|
|
7647
|
+
function saysNothingNew(entry, input) {
|
|
7648
|
+
const norm = (v) => stripControl(v ?? "").trim();
|
|
7649
|
+
const claim = input.claim;
|
|
7650
|
+
if (!claim || norm(claim.text) === "") return false;
|
|
7651
|
+
if (norm(claim.text) !== norm(previousClaim(entry))) return false;
|
|
7652
|
+
if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
|
|
7653
|
+
if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
|
|
7654
|
+
const declared = new Set(
|
|
7655
|
+
entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
|
|
7656
|
+
);
|
|
7657
|
+
return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
|
|
7658
|
+
}
|
|
7500
7659
|
function bodyStillAsserts(content, oldClaim, newClaim) {
|
|
7501
7660
|
if (oldClaim === null) return null;
|
|
7502
7661
|
const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
|
|
7503
7662
|
if (line === "") return null;
|
|
7504
7663
|
if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
|
|
7505
|
-
return
|
|
7664
|
+
return bodyOf2(content).includes(line) ? line : null;
|
|
7506
7665
|
}
|
|
7507
|
-
function
|
|
7666
|
+
function bodyOf2(content) {
|
|
7508
7667
|
const stripped = content.replace(/^/, "");
|
|
7509
7668
|
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
7510
7669
|
return fence ? stripped.slice(fence[0].length) : stripped;
|
|
@@ -8098,10 +8257,14 @@ var VIA_MEANS = {
|
|
|
8098
8257
|
flag: "because --vault named it",
|
|
8099
8258
|
pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
|
|
8100
8259
|
cwd: "because this folder is inside it",
|
|
8101
|
-
only: "because it is the only brain linked"
|
|
8260
|
+
only: "because it is the only brain linked",
|
|
8261
|
+
default: "because it is your default brain (`memgineering use --default <path>` changes it)"
|
|
8102
8262
|
};
|
|
8103
8263
|
function useCommand() {
|
|
8104
|
-
return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").
|
|
8264
|
+
return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
|
|
8265
|
+
"--default",
|
|
8266
|
+
"make this the brain used anywhere no pointer applies, instead of binding one folder"
|
|
8267
|
+
).action(async (brainPath, opts) => {
|
|
8105
8268
|
const where = await canonicalize(resolve9(opts.here ?? process.cwd()));
|
|
8106
8269
|
if (!brainPath) {
|
|
8107
8270
|
const pointer = await readPointer(where);
|
|
@@ -8149,6 +8312,22 @@ ${hint}`));
|
|
|
8149
8312
|
A pointer to an unlinked folder would fail on every command instead of at this one.`
|
|
8150
8313
|
);
|
|
8151
8314
|
}
|
|
8315
|
+
if (opts.default) {
|
|
8316
|
+
await saveConfig({ ...cfg, brain: { ...cfg.brain, default: root } });
|
|
8317
|
+
printDual({
|
|
8318
|
+
json: { default: root },
|
|
8319
|
+
human: () => {
|
|
8320
|
+
printHuman(`Default brain is now \`${root}\`.
|
|
8321
|
+
`);
|
|
8322
|
+
printHuman(
|
|
8323
|
+
c.gray(
|
|
8324
|
+
"Used wherever no `.memgineering` pointer applies and you are not inside\nanother brain \u2014 so linking a second brain no longer makes every command\nambiguous. `--vault <path>` still wins for a single run."
|
|
8325
|
+
)
|
|
8326
|
+
);
|
|
8327
|
+
}
|
|
8328
|
+
});
|
|
8329
|
+
return;
|
|
8330
|
+
}
|
|
8152
8331
|
const value = isInside(root, where) ? relative2(where, root) || "." : root;
|
|
8153
8332
|
const file = resolve9(where, BRAND.pointerFileName);
|
|
8154
8333
|
await writeThenRename(file, `${value}
|
package/package.json
CHANGED