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 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.** These files' bodies are prose, and `revise` only touches
118
- frontmatter so write the body with your normal file tools, then set the
119
- summary so recall shows the real thing rather than the prompt:
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) replaces the conclusion
225
- - `--action reinforce` keeps it and adds support
226
- - `--action conflict` records that two notes disagree without picking a winner
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
- If it says the choice is ambiguous, bind the directory once:
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
- `Pick one with --vault <path>, or bind this folder to one with \`memgineering use <path>\`.
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 join12 } from "path";
2826
- import { Command as Command4 } from "commander";
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 Command4("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(
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(join12(path, ".memgdeny"), "utf8").then(
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 OBSERVATIONS_DIR, FOLDERS;
3365
- var init_init = __esm({
3366
- "src/commands/init.ts"() {
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
- OBSERVATIONS_DIR = "20_TIMELINE/observations";
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
- await saveConfig({ ...cfg, brain: { brains: remaining } });
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 resolve5 } from "path";
5519
- import { Command as Command5 } from "commander";
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 Command3 } from "commander";
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 Command3("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) => {
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
- printHuman(
6119
- `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
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 Command5(action).description(
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 Command5(mode).description(
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 = resolve5(root, wanted);
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 Command6 } from "commander";
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 Command6("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) => {
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(({ card }) => card.confidence === "stated");
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
- printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn));
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
- out.push(`${card.confidence}${standing}${age}`);
6956
- if (glossHere) out.push(c.gray("stated = these fields came from the note, not from inference"));
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").action(async (ref, opts) => {
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("invalid_input", "nothing to revise", "Pass --claim, or --input with one.");
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: { dryRun: true, target: entry.path, before: preview.before, after: preview.after },
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 bodyOf(content).includes(line) ? line : null;
7664
+ return bodyOf2(content).includes(line) ? line : null;
7506
7665
  }
7507
- function bodyOf(content) {
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").action(async (brainPath, opts) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memgineering",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",