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 +71 -0
- package/README.md +85 -38
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-writing/SKILL.md +15 -6
- package/dist/index.js +166 -23
- package/package.json +1 -1
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
|
-
|
|
7
|
-
of them is there for the next one,
|
|
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.**
|
|
79
|
-
|
|
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
|
|
106
|
-
is decided
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
|
135
|
-
|
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
| `
|
|
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
|
|
package/assets/MEMGINEERING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: memgineering
|
|
3
3
|
description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
|
|
4
4
|
type: skill
|
|
5
|
-
version: 0.
|
|
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
|
-
|
|
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 "
|
|
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.
|
|
372
|
-
|
|
373
|
-
|
|
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 "<
|
|
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
|
-
|
|
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\`
|
|
11643
|
-
|
|
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
|
-
|
|
11805
|
-
|
|
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(
|
|
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
|
|
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
|
|
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: {
|
|
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
|
|
12328
|
-
const
|
|
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, ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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