memgineering 0.1.0 → 0.2.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 +92 -0
- package/assets/MEMGINEERING.md +18 -2
- package/assets/memgineering-memory/SKILL.md +55 -5
- package/dist/index.js +319 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,98 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.2.1] — 2026-08-09
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **The language warning no longer fires on a query that worked.** On a brain
|
|
19
|
+
holding notes in more than one language, `recall "배포"` returned the Korean
|
|
20
|
+
note it was looking for and was told, above the result, that the query could
|
|
21
|
+
not reach the brain. A query that reached a note you wrote reached the brain,
|
|
22
|
+
whatever script it was in. It now stays quiet in that case, and still speaks
|
|
23
|
+
when everything that came back was `01_BASE/` scaffolding — which `init`
|
|
24
|
+
writes in English for everybody, so matching it is not evidence the query
|
|
25
|
+
landed. Found by using 0.2.0 against a real brain; a single-language fixture
|
|
26
|
+
could not produce it.
|
|
27
|
+
|
|
28
|
+
## [0.2.0] — 2026-08-09
|
|
29
|
+
|
|
30
|
+
The record can say why, and you can see what a memory is actually worth.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`evidence <ref>`** — how much a memory has been used, and why it last
|
|
35
|
+
changed: how often recall surfaced it, how often anyone opened it, when it
|
|
36
|
+
was last read, how many times it has been revised, and the reason attached to
|
|
37
|
+
the most recent change. Every number comes from records that already existed;
|
|
38
|
+
nothing new is collected, and running it records nothing of its own — so
|
|
39
|
+
measuring a memory cannot change where `resurface` ranks it.
|
|
40
|
+
- **A standing for each memory** — `well-used`, `surfaced, never opened`, or
|
|
41
|
+
`never recalled` — computed from usage rather than declared. It is withheld
|
|
42
|
+
entirely when this machine has no recall history yet, since that log is local
|
|
43
|
+
state and a brain copied to a new machine starts empty.
|
|
44
|
+
- **`--reason` on every write verb.** `remember`, `exclude`, `unexclude` and
|
|
45
|
+
`undo` accept one for the first time; `revise`, `retire` and `unretire`
|
|
46
|
+
already did. The reason on an `undo` is usually the most valuable of the set.
|
|
47
|
+
- **`log` shows the reason** without needing `--verbose`.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`revise --reason` and `retire --reason` were silently discarded.** Both
|
|
52
|
+
flags shipped in 0.1.0, and `retire --help` described the reason as "kept in
|
|
53
|
+
the record". It was not kept anywhere: only creating a brand-new note ever
|
|
54
|
+
read the field, so on the ordinary path — changing a note that already exists
|
|
55
|
+
— the text went nowhere. Reasons given to those commands are now recorded in
|
|
56
|
+
the operation ledger, where `log` and `evidence` can show them.
|
|
57
|
+
|
|
58
|
+
- **`recall` now names the language barrier** when a query cannot reach the
|
|
59
|
+
brain. Search matches text, so an English query never reaches Korean notes
|
|
60
|
+
however it is worded — and the old zero-result footer suggested broader
|
|
61
|
+
wording, the one remedy that cannot work. Four testers hit this and came
|
|
62
|
+
within one command of telling their user the brain held nothing on a subject
|
|
63
|
+
it had notes about. It appears above results too, since the failure is rarely
|
|
64
|
+
a clean zero: a couple of scaffolding notes match and look like an answer.
|
|
65
|
+
- **`use` with no argument now says which brain you are on**, and how it
|
|
66
|
+
resolved — by pointer, by containment, or by being the only one linked. Its
|
|
67
|
+
own help had always promised this; it reported only whether a pointer file
|
|
68
|
+
existed, and three testers went and read the config file by hand instead.
|
|
69
|
+
- **`open` no longer tells you to hand-edit frontmatter** when a note states no
|
|
70
|
+
conclusion. It points at `memgineering revise --claim`, which is the
|
|
71
|
+
sanctioned path — the previous hint contradicted the bolded rule against
|
|
72
|
+
editing brain files directly, and a hand edit is invisible to `undo`.
|
|
73
|
+
- **`evidence` no longer implies a well-used memory is a correct one.** A note
|
|
74
|
+
the repository flatly contradicted was described as backed by the record.
|
|
75
|
+
Usage is attendance, and where a memory contradicts another the output now
|
|
76
|
+
says plainly that usage cannot settle which is right.
|
|
77
|
+
- **`evidence` sees a contradiction declared by either note.** The `contradicts`
|
|
78
|
+
edge lives on whichever note a revise happened to touch, so the warning above
|
|
79
|
+
fired on only one of the two — and stayed silent on the other, which is the
|
|
80
|
+
side a reader is more likely to be trusting by mistake.
|
|
81
|
+
- **`evidence` divides the open rate by the number printed beside it.** It used
|
|
82
|
+
to divide by recalls while `surfaced` also counted resurfacings, so the two
|
|
83
|
+
figures and the percentage reconciled with none of each other.
|
|
84
|
+
- **`remember --reason` help disambiguated** — it records why you are writing
|
|
85
|
+
something down, not why the fact is true. One tester nearly filed the
|
|
86
|
+
substance of a note there, where recall would never show it.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- A reason that looks like it contains credential material is refused, with the
|
|
91
|
+
pattern named and the value never echoed. The ledger travels with the brain,
|
|
92
|
+
so an unscreened reason would become a permanent synced copy of a secret —
|
|
93
|
+
and the deny layer that screens notes does not look at the ledger. The write
|
|
94
|
+
itself is unaffected: rephrase, or drop the flag.
|
|
95
|
+
- Operation records carry a `rationale` field and `rule_version` is now 2.
|
|
96
|
+
Ledgers written by 0.1.x still read correctly; an older line simply reports
|
|
97
|
+
no reason given, which is what happened.
|
|
98
|
+
|
|
99
|
+
### Agent guidance
|
|
100
|
+
|
|
101
|
+
The bundled hub and the `memgineering-memory` skill now cover `evidence` and
|
|
102
|
+
ask for `--reason` on every write, including the rule that a newer memory does
|
|
103
|
+
not automatically win. **Restart your agent session after upgrading** so the
|
|
104
|
+
new guidance is loaded.
|
|
105
|
+
|
|
14
106
|
## [0.1.0] — 2026-08-09
|
|
15
107
|
|
|
16
108
|
First release. A brain is a folder of your own notes; every agent you use
|
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, or tells you something worth keeping. 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.2.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -47,17 +47,33 @@ memgineering open <handle> --detail full # the whole note
|
|
|
47
47
|
`open` also takes a note's exact title or its path, so you do not need to
|
|
48
48
|
recall first when you already know what you want.
|
|
49
49
|
|
|
50
|
+
**Check the evidence before trusting an old memory,** or when two of them
|
|
51
|
+
disagree:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
memgineering evidence <handle>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
How often it has been surfaced, how often anyone read it, when it last changed
|
|
58
|
+
and why. **Newer does not win**: a rule that has stood for eleven months has
|
|
59
|
+
eleven months of nobody contradicting it behind it. Use whether it was actually
|
|
60
|
+
reached for, not its date.
|
|
61
|
+
|
|
50
62
|
**Remember when you learn something durable** — a decision, a constraint, a
|
|
51
63
|
correction the user made, how something actually works:
|
|
52
64
|
|
|
53
65
|
```
|
|
54
|
-
memgineering remember "deploys are manual — launchctl by hand, no CD"
|
|
66
|
+
memgineering remember "deploys are manual — launchctl by hand, no CD" --reason "watched it twice"
|
|
55
67
|
```
|
|
56
68
|
|
|
57
69
|
No approval step, by design. It is recorded and reversible (`memgineering
|
|
58
70
|
undo`), so the cost of a wrong entry is one command, not a permanent mistake.
|
|
59
71
|
Do not ask permission for ordinary observations — write them.
|
|
60
72
|
|
|
73
|
+
**Always pass `--reason`.** Every write verb takes it and it goes in the ledger.
|
|
74
|
+
It is the only part of a record that still means anything to whoever reads it
|
|
75
|
+
six months from now, and it is refused if it looks like it carries a credential.
|
|
76
|
+
|
|
61
77
|
**Revise when a conclusion changes:**
|
|
62
78
|
|
|
63
79
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memgineering-memory
|
|
3
|
-
description: Use when the user asks about something they told you before, refers to a past decision, wants their own notes searched, or says something worth keeping. Triggers include "what did I decide about X", "check my notes", "how do we deploy again", "remember this", "예전에 뭐라고 정했더라", "내 노트에서 찾아줘", "이거 기억해둬", "what do you know about my setup". Covers recall, open, remember, revise, undo, and resurface against the user's own brain folder.
|
|
3
|
+
description: Use when the user asks about something they told you before, refers to a past decision, wants their own notes searched, or says something worth keeping. Triggers include "what did I decide about X", "check my notes", "how do we deploy again", "remember this", "is this still true", "which one do I go with", "예전에 뭐라고 정했더라", "내 노트에서 찾아줘", "이거 기억해둬", "what do you know about my setup". Covers recall, open, evidence, remember, revise, undo, and resurface against the user's own brain folder.
|
|
4
4
|
type: skill
|
|
5
5
|
allowed-tools: Bash(memgineering:*)
|
|
6
6
|
---
|
|
@@ -74,20 +74,70 @@ 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
|
+
## Deciding which one to trust
|
|
78
|
+
|
|
79
|
+
When two memories disagree, or one looks old enough to doubt, do not settle it
|
|
80
|
+
by date.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
memgineering evidence deploy-manual
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
surfaced 34× (12× in this folder, 22× elsewhere)
|
|
88
|
+
opened 12× (35% of the recalls that surfaced it)
|
|
89
|
+
last used 3 days ago
|
|
90
|
+
in the ledger since 2026-05-02 · changed 2×
|
|
91
|
+
|
|
92
|
+
**well-used** — it has been reached for and read. The record backs it.
|
|
93
|
+
|
|
94
|
+
why it last changed:
|
|
95
|
+
> CD was removed from the repo
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Newer does not win.** A rule that has stood for eleven months has eleven
|
|
99
|
+
months of nobody contradicting it behind it; a note written last week has a
|
|
100
|
+
week. What separates them is whether either was ever actually used, and what
|
|
101
|
+
the last person to change their mind said. Both are in the output above.
|
|
102
|
+
|
|
103
|
+
Three standings, and each is a fact rather than a grade:
|
|
104
|
+
|
|
105
|
+
- **well-used** — recall returned it and somebody read it
|
|
106
|
+
- **surfaced, never opened** — it keeps coming back and nobody reads it. Either
|
|
107
|
+
it does not answer those questions, or its summary does not say what it knows
|
|
108
|
+
- **never recalled** — nothing has ever surfaced it. It may be perfectly true
|
|
109
|
+
and simply never needed, but nothing supports it either
|
|
110
|
+
|
|
111
|
+
If it says this machine has no recall history, the numbers mean nothing yet —
|
|
112
|
+
that log is local, so a brain synced to a new laptop starts empty. Say that
|
|
113
|
+
rather than reporting the memory as unused.
|
|
114
|
+
|
|
77
115
|
## Writing things down
|
|
78
116
|
|
|
79
117
|
```
|
|
80
|
-
memgineering remember "deploys are manual — launchctl by hand, no CD"
|
|
118
|
+
memgineering remember "deploys are manual — launchctl by hand, no CD" \
|
|
119
|
+
--reason "watched the deploy happen by hand twice"
|
|
81
120
|
```
|
|
82
121
|
|
|
83
122
|
Lands immediately as its own note and is recallable at once. There is no
|
|
84
123
|
approval queue: the design trades permission-before for correction-after, and
|
|
85
124
|
every write records how to reverse it.
|
|
86
125
|
|
|
126
|
+
**Pass `--reason` on every write.** It goes in the ledger, and it is the only
|
|
127
|
+
part of the record that survives being read six months later by somebody — the
|
|
128
|
+
user, or you in a new session — who no longer remembers the conversation. Every
|
|
129
|
+
write verb takes it: `remember`, `revise`, `retire`, `exclude`, `undo`. The one
|
|
130
|
+
on `undo` is usually the most valuable of all, because it is the moment an
|
|
131
|
+
earlier conclusion turned out to be wrong.
|
|
132
|
+
|
|
133
|
+
It is refused if it looks like it contains a credential. The ledger travels
|
|
134
|
+
with the brain, so that would be a permanent synced copy — say what changed
|
|
135
|
+
without the value, or drop the flag; the write itself is unaffected.
|
|
136
|
+
|
|
87
137
|
```
|
|
88
|
-
memgineering undo
|
|
138
|
+
memgineering undo --reason "that belonged to the other project" # the last change
|
|
89
139
|
memgineering undo <op_id> # a specific one
|
|
90
|
-
memgineering log # what
|
|
140
|
+
memgineering log # what changed, why, and what can still be undone
|
|
91
141
|
```
|
|
92
142
|
|
|
93
143
|
Write down: decisions and their reasons, constraints, corrections the user
|
|
@@ -122,7 +172,7 @@ the user's; neither this command nor you should rewrite it uninvited.
|
|
|
122
172
|
|
|
123
173
|
```
|
|
124
174
|
memgineering retire <ref> --reason "the date moved" # no longer current, still visible
|
|
125
|
-
memgineering exclude path/to/note.md
|
|
175
|
+
memgineering exclude path/to/note.md --reason "names an internal host" # stop reading it
|
|
126
176
|
```
|
|
127
177
|
|
|
128
178
|
Retiring keeps the note in recall, ranked last and labelled. Excluding takes it
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command17 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/help.ts
|
|
7
7
|
import { Help } from "commander";
|
|
@@ -145,7 +145,7 @@ var COMMAND_GROUPS = [
|
|
|
145
145
|
{
|
|
146
146
|
title: "MEMORY",
|
|
147
147
|
blurb: "day to day \u2014 what your agent reaches for while working",
|
|
148
|
-
commands: ["recall", "open", "remember", "revise", "undo", "resurface"]
|
|
148
|
+
commands: ["recall", "open", "evidence", "remember", "revise", "undo", "resurface"]
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
title: "BRAIN",
|
|
@@ -2547,12 +2547,36 @@ var OpRecordSchema = z7.object({
|
|
|
2547
2547
|
hash_after: z7.string().nullable(),
|
|
2548
2548
|
/** The frontmatter block region, for reversing a `revise`. */
|
|
2549
2549
|
patch: z7.object({ before: z7.string(), after: z7.string() }).nullable().default(null),
|
|
2550
|
+
/**
|
|
2551
|
+
* Why this was done, in the words of whoever did it. Never derived.
|
|
2552
|
+
*
|
|
2553
|
+
* Defaulted rather than required so a ledger written by 0.1.x — which had no
|
|
2554
|
+
* such field — still parses. An old line reads as "no reason was given",
|
|
2555
|
+
* which is exactly what happened.
|
|
2556
|
+
*/
|
|
2557
|
+
rationale: z7.string().nullable().default(null),
|
|
2550
2558
|
/** Which generation of the write rules produced this. */
|
|
2551
2559
|
rule_version: z7.number().int().positive(),
|
|
2552
2560
|
/** The op this reverses or replaces, when it does. */
|
|
2553
2561
|
supersedes: z7.string().nullable().default(null)
|
|
2554
2562
|
});
|
|
2555
|
-
var RULE_VERSION =
|
|
2563
|
+
var RULE_VERSION = 2;
|
|
2564
|
+
var RATIONALE_MAX = 500;
|
|
2565
|
+
function sanitizeRationale(raw) {
|
|
2566
|
+
if (raw == null) return null;
|
|
2567
|
+
const clean = stripControl(raw).trim();
|
|
2568
|
+
if (clean === "") return null;
|
|
2569
|
+
const hits = secretContentHits(clean);
|
|
2570
|
+
if (hits.length > 0) {
|
|
2571
|
+
throw memgError(
|
|
2572
|
+
"invalid_input",
|
|
2573
|
+
`the reason looks like it contains credential material (${hits.map((h) => h.code).join(", ")})`,
|
|
2574
|
+
"The ledger travels with the brain, so this would be a permanent synced copy. Say what changed and why without the value itself, or drop --reason \u2014 the write itself is unaffected."
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
const capped = clean.length > RATIONALE_MAX ? `${clean.slice(0, RATIONALE_MAX - 1)}\u2026` : clean;
|
|
2578
|
+
return capped;
|
|
2579
|
+
}
|
|
2556
2580
|
function ledgerDir(vaultRoot) {
|
|
2557
2581
|
return join6(vaultRoot, BRAND.vaultLedgerDir, "ledger");
|
|
2558
2582
|
}
|
|
@@ -2581,6 +2605,7 @@ async function appendOp(vaultRoot, op) {
|
|
|
2581
2605
|
hash_before: op.hashBefore,
|
|
2582
2606
|
hash_after: op.hashAfter,
|
|
2583
2607
|
patch: op.patch ?? null,
|
|
2608
|
+
rationale: op.rationale ?? null,
|
|
2584
2609
|
rule_version: RULE_VERSION,
|
|
2585
2610
|
supersedes: op.supersedes ?? null
|
|
2586
2611
|
});
|
|
@@ -3499,7 +3524,10 @@ function openCommand() {
|
|
|
3499
3524
|
);
|
|
3500
3525
|
printHuman(
|
|
3501
3526
|
c.gray(
|
|
3502
|
-
|
|
3527
|
+
`To state one:
|
|
3528
|
+
memgineering revise ${handle} --claim "<the conclusion>" --summary "<one line>"
|
|
3529
|
+
Add --contradicts <id> when it disagrees with another memory; that edge is what
|
|
3530
|
+
makes the two surface together as a conflict.`
|
|
3503
3531
|
)
|
|
3504
3532
|
);
|
|
3505
3533
|
}
|
|
@@ -3928,7 +3956,8 @@ Brain: ${root}`));
|
|
|
3928
3956
|
function lifecycleCommand(action) {
|
|
3929
3957
|
return new Command5(action).description(
|
|
3930
3958
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
3931
|
-
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the
|
|
3959
|
+
).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) => {
|
|
3960
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
3932
3961
|
const { brain } = await resolveBrain(opts.vault);
|
|
3933
3962
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
3934
3963
|
createPrefix: brain.observationsDir
|
|
@@ -4003,6 +4032,7 @@ function lifecycleCommand(action) {
|
|
|
4003
4032
|
hashBefore: hashContent(before.content),
|
|
4004
4033
|
hashAfter: hashContent(outcome.content),
|
|
4005
4034
|
patch: { before: preview.before, after: preview.after },
|
|
4035
|
+
rationale,
|
|
4006
4036
|
snapshotBefore: before.content
|
|
4007
4037
|
});
|
|
4008
4038
|
});
|
|
@@ -4030,7 +4060,8 @@ Undo: \`memgineering undo ${record.op_id}\``));
|
|
|
4030
4060
|
function ignoreCommand(mode) {
|
|
4031
4061
|
return new Command5(mode).description(
|
|
4032
4062
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
4033
|
-
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
|
|
4063
|
+
).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) => {
|
|
4064
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
4034
4065
|
const { brain } = await resolveBrain(opts.vault);
|
|
4035
4066
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
4036
4067
|
createPrefix: brain.observationsDir
|
|
@@ -4054,6 +4085,7 @@ function ignoreCommand(mode) {
|
|
|
4054
4085
|
hashBefore: hashContent(base.content),
|
|
4055
4086
|
hashAfter: hashContent(result.content),
|
|
4056
4087
|
patch: { before: base.content, after: result.content },
|
|
4088
|
+
rationale,
|
|
4057
4089
|
snapshotBefore: base.content
|
|
4058
4090
|
});
|
|
4059
4091
|
});
|
|
@@ -4101,12 +4133,170 @@ var unretireCommand = () => lifecycleCommand("unretire");
|
|
|
4101
4133
|
var excludeCommand = () => ignoreCommand("exclude");
|
|
4102
4134
|
var unexcludeCommand = () => ignoreCommand("unexclude");
|
|
4103
4135
|
|
|
4136
|
+
// src/commands/evidence.ts
|
|
4137
|
+
import { Command as Command6 } from "commander";
|
|
4138
|
+
function evidenceCommand() {
|
|
4139
|
+
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) => {
|
|
4140
|
+
const { brain } = await resolveBrain(opts.vault);
|
|
4141
|
+
const adapter = await openBrainOrExplain(brain.root);
|
|
4142
|
+
const built = await openIndex(adapter, brain.root);
|
|
4143
|
+
const entry = resolveRef(built.entries, ref);
|
|
4144
|
+
const [events, ledger] = await Promise.all([readEvents(brain.root), readOps(brain.root)]);
|
|
4145
|
+
const now = /* @__PURE__ */ new Date();
|
|
4146
|
+
const report = assembleEvidence(entry, events, ledger.ops, {
|
|
4147
|
+
contextDir: process.cwd(),
|
|
4148
|
+
now,
|
|
4149
|
+
brain: built.entries
|
|
4150
|
+
});
|
|
4151
|
+
printDual({
|
|
4152
|
+
json: report,
|
|
4153
|
+
human: () => {
|
|
4154
|
+
printHuman(`## evidence: ${entry.memory.title}
|
|
4155
|
+
`);
|
|
4156
|
+
printHuman(`\`id: ${report.id}\` \xB7 \`${report.path}\`
|
|
4157
|
+
`);
|
|
4158
|
+
if (report.critical) {
|
|
4159
|
+
printHuman(c.yellow("\u26A0 critical \u2014 this note defines how the agent behaves.") + "\n");
|
|
4160
|
+
}
|
|
4161
|
+
if (!report.brain_has_history) {
|
|
4162
|
+
printHuman(
|
|
4163
|
+
c.yellow("This machine has no recall history for this brain yet.") + c.gray(
|
|
4164
|
+
"\nThe usage numbers below are zero because nothing has been recorded here,\nnot because the memory is unused. The log is local state and does not sync.\n"
|
|
4165
|
+
)
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
const rows = [
|
|
4169
|
+
[
|
|
4170
|
+
"surfaced",
|
|
4171
|
+
report.surfaced === 0 ? "never" : `${report.surfaced}\xD7 (${report.surfaced_here}\xD7 in this folder, ${report.surfaced - report.surfaced_here}\xD7 elsewhere)`
|
|
4172
|
+
],
|
|
4173
|
+
[
|
|
4174
|
+
"opened",
|
|
4175
|
+
report.opened === 0 ? "never" : `${report.opened}\xD7${report.open_rate === null ? "" : ` (${report.opened} of the ${report.surfaced} times it surfaced)`}`
|
|
4176
|
+
],
|
|
4177
|
+
["last used", report.last_touched ? ago(report.last_touched, now) : "\u2014"],
|
|
4178
|
+
[
|
|
4179
|
+
"in the ledger",
|
|
4180
|
+
report.first_recorded ? `since ${report.first_recorded.slice(0, 10)} \xB7 changed ${report.changed}\xD7${report.undone > 0 ? ` \xB7 undone ${report.undone}\xD7` : ""}` : "nothing recorded \u2014 this note predates the ledger, or was only edited by hand"
|
|
4181
|
+
]
|
|
4182
|
+
];
|
|
4183
|
+
if (report.declared_from) {
|
|
4184
|
+
rows.push(["declared", `valid from ${report.declared_from.slice(0, 10)}`]);
|
|
4185
|
+
}
|
|
4186
|
+
if (report.supersedes > 0 || report.contradicts > 0) {
|
|
4187
|
+
rows.push([
|
|
4188
|
+
"relations",
|
|
4189
|
+
[
|
|
4190
|
+
report.supersedes > 0 ? `supersedes ${report.supersedes}` : null,
|
|
4191
|
+
report.contradicts > 0 ? `contradicts ${report.contradicts}` : null
|
|
4192
|
+
].filter(Boolean).join(" \xB7 ")
|
|
4193
|
+
]);
|
|
4194
|
+
}
|
|
4195
|
+
const pad = rows.reduce((w, [label]) => Math.max(w, label.length), 0) + 2;
|
|
4196
|
+
for (const [label, value] of rows) printHuman(` ${label.padEnd(pad)}${value}`);
|
|
4197
|
+
if (report.standing) {
|
|
4198
|
+
printHuman(`
|
|
4199
|
+
**${report.standing}** \u2014 ${STANDING_MEANS[report.standing]}`);
|
|
4200
|
+
}
|
|
4201
|
+
if (report.contradicts > 0) {
|
|
4202
|
+
printHuman(
|
|
4203
|
+
c.yellow(
|
|
4204
|
+
`
|
|
4205
|
+
\u26A0 this memory contradicts ${report.contradicts} other${report.contradicts === 1 ? "" : "s"}.`
|
|
4206
|
+
) + c.gray(
|
|
4207
|
+
"\n Both sides can be well-used. Usage cannot settle which one is right \u2014 compare the\n reasons above, or check something outside the brain."
|
|
4208
|
+
)
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
if (report.last_reason) {
|
|
4212
|
+
printHuman(
|
|
4213
|
+
report.last_reason.verb === "remember" ? "\nwhy it was recorded:" : "\nwhy it last changed:"
|
|
4214
|
+
);
|
|
4215
|
+
printHuman(quoteBlock(report.last_reason.text));
|
|
4216
|
+
printHuman(
|
|
4217
|
+
c.gray(` ${report.last_reason.verb}, ${report.last_reason.ts.slice(0, 10)}`)
|
|
4218
|
+
);
|
|
4219
|
+
} else if (report.changed > 0) {
|
|
4220
|
+
printHuman(
|
|
4221
|
+
c.gray(
|
|
4222
|
+
"\nIt has changed, and no reason was recorded. Pass `--reason` when you write and the next one will say why."
|
|
4223
|
+
)
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
});
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
var STANDING_MEANS = {
|
|
4231
|
+
"well-used": "it gets reached for and read. That is attendance, not correctness.",
|
|
4232
|
+
"surfaced, never opened": "recall keeps returning it and nobody reads it. Either it does not answer those questions, or its summary does not say what it knows.",
|
|
4233
|
+
"never recalled": "nothing has ever surfaced it. It may be true and simply never needed \u2014 but nothing here supports it either."
|
|
4234
|
+
};
|
|
4235
|
+
function assembleEvidence(entry, events, ops, opts) {
|
|
4236
|
+
const id = entry.memory.id;
|
|
4237
|
+
const surfacing = events.filter(
|
|
4238
|
+
(e) => (e.verb === "recall" || e.verb === "resurface") && e.returned_ids.includes(id)
|
|
4239
|
+
);
|
|
4240
|
+
const opens = events.filter((e) => e.verb === "open" && e.opened_id === id);
|
|
4241
|
+
const touched = [...surfacing, ...opens].map((e) => e.ts).sort().at(-1);
|
|
4242
|
+
const mine = ops.filter((o) => o.target === entry.path || o.memory_id === id);
|
|
4243
|
+
const changed = mine.filter((o) => o.verb !== "undo" && o.verb !== "remember").length;
|
|
4244
|
+
const undone = mine.filter((o) => o.verb === "undo").length;
|
|
4245
|
+
const withReason = mine.filter((o) => o.rationale !== null).at(-1);
|
|
4246
|
+
const relationsOfType = (type) => entry.relations.filter((r) => r.type === type && r.from === id).length;
|
|
4247
|
+
const disputedWith = /* @__PURE__ */ new Set();
|
|
4248
|
+
for (const relation of entry.relations) {
|
|
4249
|
+
if (relation.type === "contradicts" && relation.from === id) disputedWith.add(relation.to);
|
|
4250
|
+
}
|
|
4251
|
+
for (const other of opts.brain ?? []) {
|
|
4252
|
+
if (other.memory.id === id) continue;
|
|
4253
|
+
for (const relation of other.relations) {
|
|
4254
|
+
if (relation.type === "contradicts" && relation.to === id) disputedWith.add(relation.from);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
return {
|
|
4258
|
+
id,
|
|
4259
|
+
path: entry.path,
|
|
4260
|
+
title: entry.memory.title,
|
|
4261
|
+
critical: entry.path.startsWith("01_BASE/"),
|
|
4262
|
+
brain_has_history: events.length > 0,
|
|
4263
|
+
surfaced: surfacing.length,
|
|
4264
|
+
surfaced_here: surfacing.filter((e) => isInside(e.context_dir, opts.contextDir)).length,
|
|
4265
|
+
opened: opens.length,
|
|
4266
|
+
// Divided by the number printed directly above it. It used to be divided by
|
|
4267
|
+
// recalls only, while `surfaced` also counted resurfacings — so a reader saw
|
|
4268
|
+
// two numbers and a percentage that reconciled with neither.
|
|
4269
|
+
open_rate: surfacing.length === 0 ? null : opens.length / surfacing.length,
|
|
4270
|
+
last_touched: touched ?? null,
|
|
4271
|
+
first_recorded: mine[0]?.ts ?? null,
|
|
4272
|
+
declared_from: entry.claims.find((claim) => claim.valid_from)?.valid_from ?? null,
|
|
4273
|
+
changed,
|
|
4274
|
+
undone,
|
|
4275
|
+
supersedes: relationsOfType("supersedes"),
|
|
4276
|
+
contradicts: disputedWith.size,
|
|
4277
|
+
last_reason: withReason ? { verb: withReason.verb, ts: withReason.ts, text: withReason.rationale } : null,
|
|
4278
|
+
// Withheld rather than guessed when the log is empty. "Never recalled" on a
|
|
4279
|
+
// brain that arrived this morning would be a confident wrong answer, which
|
|
4280
|
+
// is the one thing a memory tool must not produce.
|
|
4281
|
+
standing: events.length === 0 ? null : opens.length > 0 ? "well-used" : surfacing.length > 0 ? "surfaced, never opened" : "never recalled"
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
function ago(iso, now) {
|
|
4285
|
+
const days = Math.floor((now.getTime() - Date.parse(iso)) / 864e5);
|
|
4286
|
+
if (!Number.isFinite(days)) return iso;
|
|
4287
|
+
if (days <= 0) return "today";
|
|
4288
|
+
if (days === 1) return "yesterday";
|
|
4289
|
+
if (days < 30) return `${days} days ago`;
|
|
4290
|
+
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
4291
|
+
return `${Math.floor(days / 365)} years ago`;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4104
4294
|
// src/commands/init.ts
|
|
4105
4295
|
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
4106
4296
|
import { join as join13, resolve as resolve6 } from "path";
|
|
4107
|
-
import { Command as
|
|
4297
|
+
import { Command as Command7 } from "commander";
|
|
4108
4298
|
function initCommand() {
|
|
4109
|
-
return new
|
|
4299
|
+
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) => {
|
|
4110
4300
|
const root = resolve6(path);
|
|
4111
4301
|
const existing = await readdir5(root).catch(() => null);
|
|
4112
4302
|
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
@@ -4274,9 +4464,9 @@ function files(name) {
|
|
|
4274
4464
|
}
|
|
4275
4465
|
|
|
4276
4466
|
// src/commands/log.ts
|
|
4277
|
-
import { Command as
|
|
4467
|
+
import { Command as Command8 } from "commander";
|
|
4278
4468
|
function logCommand() {
|
|
4279
|
-
return new
|
|
4469
|
+
return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").action(async (opts) => {
|
|
4280
4470
|
const { brain } = await resolveBrain(opts.vault);
|
|
4281
4471
|
const adapter = await openBrainOrExplain(brain.root);
|
|
4282
4472
|
const { ops, skipped } = await readOps(brain.root);
|
|
@@ -4322,6 +4512,7 @@ function logCommand() {
|
|
|
4322
4512
|
`- \`${op.op_id}\` ${op.verb} \`${op.target}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
|
|
4323
4513
|
);
|
|
4324
4514
|
printHuman(c.gray(` ${op.ts} \xB7 ${op.actor}`));
|
|
4515
|
+
if (op.rationale) printHuman(c.gray(quoteBlock(op.rationale, " > ")));
|
|
4325
4516
|
if (opts.verbose) {
|
|
4326
4517
|
printHuman(c.gray(` ${op.hash_before ?? "\u2205"} \u2192 ${op.hash_after ?? "\u2205"}`));
|
|
4327
4518
|
}
|
|
@@ -4357,6 +4548,7 @@ function summarize(op) {
|
|
|
4357
4548
|
verb: op.verb,
|
|
4358
4549
|
target: op.target,
|
|
4359
4550
|
memory_id: op.memory_id,
|
|
4551
|
+
rationale: op.rationale,
|
|
4360
4552
|
supersedes: op.supersedes
|
|
4361
4553
|
};
|
|
4362
4554
|
}
|
|
@@ -4369,9 +4561,9 @@ async function readOrNull(adapter, path) {
|
|
|
4369
4561
|
}
|
|
4370
4562
|
|
|
4371
4563
|
// src/commands/recall.ts
|
|
4372
|
-
import { Command as
|
|
4564
|
+
import { Command as Command9 } from "commander";
|
|
4373
4565
|
function recallCommand() {
|
|
4374
|
-
return new
|
|
4566
|
+
return new Command9("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option("--detail <level>", "title | card | summary | chunks | full (default: card)").action(
|
|
4375
4567
|
async (query, opts) => {
|
|
4376
4568
|
const detail = parseDetail(opts.detail, "card");
|
|
4377
4569
|
const { brain } = await resolveBrain(opts.vault);
|
|
@@ -4411,10 +4603,30 @@ function recallCommand() {
|
|
|
4411
4603
|
conflicts: result.conflicts
|
|
4412
4604
|
},
|
|
4413
4605
|
human: () => {
|
|
4606
|
+
const reachedSomethingWritten = rows.some(
|
|
4607
|
+
({ card }) => !(byId.get(card.id)?.path ?? "").startsWith("01_BASE/")
|
|
4608
|
+
);
|
|
4609
|
+
const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
|
|
4610
|
+
const sayWhy = () => {
|
|
4611
|
+
printHuman(
|
|
4612
|
+
c.yellow(
|
|
4613
|
+
`\u26A0 This brain's notes are mostly written in ${otherScript}, and this query is not.`
|
|
4614
|
+
) + c.gray(
|
|
4615
|
+
`
|
|
4616
|
+
Search matches text, not meaning, so no rewording in another language will reach them.
|
|
4617
|
+
Ask again in ${otherScript}, or run \`memgineering resurface\` to see what is here.
|
|
4618
|
+
`
|
|
4619
|
+
)
|
|
4620
|
+
);
|
|
4621
|
+
};
|
|
4414
4622
|
if (rows.length === 0) {
|
|
4415
4623
|
printHuman(`## recall: ${query}
|
|
4416
4624
|
`);
|
|
4417
4625
|
printHuman("_Nothing matched._\n");
|
|
4626
|
+
if (otherScript) {
|
|
4627
|
+
sayWhy();
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4418
4630
|
printHuman(
|
|
4419
4631
|
c.gray(
|
|
4420
4632
|
"Only titles, aliases and summaries are searched \u2014 try fewer or broader words, or drop --scope."
|
|
@@ -4424,6 +4636,7 @@ function recallCommand() {
|
|
|
4424
4636
|
}
|
|
4425
4637
|
printHuman(`## recall: ${query} (${rows.length} cards)
|
|
4426
4638
|
`);
|
|
4639
|
+
if (otherScript) sayWhy();
|
|
4427
4640
|
for (const [i, { card, handle, stage }] of rows.entries()) {
|
|
4428
4641
|
printHuman(renderCard(i + 1, card, handle, detail, stage));
|
|
4429
4642
|
}
|
|
@@ -4471,11 +4684,62 @@ function renderCard(n, card, handle, detail, stage) {
|
|
|
4471
4684
|
out.push("");
|
|
4472
4685
|
return out.join("\n");
|
|
4473
4686
|
}
|
|
4687
|
+
function unreachableScript(query, entries) {
|
|
4688
|
+
const queryScripts = scriptsIn(query);
|
|
4689
|
+
if (queryScripts.size === 0) return null;
|
|
4690
|
+
const written = entries.filter((e) => !e.path.startsWith("01_BASE/"));
|
|
4691
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4692
|
+
for (const entry of written) {
|
|
4693
|
+
const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
|
|
4694
|
+
if (script) counts.set(script, (counts.get(script) ?? 0) + 1);
|
|
4695
|
+
}
|
|
4696
|
+
if (counts.size === 0) return null;
|
|
4697
|
+
const [dominant, seen] = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
4698
|
+
if (queryScripts.has(dominant)) return null;
|
|
4699
|
+
return seen >= Math.max(2, written.length / 2) ? dominant : null;
|
|
4700
|
+
}
|
|
4701
|
+
var SCRIPT_RANGES = [
|
|
4702
|
+
["Korean", /[가-힣ᄀ-ᇿ]/g],
|
|
4703
|
+
["Kana", /[-ヿ]/g],
|
|
4704
|
+
["Han", /[一-鿿]/g],
|
|
4705
|
+
["Cyrillic", /[Ѐ-ӿ]/g],
|
|
4706
|
+
["Arabic", /[-ۿ]/g],
|
|
4707
|
+
["English", /[A-Za-z]/g]
|
|
4708
|
+
];
|
|
4709
|
+
function scriptCounts(text) {
|
|
4710
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4711
|
+
for (const [name, pattern] of SCRIPT_RANGES) {
|
|
4712
|
+
const n = (text.match(pattern) ?? []).length;
|
|
4713
|
+
if (n > 0) counts.set(name, n);
|
|
4714
|
+
}
|
|
4715
|
+
const han = counts.get("Han") ?? 0;
|
|
4716
|
+
if (counts.has("Kana")) {
|
|
4717
|
+
counts.set("Japanese", (counts.get("Kana") ?? 0) + han);
|
|
4718
|
+
counts.delete("Kana");
|
|
4719
|
+
counts.delete("Han");
|
|
4720
|
+
} else if (han > 0) {
|
|
4721
|
+
counts.set("Chinese", han);
|
|
4722
|
+
counts.delete("Han");
|
|
4723
|
+
}
|
|
4724
|
+
return counts;
|
|
4725
|
+
}
|
|
4726
|
+
function dominantScriptOf(text) {
|
|
4727
|
+
const ranked = [...scriptCounts(text)].sort((a, b) => b[1] - a[1]);
|
|
4728
|
+
if (ranked.length === 0) return null;
|
|
4729
|
+
if (ranked.length > 1 && ranked[0][1] === ranked[1][1]) return null;
|
|
4730
|
+
return ranked[0][0];
|
|
4731
|
+
}
|
|
4732
|
+
function scriptsIn(text) {
|
|
4733
|
+
return new Set(scriptCounts(text).keys());
|
|
4734
|
+
}
|
|
4474
4735
|
|
|
4475
4736
|
// src/commands/remember.ts
|
|
4476
|
-
import { Command as
|
|
4737
|
+
import { Command as Command10 } from "commander";
|
|
4477
4738
|
function rememberCommand() {
|
|
4478
|
-
return new
|
|
4739
|
+
return new Command10("remember").description("write something down now, with no approval step").argument("<text>", "what to remember").option("--vault <path>", "which brain to write to").option("--scope <scope>", "what this is about \u2014 a project, an area").option(
|
|
4740
|
+
"--reason <text>",
|
|
4741
|
+
"why you are recording this \u2014 goes in the ledger, not in the note (the fact itself belongs in <text>)"
|
|
4742
|
+
).action(async (text, opts) => {
|
|
4479
4743
|
const body = stripControl(text).trim();
|
|
4480
4744
|
if (body === "") {
|
|
4481
4745
|
throw memgError(
|
|
@@ -4484,6 +4748,7 @@ function rememberCommand() {
|
|
|
4484
4748
|
'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
|
|
4485
4749
|
);
|
|
4486
4750
|
}
|
|
4751
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
4487
4752
|
const { brain } = await resolveBrain(opts.vault);
|
|
4488
4753
|
await openBrainOrExplain(brain.root);
|
|
4489
4754
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -4519,6 +4784,7 @@ function rememberCommand() {
|
|
|
4519
4784
|
memoryId: id,
|
|
4520
4785
|
hashBefore: null,
|
|
4521
4786
|
hashAfter: hashContent(content),
|
|
4787
|
+
rationale,
|
|
4522
4788
|
now
|
|
4523
4789
|
});
|
|
4524
4790
|
});
|
|
@@ -4562,9 +4828,9 @@ function yamlString(value) {
|
|
|
4562
4828
|
}
|
|
4563
4829
|
|
|
4564
4830
|
// src/commands/resurface.ts
|
|
4565
|
-
import { Command as
|
|
4831
|
+
import { Command as Command11 } from "commander";
|
|
4566
4832
|
function resurfaceCommand() {
|
|
4567
|
-
return new
|
|
4833
|
+
return new Command11("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").action(async (opts) => {
|
|
4568
4834
|
const contextDir = opts.contextDir ?? process.cwd();
|
|
4569
4835
|
const { brain } = await resolveBrain(opts.vault, contextDir);
|
|
4570
4836
|
const adapter = await openBrainOrExplain(brain.root);
|
|
@@ -4690,9 +4956,9 @@ function rank(entries, events, opts) {
|
|
|
4690
4956
|
|
|
4691
4957
|
// src/commands/revise.ts
|
|
4692
4958
|
import { readFile as readFile14 } from "fs/promises";
|
|
4693
|
-
import { Command as
|
|
4959
|
+
import { Command as Command12 } from "commander";
|
|
4694
4960
|
function reviseCommand() {
|
|
4695
|
-
return new
|
|
4961
|
+
return new Command12("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("--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) => {
|
|
4696
4962
|
const { brain } = await resolveBrain(opts.vault);
|
|
4697
4963
|
await openBrainOrExplain(brain.root);
|
|
4698
4964
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
@@ -4701,6 +4967,9 @@ function reviseCommand() {
|
|
|
4701
4967
|
const built = await openIndex(adapter, brain.root);
|
|
4702
4968
|
const entry = resolveRef(built.entries, ref);
|
|
4703
4969
|
const input = await buildCommitInput(entry, opts);
|
|
4970
|
+
const rationale = sanitizeRationale(
|
|
4971
|
+
input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
|
|
4972
|
+
);
|
|
4704
4973
|
const result = commit(asSingleMatch(entry), input);
|
|
4705
4974
|
if (result.action === "ignore") {
|
|
4706
4975
|
throw memgError("invalid_input", "nothing to revise", "Pass --claim, or --input with one.");
|
|
@@ -4756,6 +5025,7 @@ function reviseCommand() {
|
|
|
4756
5025
|
// would build a permanent second copy of something the author may
|
|
4757
5026
|
// later delete on purpose.
|
|
4758
5027
|
patch: { before: preview.before, after: preview.after },
|
|
5028
|
+
rationale,
|
|
4759
5029
|
snapshotBefore: before.content
|
|
4760
5030
|
});
|
|
4761
5031
|
return { record, content: outcome.content };
|
|
@@ -4788,6 +5058,7 @@ Undo: \`memgineering undo ${applied.record.op_id}\``));
|
|
|
4788
5058
|
});
|
|
4789
5059
|
});
|
|
4790
5060
|
}
|
|
5061
|
+
var NO_REASON_GIVEN = "revised directly";
|
|
4791
5062
|
async function buildCommitInput(entry, opts) {
|
|
4792
5063
|
if (opts.input) {
|
|
4793
5064
|
const raw = await readInput(opts.input);
|
|
@@ -4823,7 +5094,7 @@ async function buildCommitInput(entry, opts) {
|
|
|
4823
5094
|
relates_to: [],
|
|
4824
5095
|
contradicts: opts.contradicts ?? []
|
|
4825
5096
|
},
|
|
4826
|
-
reason: opts.reason ??
|
|
5097
|
+
reason: opts.reason ?? NO_REASON_GIVEN,
|
|
4827
5098
|
// The engine refuses to submit without an excerpt, because approving a
|
|
4828
5099
|
// diff on an assertion alone is not consent. Nothing here is approved by a
|
|
4829
5100
|
// person, but the requirement still earns its keep: it forces the record
|
|
@@ -4890,10 +5161,10 @@ import { access, mkdir as mkdir9, readFile as readFile15 } from "fs/promises";
|
|
|
4890
5161
|
import { homedir as homedir3 } from "os";
|
|
4891
5162
|
import { dirname as dirname3, join as join14 } from "path";
|
|
4892
5163
|
import { fileURLToPath } from "url";
|
|
4893
|
-
import { Command as
|
|
5164
|
+
import { Command as Command13 } from "commander";
|
|
4894
5165
|
import prompts2 from "prompts";
|
|
4895
5166
|
function setupCommand() {
|
|
4896
|
-
return new
|
|
5167
|
+
return new Command13("setup").description("install into your agents, and point at a brain").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--tools <list>", "comma-separated: claude, codex, gemini (default: all detected)").option("--auto-update <choice>", "on | off (default: on, recommended)").option("--hook <choice>", "on | off \u2014 SessionStart resurface, Claude Code only").option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").action(async (opts) => {
|
|
4897
5168
|
const mode = await resolveMode(opts);
|
|
4898
5169
|
const detected = await detectTools();
|
|
4899
5170
|
if (detected.length === 0) {
|
|
@@ -5258,7 +5529,7 @@ async function exists(path) {
|
|
|
5258
5529
|
}
|
|
5259
5530
|
|
|
5260
5531
|
// src/commands/update.ts
|
|
5261
|
-
import { Command as
|
|
5532
|
+
import { Command as Command14 } from "commander";
|
|
5262
5533
|
|
|
5263
5534
|
// src/version.ts
|
|
5264
5535
|
import { readFileSync } from "fs";
|
|
@@ -5500,7 +5771,7 @@ function shortMessage(err) {
|
|
|
5500
5771
|
|
|
5501
5772
|
// src/commands/update.ts
|
|
5502
5773
|
function updateCommand() {
|
|
5503
|
-
return new
|
|
5774
|
+
return new Command14("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
|
|
5504
5775
|
const status = await checkForUpdate(VERSION);
|
|
5505
5776
|
const provenance = await detectProvenance();
|
|
5506
5777
|
const previous = await readUpdateState();
|
|
@@ -5608,9 +5879,10 @@ function reportBelowMinimum(below) {
|
|
|
5608
5879
|
// src/commands/undo.ts
|
|
5609
5880
|
import { readFile as readFile17, rm as rm8 } from "fs/promises";
|
|
5610
5881
|
import { join as join18 } from "path";
|
|
5611
|
-
import { Command as
|
|
5882
|
+
import { Command as Command15 } from "commander";
|
|
5612
5883
|
function undoCommand() {
|
|
5613
|
-
return new
|
|
5884
|
+
return new Command15("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").action(async (opId, opts) => {
|
|
5885
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
5614
5886
|
const { brain } = await resolveBrain(opts.vault);
|
|
5615
5887
|
await openBrainOrExplain(brain.root);
|
|
5616
5888
|
const { ops } = await readOps(brain.root);
|
|
@@ -5658,6 +5930,7 @@ function undoCommand() {
|
|
|
5658
5930
|
memoryId: target.memory_id,
|
|
5659
5931
|
hashBefore: actualHash,
|
|
5660
5932
|
hashAfter: reverted === null ? null : hashContent(reverted),
|
|
5933
|
+
rationale,
|
|
5661
5934
|
supersedes: target.op_id
|
|
5662
5935
|
});
|
|
5663
5936
|
});
|
|
@@ -5747,25 +6020,32 @@ The text from before that change is still on this machine: ${join18(snapshotDir(
|
|
|
5747
6020
|
|
|
5748
6021
|
// src/commands/use.ts
|
|
5749
6022
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve8 } from "path";
|
|
5750
|
-
import { Command as
|
|
6023
|
+
import { Command as Command16 } from "commander";
|
|
6024
|
+
var VIA_MEANS = {
|
|
6025
|
+
flag: "because --vault named it",
|
|
6026
|
+
pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
|
|
6027
|
+
cwd: "because this folder is inside it",
|
|
6028
|
+
only: "because it is the only brain linked"
|
|
6029
|
+
};
|
|
5751
6030
|
function useCommand() {
|
|
5752
|
-
return new
|
|
6031
|
+
return new Command16("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) => {
|
|
5753
6032
|
const where = await canonicalize(resolve8(opts.here ?? process.cwd()));
|
|
5754
6033
|
if (!brainPath) {
|
|
5755
6034
|
const pointer = await readPointer(where);
|
|
6035
|
+
const resolved = await resolveBrain(void 0, where);
|
|
5756
6036
|
printDual({
|
|
5757
|
-
json:
|
|
6037
|
+
json: {
|
|
6038
|
+
brain: resolved.brain.root,
|
|
6039
|
+
via: resolved.via,
|
|
6040
|
+
pointer: pointer ? pointer.from : null
|
|
6041
|
+
},
|
|
5758
6042
|
human: () => {
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
)
|
|
5765
|
-
);
|
|
5766
|
-
return;
|
|
6043
|
+
printHuman(`Using \`${resolved.brain.root}\`
|
|
6044
|
+
`);
|
|
6045
|
+
printHuman(c.gray(` ${VIA_MEANS[resolved.via]}`));
|
|
6046
|
+
if (resolved.via !== "pointer") {
|
|
6047
|
+
printHuman(c.gray(" Bind this folder explicitly with `memgineering use <brain>`."));
|
|
5767
6048
|
}
|
|
5768
|
-
printHuman(`\`${pointer.from}\` \u2192 \`${pointer.root}\``);
|
|
5769
6049
|
}
|
|
5770
6050
|
});
|
|
5771
6051
|
return;
|
|
@@ -5804,13 +6084,14 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
5804
6084
|
|
|
5805
6085
|
// src/program.ts
|
|
5806
6086
|
function buildProgram() {
|
|
5807
|
-
const program = new
|
|
6087
|
+
const program = new Command17("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", (thisCommand) => {
|
|
5808
6088
|
const opts = thisCommand.optsWithGlobals();
|
|
5809
6089
|
if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
|
|
5810
6090
|
});
|
|
5811
6091
|
configureGroupedHelp(program);
|
|
5812
6092
|
program.addCommand(recallCommand());
|
|
5813
6093
|
program.addCommand(openCommand());
|
|
6094
|
+
program.addCommand(evidenceCommand());
|
|
5814
6095
|
program.addCommand(rememberCommand());
|
|
5815
6096
|
program.addCommand(reviseCommand());
|
|
5816
6097
|
program.addCommand(undoCommand());
|
package/package.json
CHANGED