memgineering 0.12.0 → 0.13.1

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,77 @@ language the reader wants. The bilingual rule the monorepo applies to
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.13.1] — 2026-08-28
15
+
16
+ ### Changed
17
+
18
+ - **The README matches the CLI it ships with.** It had drifted in the ways
19
+ that mislead: "Nothing is uploaded" predated hosted brains (now: nothing is
20
+ uploaded until you `push` or connect one); the brain-resolution order
21
+ omitted the signed-in machine pointer, which outranks every folder rule;
22
+ the command table was missing thirteen verbs, including the entire account
23
+ group; and storage boundaries — the control "Your notes stay yours" is
24
+ about — were absent entirely. Also fixed: the environment table now lists
25
+ `MEMGINEERING_API_URL` and `MEMGINEERING_AGENT_HOME`, the tool list names
26
+ what `setup` actually installs into (Claude Code, Codex, Grok, Gemini CLI),
27
+ and `resurface`'s folder-local ranking signal is described as existing only
28
+ for a brain on this machine. No behaviour changed.
29
+
30
+ ## [0.13.0] — 2026-08-28
31
+
32
+ ### Added
33
+
34
+ - **Widening a boundary is one word, not a retyped list.**
35
+ `revise <ref> --claim "…" --never-store-add "therapy"` adds words to a
36
+ note's declared boundary while keeping every word already there —
37
+ `--never-store` REPLACES the list, so the old remedy was to retype it
38
+ and one transcription slip silently narrowed a boundary. Only the new
39
+ words are validated; the declared ones are grandfathered, so a list an
40
+ older rule set accepted can still be widened. Refused: a note that
41
+ declares no boundary (start one with `--never-store`), the flag beside
42
+ `--never-store` / `--no-never-store` / `--input`, and an addition
43
+ where every word is already held. After a hosted widening the CLI
44
+ reads the note back and warns loudly — on the human line and as
45
+ `never_store_dropped` in `--json` — if the words did not land; honest
46
+ framing: on today's servers that read is drift-defense (a server that
47
+ rewrites or partially accepts the list), not version-skew protection,
48
+ because a server too old for boundaries makes the no-declaration
49
+ refusal fire first. If the read-back itself fails after the write
50
+ landed, the command reports "could not confirm" instead of failing a
51
+ revision the server already holds.
52
+ - **The semantic suggestion now recommends the additive form.** The
53
+ remedy command printed after a near-boundary write — human line and
54
+ `--json` hint alike — is built by the CLI and says `--never-store-add
55
+ "<the word>"`, so following it can no longer delete the words already
56
+ declared. The regression pin is a negative match, because
57
+ `--never-store-add` contains `--never-store` and a substring check
58
+ would pass on the destructive form.
59
+
60
+ ### Fixed
61
+
62
+ - **`proposals approve|reject|rebase --vault <path>` acts on the brain it
63
+ names.** The parent `proposals` command declares `--vault`/`--local`
64
+ for its own listing, and commander parses a parent's options out of
65
+ the whole argv — so the subcommands received nothing at ANY flag
66
+ position and fell through to the linked brain: on a machine signed in
67
+ to a hosted brain, an approval aimed at a folder went to the cloud.
68
+ The subcommands now hear the flags wherever they are written (the last
69
+ occurrence on the line binds), and the undo hint printed after an
70
+ approval names the brain the write actually landed on.
71
+ - **The local `revise --input` door screens `claim.scope`.** It was the
72
+ one user-prose field reachable through `--input` that the boundary
73
+ screen did not see. Honest framing: the engine currently discards
74
+ scope on this path, so this shuts a door rather than plugs a leak —
75
+ it guards against that internal list ceasing to be true.
76
+
77
+ ### Agent guidance
78
+
79
+ - The `memgineering-writing` skill's widening section changed: add a
80
+ word with `--never-store-add`, replace or narrow a list with
81
+ `--never-store`, and a repeated did-not-land warning means stop and
82
+ tell the user. Restart your agent session after upgrading so the new
83
+ guidance is what gets injected.
84
+
14
85
  ## [0.12.0] — 2026-08-28
15
86
 
16
87
  ### Added
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  One memory for the AI you connect.
4
4
 
5
5
  Your notes stay in a folder you own. Every agent you use — Claude Code, Codex,
6
- Antigravity reads and writes the same brain through one CLI, so what you told one
7
- of them is there for the next one, tomorrow, and in whatever tool you move to
8
- after that.
6
+ Grok, Gemini CLI, anything that can run a command reads and writes the same
7
+ brain through one CLI, so what you told one of them is there for the next one,
8
+ tomorrow, and in whatever tool you move to after that.
9
9
 
