memoir-cli 3.11.3 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -124
- package/bin/memoir-work.js +9 -0
- package/bin/memoir.js +72 -8
- package/docs/AUDIT-REMEDIATION.md +55 -0
- package/docs/CASE_TAPE_AMNESIA.md +39 -0
- package/docs/HANDOFF-SECURITY-AUDIT.md +106 -0
- package/docs/LOCAL-HANDOFF-VALIDATION.md +129 -0
- package/docs/MCP-V2-MIGRATION.md +17 -0
- package/docs/PROJECT-HANDOFF.md +255 -0
- package/docs/PROJECT-VIEW-DEBUG.md +66 -0
- package/docs/PROJECT-VIEW-VALIDATION.md +136 -0
- package/docs/RELEASE-3.14-VALIDATION.md +36 -0
- package/docs/RELIABILITY-ROLLOUT.md +57 -0
- package/docs/RETRIEVAL-INDEX.md +45 -0
- package/docs/RETRIEVAL-RESULTS.md +26 -0
- package/docs/SPEC.md +684 -0
- package/evals/CONTINUITY-PROTOCOL.md +45 -0
- package/evals/cases.json +200 -0
- package/evals/results/retrieval-2026-09-05.json +5333 -0
- package/evals/retrieval-performance.mjs +99 -0
- package/evals/run.mjs +87 -0
- package/package.json +13 -5
- package/src/adapters/index.js +13 -6
- package/src/adapters/restore.js +83 -36
- package/src/cloud/auth.js +12 -15
- package/src/cloud/constants.js +6 -2
- package/src/cloud/storage.js +130 -93
- package/src/commands/activate.js +43 -9
- package/src/commands/cloud.js +56 -5
- package/src/commands/consolidate.js +49 -10
- package/src/commands/diff.js +2 -2
- package/src/commands/doctor.js +3 -3
- package/src/commands/forget.js +100 -0
- package/src/commands/push.js +164 -161
- package/src/commands/recall.js +42 -0
- package/src/commands/restore.js +32 -44
- package/src/commands/resume.js +15 -164
- package/src/commands/session.js +51 -9
- package/src/commands/snapshot.js +6 -7
- package/src/commands/status.js +23 -1
- package/src/commands/upgrade.js +13 -11
- package/src/commands/validate.js +16 -0
- package/src/commands/view.js +2 -2
- package/src/commands/why.js +4 -3
- package/src/config.js +9 -40
- package/src/context/capture.js +135 -33
- package/src/context/handoffs.js +72 -0
- package/src/events/summary.js +122 -0
- package/src/integrations/setup.js +88 -0
- package/src/mcp.js +151 -283
- package/src/memory/lexical-index.js +65 -0
- package/src/memory/repository.js +16 -0
- package/src/memory/scope.js +65 -0
- package/src/memory/search.js +598 -0
- package/src/memory/store.js +141 -0
- package/src/providers/index.js +182 -51
- package/src/providers/restore.js +5 -1
- package/src/security/encryption.js +34 -60
- package/src/security/files.js +155 -0
- package/src/session/brief.js +47 -0
- package/src/session/inject.js +12 -6
- package/src/session/lock.js +39 -118
- package/src/session/migrations.js +6 -0
- package/src/session/render.js +34 -4
- package/src/session/state.js +305 -34
- package/src/work/cli.js +64 -0
- package/src/work/errors.js +8 -0
- package/src/work/server.js +28 -0
- package/src/work/setup.js +96 -0
- package/src/work/store.js +340 -0
- package/src/work/ui/app.js +205 -0
- package/src/work/ui/index.html +30 -0
- package/src/work/ui/style.css +3 -0
- package/src/work/view.js +93 -0
- package/src/workspace/tracker.js +84 -332
- package/supabase/migrations/202609050001_backup_versions.sql +50 -0
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
# The memoir format, v0.1.2 (draft)
|
|
2
|
+
|
|
3
|
+
**Status:** Draft. Version 0.1.1. Seeking implementations and critique.
|
|
4
|
+
|
|
5
|
+
This document specifies the memoir format: an open, portable, plain-text
|
|
6
|
+
format for an AI assistant's accumulated working context — what it has
|
|
7
|
+
learned, decided, been corrected on, and is in the middle of doing.
|
|
8
|
+
|
|
9
|
+
The format is extracted from a working implementation
|
|
10
|
+
([memoir-cli](https://github.com/camgitt/memoir), MIT), not designed on a
|
|
11
|
+
whiteboard. Where this document and shipped behavior disagree, that is a bug
|
|
12
|
+
in this document; please file it. Normative words (MUST, SHOULD, MAY) are
|
|
13
|
+
used in the RFC 2119 sense.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Why an open format
|
|
18
|
+
|
|
19
|
+
Every AI vendor is shipping memory, and every one of them is shipping it
|
|
20
|
+
locked in. Memory is the strongest lock-in there is, because it compounds:
|
|
21
|
+
the assistant that knows your preferences, your past decisions and their
|
|
22
|
+
reasons, and the mistakes it must not repeat is worth more every week — and
|
|
23
|
+
none of that accumulates anywhere you can take with you. Email had the same
|
|
24
|
+
problem, and solved it with open formats and protocols (mbox, Maildir, IMAP):
|
|
25
|
+
your mail is yours, in files and mailboxes any client can read, and clients
|
|
26
|
+
compete on experience rather than on holding your archive hostage.
|
|
27
|
+
|
|
28
|
+
The memoir format is that seat for AI working context. It deliberately covers
|
|
29
|
+
*experience*, not just facts: a decision with its rationale and the
|
|
30
|
+
alternative that was rejected; a lesson with the trigger that should fire it
|
|
31
|
+
and the behavior change it demands; the live working set of goals and next
|
|
32
|
+
actions that lets a different tool — or the same tool on a different machine —
|
|
33
|
+
pick up mid-thought. It is plain markdown and JSON on disk, owned by the
|
|
34
|
+
user, versioned however the user versions files. Any tool that can read a
|
|
35
|
+
directory can read it. That is the whole point.
|
|
36
|
+
|
|
37
|
+
## 2. The store
|
|
38
|
+
|
|
39
|
+
A **memoir store** is:
|
|
40
|
+
|
|
41
|
+
1. a directory of **entry files** — one entry per file, markdown with YAML
|
|
42
|
+
frontmatter (section 3), and
|
|
43
|
+
2. a **session state file** — a single JSON document holding the live working
|
|
44
|
+
set (section 5).
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
<store>/
|
|
48
|
+
memory/
|
|
49
|
+
decision_local-db-choice.md
|
|
50
|
+
lesson_migrations-staging-first.md
|
|
51
|
+
preference_commit-style.md
|
|
52
|
+
fact_staging-endpoints.md
|
|
53
|
+
...
|
|
54
|
+
session.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The reference implementation currently keeps these in two places for
|
|
58
|
+
tool-compatibility reasons (entry files where the host assistant reads its
|
|
59
|
+
memory, e.g. `~/.claude/projects/<project-slug>/memory/`; session state at
|
|
60
|
+
`~/.config/memoir/session.json`). The format does not care where the store
|
|
61
|
+
lives. A store MAY be a git repository; the format is designed so that it
|
|
62
|
+
diffs and merges well when it is (section 4.2).
|
|
63
|
+
|
|
64
|
+
Entry filenames SHOULD be lowercase slugs and SHOULD carry the entry type as
|
|
65
|
+
a prefix (`decision_`, `lesson_`, ...), purely for human scanability. The
|
|
66
|
+
filename (without `.md`) is the entry's **link name**: other entries
|
|
67
|
+
reference it with `[[wikilinks]]` in their body text.
|
|
68
|
+
|
|
69
|
+
## 3. Entry files
|
|
70
|
+
|
|
71
|
+
### 3.1 File format
|
|
72
|
+
|
|
73
|
+
An entry file is UTF-8 markdown. It SHOULD begin with a YAML frontmatter
|
|
74
|
+
block delimited by `---` lines. Everything after the closing `---` is the
|
|
75
|
+
**body**: free markdown, as long or short as the entry deserves.
|
|
76
|
+
|
|
77
|
+
Plain text is a feature, not a compromise, and implementations MUST NOT
|
|
78
|
+
"upgrade" the store to a database as the canonical form:
|
|
79
|
+
|
|
80
|
+
- **Human-readable.** The user can open, audit, and edit their AI's memory
|
|
81
|
+
with any editor. Memory the user cannot read is memory the user cannot
|
|
82
|
+
trust or correct.
|
|
83
|
+
- **Git-diffable.** One entry per file means a change to one memory is a
|
|
84
|
+
one-file diff. History, blame, review, and rollback come free from tools
|
|
85
|
+
the user already has.
|
|
86
|
+
- **Mergeable.** Text files union across machines with well-understood
|
|
87
|
+
conflict surfaces (section 4). A database file is an opaque blob that can
|
|
88
|
+
only be replaced wholesale — which is exactly the failure mode this format
|
|
89
|
+
exists to avoid.
|
|
90
|
+
- **Durable.** Markdown will be readable in twenty years by tools that have
|
|
91
|
+
never heard of memoir.
|
|
92
|
+
|
|
93
|
+
Implementations MAY build derived indexes (embeddings, full-text, SQLite
|
|
94
|
+
caches) but these are caches: the files are the truth, and a conforming
|
|
95
|
+
implementation MUST be able to rebuild any index from the files alone.
|
|
96
|
+
|
|
97
|
+
The frontmatter is restricted to a simple YAML subset so that it can be
|
|
98
|
+
parsed without a full YAML engine: scalar `key: value` pairs, one level of
|
|
99
|
+
nested mapping, and simple `- item` string lists. Writers MUST NOT emit
|
|
100
|
+
anchors, multi-line block scalars, or deeper nesting in frontmatter; prose
|
|
101
|
+
belongs in the body.
|
|
102
|
+
|
|
103
|
+
### 3.2 Common frontmatter fields
|
|
104
|
+
|
|
105
|
+
| Field | Requirement | Meaning |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `type` | REQUIRED | One of the six entry types (3.3). |
|
|
108
|
+
| `name` | REQUIRED | Human-readable title (or slug) for the entry. |
|
|
109
|
+
| `description` | SHOULD | One line. Indexes and pickers render this; an entry without one is invisible in lists. |
|
|
110
|
+
| `created` | SHOULD | ISO 8601 date or date-time the entry was first written. |
|
|
111
|
+
| `updated` | MAY | ISO 8601 date or date-time of last substantive edit. |
|
|
112
|
+
| `schema_version` | MAY | Integer. Absent means `1`. See section 6. |
|
|
113
|
+
| `project` | MAY | Project this entry is scoped to; absent means global. |
|
|
114
|
+
| `tags` | MAY | List of strings. |
|
|
115
|
+
| `aliases` | MAY | List of strings: other names, nicknames, or phrasings a reader might search for this entry under (e.g. an entry about a "vertical swipe feed" surface: `tiktok`, `reels`, `/tape`). Retrieval SHOULD weight this field above body prose. Concept-level recall in a plain-text format is a write-time job — the writer knows what else the thing is called; a substring search later does not. |
|
|
116
|
+
| `origin` | MAY | Mapping with any of `tool`, `session_id`, `machine_id` — where the entry came from. |
|
|
117
|
+
|
|
118
|
+
Readers MUST ignore fields they do not recognize. Writers MUST preserve
|
|
119
|
+
fields they do not recognize when rewriting an entry. (Unknown fields are how
|
|
120
|
+
the format extends without breaking deployed tools.)
|
|
121
|
+
|
|
122
|
+
### 3.3 Entry types
|
|
123
|
+
|
|
124
|
+
Six types. A type answers the question "what should a tool *do* with this
|
|
125
|
+
entry when it loads?" — inform (fact), constrain style (preference), prevent
|
|
126
|
+
relitigation (decision), change behavior (lesson), orient (goal), or resume
|
|
127
|
+
work (next_action).
|
|
128
|
+
|
|
129
|
+
Machine-readable versions of every constraint in this section are in
|
|
130
|
+
[`schema/entry.schema.json`](../schema/entry.schema.json).
|
|
131
|
+
|
|
132
|
+
#### 3.3.1 `fact`
|
|
133
|
+
|
|
134
|
+
Something true about the world, the project, or the environment, worth
|
|
135
|
+
loading into future context. Facts are the baseline type: when in doubt, an
|
|
136
|
+
entry is a fact.
|
|
137
|
+
|
|
138
|
+
Extra fields: `source` (MAY) — where the fact was established.
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
---
|
|
142
|
+
type: fact
|
|
143
|
+
name: Staging API endpoints
|
|
144
|
+
description: Base URLs and auth mode for the staging environment
|
|
145
|
+
created: 2026-05-14
|
|
146
|
+
project: acme-api
|
|
147
|
+
---
|
|
148
|
+
Staging runs at `https://staging.api.example.com`, bearer-token auth,
|
|
149
|
+
tokens minted by `scripts/mint-token.sh`. Rate limit is 60 req/min and it
|
|
150
|
+
is enforced. See [[decision_local-db-choice]] for why staging has no
|
|
151
|
+
Postgres.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 3.3.2 `preference`
|
|
155
|
+
|
|
156
|
+
How the user wants things done. Preferences are standing instructions:
|
|
157
|
+
tone, formatting, workflow, tooling defaults.
|
|
158
|
+
|
|
159
|
+
Extra fields: `scope` (MAY) — `global` or `project`.
|
|
160
|
+
|
|
161
|
+
```markdown
|
|
162
|
+
---
|
|
163
|
+
type: preference
|
|
164
|
+
name: Commit message style
|
|
165
|
+
description: Imperative mood, no trailers, reference the issue number
|
|
166
|
+
scope: global
|
|
167
|
+
---
|
|
168
|
+
Commit subjects in imperative mood ("Add retry", not "Added retry").
|
|
169
|
+
No generated trailers. Reference the issue as `(#123)` at the end of the
|
|
170
|
+
subject when one exists.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### 3.3.3 `decision`
|
|
174
|
+
|
|
175
|
+
Something that was decided, recorded so it does not get silently relitigated
|
|
176
|
+
three sessions later. A decision's value is almost entirely in its `why` and
|
|
177
|
+
its `rejected` alternative — "we use Postgres" is trivia; "we use Postgres
|
|
178
|
+
*because X*, and we rejected SQLite *because Y*" is experience.
|
|
179
|
+
|
|
180
|
+
Extra fields:
|
|
181
|
+
|
|
182
|
+
| Field | Requirement | Meaning |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `date` | SHOULD | When it was decided (ISO 8601). Merge recency key (4.2). |
|
|
185
|
+
| `why` | SHOULD (strongly — see below) | The rationale. |
|
|
186
|
+
| `rejected` | SHOULD | The alternative considered and rejected, and why it lost. |
|
|
187
|
+
| `hidden` | MAY | `true` marks an absolute tombstone (4.3.1). |
|
|
188
|
+
| `hidden_at` | MUST when `hidden: true` | When it was tombstoned. |
|
|
189
|
+
| `superseded_by` | MAY | Link name of the decision that replaced this one. |
|
|
190
|
+
|
|
191
|
+
`why` and `rejected` are *required-encouraged*: their absence is not a
|
|
192
|
+
validation error (auto-captured decisions legitimately lack them at capture
|
|
193
|
+
time), but interactive writers SHOULD prompt for both, and a decision entry
|
|
194
|
+
without a `why` SHOULD be flagged by validators as a warning. A decision
|
|
195
|
+
without a why is half a decision.
|
|
196
|
+
|
|
197
|
+
> Naming note: the field is `rejected`, not `rejected_alternative` — this
|
|
198
|
+
> matches every deployed store and the reference implementation's session
|
|
199
|
+
> records. The longer name lost to reality.
|
|
200
|
+
|
|
201
|
+
```markdown
|
|
202
|
+
---
|
|
203
|
+
type: decision
|
|
204
|
+
name: Local store is SQLite, not Postgres
|
|
205
|
+
description: Single-file embedded DB for the local cache; Postgres rejected
|
|
206
|
+
date: 2026-05-02
|
|
207
|
+
why: Zero-dependency install matters more than concurrent writers for a per-user local cache.
|
|
208
|
+
rejected: Postgres — better concurrency, but a daemon dependency in a CLI install is a support burden we measured and declined.
|
|
209
|
+
---
|
|
210
|
+
Applies to the local derived cache only; the canonical store stays plain
|
|
211
|
+
files per [[fact_staging-endpoints]] discussion.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### 3.3.4 `lesson`
|
|
215
|
+
|
|
216
|
+
A behavior correction learned from experience — usually from a mistake. A
|
|
217
|
+
lesson is the format's most valuable and most structured type: it must say
|
|
218
|
+
*when it applies* (`trigger`) and *what to do differently*
|
|
219
|
+
(`how_to_apply`), or it is an anecdote, not a lesson.
|
|
220
|
+
|
|
221
|
+
Extra fields:
|
|
222
|
+
|
|
223
|
+
| Field | Requirement | Meaning |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `trigger` | REQUIRED | The situation that should fire this lesson ("before deleting a directory", "when a test needs `$HOME`"). |
|
|
226
|
+
| `how_to_apply` | REQUIRED | The concrete behavior change. |
|
|
227
|
+
| `fired_count` | MAY | Integer ≥ 0. Times this lesson demonstrably fired and changed behavior. |
|
|
228
|
+
| `last_fired` | MAY | ISO 8601 date-time it last fired. |
|
|
229
|
+
|
|
230
|
+
`fired_count` / `last_fired` are the hook for the feedback loop (a later
|
|
231
|
+
phase of this spec's roadmap): once tools report firings, lessons acquire an
|
|
232
|
+
outcome record — which lessons actually prevent repeats, and which are dead
|
|
233
|
+
weight a consolidation pass can retire. Writers MAY maintain them; readers
|
|
234
|
+
MUST NOT require them.
|
|
235
|
+
|
|
236
|
+
The body SHOULD carry the evidence: what happened, what it cost, why the
|
|
237
|
+
rule is what it is. Established convention (predating this spec) renders
|
|
238
|
+
these as bold-label sections — `**Why:**` and `**How to apply:**` — in the
|
|
239
|
+
body; validators MUST accept `how_to_apply` expressed as a
|
|
240
|
+
`**How to apply:**` body section in legacy entries (Appendix A).
|
|
241
|
+
|
|
242
|
+
```markdown
|
|
243
|
+
---
|
|
244
|
+
type: lesson
|
|
245
|
+
name: Rehearse destructive migrations on a copy first
|
|
246
|
+
description: Never run a schema migration against the only copy of the data
|
|
247
|
+
trigger: About to run a migration that drops, renames, or rewrites columns
|
|
248
|
+
how_to_apply: Restore the latest backup to a scratch database and run the full migration train there first; only then touch the real one.
|
|
249
|
+
created: 2026-04-11
|
|
250
|
+
---
|
|
251
|
+
**Why:** A rename-and-backfill migration was run directly against the only
|
|
252
|
+
database; the backfill had an off-by-one and the pre-rename column was
|
|
253
|
+
already gone. Recovery took a day. A five-minute rehearsal on a restored
|
|
254
|
+
copy would have caught it.
|
|
255
|
+
|
|
256
|
+
**How to apply:** Restore latest backup → scratch DB → run the FULL
|
|
257
|
+
migration train → verify row counts → only then run for real.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### 3.3.5 `goal`
|
|
261
|
+
|
|
262
|
+
What the work is currently *for* — the standing objective that orients
|
|
263
|
+
sessions. Goals are few (the reference implementation caps the live set at
|
|
264
|
+
3) and change rarely.
|
|
265
|
+
|
|
266
|
+
Extra fields: `set_on` (SHOULD, ISO 8601 — merge recency key), `done_at`
|
|
267
|
+
(MAY — temporal tombstone, same semantics as next_action, 4.3.2).
|
|
268
|
+
|
|
269
|
+
```markdown
|
|
270
|
+
---
|
|
271
|
+
type: goal
|
|
272
|
+
name: Ship v1 of the importer
|
|
273
|
+
description: End-to-end import from the three legacy formats, behind a flag
|
|
274
|
+
set_on: 2026-06-01
|
|
275
|
+
---
|
|
276
|
+
Done means: all three legacy formats round-trip in CI and the flag defaults
|
|
277
|
+
on for new installs.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### 3.3.6 `next_action`
|
|
281
|
+
|
|
282
|
+
A concrete, resumable piece of work. Next actions are the handoff unit: the
|
|
283
|
+
thing a different machine or session picks up.
|
|
284
|
+
|
|
285
|
+
Extra fields:
|
|
286
|
+
|
|
287
|
+
| Field | Requirement | Meaning |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `added` | REQUIRED | ISO 8601 date-time the action was added. The temporal-tombstone comparison (4.3.2) is against this field; without it, completion semantics are undefined. |
|
|
290
|
+
| `done_at` | MAY | ISO 8601 date-time it was completed. Presence marks the entry a temporal tombstone. |
|
|
291
|
+
| `machine_id` | MAY | Machine that added it. |
|
|
292
|
+
|
|
293
|
+
```markdown
|
|
294
|
+
---
|
|
295
|
+
type: next_action
|
|
296
|
+
name: Add retry with backoff to the sync client
|
|
297
|
+
description: Sync fails hard on transient 5xx; wrap in 3-try exponential backoff
|
|
298
|
+
added: 2026-06-12T09:30:00Z
|
|
299
|
+
---
|
|
300
|
+
Touch only `src/sync/client.js`. The failing case is a 503 mid-upload;
|
|
301
|
+
there is a repro in `test-fixtures/sync-503.json`.
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 3.4 Body conventions
|
|
305
|
+
|
|
306
|
+
- `[[wikilinks]]` reference other entries by link name (filename without
|
|
307
|
+
`.md`). Readers SHOULD resolve them within the store; unresolvable links
|
|
308
|
+
are not an error.
|
|
309
|
+
- Bold-label lines (`**Why:**`, `**How to apply:**`) are established body
|
|
310
|
+
structure for lessons and MAY be used generally. They are convention, not
|
|
311
|
+
syntax: readers MUST NOT require them (except as the legacy fallback in
|
|
312
|
+
Appendix A).
|
|
313
|
+
- Entries SHOULD NOT contain secrets. That rule's enforcement (scanning,
|
|
314
|
+
redaction) is an implementation concern, not a format concern.
|
|
315
|
+
|
|
316
|
+
## 4. Session state file
|
|
317
|
+
|
|
318
|
+
### 4.1 Shape
|
|
319
|
+
|
|
320
|
+
The session state file (`session.json`) is a single JSON document: the live
|
|
321
|
+
working set plus a bounded history. It is the "hot" half of the store —
|
|
322
|
+
small, rewritten atomically as a whole, merged structurally (never
|
|
323
|
+
line-by-line). This section describes exactly what the reference
|
|
324
|
+
implementation reads and writes; the machine-readable version is
|
|
325
|
+
[`schema/session.schema.json`](../schema/session.schema.json).
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"version": 1,
|
|
330
|
+
"created_at": "2026-04-18T08:22:36.703Z",
|
|
331
|
+
"updated_at": "2026-08-06T21:10:03.001Z",
|
|
332
|
+
"machines": {
|
|
333
|
+
"<machine-uuid>": { "label": "work-laptop", "last_seen": "2026-08-06T21:10:03.001Z" }
|
|
334
|
+
},
|
|
335
|
+
"current": {
|
|
336
|
+
"goals": [ { "text": "...", "machine_id": "...", "set_on": "..." } ],
|
|
337
|
+
"next_actions": [ { "text": "...", "machine_id": "...", "added": "..." } ],
|
|
338
|
+
"parked_actions": [ { "text": "...", "machine_id": "...", "added": "...", "parked_at": "..." } ],
|
|
339
|
+
"completed_actions": [ { "text": "...", "done_at": "..." } ],
|
|
340
|
+
"completed_goals": [ { "text": "...", "done_at": "..." } ],
|
|
341
|
+
"open_questions": [ { "text": "...", "machine_id": "...", "asked": "..." } ],
|
|
342
|
+
"decisions": [ { "text": "...", "why": "...", "rejected": "...",
|
|
343
|
+
"hidden": true, "hidden_at": "...",
|
|
344
|
+
"machine_id": "...", "date": "..." } ]
|
|
345
|
+
},
|
|
346
|
+
"history": [
|
|
347
|
+
{ "date": "...", "machine_id": "...", "summary": "...",
|
|
348
|
+
"files_touched": ["..."], "duration_min": 42, "session_id": "..." }
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Field notes:
|
|
354
|
+
|
|
355
|
+
- `version` — integer schema version of this file (section 6). Note the
|
|
356
|
+
asymmetry with entry files' `schema_version`; both names are kept because
|
|
357
|
+
both are deployed.
|
|
358
|
+
- `machines` — map of stable machine UUID → `{ label, last_seen }`. Machine
|
|
359
|
+
identity is a UUID persisted per machine, paired with a human label
|
|
360
|
+
(hostname) for display. Labels may change; UUIDs MUST NOT.
|
|
361
|
+
- `current.*` — every item carries `text` (the identity key, 4.2) and the
|
|
362
|
+
list's date field (`set_on` / `added` / `asked` / `date`). `machine_id`
|
|
363
|
+
SHOULD be stamped by writers.
|
|
364
|
+
- `completed_actions` — temporal tombstones for `next_actions` (5.3.2):
|
|
365
|
+
`{ text, done_at }`. This list MAY be absent in files written before any
|
|
366
|
+
completion occurred; readers MUST treat absence as empty.
|
|
367
|
+
- `parked_actions` (v0.1.2) — the overflow of `next_actions`. When the live
|
|
368
|
+
list is at its cap, the oldest actions (by `added`) move here rather than
|
|
369
|
+
being discarded. A parked action is still *open*: it MUST be rendered (it
|
|
370
|
+
may be rendered more compactly), it MUST be completable (completing it
|
|
371
|
+
records the same `completed_actions` tombstone), and adding an action
|
|
372
|
+
whose identity is parked MUST return it to `next_actions` rather than
|
|
373
|
+
create a duplicate. `parked_at` records when it overflowed; `added` is
|
|
374
|
+
unchanged and remains the merge and tombstone comparison key. This list
|
|
375
|
+
has its own, larger cap; only when *it* overflows may an action be
|
|
376
|
+
dropped, and an implementation SHOULD log that it did. MAY be absent.
|
|
377
|
+
- `completed_goals` (v0.1.2) — temporal tombstones for `goals`, exactly as
|
|
378
|
+
`completed_actions` are for `next_actions`, compared against `set_on`
|
|
379
|
+
(5.3.2). Without them a retired goal returns on the next merge with any
|
|
380
|
+
replica still carrying it. MAY be absent.
|
|
381
|
+
- `open_questions` — captured questions awaiting an answer. (Draft note:
|
|
382
|
+
there is deliberately no `question` *entry type* in v0.1 — questions have
|
|
383
|
+
so far only proven useful in the live working set. If a portable form
|
|
384
|
+
earns its keep, it becomes a seventh type in a later version.)
|
|
385
|
+
- `history` — bounded roll-up of past sessions, newest first, deduplicated
|
|
386
|
+
by `session_id` when present (v0.1.2; the same session updates its own
|
|
387
|
+
row in place instead of adding one per write), otherwise by
|
|
388
|
+
`(date, machine_id, summary)`.
|
|
389
|
+
- **Bounded lists.** The live lists are capped (reference implementation:
|
|
390
|
+
goals 3, next_actions 8, parked_actions 20, open_questions 5, recent
|
|
391
|
+
decisions 10, completed tombstones 50, history 30). Caps are
|
|
392
|
+
implementation-chosen QUALITY parameters, not format constants — but
|
|
393
|
+
*some* cap is normative: the working set must stay loadable-into-context
|
|
394
|
+
small, with rotation oldest-by-date-out. Rotation out of `next_actions`
|
|
395
|
+
MUST go to `parked_actions`, not to nowhere (v0.1.2): the reference
|
|
396
|
+
implementation silently discarded the oldest action for five months, and
|
|
397
|
+
the loss was only noticed when three live items vanished in one week.
|
|
398
|
+
A cap on the *visible* list is a rendering budget; it is not permission
|
|
399
|
+
to forget. In particular the completed-tombstone retention MUST be large
|
|
400
|
+
enough to outlive any stale replica that might still carry the completed
|
|
401
|
+
item, or completions resurrect (5.3.2).
|
|
402
|
+
|
|
403
|
+
Writers MUST write the file atomically (write temp file, rename) and MUST
|
|
404
|
+
serialize concurrent read-mutate-write cycles (e.g. a lock file). Locking
|
|
405
|
+
only the write is insufficient: two processes can both read the same stale
|
|
406
|
+
snapshot before either writes. The entire read → mutate → write cycle is the
|
|
407
|
+
critical section.
|
|
408
|
+
|
|
409
|
+
Readers encountering unparseable JSON MUST NOT discard the file silently:
|
|
410
|
+
quarantine the original (e.g. copy to a timestamped backup), then degrade to
|
|
411
|
+
a safe empty state.
|
|
412
|
+
|
|
413
|
+
## 5. Merge semantics (normative)
|
|
414
|
+
|
|
415
|
+
This section is the hard-won part of the format. Every rule here exists
|
|
416
|
+
because its absence produced a real data-loss or data-resurrection bug in
|
|
417
|
+
production. Implementations claiming Full conformance (section 8) MUST
|
|
418
|
+
implement this section exactly.
|
|
419
|
+
|
|
420
|
+
### 5.1 Identity
|
|
421
|
+
|
|
422
|
+
The identity of a session-list item is its **normalized text**: `text`,
|
|
423
|
+
whitespace-trimmed, case-folded. Two items with the same normalized text are
|
|
424
|
+
the same item. A purged decision tombstone (5.3.1) carries that identity as
|
|
425
|
+
`text_hash` (SHA-256 of the normalized text) instead of the text. The
|
|
426
|
+
identity of an entry file is its link name (filename).
|
|
427
|
+
|
|
428
|
+
### 5.2 Union, newest wins
|
|
429
|
+
|
|
430
|
+
Merging two replicas (local and remote) of a session list:
|
|
431
|
+
|
|
432
|
+
1. **Union by identity.** The merged list contains every distinct identity
|
|
433
|
+
from both sides. A merge MUST NOT drop an item merely because the other
|
|
434
|
+
side lacks it — "never clobber." The only way items leave is cap
|
|
435
|
+
rotation (oldest by date field, off the end) and tombstoning. For
|
|
436
|
+
`next_actions` (v0.1.2) the union pools the live *and* parked lists of
|
|
437
|
+
both sides, applies the tombstones, then re-splits: the newest N by
|
|
438
|
+
`added` are live, the rest are parked (keeping any existing `parked_at`).
|
|
439
|
+
A capped union of the live list alone evicts on merge exactly as
|
|
440
|
+
silently as a capped append does.
|
|
441
|
+
2. **Newest wins per identity.** When both sides carry the same identity,
|
|
442
|
+
the copy with the newer value in the list's date field (`set_on`,
|
|
443
|
+
`added`, `asked`, `date`) wins wholesale. A missing or unparseable date
|
|
444
|
+
compares as the epoch (loses to any real date).
|
|
445
|
+
3. `machines` merges per-UUID, keeping the entry with the newer `last_seen`.
|
|
446
|
+
4. `history` unions, deduplicates by `(date, machine_id, summary)`, sorts
|
|
447
|
+
newest-first, and caps.
|
|
448
|
+
5. `created_at` takes the earlier of the two; `updated_at` the later.
|
|
449
|
+
|
|
450
|
+
Entry files merge by the same principle at file granularity: union of files;
|
|
451
|
+
same filename on both sides resolves by newest (`updated`, falling back to
|
|
452
|
+
`created`), or by the user's VCS if the store is under one. v0.1 does not
|
|
453
|
+
define intra-file three-way merge for entry bodies; that is what git is for.
|
|
454
|
+
|
|
455
|
+
### 5.3 Tombstones are sticky
|
|
456
|
+
|
|
457
|
+
Deletion under union-merge is the hard problem: plain removal does not
|
|
458
|
+
survive, because any replica still holding the removed item re-unions it
|
|
459
|
+
straight back on the next merge. A conforming implementation MUST therefore
|
|
460
|
+
represent removals as tombstones, and tombstones MUST be *sticky* — they
|
|
461
|
+
propagate through merges. There are two classes, and the difference between
|
|
462
|
+
them is deliberate.
|
|
463
|
+
|
|
464
|
+
#### 5.3.1 Absolute tombstones (`hidden`) — decisions
|
|
465
|
+
|
|
466
|
+
Setting `hidden: true` (with `hidden_at`) on a decision suppresses it
|
|
467
|
+
permanently: hidden decisions are excluded from rendering and lookup but
|
|
468
|
+
retained in the data so the tombstone keeps propagating.
|
|
469
|
+
|
|
470
|
+
**Stickiness rule:** hidden is monotonic. If *any* copy of an identity on
|
|
471
|
+
*either* side of a merge carries `hidden: true`, the merged copy carries
|
|
472
|
+
`hidden: true` (and that copy's `hidden_at`) — **regardless of the date
|
|
473
|
+
comparison in 5.2**. The winner-by-date inherits the tombstone from the
|
|
474
|
+
loser if it lacks one.
|
|
475
|
+
|
|
476
|
+
**Purged form.** A hidden decision MAY additionally have its `text` replaced
|
|
477
|
+
by the literal `[purged]` (and `why` / `rejected` removed) and carry
|
|
478
|
+
`text_hash`: the lowercase hex SHA-256 of the identity it replaces (the
|
|
479
|
+
normalized text, 5.1). This exists for the case where the text itself must
|
|
480
|
+
leave the file — a pasted credential, a client name — not merely leave the
|
|
481
|
+
render. Readers MUST treat a purged tombstone as matching every decision
|
|
482
|
+
whose identity hashes to `text_hash`, and on merge the purged form MUST win
|
|
483
|
+
over any un-purged copy of the same identity regardless of `date`, so the
|
|
484
|
+
redacted text never returns via a stale replica. Implementations MUST NOT
|
|
485
|
+
purge without hiding; the hash is not an identity for a live decision.
|
|
486
|
+
|
|
487
|
+
The date-independence is not an optimization; it is the correctness
|
|
488
|
+
condition. Tombstoning legitimately does not touch the item's `date` (the
|
|
489
|
+
tombstoned copy may well *lose* the newest-wins comparison), and a replica
|
|
490
|
+
that has not yet pulled the tombstone holds an older un-hidden copy that
|
|
491
|
+
would otherwise win or re-union. If hiding were just another field on
|
|
492
|
+
whichever copy has the newer date, every un-synced replica would resurrect
|
|
493
|
+
the item on its next merge, and the same junk would need re-hiding on every
|
|
494
|
+
machine forever. Suppression must be monotonic or it is not suppression.
|
|
495
|
+
|
|
496
|
+
#### 5.3.2 Temporal tombstones (`done_at`) — next actions
|
|
497
|
+
|
|
498
|
+
Completing a next action removes it from `next_actions` AND records
|
|
499
|
+
`{ text, done_at }` in `completed_actions`. Removal alone is insufficient
|
|
500
|
+
for exactly the resurrection reason above — any stale replica (another
|
|
501
|
+
machine, a backup, a long-lived process's in-memory copy) re-unions the
|
|
502
|
+
finished item on merge.
|
|
503
|
+
|
|
504
|
+
**Merge rule.** After the 5.2 union:
|
|
505
|
+
|
|
506
|
+
1. Union `completed_actions` from both sides by identity, keeping the
|
|
507
|
+
newest `done_at` per identity, capped (newest-first).
|
|
508
|
+
2. Filter the merged `next_actions`: an action is suppressed if a tombstone
|
|
509
|
+
with its identity exists **and** the action's `added` is not later than
|
|
510
|
+
the tombstone's `done_at`.
|
|
511
|
+
|
|
512
|
+
Unlike `hidden`, this tombstone is *temporal on purpose*: a copy whose
|
|
513
|
+
`added` postdates `done_at` is not a stale straggler — it is a deliberate
|
|
514
|
+
re-add of the same text after completion, and it MUST survive. "Fix the
|
|
515
|
+
flaky test" can legitimately be finished and later added again. Decisions
|
|
516
|
+
get absolute tombstones because a suppressed decision text is junk forever;
|
|
517
|
+
actions get temporal ones because finished work can recur. Implementations
|
|
518
|
+
MUST NOT substitute one class for the other.
|
|
519
|
+
|
|
520
|
+
The same `done_at`-vs-`added`/`set_on` comparison applies to `goal` and
|
|
521
|
+
`next_action` *entry files* that carry `done_at`, and (v0.1.2) to the
|
|
522
|
+
session's `goals` list against `completed_goals`, compared on `set_on`.
|
|
523
|
+
|
|
524
|
+
### 5.4 Merge invariants (summary)
|
|
525
|
+
|
|
526
|
+
For any replicas A and B:
|
|
527
|
+
|
|
528
|
+
- **No silent loss:** every identity in A ∪ B is in merge(A, B), as a live
|
|
529
|
+
item or a tombstone-suppressed one (up to cap rotation).
|
|
530
|
+
- **Convergence:** merge is commutative and idempotent up to ordering —
|
|
531
|
+
merge(A, B) and merge(B, A) render identically, and merging a replica
|
|
532
|
+
with itself is a no-op.
|
|
533
|
+
- **Monotone suppression:** once hidden everywhere-merged, hidden in every
|
|
534
|
+
future merge; a completed action stays completed against every copy whose
|
|
535
|
+
`added` predates its `done_at`.
|
|
536
|
+
- **Unknown fields pass through (v0.1.2):** a merge MUST carry every key
|
|
537
|
+
under `current` (and every top-level key) that it does not recognise,
|
|
538
|
+
taking the local copy when both sides have one. A merger rebuilt from the
|
|
539
|
+
keys it knows erases whatever a newer writer added — the reference
|
|
540
|
+
implementation lost two goal retirements and a parked action minutes
|
|
541
|
+
after those fields existed, to a push still running the previous version.
|
|
542
|
+
Forward compatibility of the *file* (section 6) is not enough; the merge
|
|
543
|
+
has to be forward compatible too.
|
|
544
|
+
|
|
545
|
+
## 6. Versioning
|
|
546
|
+
|
|
547
|
+
- Entry frontmatter MAY carry `schema_version` (integer; absent = 1). The
|
|
548
|
+
session file MUST carry `version` (integer). Both are governed by the
|
|
549
|
+
same rules below.
|
|
550
|
+
- This spec is v0.1 and describes data schema version **1**.
|
|
551
|
+
- **Backward:** implementations MUST migrate older versions forward through
|
|
552
|
+
a per-version ladder (a pure step from N to N+1 for each N), filling
|
|
553
|
+
defaults for missing fields. Migration MUST NOT throw on malformed input;
|
|
554
|
+
the worst case is a fresh empty state, with the original quarantined.
|
|
555
|
+
- **Forward (normative — quarantine and degrade, never guess):** an
|
|
556
|
+
implementation encountering a version **newer** than it understands MUST
|
|
557
|
+
NOT attempt to interpret the data. It MUST preserve the original
|
|
558
|
+
(quarantine — e.g. a timestamped backup copy), MUST degrade to safe
|
|
559
|
+
behavior (an empty-but-valid state, or read-only), and SHOULD tell the
|
|
560
|
+
user once to upgrade. A newer schema's fields may have semantics —
|
|
561
|
+
tombstones especially — that an old reader would destroy by "mostly
|
|
562
|
+
understanding" them. Merging is the dangerous path: a degraded read MUST
|
|
563
|
+
contribute nothing to a merge rather than contribute a misreading.
|
|
564
|
+
- Unknown *fields* at a known version are the extension mechanism and are
|
|
565
|
+
NOT a version violation: readers ignore, writers preserve (3.2).
|
|
566
|
+
|
|
567
|
+
## 7. What this spec does not cover
|
|
568
|
+
|
|
569
|
+
Deliberately out of scope — these are implementations' business, and
|
|
570
|
+
competing on them is the point of an open format:
|
|
571
|
+
|
|
572
|
+
- **Sync transport.** Git, object storage, a vendor cloud, a USB stick.
|
|
573
|
+
The format defines what a correct merge produces, not how bytes move.
|
|
574
|
+
- **Encryption at rest and in transit.** Stores are plain text; protecting
|
|
575
|
+
them (client-side encryption before upload, disk encryption) is an
|
|
576
|
+
implementation/deployment concern.
|
|
577
|
+
- **Cloud services.** Accounts, auth, storage tiers, sharing links.
|
|
578
|
+
- **Capture quality.** What is *worth* remembering, secret scanning,
|
|
579
|
+
consolidation/deduplication passes, relevance ranking, embeddings.
|
|
580
|
+
- **Rendering.** How entries and the working set are surfaced into an
|
|
581
|
+
assistant's context (pinned blocks, system prompts, retrieval) is a
|
|
582
|
+
competitive surface, not a format rule.
|
|
583
|
+
|
|
584
|
+
## 8. Conformance levels
|
|
585
|
+
|
|
586
|
+
Three levels, cumulative.
|
|
587
|
+
|
|
588
|
+
- **memoir Reader.** Can parse and render a store: frontmatter + body of
|
|
589
|
+
every entry type, and the session file. MUST ignore unknown fields, MUST
|
|
590
|
+
tolerate unknown entry types (render as opaque), MUST honor tombstone
|
|
591
|
+
visibility (never display `hidden` decisions or completed actions as
|
|
592
|
+
live), and MUST apply the forward-version rule (6) on read.
|
|
593
|
+
- **memoir Writer.** Reader, plus can add and edit entries without
|
|
594
|
+
corrupting the store: atomic session writes with a locked
|
|
595
|
+
read-mutate-write cycle, preserves unknown fields on rewrite, stamps
|
|
596
|
+
identity and date fields (`added`, `set_on`, `date`, `machine_id`), and
|
|
597
|
+
never rewrites entries it was not asked to touch.
|
|
598
|
+
- **memoir Full.** Writer, plus implements section 5 exactly: union-by-text
|
|
599
|
+
with newest-wins, both tombstone classes with their stickiness rules, and
|
|
600
|
+
the merge invariants of 5.4.
|
|
601
|
+
|
|
602
|
+
A conformance claim names its level: "memoir Reader (v0.1)".
|
|
603
|
+
|
|
604
|
+
## 9. Security and privacy considerations
|
|
605
|
+
|
|
606
|
+
A memoir store aggregates exactly the information an attacker would want:
|
|
607
|
+
project internals, decision history, environment facts. Implementations
|
|
608
|
+
SHOULD scan for and warn about secrets before any entry leaves the machine,
|
|
609
|
+
and MUST NOT transmit store contents anywhere without explicit user
|
|
610
|
+
configuration. The format's plain-text nature is a privacy *feature* — the
|
|
611
|
+
user can always audit precisely what their AI knows — and implementations
|
|
612
|
+
should preserve that auditability rather than obscure it.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Appendix A: Legacy dialects (informative)
|
|
617
|
+
|
|
618
|
+
The format formalizes a convention that grew in the wild, and existing
|
|
619
|
+
stores predate it. A survey of one long-lived production store (326 entry
|
|
620
|
+
files, April–August 2026) found:
|
|
621
|
+
|
|
622
|
+
- **Two frontmatter dialects.** About half the files carry `type` as a
|
|
623
|
+
top-level key; the other half nest it as `metadata.type` (often with
|
|
624
|
+
`metadata.node_type: memory`). `originSessionId` likewise appears both
|
|
625
|
+
top-level and nested.
|
|
626
|
+
- **Bare files.** ~6% have no frontmatter at all — plain markdown with an
|
|
627
|
+
`# H1` title.
|
|
628
|
+
- **Legacy type vocabulary.** Deployed values are `user`, `feedback`,
|
|
629
|
+
`project`, `reference` — not the six types of this spec.
|
|
630
|
+
- **Body-borne lesson fields.** Legacy `feedback` entries carry the trigger
|
|
631
|
+
in `description` and the application rule as a `**How to apply:**` body
|
|
632
|
+
section, not as frontmatter keys.
|
|
633
|
+
|
|
634
|
+
Conforming Readers SHOULD apply these compatibility rules when reading
|
|
635
|
+
pre-v0.1 stores, and validators SHOULD downgrade the corresponding errors to
|
|
636
|
+
warnings for legacy-dialect files:
|
|
637
|
+
|
|
638
|
+
| Legacy | Read as |
|
|
639
|
+
|---|---|
|
|
640
|
+
| `metadata.type: X` | `type: X` |
|
|
641
|
+
| top-level `originSessionId` / `metadata.originSessionId` | `origin.session_id` |
|
|
642
|
+
| `type: user` | `preference` |
|
|
643
|
+
| `type: feedback` | `lesson` (trigger ← `description`; how_to_apply ← `**How to apply:**` body section) |
|
|
644
|
+
| `type: reference` | `fact` |
|
|
645
|
+
| `type: project` | no atomic mapping — a project *dossier* aggregates many would-be entries in one file; read as an opaque legacy entry |
|
|
646
|
+
| no frontmatter | opaque legacy entry; `name` ← first `# H1` |
|
|
647
|
+
|
|
648
|
+
Writers MUST NOT emit legacy dialects for new entries. Rewriting legacy
|
|
649
|
+
files into canonical form is an implementation choice (a consolidation
|
|
650
|
+
pass), not an obligation — a Reader must handle both indefinitely.
|
|
651
|
+
|
|
652
|
+
## Appendix B: Relationship to the reference implementation (informative)
|
|
653
|
+
|
|
654
|
+
memoir-cli (npm) implements: session state exactly as section 4 at
|
|
655
|
+
`~/.config/memoir/session.json` (schema version 1); the merge semantics of
|
|
656
|
+
section 5 in its cross-machine sync (`mergeSessions`); the forward-version
|
|
657
|
+
quarantine of section 6 (`.pre-migration-<ts>` backups, degrade to empty);
|
|
658
|
+
and `memoir validate`, a structural checker for both file kinds against this
|
|
659
|
+
spec. Entry-file writing currently passes agent-authored markdown through
|
|
660
|
+
verbatim — validation, not generation, is where the convention is enforced.
|
|
661
|
+
|
|
662
|
+
## Appendix C: Changes
|
|
663
|
+
|
|
664
|
+
- **v0.1.2 (2026-09-04)** — additive. `parked_actions`: overflow from the
|
|
665
|
+
live next-action list is parked, rendered and completable, never dropped
|
|
666
|
+
(4.1); merge pools live + parked and re-splits (5.2). `completed_goals`:
|
|
667
|
+
temporal tombstones for goals (5.3.2). `history[].session_id` for
|
|
668
|
+
in-place session rows (4.1). New invariant: unknown fields pass through a
|
|
669
|
+
merge (5.4). Shipped in memoir-cli 3.13.x. Motivation, all first-party:
|
|
670
|
+
three live next-actions silently evicted in one week by a `slice(-8)`;
|
|
671
|
+
a goal set in June still pinned in September because nothing could
|
|
672
|
+
retire it; and the new fields themselves erased by an older build's
|
|
673
|
+
merge on the day they shipped.
|
|
674
|
+
- **v0.1.1 (2026-08-18)** — additive. `aliases` common frontmatter field
|
|
675
|
+
(3.2). Purged form of the absolute decision tombstone: `[purged]` +
|
|
676
|
+
`text_hash`, hash-matched and merge-winning (5.1, 5.3.1). Both shipped in
|
|
677
|
+
memoir-cli 3.12.0 (`memoir_remember aliases:`; `memoir forget [--purge]`).
|
|
678
|
+
Motivation for the first: a real retrieval failure where a shipped surface
|
|
679
|
+
was findable only by the exact words its one memory happened to use. For
|
|
680
|
+
the second: nothing shipped could create an absolute tombstone at all
|
|
681
|
+
(thanks to the agent-memory-atlas review for stating it that plainly).
|
|
682
|
+
- **v0.1 (2026-08-06)** — first public draft. Extracted from memoir-cli
|
|
683
|
+
3.10.x behavior. Seeking implementations and critique:
|
|
684
|
+
https://github.com/camgitt/memoir/issues
|