10
10
  **You do not have to run any of this yourself.** Ask whichever agent you are
11
11
  already talking to — "install memgineering and set it up" — and it does the
@@ -75,14 +75,23 @@ away.
75
75
 
76
76
  ## Your notes stay yours
77
77
 
78
- - **Nothing is uploaded.** The index is derived and lives outside your notes
79
- folder; deleting it costs a rebuild and nothing else.
78
+ - **Nothing is uploaded until you say so.** A brain is a folder on this disk;
79
+ only `push`, or connecting a hosted brain, carries notes to your account. The
80
+ index is derived and lives outside your notes folder; deleting it costs a
81
+ rebuild and nothing else.
80
82
  - **Prose is never rewritten.** `revise` only touches the memory block in a
81
83
  note's frontmatter. Your paragraphs are yours.
82
84
  - **You decide what is read.** `link` shows the actual lines that would be
83
85
  stored — not a description of them — before anything is indexed. Notes whose
84
86
  content looks like a credential are refused automatically; anything else you
85
87
  want left alone goes in `.memgdeny`.
88
+ - **You decide what is never written.** Say "nothing about my health goes in
89
+ here" and the declaration is enforced, not advised:
90
+ `remember "…" --never-store "health, hospital"` records the boundary in your
91
+ own words, and any later write carrying one of them is refused with the word
92
+ and your declaration quoted back — whether or not the agent read anything
93
+ first. Widen it one word at a time with `revise --never-store-add`; retire
94
+ the declaration to lift it.
86
95
  - **`memgineering unlink --purge`** removes every trace this tool kept locally.
87
96
 
88
97
  ## Progressive reading
@@ -102,19 +111,23 @@ and read it" in a single call.
102
111
 
103
112
  ## Several brains
104
113
 
105
- A personal one, a team folder that syncs, one per repository. Which one answers
106
- is decided by where you are:
114
+ A personal one, a team folder that syncs, one per repository, one in your
115
+ account. Which one answers is decided in this order:
107
116
 
108
- 1. `--vault <path>`
109
- 2. a `.memgineering` pointer, found by walking up from the current directory
110
- 3. the brain the current directory is inside
111
- 4. the only one linked
117
+ 1. an explicit flag — `--vault <path>`, `--brain <name>` (a hosted brain, without
118
+ moving the machine's pointer), or `--local`
119
+ 2. the hosted brain this machine is pointed at, when you are signed in
120
+ 3. a `.memgineering` pointer, found by walking up from the current directory
121
+ 4. the brain the current directory is inside
122
+ 5. the only one linked
112
123
 
113
124
  If nothing settles it, memgineering refuses and lists the candidates rather
114
- than guessing. To bind a directory once — a repository root, say:
125
+ than guessing. To bind a directory once — a repository root, say — or to point
126
+ the whole machine at a hosted brain:
115
127
 
116
128
  ```bash
117
129
  memgineering use ~/brains/work # writes a relative path when it can, so it commits
130
+ memgineering use --brain "work" # this machine now reads that hosted brain
118
131
  ```
119
132
 
120
133
  ## Starting a session
@@ -123,40 +136,74 @@ memgineering use ~/brains/work # writes a relative path when it can, so it co
123
136
  memgineering resurface
124
137
  ```
125
138
 
126
- No query. It ranks by what has been recalled in this folder before, how
127
- recently, and which of your base notes have gone unread. `memgineering setup`
128
- can register it to run automatically when a Claude Code session starts.
139
+ No query. It ranks by what has been recalled before, how recently, and which of
140
+ your base notes have gone unread a folder-local signal joins in when the
141
+ brain lives on this machine. `memgineering setup` can register it to run
142
+ automatically when a Claude Code session starts.
129
143
 
130
144
  ## Commands
131
145
 
132
- | | |
133
- | ----------------------------------- | ----------------------------------------------------- |
134
- | `recall <query>` | recall memory cards for a question |
135
- | `open <ref>` | open one memory — by handle, id, path, or exact title |
136
- | `remember <text>` | write something down now |
137
- | `revise <ref>` | change a memory's conclusion |
138
- | `undo [op]` | take back the last change, or a named one |
139
- | `resurface` | what is worth having in view, unasked |
140
- | `init <path>` | create a brain, laid out and ready |
141
- | `link <path>` | read notes you already keep |
142
- | `use [brain]` | bind this folder to one brain |
143
- | `push` · `pull` | carry a brain to your account, and back down again |
144
- | `unlink` · `reindex` · `sync-rules` | brain housekeeping |
145
- | `log` | what changed, and what can still be undone |
146
- | `retire` · `unretire` | mark a memory no longer current, or current again |
147
- | `exclude` · `unexclude` | stop reading a note entirely, or resume |
148
- | `setup` | install into your agents |
149
- | `update` | update memgineering itself |
146
+ **Memory — day to day**
147
+
148
+ | | |
149
+ | ----------------- | ----------------------------------------------------- |
150
+ | `recall <query>` | recall memory cards for a question |
151
+ | `open <ref>` | open one memory — by handle, id, path, or exact title |
152
+ | `evidence <ref>` | how much a memory has been used, and why it changed |
153
+ | `remember <text>` | write something down now |
154
+ | `revise <ref>` | change a memory's conclusion |
155
+ | `undo [op]` | take back the last change, or a named one |
156
+ | `resurface` | what is worth having in view, unasked |
157
+ | `rules` | the standing decisions that bind here |
158
+
159
+ **Brain set up and point**
160
+
161
+ | | |
162
+ | ----------------------------------- | ------------------------------------------------------------------------ |
163
+ | `init <path>` | create a brain, laid out and ready |
164
+ | `onboard` | what to ask so it knows who it belongs to |
165
+ | `link <path>` | read notes you already keep |
166
+ | `use [brain]` | bind this folder — or, with `--brain`, point the machine at a hosted one |
167
+ | `unlink` · `reindex` · `sync-rules` | brain housekeeping |
168
+ | `log` | what changed, and what can still be undone |
169
+
170
+ **Curation — occasional**
171
+
172
+ | | |
173
+ | ----------------------- | --------------------------------------------------- |
174
+ | `retire` · `unretire` | mark a memory no longer current, or current again |
175
+ | `exclude` · `unexclude` | stop reading a note entirely, or resume |
176
+ | `consolidate` | find memories worth merging — reads, writes nothing |
177
+ | `propose <action>` | draft a change for approval — writes no note |
178
+ | `proposals` | review what is waiting — approve, reject, rebase |
179
+
180
+ **Account — optional; everything above works without it**
181
+
182
+ | | |
183
+ | ------------------------------------ | --------------------------------------------------- |
184
+ | `login` · `logout` · `whoami` | the account session on this machine |
185
+ | `brains` | list your hosted brains, and which one answers here |
186
+ | `create` · `rename` · `delete-brain` | make, rename, or delete a hosted brain |
187
+ | `push` · `pull` | carry a brain to your account, and back down again |
188
+
189
+ **System**
190
+
191
+ | | |
192
+ | -------- | -------------------------- |
193
+ | `setup` | install into your agents |
194
+ | `update` | update memgineering itself |
150
195
 
151
196
  Every command takes `--json`.
152
197
 
153
198
  ## Environment
154
199
 
155
- | | |
156
- | -------------------------- | --------------------------------------------------- |
157
- | `MEMGINEERING_HOME` | state and derived index (default `~/.memgineering`) |
158
- | `MEMGINEERING_JSON=1` | force JSON output without the flag |
159
- | `MEMGINEERING_NO_UPDATE=1` | skip the version check for one run |
200
+ | | |
201
+ | -------------------------- | ------------------------------------------------------------ |
202
+ | `MEMGINEERING_HOME` | state and derived index (default `~/.memgineering`) |
203
+ | `MEMGINEERING_JSON=1` | force JSON output without the flag |
204
+ | `MEMGINEERING_NO_UPDATE=1` | skip the version check for one run |
205
+ | `MEMGINEERING_API_URL` | hosted brain server (default `https://api.memgineering.com`) |
206
+ | `MEMGINEERING_AGENT_HOME` | where `setup` installs agent files (default: your home) |
160
207
 
161
208
  ## A brain that is not on this machine
162
209
 
@@ -2,7 +2,7 @@
2
2
  name: memgineering
3
3
  description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
4
4
  type: skill
5
- version: 0.12.0
5
+ version: 0.13.1
6
6
  ---
7
7
 
8
8
  # memgineering
@@ -358,19 +358,28 @@ what they have declared.
358
358
 
359
359
  ### Widening, narrowing, lifting
360
360
 
361
- `--never-store` **REPLACES** the list; it does not add to it. So to add a word,
362
- read the declaration first and name every word it should still hold:
361
+ To add a word, name ONLY the new word the note's words stay:
363
362
 
364
363
  ```bash
365
364
  memgineering revise <ref> \
366
365
  --claim "<the boundary, restated to cover the new word>" \
367
- --never-store "health, hospital, clinic, diagnosis, symptom, medication, therapy"
366
+ --never-store-add "therapy"
368
367
  ```
369
368
 
370
369
  `--claim` is required, and an unchanged claim is refused — restate the boundary
371
- rather than copying the line already there. **Naming only the new word deletes
372
- the words already declared**, which is the opposite of what anybody asking for
373
- this wants.
370
+ rather than copying the line already there. A note that declares no boundary
371
+ refuses `-add` and points you at `--never-store` that is also what you will
372
+ see for every note on a hosted brain older than boundaries, where there is
373
+ nothing to widen yet. After a hosted widening the CLI reads the note back and
374
+ warns out loud if the words did not land; if that warning repeats on a retry,
375
+ the server is not storing what it accepts — stop and tell the user.
376
+
377
+ To narrow or rewrite the list, `--never-store` **REPLACES** it wholesale — name
378
+ every word the note should still hold, because a word you leave out is a word
379
+ the boundary loses. That is what makes it the narrowing verb and the wrong verb
380
+ for adding: **with `--never-store`, naming only the new word deletes the words
381
+ already declared.** The CLI refuses the two flags together rather than guessing
382
+ which you meant.
374
383
 
375
384
  To lift a boundary entirely: `memgineering retire <the declaration>`. That is
376
385
  the un-declare path, and it is the user's decision, never yours.
package/dist/index.js CHANGED
@@ -220,7 +220,10 @@ function undoHint(opId, vault) {
220
220
  return vault ? `memgineering undo ${opId} --vault ${quoteArg(vault)}` : `memgineering undo ${opId}`;
221
221
  }
222
222
  function widenBoundaryHint(path) {
223
- return `memgineering revise ${quoteArg(oneLineWhole(path))} --claim "<the boundary, restated to cover the new word>" --never-store "<every word it should hold, including the new one>"`;
223
+ return `memgineering revise ${quoteArg(oneLineWhole(path))} --claim "<the boundary, restated to cover the new word>" --never-store-add "<the word>"`;
224
+ }
225
+ function boundarySuggestionHint(path) {
226
+ return `This WAS stored and nothing was refused \u2014 no boundary of yours was broken. It only reads as though it might belong with the boundary declared in \`${oneLineWhole(path)}\`, which lists no word this note carries. Tell the person whose brain this is and let them decide, rather than acting on it: if it should not have been stored, \`undo\` on the \`op_id\` in this response takes it back. To close the gap for next time, add the missing word to that declaration \u2014 this keeps the words it already holds: \`${widenBoundaryHint(path)}\`. The word list is what refuses; this check only notices SOME of what it misses, so silence from it is not permission.`;
224
227
  }
225
228
  function quoteArg(value) {
226
229
  return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
@@ -11584,7 +11587,21 @@ function rememberCommand() {
11584
11587
  // always, and is also what a server too old to send it looks
11585
11588
  // like. Those two are not worth telling apart: neither one
11586
11589
  // means anything was dropped.
11587
- ...written.boundary_suggestion == null ? {} : { boundary_suggestion: written.boundary_suggestion }
11590
+ //
11591
+ // The hint is REBUILT here, never forwarded. The server's own
11592
+ // sentence embeds a remedy command, and the flags in it belong
11593
+ // to this binary — a command travelling as a string between two
11594
+ // separately deployed programs goes stale with nothing on
11595
+ // either side able to see it, and that one already had. What
11596
+ // comes from the server is the declaration and the score;
11597
+ // neither is a sentence. `boundarySuggestionHint` has the rest.
11598
+ ...written.boundary_suggestion == null ? {} : {
11599
+ boundary_suggestion: {
11600
+ declared_in: written.boundary_suggestion.declared_in,
11601
+ similarity: written.boundary_suggestion.similarity,
11602
+ hint: boundarySuggestionHint(written.boundary_suggestion.declared_in)
11603
+ }
11604
+ }
11588
11605
  },
11589
11606
  human: () => {
11590
11607
  printHuman(`Remembered in ${oneLine(target.name, WIDTH.title)}.
@@ -11639,8 +11656,8 @@ function rememberCommand() {
11639
11656
  c.gray(
11640
11657
  `close: this might belong with a boundary you declared \u2014 \`${oneLine(suggestion.declared_in, WIDTH.path)}\`
11641
11658
  Nothing was refused; none of the words you listed are in it. Take this note back
11642
- with the undo above \u2014 or widen that boundary. \`--never-store\` REPLACES the whole
11643
- list, so read that note first and name every word it should keep:
11659
+ with the undo above \u2014 or widen that boundary. \`--never-store-add\` keeps the words
11660
+ that note already declares, so only the missing one is named here:
11644
11661
  \`${widenBoundaryHint(suggestion.declared_in)}\``
11645
11662
  )
11646
11663
  );
@@ -11783,7 +11800,7 @@ function parseKind(raw) {
11783
11800
  `Nothing was written. Use one of: ${MemoryKindSchema.options.join(", ")} \u2014 or leave --kind off, which is fine and common. Your own word for it belongs in the note's \`type\`.`
11784
11801
  );
11785
11802
  }
11786
- function parseNeverStore(raw) {
11803
+ function parseNeverStore(raw, flag = "--never-store") {
11787
11804
  if (raw === void 0) return [];
11788
11805
  const out = [];
11789
11806
  for (const piece of raw.split(",")) {
@@ -11801,8 +11818,8 @@ function parseNeverStore(raw) {
11801
11818
  if (out.length === 0) {
11802
11819
  throw memgError(
11803
11820
  "invalid_input",
11804
- "--never-store was given no usable words",
11805
- 'Nothing was written. Pass the words you mean \u2014 `--never-store "\uBCD1\uC6D0, \uC6B0\uC6B8\uC99D"`. To REMOVE a declaration instead, that is `revise <ref> --no-never-store`, which is a different flag on purpose.'
11821
+ `${flag} was given no usable words`,
11822
+ `Nothing was written. Pass the words you mean \u2014 \`${flag} "\uBCD1\uC6D0, \uC6B0\uC6B8\uC99D"\`. To REMOVE a declaration instead, that is \`revise <ref> --no-never-store\`, which is a different flag on purpose.`
11806
11823
  );
11807
11824
  }
11808
11825
  return out;
@@ -11824,7 +11841,10 @@ function reviseCommand() {
11824
11841
  return new Command6("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(
11825
11842
  "--valid-from <iso>",
11826
11843
  "when this became true (defaults: supersede = now, otherwise unchanged)"
11827
- ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--rule", "this memory is now a decision that binds, not a fact to look up").option("--no-rule", "it no longer binds \u2014 an ordinary memory again").option("--never-store <words>", "replace this note\u2019s boundary words (comma-separated)").option("--no-never-store", "this note no longer declares a boundary").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").option("--local", "use the brain on this machine, even when signed in to a hosted one").addHelpText(
11844
+ ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--rule", "this memory is now a decision that binds, not a fact to look up").option("--no-rule", "it no longer binds \u2014 an ordinary memory again").option("--never-store <words>", "replace this note\u2019s boundary words (comma-separated)").option("--no-never-store", "this note no longer declares a boundary").option(
11845
+ "--never-store-add <words>",
11846
+ "add words to this note\u2019s boundary, keeping the ones already there"
11847
+ ).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").option("--local", "use the brain on this machine, even when signed in to a hosted one").addHelpText(
11828
11848
  "after",
11829
11849
  [
11830
11850
  "",
@@ -11840,12 +11860,13 @@ function reviseCommand() {
11840
11860
  "always true, you have only just been told it. `supersede` would date it today."
11841
11861
  ].join("\n")
11842
11862
  ).action(async (ref, opts) => {
11863
+ const added = boundaryAddition(opts);
11843
11864
  const where = await resolveTarget({
11844
11865
  ...opts.local === void 0 ? {} : { local: opts.local },
11845
11866
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
11846
11867
  });
11847
11868
  if (where.kind === "cloud") {
11848
- await reviseHosted(where, ref, opts);
11869
+ await reviseHosted(where, ref, opts, added);
11849
11870
  return;
11850
11871
  }
11851
11872
  const { brain } = await resolveBrain(opts.vault);
@@ -11855,7 +11876,8 @@ function reviseCommand() {
11855
11876
  });
11856
11877
  const built = await openIndex(adapter, brain.root);
11857
11878
  const entry = resolveRef(built.entries, ref);
11858
- const input = await buildCommitInput(entry, opts);
11879
+ const widened = widenedBoundary(entry.memory.never_store, added, ref);
11880
+ const input = await buildCommitInput(entry, opts, widened);
11859
11881
  const rationale = sanitizeRationale(
11860
11882
  input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
11861
11883
  );
@@ -11865,6 +11887,28 @@ function reviseCommand() {
11865
11887
  input.claim?.summary,
11866
11888
  input.claim?.title,
11867
11889
  input.claim?.valid_from,
11890
+ // Free prose, and the last field on this door that was not read.
11891
+ // No flag sets it — only `--input` can.
11892
+ //
11893
+ // What this screens is what the write CARRIES, not what today's
11894
+ // patch happens to keep: `computePatch` currently drops `scope` via
11895
+ // `NEVER_WRITTEN_KEYS`, so on this path the word reaches neither the
11896
+ // note nor the ledger. Screening it anyway, because that list is not
11897
+ // a boundary and has never been treated as one — `title` moved off
11898
+ // it into `PATCHABLE_KEYS` once already — and `scope` is search-
11899
+ // scored by design, so the day it becomes writable is the day the
11900
+ // user's word is stored and findable by the phrase they asked never
11901
+ // to keep. A boundary that holds only while an unrelated list stays
11902
+ // the way it is, is one the user tested once and is wrong about
11903
+ // afterwards.
11904
+ //
11905
+ // Guarded on the type rather than trusted, because `readInput` is a
11906
+ // bare `JSON.parse` with no schema in front of it: a malformed
11907
+ // `--input` puts whatever it likes here, and a screen that threw a
11908
+ // TypeError on it would refuse to say what was actually wrong.
11909
+ // Anything that is not a string falls through to `commit`, which
11910
+ // parses this input properly and reports it as malformed.
11911
+ ...typeof input.claim?.scope === "string" ? [input.claim.scope] : [],
11868
11912
  input.source_excerpt
11869
11913
  ],
11870
11914
  ...input.claim?.never_store != null && input.claim.never_store.length > 0 ? { declares: input.claim.never_store } : (entry.memory.never_store?.length ?? 0) > 0 ? { declares: entry.memory.never_store } : {}
@@ -12025,7 +12069,7 @@ Undo: \`${undoHint(applied.record.op_id, opts.vault)}\``));
12025
12069
  });
12026
12070
  });
12027
12071
  }
12028
- async function reviseHosted(where, ref, opts) {
12072
+ async function reviseHosted(where, ref, opts, added) {
12029
12073
  if (opts.input) {
12030
12074
  throw memgError(
12031
12075
  "invalid_input",
@@ -12040,8 +12084,11 @@ async function reviseHosted(where, ref, opts) {
12040
12084
  'Pass `--claim "<the new conclusion>"`.'
12041
12085
  );
12042
12086
  }
12043
- const boundary = boundaryIntent(opts.neverStore);
12087
+ const replaced = boundaryIntent(opts.neverStore);
12044
12088
  const brain = await openCloudBrain(where);
12089
+ const declared = added === void 0 ? void 0 : declaredWords(await cloudOpen(brain, ref));
12090
+ const widened = widenedBoundary(declared, added, ref);
12091
+ const boundary = widened ?? replaced;
12045
12092
  const result = await cloudRevise(brain, {
12046
12093
  ref,
12047
12094
  claim: opts.claim,
@@ -12062,8 +12109,22 @@ async function reviseHosted(where, ref, opts) {
12062
12109
  ...boundary === void 0 ? {} : { neverStore: boundary },
12063
12110
  ...opts.dryRun === true ? { dryRun: true } : {}
12064
12111
  });
12112
+ let dropped = [];
12113
+ let unconfirmed = false;
12114
+ if (widened !== void 0 && result.dry_run !== true) {
12115
+ try {
12116
+ dropped = missingWords(widened, declaredWords(await cloudOpen(brain, ref)));
12117
+ } catch {
12118
+ unconfirmed = true;
12119
+ }
12120
+ }
12065
12121
  printDual({
12066
- json: { brain: where.name, ...result },
12122
+ json: {
12123
+ brain: where.name,
12124
+ ...result,
12125
+ ...dropped.length === 0 ? {} : { never_store_dropped: dropped },
12126
+ ...unconfirmed ? { never_store_unconfirmed: true } : {}
12127
+ },
12067
12128
  human: () => {
12068
12129
  if (result.dry_run) {
12069
12130
  printHuman(`## revise (dry run) \u2014 ${oneLine(where.name, WIDTH.title)}
@@ -12099,6 +12160,25 @@ async function reviseHosted(where, ref, opts) {
12099
12160
  )
12100
12161
  );
12101
12162
  }
12163
+ if (dropped.length > 0) {
12164
+ printHuman(
12165
+ c.yellow("\n\u26A0 this brain's server did not store the widened boundary") + c.gray(
12166
+ `
12167
+ Missing from the declaration: ${dropped.map((t) => `\`${t}\``).join(", ")}
12168
+ The revision itself is saved, but the note read back without these words, so
12169
+ it refuses exactly what it refused before. Run this again; if it repeats,
12170
+ the server is not storing what it accepts \u2014 check \`memgineering open\` and
12171
+ tell your owner. Nothing here needs undoing first.`
12172
+ )
12173
+ );
12174
+ }
12175
+ if (unconfirmed) {
12176
+ printHuman(
12177
+ c.gray(
12178
+ "\nCould not read the note back to confirm the widened boundary \u2014 the revision\nitself is saved. Check with `memgineering open` when the connection recovers."
12179
+ )
12180
+ );
12181
+ }
12102
12182
  if (result.critical_target) {
12103
12183
  printHuman(
12104
12184
  c.yellow(
@@ -12123,6 +12203,67 @@ ${oneLine(result.reverse.hint, WIDTH.text)}`));
12123
12203
  }
12124
12204
  });
12125
12205
  }
12206
+ function boundaryAddition(opts) {
12207
+ if (opts.neverStoreAdd === void 0) return void 0;
12208
+ if (opts.neverStore === false) {
12209
+ throw memgError(
12210
+ "invalid_input",
12211
+ "--never-store-add and --no-never-store contradict each other",
12212
+ "Nothing was written. One widens this note\u2019s boundary and the other removes it entirely; there is no order in which both are what you meant. Pass whichever one you did."
12213
+ );
12214
+ }
12215
+ if (opts.neverStore !== void 0) {
12216
+ throw memgError(
12217
+ "invalid_input",
12218
+ "--never-store-add and --never-store contradict each other",
12219
+ 'Nothing was written. `--never-store` REPLACES the whole list and `--never-store-add` keeps it, so together they decide the same field twice. To widen the declaration: `--never-store-add "<the new word>"`. To set the list outright: `--never-store "<every word it should hold>"`.'
12220
+ );
12221
+ }
12222
+ if (opts.input !== void 0) {
12223
+ throw memgError(
12224
+ "invalid_input",
12225
+ "--never-store-add cannot be combined with --input",
12226
+ "Nothing was written. An `--input` document carries its own `claim.never_store`, and that field REPLACES the list \u2014 so the JSON and the flag would be answering the same question differently. Put the whole list in the JSON, or drop `--input` and widen with the flag."
12227
+ );
12228
+ }
12229
+ return parseNeverStore(opts.neverStoreAdd, "--never-store-add");
12230
+ }
12231
+ function widenedBoundary(declared, added, ref) {
12232
+ if (added === void 0) return void 0;
12233
+ const existing = [...declared ?? []];
12234
+ if (existing.length === 0) {
12235
+ throw memgError(
12236
+ "invalid_input",
12237
+ `${ref} declares no boundary to add to`,
12238
+ `Nothing was written. \`--never-store-add\` keeps the words a note already declares, and this one declares none. Start the declaration with the flag that sets it:
12239
+ memgineering revise ${ref} --claim "<what this brain must never hold>" --never-store "${added.join(", ")}"
12240
+ To see what this note says now: \`memgineering open ${ref}\``
12241
+ );
12242
+ }
12243
+ const fresh = added.filter((term) => !existing.some((word) => sameBoundaryWord(word, term)));
12244
+ if (fresh.length === 0) {
12245
+ const words = added.map((term) => `\`${term}\``).join(", ");
12246
+ throw memgError(
12247
+ "nothing_to_change",
12248
+ `${ref} already declares ${words} \u2014 nothing was written`,
12249
+ `Nothing was added and nothing was changed: every word passed is already on this note\u2019s list, so the write would have recorded an operation that moved nothing and an \`undo\` that reversed nothing.
12250
+ To see the declaration as it stands: \`memgineering open ${ref}\`
12251
+ To change what the note CONCLUDES without touching the words, leave \`--never-store-add\` off.`
12252
+ );
12253
+ }
12254
+ return [...existing, ...fresh];
12255
+ }
12256
+ function sameBoundaryWord(a, b) {
12257
+ const shape = (value) => value.normalize("NFC").replace(/\s+/gu, " ").trim().toLowerCase();
12258
+ return shape(a) === shape(b);
12259
+ }
12260
+ function missingWords(intended, stored) {
12261
+ return intended.filter((term) => !stored.some((word) => sameBoundaryWord(word, term)));
12262
+ }
12263
+ function declaredWords(note) {
12264
+ const raw = note.memory["never_store"];
12265
+ return Array.isArray(raw) ? raw.filter((term) => typeof term === "string") : [];
12266
+ }
12126
12267
  function boundaryIntent(raw) {
12127
12268
  if (raw === void 0) return void 0;
12128
12269
  return raw === false ? [] : parseNeverStore(raw);
@@ -12140,7 +12281,7 @@ function checkedTerms(terms2) {
12140
12281
  return checked.term;
12141
12282
  });
12142
12283
  }
12143
- async function buildCommitInput(entry, opts) {
12284
+ async function buildCommitInput(entry, opts, widened) {
12144
12285
  if (opts.input) {
12145
12286
  const raw = await readInput(opts.input);
12146
12287
  return {
@@ -12164,7 +12305,7 @@ async function buildCommitInput(entry, opts) {
12164
12305
  'Pass `--claim "<the new conclusion>"`, or `--input` with the whole change as JSON.'
12165
12306
  );
12166
12307
  }
12167
- const boundary = boundaryIntent(opts.neverStore);
12308
+ const boundary = widened ?? boundaryIntent(opts.neverStore);
12168
12309
  const action = opts.action;
12169
12310
  if (action !== "reinforce" && action !== "supersede" && action !== "conflict") {
12170
12311
  throw memgError(
@@ -12323,12 +12464,13 @@ function renderSummary(p) {
12323
12464
  function decisionCommand(decision) {
12324
12465
  return new Command7(decision).description(
12325
12466
  decision === "approve" ? "apply a proposal to the note \u2014 recorded in the ledger, reversible with undo" : "decline a proposal; the note is not touched"
12326
- ).argument("<id>", "proposal id, or enough of it to be unambiguous").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the proposal belongs to").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (id, opts) => {
12327
- const rationale = sanitizeRationale(opts.reason);
12328
- const where = await whichBrain(opts);
12467
+ ).argument("<id>", "proposal id, or enough of it to be unambiguous").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the proposal belongs to").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (id, opts, cmd) => {
12468
+ const merged = { ...cmd.parent?.opts() ?? {}, ...opts };
12469
+ const rationale = sanitizeRationale(merged.reason);
12470
+ const where = await whichBrain(merged);
12329
12471
  if (where.kind === "cloud") {
12330
12472
  const brain2 = await openCloudBrain(where);
12331
- const args = { id, ...opts.reason === void 0 ? {} : { reason: opts.reason } };
12473
+ const args = { id, ...merged.reason === void 0 ? {} : { reason: merged.reason } };
12332
12474
  if (decision === "reject") {
12333
12475
  const rejected = await cloudReject(brain2, args);
12334
12476
  renderRejection(rejected.proposal_id, rejected.state, rejected.target);
@@ -12346,7 +12488,7 @@ function decisionCommand(decision) {
12346
12488
  before: applied2.before,
12347
12489
  after: applied2.after
12348
12490
  },
12349
- opts.vault
12491
+ merged.vault
12350
12492
  );
12351
12493
  return;
12352
12494
  }
@@ -12421,7 +12563,7 @@ function decisionCommand(decision) {
12421
12563
  before: preview.before,
12422
12564
  after: preview.after
12423
12565
  },
12424
- opts.vault
12566
+ merged.vault
12425
12567
  );
12426
12568
  });
12427
12569
  }
@@ -12459,8 +12601,9 @@ function renderRejection(id, state, target) {
12459
12601
  });
12460
12602
  }
12461
12603
  function rebaseCommand() {
12462
- return new Command7("rebase").description("recompute a stale proposal against the note as it is now").argument("<id>", "proposal id, or enough of it to be unambiguous").option("--vault <path>", "which brain the proposal belongs to").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (id, opts) => {
12463
- const where = await whichBrain(opts);
12604
+ return new Command7("rebase").description("recompute a stale proposal against the note as it is now").argument("<id>", "proposal id, or enough of it to be unambiguous").option("--vault <path>", "which brain the proposal belongs to").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (id, opts, cmd) => {
12605
+ const merged = { ...cmd.parent?.opts() ?? {}, ...opts };
12606
+ const where = await whichBrain(merged);
12464
12607
  if (where.kind === "cloud") {
12465
12608
  const rebased = await cloudRebase(await openCloudBrain(where), { id });
12466
12609
  renderRebase(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memgineering",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "private": false,
5
5
  "description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share \u2014 stored in your own folder.",
6
6
  "license": "Apache-2.0",