memoir-cli 3.14.0 → 3.16.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 +10 -5
- package/docs/LOCAL-PRODUCTION-VALIDATION.md +71 -0
- package/docs/PROJECT-HANDOFF.md +50 -0
- package/docs/PROJECT-MAP-TRIAL.md +149 -0
- package/docs/PROJECT-RECOVERY.md +139 -0
- package/docs/RELIABILITY-ROLLOUT.md +6 -1
- package/package.json +1 -1
- package/src/security/files.js +14 -2
- package/src/session/lock.js +18 -6
- package/src/work/cli.js +33 -2
- package/src/work/errors.js +3 -3
- package/src/work/recovery.js +142 -0
- package/src/work/server.js +1 -1
- package/src/work/setup.js +1 -1
- package/src/work/snapshots.js +87 -0
- package/src/work/store.js +36 -16
- package/src/work/ui/app.js +220 -27
- package/src/work/ui/index.html +26 -11
- package/src/work/ui/style.css +248 -3
- package/src/work/view.js +3 -2
package/README.md
CHANGED
|
@@ -8,14 +8,14 @@ The reliability changes are described in [the remediation record](docs/AUDIT-REM
|
|
|
8
8
|
|
|
9
9
|
## Continue between Codex and Cursor on this computer
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Memoir includes a separate, project-only handoff. It carries answered questions,
|
|
12
12
|
decisions, next actions and receipts from checks actually run through Memoir. It
|
|
13
13
|
does not import personal memory or transcripts.
|
|
14
14
|
|
|
15
|
-
The project handoff
|
|
15
|
+
The project handoff, browser view and recovery commands below require version 3.16.0 or later:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm install -g memoir-cli@3.
|
|
18
|
+
npm install -g memoir-cli@3.16.0
|
|
19
19
|
cd /path/to/your/project
|
|
20
20
|
memoir work setup
|
|
21
21
|
memoir work resume
|
|
@@ -51,9 +51,14 @@ sync those files through GitHub or bridge different checkouts. See the
|
|
|
51
51
|
privacy boundaries and when a check needs to run again. The feature runs locally even when installed from npm; publishing the package
|
|
52
52
|
does not upload your project ledger.
|
|
53
53
|
|
|
54
|
+
Project handoffs now have automatic local recovery snapshots. Run `memoir work
|
|
55
|
+
doctor` to check them, or `memoir work backup --output /path/to/backup.memoir`
|
|
56
|
+
for an encrypted copy outside the project. Recovery previews changes before
|
|
57
|
+
applying them and preserves the original file. See [backup and recovery](docs/PROJECT-RECOVERY.md).
|
|
58
|
+
|
|
54
59
|
## Existing memory and backup workflow
|
|
55
60
|
|
|
56
|
-
Node.js 18 or later
|
|
61
|
+
Node.js 18 or later can run the CLI. For production use, choose a maintained LTS release (Node 22 or 24); Node 18/20 remain compatibility-test targets. See the [Node release policy](https://nodejs.org/en/about/previous-releases).
|
|
57
62
|
|
|
58
63
|
```sh
|
|
59
64
|
npm install -g memoir-cli
|
|
@@ -168,6 +173,6 @@ npm audit --omit=dev
|
|
|
168
173
|
npm pack --ignore-scripts
|
|
169
174
|
```
|
|
170
175
|
|
|
171
|
-
Tests use synthetic homes and local Git remotes. CI declares macOS/Linux/Windows with Node 18/20/22; shell suites skip Windows. See [remediation status](docs/AUDIT-REMEDIATION.md) and [release gates](docs/RELIABILITY-ROLLOUT.md) for verification limits.
|
|
176
|
+
Tests use synthetic homes and local Git remotes. CI declares macOS/Linux/Windows with Node 18/20/22/24; shell suites skip Windows. See [remediation status](docs/AUDIT-REMEDIATION.md) and [release gates](docs/RELIABILITY-ROLLOUT.md) for verification limits.
|
|
172
177
|
|
|
173
178
|
[MIT license](LICENSE)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Local handoff hardening — September 6, 2026
|
|
2
|
+
|
|
3
|
+
This change targets the single-user CLI/MCP workflow and its loopback browser
|
|
4
|
+
companion. It does not turn the companion into a public multi-user service.
|
|
5
|
+
Publication is subject to the release workflow; registry availability and CI
|
|
6
|
+
results must be checked against the actual release rather than this document.
|
|
7
|
+
|
|
8
|
+
## What changed
|
|
9
|
+
|
|
10
|
+
- Automatic bounded recovery snapshots protect every acknowledged project save.
|
|
11
|
+
Existing version-1 ledgers are checkpointed on the updated resume path.
|
|
12
|
+
- `work doctor`, `work backup` and `work recover` expose health, encrypted export
|
|
13
|
+
and preview-bound recovery. Current bytes are preserved before replacement.
|
|
14
|
+
- A recovery generation prevents stale CLI/MCP/browser sessions and in-flight
|
|
15
|
+
checks from overwriting a restored handoff. Normal revision guards remain.
|
|
16
|
+
- Publication now depends on the entire reusable CI workflow. Node 24 is added
|
|
17
|
+
to the Node 18/20/22 matrix across Linux, macOS and Windows. Node 22/24 are the
|
|
18
|
+
maintained LTS targets for production; older versions are compatibility tests.
|
|
19
|
+
|
|
20
|
+
## Observed local evidence
|
|
21
|
+
|
|
22
|
+
| Exercise | Result |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Full local suite | 25 suites passed on macOS/Node 26.7.0; subsequent focused checks cover the added crash and browser-generation cases |
|
|
25
|
+
| Recovery regression | 15 groups cover acknowledged snapshots, old-ledger upgrade, damaged/missing ledgers, exact preview conflicts, stale writers, concurrent recovery/check execution, all-branch encrypted round trips, malformed/secret-bearing imports, symlinks, failed writes, process crash, retention, CLI errors and Windows delete-pending retries |
|
|
26
|
+
| Browser harness | 31 scenarios pass, including retaining an open draft's old generation until the recovered record is explicitly reviewed |
|
|
27
|
+
| Existing regressions | 17 handoff groups, 21 adversarial scenarios and 14 browser API/security groups pass |
|
|
28
|
+
| Installed tarball | CLI, project MCP, local view, encrypted export, damaged-ledger recovery, unchanged client settings and post-recovery MCP continuation passed in a synthetic home |
|
|
29
|
+
| Dependency advisory audit | No known production-dependency advisories were returned at test time |
|
|
30
|
+
|
|
31
|
+
The actual Cursor desktop connection was reloaded using its existing **Reload**
|
|
32
|
+
control. Direct `memoir_work_resume` returned the current delivery and privacy
|
|
33
|
+
answers and identified `src/work/store.js` as changed. Cursor saved
|
|
34
|
+
`decision.cursor-production-resume` at revision 119 through MCP and read it back.
|
|
35
|
+
Returning Codex read that record. No fallback, repeated question, check rerun or
|
|
36
|
+
source edit was requested from Cursor in that bounded exercise.
|
|
37
|
+
|
|
38
|
+
In an actual in-app browser, a synthetic project answer was opened for editing.
|
|
39
|
+
The project was recovered while that draft stayed open. Saving was refused;
|
|
40
|
+
the draft remained intact. **Review latest version → Keep my draft and continue
|
|
41
|
+
→ Save correction** saved the deliberate correction. A separate CLI reader
|
|
42
|
+
recovered that answer at revision 2, and `doctor` reported a valid current copy.
|
|
43
|
+
The temporary project and browser were removed after validation.
|
|
44
|
+
|
|
45
|
+
A separate local drill restored a snapshot of the real project ledger at
|
|
46
|
+
revision 120 into a temporary folder after deliberately damaging only that
|
|
47
|
+
copy. All 64 record-history entries, 54 check receipts and two retractions
|
|
48
|
+
matched the source. The real ledger remained unchanged. No real project data
|
|
49
|
+
was uploaded, and no encrypted export passphrase was created for the user.
|
|
50
|
+
|
|
51
|
+
## What these results establish
|
|
52
|
+
|
|
53
|
+
The new failure handling was exercised in source tests, an installed artifact,
|
|
54
|
+
the actual browser, Cursor MCP and a real-data copy. Local protection is
|
|
55
|
+
enabled for this checkout, and the original app settings remain in place.
|
|
56
|
+
The release pipeline must separately establish cross-platform results and
|
|
57
|
+
validate the published npm artifact.
|
|
58
|
+
|
|
59
|
+
These tests are not an independent security certification, a power-cut test of
|
|
60
|
+
every filesystem, or proof of productivity gains over multiple days. Receipts
|
|
61
|
+
remain local and unauthenticated; project text is untrusted. Local snapshots
|
|
62
|
+
and quarantine are plaintext, and secret screening is heuristic. Losing the
|
|
63
|
+
entire folder/disk needs a separately stored export. See the
|
|
64
|
+
[recovery guide](PROJECT-RECOVERY.md) for downgrade restrictions, capacity,
|
|
65
|
+
retention, filesystem boundaries and the remaining manual lock-repair case.
|
|
66
|
+
|
|
67
|
+
The first expanded CI run caught a Windows/Node 18 delete-pending race on
|
|
68
|
+
`work.lock`. Path inspection and exclusive lock acquisition now retry transient
|
|
69
|
+
Windows access failures without skipping validation or entering without a lock.
|
|
70
|
+
A deterministic denial/concurrency regression accompanies the fix. Manual
|
|
71
|
+
publication also requires a version tag matching the package.
|
package/docs/PROJECT-HANDOFF.md
CHANGED
|
@@ -18,6 +18,52 @@ This documents the local project handoff in `src/work/` and `bin/memoir-work.js`
|
|
|
18
18
|
|
|
19
19
|
## See and correct what Memoir remembers
|
|
20
20
|
|
|
21
|
+
Memoir remains a CLI and tool integration. This optional browser companion reads
|
|
22
|
+
the same project record; it is not a separate memory service. You can keep using
|
|
23
|
+
the CLI, Codex or Cursor without opening the page.
|
|
24
|
+
|
|
25
|
+
The default **Records** view shows open actions, saved answers and check evidence.
|
|
26
|
+
**Records** and **Map** share one workspace, search box and category navigation.
|
|
27
|
+
Switching views keeps the search and category. Records use readable rows with
|
|
28
|
+
short headings and expandable text; source history stays available on each entry.
|
|
29
|
+
|
|
30
|
+
The **Map** view connects the current branch's project entries. Select a
|
|
31
|
+
node to read its full text, source and history, and inspect connected entries.
|
|
32
|
+
Solid lines show project membership, explicit record references, or a named file
|
|
33
|
+
that a check declares as input. A file link does not prove the entry's claims.
|
|
34
|
+
Suggested links are off by default. Enable **Suggested links** to include possible
|
|
35
|
+
shared topics around a selected entry; dashed lines distinguish these word
|
|
36
|
+
matches from recorded references. Each suggestion explains its words and is not
|
|
37
|
+
saved as a relationship. This prototype does not infer
|
|
38
|
+
causes, automatically determine affected work, or use personal memory.
|
|
39
|
+
|
|
40
|
+
Search and category filters narrow the map. The overview shows up to six entries
|
|
41
|
+
around the project; selecting an entry centers it and shows up to six direct
|
|
42
|
+
neighbors, with recorded references first. Lines connect only to that center.
|
|
43
|
+
It computes connections within up to 120 entries, prioritizing matches and the
|
|
44
|
+
selected entry. Search covers all active entries, including older ones, but the
|
|
45
|
+
visible map and connection list are not exhaustive. No text leaves the browser
|
|
46
|
+
to generate these connections.
|
|
47
|
+
|
|
48
|
+
**Records** provides the overview and category lists for editing. The overview
|
|
49
|
+
shows every open action as a short row; **Details** opens its explanation and
|
|
50
|
+
controls. Saved answers and recent decisions start collapsed. Matching checks
|
|
51
|
+
remain available from the summary and Checks category; only checks needing
|
|
52
|
+
review appear in the overview. Long entries in category lists expand with
|
|
53
|
+
**Read full entry**; completed actions remain under **Next actions**. Both views
|
|
54
|
+
use the same correction controls and project record. **Connections** opens a
|
|
55
|
+
record in the map; **Open in Records** returns to that entry's category. Removed
|
|
56
|
+
items use the Records recovery list and do not enter the active map.
|
|
57
|
+
|
|
58
|
+
Selecting a map entry opens its details beside the map in wide windows and below
|
|
59
|
+
it in narrow windows. Keyboard focus follows the selected context. Saving from
|
|
60
|
+
either view selects the saved entry, clears the old search and opens its category
|
|
61
|
+
so a filter cannot hide a successful save. Both views search covered file paths as
|
|
62
|
+
well as record text and sources. Overview search includes all matching records,
|
|
63
|
+
including completed actions and goals, without the overview's two-per-group limit.
|
|
64
|
+
See [the project map trial](PROJECT-MAP-TRIAL.md) for
|
|
65
|
+
tested behavior and the limits of suggested connections.
|
|
66
|
+
|
|
21
67
|
Run `memoir work view` in your project (or `node bin/memoir.js work view`
|
|
22
68
|
from a source checkout), or ask the agent
|
|
23
69
|
“Open my Memoir project view.” The browser shows the current branch's answers,
|
|
@@ -253,3 +299,7 @@ The saved `check.project-handoff` result (revision 5) still matched its declared
|
|
|
253
299
|
A later resume carried forward `decision.client-route` (revision 10), whose source reports that Cursor's project connection was enabled, showed four tools, and successfully called `memoir_work_resume` on 2026-09-06. This is saved project evidence from the local client test; the documentation review did not repeat that test or inspect external app settings.
|
|
254
300
|
|
|
255
301
|
In this Codex session, the configured MCP resume call returned “MCP tool call requires approval, but approval policy is never.” The documented CLI fallback succeeded. No policy or app setting was changed by this review. Successful CLI continuation does not establish successful Codex MCP use. Source labels are claims, not authentication, and saved client observations do not verify current external settings. Those settings and current client acceptance need verification before claiming they still work; such verification was outside this documentation-only continuation.
|
|
302
|
+
|
|
303
|
+
## Backup and recovery
|
|
304
|
+
|
|
305
|
+
Saves now include automatic local snapshots. Run `memoir work doctor` to check them. See [the recovery guide](PROJECT-RECOVERY.md) for encrypted exports, recovery previews, and the required post-recovery `expected_recovery` value.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Project map trial — 2026-09-06
|
|
2
|
+
|
|
3
|
+
The map is functional, but this trial does not establish that it is faster than
|
|
4
|
+
the Records view. It is useful for following explicit references to check
|
|
5
|
+
evidence. Direct answers are already easy to find in Records, and keyword
|
|
6
|
+
suggestions still include weak connections.
|
|
7
|
+
|
|
8
|
+
## Actual browser tasks
|
|
9
|
+
|
|
10
|
+
Tested against this checkout's real project record in the Codex in-app browser.
|
|
11
|
+
The starting ledger was revision 70, with 27 active entries. Existing answers and
|
|
12
|
+
check evidence were read before starting; no setup questions were repeated.
|
|
13
|
+
|
|
14
|
+
| Task | Map | Records | Finding |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| Find the saved privacy boundary | One node selection from the default map | Answer already present in Overview | No advantage for the map on this direct lookup |
|
|
17
|
+
| Read the complete saved publication blocker | One node selection | One **Read full entry** activation from Overview | Both expose the same saved blocker; external account state was not reverified |
|
|
18
|
+
| Find checks covering `docs/PROJECT-HANDOFF.md` | Evidence filter plus file search | Checks category plus file search | Records initially returned no result; fixed to search covered files too |
|
|
19
|
+
| Follow the documentation decision to its check | Search for the document, select the decision, follow **Recorded link** | Category/file search finds the check separately | The map exposes the reference and explains the file relationship |
|
|
20
|
+
| Decide which checks need another look | Linked receipt named changed UI files | The same receipt named the same changes | Changed inputs were visible; unchanged CLI/MCP receipts were reused |
|
|
21
|
+
|
|
22
|
+
These are observed control activations, excluding scrolling, switching views and
|
|
23
|
+
test resets. They are not timed human measurements or a randomized comparison.
|
|
24
|
+
The tester already knew the project. Repeated questions: zero in this task;
|
|
25
|
+
that observation is not proof of an improvement against an unassisted baseline.
|
|
26
|
+
|
|
27
|
+
## Problems reproduced and fixed
|
|
28
|
+
|
|
29
|
+
1. **Detail panel blocked the map and Reset.** At the actual 742px window width,
|
|
30
|
+
the fixed panel overlapped Reset; a hit test returned the panel. Below 950px,
|
|
31
|
+
details now follow the map in normal flow. Selecting a node focuses its
|
|
32
|
+
heading; Back returns focus to the project. Reset was retested successfully.
|
|
33
|
+
2. **Filtering lost keyboard focus.** After a category click, focus was on the
|
|
34
|
+
document body because the filter had been replaced. Focus now moves to the
|
|
35
|
+
replacement active filter. Verified in the real browser and regression test.
|
|
36
|
+
3. **File search disagreed between views.** Records omitted check input paths and
|
|
37
|
+
source text. Both views now use the same searchable fields. The actual
|
|
38
|
+
documentation-file search returns the same check in both views.
|
|
39
|
+
4. **A newly saved entry disappeared into the overview.** A real browser save
|
|
40
|
+
succeeded but did not select the new record; focus fell back to the body.
|
|
41
|
+
A regression test reproduced it. Map saves now select the saved entry and
|
|
42
|
+
return focus to its visible correction control. A second real creation and
|
|
43
|
+
correction verified this behavior. Long entries exposed another case: the
|
|
44
|
+
first button was a hidden expansion control. Focus now targets card actions
|
|
45
|
+
explicitly; the regression test includes a long entry.
|
|
46
|
+
5. **Confirmation text had poor contrast.** The dark map inherited light text
|
|
47
|
+
on the old pale notification background. Map success/error notices now have
|
|
48
|
+
explicit dark backgrounds and contrasting text. The success message's
|
|
49
|
+
computed text/background contrast was 10.70:1 in the actual browser.
|
|
50
|
+
|
|
51
|
+
## Persistence and boundaries
|
|
52
|
+
|
|
53
|
+
Two project-only trial observations were saved through the actual editor. The
|
|
54
|
+
result record was corrected, temporarily removed and restored through Records.
|
|
55
|
+
A separate process read agent resume after removal and confirmed the result was
|
|
56
|
+
absent. After restoration it recovered the corrected version and preserved
|
|
57
|
+
history. The original privacy and delivery answers remained at revisions 3 and
|
|
58
|
+
50. Existing project decisions, settings and private memory were not rewritten
|
|
59
|
+
for this test.
|
|
60
|
+
|
|
61
|
+
The map was checked at 390px, the normal 742px window, and 1280px. Details no
|
|
62
|
+
longer overlap the map, and no horizontal overflow was observed. Wide windows
|
|
63
|
+
retain a side panel. This is a focused layout/keyboard check, not a full
|
|
64
|
+
accessibility audit.
|
|
65
|
+
|
|
66
|
+
At this initial trial, the UI regression suite contained 21 scenarios, including map focus, saving,
|
|
67
|
+
consistent search, exact versus suggested links, older-entry search, and literal
|
|
68
|
+
rendering of synthetic hostile markup. The existing 14 API/security groups cover
|
|
69
|
+
authentication, cross-origin access, stale edits, removal/restore, secret/scope
|
|
70
|
+
rejection and unavailable execution routes. These targeted checks and the guide
|
|
71
|
+
check were captured in the local ledger after the relevant files changed.
|
|
72
|
+
Matching CLI/MCP and adversarial handoff receipts were reused. This was not a new
|
|
73
|
+
production penetration test or a fresh Cursor round trip.
|
|
74
|
+
|
|
75
|
+
## What remains to establish
|
|
76
|
+
|
|
77
|
+
- Keyword suggestions are noisy. The privacy answer was linked through generic
|
|
78
|
+
words such as “user”, “evidence” and “handoff”; the labels explain the match but
|
|
79
|
+
cannot make it useful. Evaluate suggestions against human-labeled relevant
|
|
80
|
+
pairs before treating the map as an intelligent project model.
|
|
81
|
+
- The graph computes relationships within at most 120 entries.
|
|
82
|
+
Search covers all active entries, but connections are not exhaustive.
|
|
83
|
+
- There is no saved relationship editor, automatic impact analysis, natural
|
|
84
|
+
language retrieval, or measured reduction in time/repeated work from the map.
|
|
85
|
+
- Keep both views available. Next, test unfamiliar users on real project
|
|
86
|
+
questions and count successful answers, mistaken interpretations and time.
|
|
87
|
+
|
|
88
|
+
The trial used local, uncommitted changes. No publication, server deployment,
|
|
89
|
+
new account setup or personal-memory import was needed to perform it.
|
|
90
|
+
|
|
91
|
+
## Follow-up: reduce graph clutter
|
|
92
|
+
|
|
93
|
+
The user found the graph messy after this trial. The observed overview contained
|
|
94
|
+
24 lines and 11 nodes. The revised view shows six entries around the project,
|
|
95
|
+
with six straight membership lines and no lines between the surrounding entries.
|
|
96
|
+
Selecting an entry moves it to the center and shows only its direct neighbors,
|
|
97
|
+
with at most six links. Labels have larger type and a two-line limit; full text
|
|
98
|
+
remains in the inspector. Suggested links are now off by default and explicitly
|
|
99
|
+
enabled with **Suggested links**. The graph and inspector apply the same filter.
|
|
100
|
+
|
|
101
|
+
The actual browser showed six lines and seven nodes in the new overview. A check
|
|
102
|
+
with eight recorded connections displayed six; selecting an answer with no
|
|
103
|
+
recorded references showed zero links, then six dashed links after enabling
|
|
104
|
+
suggestions. At 390px, the seven node boxes did not overlap and the page had no
|
|
105
|
+
horizontal overflow. Two new regression cases cover the overview limit and
|
|
106
|
+
direct-neighbor filtering with opt-in suggestions, bringing the UI suite to 23
|
|
107
|
+
scenarios. The underlying keyword-matching quality is unchanged by this cleanup.
|
|
108
|
+
# Shared workspace follow-up — 6 September 2026
|
|
109
|
+
|
|
110
|
+
Further feedback was that even the unified page required too much reading.
|
|
111
|
+
The overview now shows all open actions as short rows with a **Details** button.
|
|
112
|
+
Saved answers and recent decisions start collapsed; matching check receipts are
|
|
113
|
+
available through the summary and Checks category. Only checks needing review
|
|
114
|
+
appear in the overview. Existing action text was shortened and the explanation
|
|
115
|
+
moved into its rationale; previous full text remains in local history. The npm
|
|
116
|
+
action now reflects the observed password-confirmation screen in Chrome.
|
|
117
|
+
|
|
118
|
+
The user reported that Map and Records still looked incoherent. The browser now
|
|
119
|
+
opens Records first and uses one header, search, category navigation, color
|
|
120
|
+
palette and correction dialog for both views. Records have a single column of
|
|
121
|
+
readable rows. The former separate sidebar, duplicate introductions and light
|
|
122
|
+
card grid are removed.
|
|
123
|
+
|
|
124
|
+
Switching views preserves search and category. **Connections** opens the same
|
|
125
|
+
record in the map; **Open in Records** returns to its category. A successful save
|
|
126
|
+
reveals the saved entry and clears filters that could hide it. Overview search
|
|
127
|
+
now includes every matching entry, including goals and completed actions.
|
|
128
|
+
|
|
129
|
+
Observed in the actual local browser:
|
|
130
|
+
|
|
131
|
+
- Found the existing privacy answer, opened its correction dialog and cancelled
|
|
132
|
+
without changing the answer, followed Connections and returned to Records.
|
|
133
|
+
- Searched `src/work/ui/style.css` and found the same three check receipts in
|
|
134
|
+
Records and Map, including specific changed-input warnings.
|
|
135
|
+
- A search with no results stayed empty after switching views.
|
|
136
|
+
- Created the actual workspace-design decision while an empty search was active.
|
|
137
|
+
Saving cleared the search, opened Decisions and showed the saved entry.
|
|
138
|
+
- Corrected that decision from Map and confirmed keyboard focus returned to
|
|
139
|
+
the visible Correct button. Returning to Records showed the correction and
|
|
140
|
+
its earlier version. Focus restoration now scopes itself to the active view.
|
|
141
|
+
- Inspected Records at the normal window width and 390px, Map at 1280px, and the
|
|
142
|
+
shared dark editor at 1280px. At 390px the seven overview node boxes did not
|
|
143
|
+
overlap; the document had no horizontal overflow. Narrow category navigation
|
|
144
|
+
scrolls horizontally, and map details follow the graph in normal flow.
|
|
145
|
+
|
|
146
|
+
The changed UI requires targeted UI/API and documentation rechecks. Unchanged
|
|
147
|
+
CLI/MCP handoff and adversarial receipts remain reusable. This is local design
|
|
148
|
+
and functional validation; it adds no independent-user or timed utility study.
|
|
149
|
+
The npm publishing step remains separate from this local preview.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Project handoff backup and recovery
|
|
2
|
+
|
|
3
|
+
Memoir protects `.memoir/work.json` automatically. You keep using the same
|
|
4
|
+
project folder and branch in Codex and Cursor; no extra command is needed for
|
|
5
|
+
each save. The optional browser uses the same protection.
|
|
6
|
+
|
|
7
|
+
## Check that it is protected
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
memoir work doctor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The result reports ledger health, whether its current contents have a verified
|
|
14
|
+
snapshot, damaged copies and the available snapshot IDs. An unhealthy existing
|
|
15
|
+
handoff exits with status 1 so scripts can detect problems. `empty` describes a
|
|
16
|
+
project with no saved records and exits 0. This checks local data, not account
|
|
17
|
+
security, remote services or the accuracy of stored claims.
|
|
18
|
+
|
|
19
|
+
Existing handoffs are protected on the first updated `resume` or next write.
|
|
20
|
+
You can also create a copy explicitly:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
memoir work backup
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Keep a copy outside this project
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
memoir work backup --output /path/to/backups/project-handoff.memoir
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Choose an existing destination folder and a **new filename**. The interactive
|
|
33
|
+
terminal asks for a passphrase twice. Use a strong passphrase of at least 12
|
|
34
|
+
characters, kept separately in your password manager. Memoir cannot recover it.
|
|
35
|
+
An export never overwrites an existing file.
|
|
36
|
+
|
|
37
|
+
For automation, a secret manager can inject `MEMOIR_WORK_PASSPHRASE` into the
|
|
38
|
+
environment. There is no command-line password flag. Do not put that value in
|
|
39
|
+
shell history, project files, a handoff record, or a conversation with an agent.
|
|
40
|
+
The older `MEMOIR_PASSPHRASE` setting is separate and is not changed.
|
|
41
|
+
|
|
42
|
+
The encrypted file includes **all branches and complete history** of this
|
|
43
|
+
project ledger, including retracted records and check receipts. It excludes
|
|
44
|
+
personal memory, transcripts, raw command output, client settings, other files
|
|
45
|
+
inside `.memoir/`, and recovery quarantine. AES-256-GCM with the existing Memoir
|
|
46
|
+
scrypt passphrase derivation protects the export; import authenticates it before
|
|
47
|
+
parsing. Secret screening remains heuristic. Review project records before
|
|
48
|
+
deliberately sharing an export with anyone else.
|
|
49
|
+
|
|
50
|
+
Store the export on a separate backed-up device or destination you control.
|
|
51
|
+
Copies inside `.memoir/` cannot protect against loss of the entire folder or
|
|
52
|
+
disk. Git commits, GitHub pushes and the older `memoir push` command do **not**
|
|
53
|
+
transport this ledger. To move to another machine, transfer the encrypted file,
|
|
54
|
+
recover it there, open the matching branch and run `memoir work setup` there.
|
|
55
|
+
Changed files and runtime still cause targeted check warnings.
|
|
56
|
+
|
|
57
|
+
## Recover a damaged, missing or mistaken handoff
|
|
58
|
+
|
|
59
|
+
Start with a preview. It does not replace the current ledger:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
memoir work recover
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This selects the newest valid local copy. Review its revision, branch list and
|
|
66
|
+
record counts. To choose another copy, pass an ID returned by `doctor`:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
memoir work recover SNAPSHOT_ID
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Apply the exact reviewed source and fingerprint:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
memoir work recover SNAPSHOT_ID --apply --expect FINGERPRINT_FROM_PREVIEW
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For an encrypted export, use its filename in both steps:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
memoir work recover --from /path/to/backups/project-handoff.memoir
|
|
82
|
+
memoir work recover --from /path/to/backups/project-handoff.memoir --apply --expect FINGERPRINT_FROM_PREVIEW
|
|
83
|
+
memoir work resume
|
|
84
|
+
memoir work doctor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Recovery replaces the **whole project ledger**, across all branches, rather
|
|
88
|
+
than merging selected records. Review an imported backup in an empty recovery
|
|
89
|
+
folder first if you need to inspect its contents. The fingerprint binds the
|
|
90
|
+
preview to the destination folder, exact current file and source file. A
|
|
91
|
+
concurrent save, changed export, or different target requires a fresh preview.
|
|
92
|
+
Wrong keys, tampering, unsupported formats, invalid history, detected secrets
|
|
93
|
+
and unsafe paths are rejected before replacement.
|
|
94
|
+
|
|
95
|
+
The original bytes are preserved under `.memoir/work-quarantine/`, including a
|
|
96
|
+
damaged original. Quarantine is private local data and is never exported by this
|
|
97
|
+
command. Do not publish it; malformed originals might contain secrets. If local
|
|
98
|
+
quarantine cannot be written, recovery stops. A missing ledger with surviving
|
|
99
|
+
snapshots also stops normal writes instead of silently starting over.
|
|
100
|
+
|
|
101
|
+
After recovery, **resume in every tool before saving again**. The handoff
|
|
102
|
+
returns a `recovery_id`; record and retract calls must pass it as
|
|
103
|
+
`expected_recovery` in addition to the existing revision guard. The browser
|
|
104
|
+
handles this itself: stale drafts remain available, and reviewing the restored
|
|
105
|
+
version allows a deliberate correction. CLI retraction takes `--recovery ID`.
|
|
106
|
+
Checks that were still running during recovery cannot overwrite restored
|
|
107
|
+
evidence. New checks capture the current generation automatically.
|
|
108
|
+
|
|
109
|
+
## Storage, failure handling and upgrades
|
|
110
|
+
|
|
111
|
+
- Before acknowledging a save, Memoir writes verified-content snapshots of the
|
|
112
|
+
previous and proposed states, flushes file contents and atomically replaces
|
|
113
|
+
the ledger. An interrupted save can leave a proposed snapshot that was not
|
|
114
|
+
acknowledged. The recovery preview says so; it is never restored silently.
|
|
115
|
+
- Normal retention keeps the newest 20 snapshots, plus at most two protected
|
|
116
|
+
boundary copies. Each contains complete history. Pruning happens only after
|
|
117
|
+
a successful write; a cleanup problem never makes a committed write look
|
|
118
|
+
like a failed save. `doctor` reports damaged copies or excess retention.
|
|
119
|
+
- Files are created owner-readable/writable and directories owner-only on
|
|
120
|
+
POSIX. Local ledgers, snapshots and quarantine are plaintext. Directory
|
|
121
|
+
flushes run on POSIX; Windows uses flushed files and atomic replacement but
|
|
122
|
+
cannot guarantee directory power-loss persistence through Node. Filesystem,
|
|
123
|
+
hardware and same-user compromise remain outside this protection.
|
|
124
|
+
- The ledger limit remains 2 MiB. Reaching it refuses additional writes without
|
|
125
|
+
dropping history. Keep an encrypted export and start a separate project
|
|
126
|
+
handoff when the working ledger is full; automatic compaction is not provided.
|
|
127
|
+
- Upgrade all connected clients before recovery. Ordinary version-1 ledgers
|
|
128
|
+
remain readable; a recovered ledger adds a recovery generation. Older Memoir
|
|
129
|
+
versions reject that field rather than safely participating. Keep a
|
|
130
|
+
pre-upgrade export if you need a downgrade. Restart existing MCP connections
|
|
131
|
+
after upgrading so they load the current server; preserve their approvals.
|
|
132
|
+
- A busy lock does not justify deleting it while another process is running.
|
|
133
|
+
Retry once that operation ends. A leftover `.reaper` lock after a crash needs
|
|
134
|
+
local inspection with all Memoir writers stopped; this release does not
|
|
135
|
+
provide a general filesystem lock repair tool.
|
|
136
|
+
|
|
137
|
+
Recovery preserves local observations; it does not authenticate their authors
|
|
138
|
+
or certify their conclusions. Existing check-input comparisons still apply.
|
|
139
|
+
External settings need fresh verification independently of any backup.
|
|
@@ -43,7 +43,7 @@ Random vault-key wrapping, enrollment, cross-device rotation, hardware-backed st
|
|
|
43
43
|
- CI confirms the declared OS/Node matrix; a local macOS run cannot establish Windows readiness.
|
|
44
44
|
- Real Claude Code, Codex, and Cursor installations accept configuration and complete save/recall/restart.
|
|
45
45
|
- Hosted migration and account-isolation tests pass.
|
|
46
|
-
- The
|
|
46
|
+
- The npm trusted publisher for `camgitt/memoir`, workflow `publish.yml`, was verified for releases 3.14.0 and 3.15.0. Subsequent publication must pass the full CI matrix and retain provenance.
|
|
47
47
|
- Select and synchronize the release version across package.json, lockfile, and server.json. Publish only the reviewed commit/tag, then repeat the smoke check against the registry artifact.
|
|
48
48
|
|
|
49
49
|
The release gates above also apply to future deployments. The linked validation report records what was actually observed for this release. No independent security certification is implied.
|
|
@@ -55,3 +55,8 @@ Local locks fail when busy. A crash while reaping can leave a `.reaper` file req
|
|
|
55
55
|
Cloud merges retained states, increasing download cost. It cannot recover versions already removed by retention. Strong concurrent-cloud consistency needs a server-side conditional commit/merge protocol and race-tested retention.
|
|
56
56
|
|
|
57
57
|
Legacy scope migration, renamed projects, unusual remote aliases, and profile isolation remain work. Concurrent hostile parent-directory replacement requires further filesystem review: current validation/no-follow opens are not an openat-based sandbox.
|
|
58
|
+
|
|
59
|
+
## Project handoff recovery
|
|
60
|
+
|
|
61
|
+
The project-only ledger now has automatic local snapshots and encrypted export/recovery.
|
|
62
|
+
These are separate from legacy/cloud backup storage. See [the project recovery guide](PROJECT-RECOVERY.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/security/files.js
CHANGED
|
@@ -7,6 +7,18 @@ export const MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
|
7
7
|
export const MAX_SNAPSHOT_BYTES = 256 * 1024 * 1024;
|
|
8
8
|
export const MAX_SNAPSHOT_FILES = 50_000;
|
|
9
9
|
|
|
10
|
+
// Windows can briefly report access denied while another process deletes a
|
|
11
|
+
// file (notably the project lock). Retry the inspection, never skip it.
|
|
12
|
+
export async function inspectFile(file) {
|
|
13
|
+
for (let attempt = 0; ; attempt++) {
|
|
14
|
+
try { return await fs.lstat(file); }
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code) || attempt >= 5) throw error;
|
|
17
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
// Validate both Windows and POSIX paths regardless of the restoring OS.
|
|
11
23
|
export function relativeFile(value) {
|
|
12
24
|
if (typeof value !== 'string' || !value || /[\x00-\x1f:]/.test(value) || path.posix.isAbsolute(value) || path.win32.isAbsolute(value)) throw new Error('Invalid relative memory path');
|
|
@@ -31,12 +43,12 @@ export async function safePath(root, relative, { createParents = false } = {}) {
|
|
|
31
43
|
for (let i = 0; i < parts.length; i++) {
|
|
32
44
|
current = path.join(current, parts[i]);
|
|
33
45
|
let st;
|
|
34
|
-
try { st = await
|
|
46
|
+
try { st = await inspectFile(current); }
|
|
35
47
|
catch (err) {
|
|
36
48
|
if (err.code !== 'ENOENT') throw err;
|
|
37
49
|
if (createParents && i < parts.length - 1) {
|
|
38
50
|
await fs.mkdir(current, { mode: 0o700 }).catch(err => { if (err.code !== 'EEXIST') throw err; });
|
|
39
|
-
st = await
|
|
51
|
+
st = await inspectFile(current);
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
if (st?.isSymbolicLink()) throw new Error('Symlinks are not allowed in memory paths');
|
package/src/session/lock.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
+
import { inspectFile } from '../security/files.js';
|
|
5
|
+
|
|
6
|
+
const windowsBusy = error => process.platform === 'win32' && ['EPERM', 'EACCES'].includes(error.code);
|
|
4
7
|
|
|
5
8
|
function alive(pid) {
|
|
6
9
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -19,12 +22,12 @@ export async function withSessionLock(lockPath, fn, { maxWaitMs = 5000, staleMs
|
|
|
19
22
|
fs.writeSync(fd, String(process.pid));
|
|
20
23
|
} catch (err) {
|
|
21
24
|
if (fd !== undefined) { fs.closeSync(fd); await fs.remove(lockPath); throw err; }
|
|
22
|
-
if (err.code !== 'EEXIST') throw err;
|
|
25
|
+
if (err.code !== 'EEXIST' && !windowsBusy(err)) throw err;
|
|
23
26
|
let reaper;
|
|
24
27
|
const reaperPath = lockPath + '.reaper';
|
|
25
28
|
try {
|
|
26
29
|
reaper = await fs.open(reaperPath, 'wx', 0o600);
|
|
27
|
-
const st = await
|
|
30
|
+
const st = await inspectFile(lockPath);
|
|
28
31
|
if (st.isSymbolicLink()) throw new Error('Session lock must not be a symlink');
|
|
29
32
|
const owner = Number((await fs.readFile(lockPath, 'utf8')).trim());
|
|
30
33
|
if (Date.now() - st.mtimeMs > staleMs && !alive(owner)) {
|
|
@@ -32,7 +35,7 @@ export async function withSessionLock(lockPath, fn, { maxWaitMs = 5000, staleMs
|
|
|
32
35
|
await fs.rename(lockPath, abandoned);
|
|
33
36
|
await fs.remove(abandoned);
|
|
34
37
|
}
|
|
35
|
-
} catch (err) { if (!['ENOENT', 'EEXIST'].includes(err.code)) throw err; }
|
|
38
|
+
} catch (err) { if (!['ENOENT', 'EEXIST'].includes(err.code) && !windowsBusy(err)) throw err; }
|
|
36
39
|
finally {
|
|
37
40
|
if (reaper !== undefined) {
|
|
38
41
|
await fs.close(reaper);
|
|
@@ -49,9 +52,18 @@ export async function withSessionLock(lockPath, fn, { maxWaitMs = 5000, staleMs
|
|
|
49
52
|
}
|
|
50
53
|
try { return await fn(); }
|
|
51
54
|
finally {
|
|
52
|
-
let ours = false;
|
|
53
|
-
try {
|
|
55
|
+
let ours = false, owned;
|
|
56
|
+
try { owned = fs.fstatSync(fd); const current = await inspectFile(lockPath); ours = owned.ino === current.ino && owned.dev === current.dev; } catch {}
|
|
54
57
|
fs.closeSync(fd);
|
|
55
|
-
if (ours)
|
|
58
|
+
if (ours) for (let attempt = 0; ; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
if (attempt) { const current = await inspectFile(lockPath); if (owned.ino !== current.ino || owned.dev !== current.dev) break; }
|
|
61
|
+
await fs.unlink(lockPath); break;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!windowsBusy(error) || attempt >= 5) break;
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
}
|
|
57
69
|
}
|
package/src/work/cli.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { recordWork, runWorkCheck, retractWork, refreshWork, formatWork } from './store.js';
|
|
3
3
|
import { setupWork } from './setup.js';
|
|
4
|
+
import { backupWork, doctorWork, recoverWork } from './recovery.js';
|
|
4
5
|
import { readSafeFile } from '../security/files.js';
|
|
5
6
|
|
|
7
|
+
async function recoveryPassphrase(confirm = false) {
|
|
8
|
+
if (process.env.MEMOIR_WORK_PASSPHRASE) return process.env.MEMOIR_WORK_PASSPHRASE;
|
|
9
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('Set MEMOIR_WORK_PASSPHRASE through your secret manager, or run this command in an interactive terminal. Never put the passphrase in a command argument or project record.');
|
|
10
|
+
const { default: inquirer } = await import('inquirer');
|
|
11
|
+
const questions = [{ type: 'password', name: 'passphrase', message: 'Recovery passphrase (at least 12 characters):', mask: '*' }];
|
|
12
|
+
if (confirm) questions.push({ type: 'password', name: 'confirmation', message: 'Repeat recovery passphrase:', mask: '*' });
|
|
13
|
+
const answer = await inquirer.prompt(questions);
|
|
14
|
+
if (confirm && answer.confirmation !== answer.passphrase) throw new Error('Passphrases did not match. Nothing was exported.');
|
|
15
|
+
return answer.passphrase;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export async function workCli(argv) {
|
|
7
19
|
const program = new Command('memoir work').description('Local project continuity for Codex and Cursor')
|
|
8
20
|
.option('--project <path>', 'Project directory', process.env.MEMOIR_PROJECT_ROOT || process.cwd());
|
|
@@ -14,6 +26,25 @@ export async function workCli(argv) {
|
|
|
14
26
|
const view = await refreshWork(project());
|
|
15
27
|
console.log(options.json ? JSON.stringify(view, null, 2) : formatWork(view));
|
|
16
28
|
});
|
|
29
|
+
program.command('doctor').description('Check the project handoff and recovery snapshots').action(async () => {
|
|
30
|
+
const result = await doctorWork(project());
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
if (!result.healthy && result.state !== 'empty') process.exitCode = 1;
|
|
33
|
+
});
|
|
34
|
+
program.command('backup').description('Save a local snapshot, or export an encrypted project handoff')
|
|
35
|
+
.option('--output <path>', 'New encrypted backup file; never overwrites an existing file').action(async options => {
|
|
36
|
+
const passphrase = options.output ? await recoveryPassphrase(true) : undefined;
|
|
37
|
+
console.log(JSON.stringify(await backupWork(project(), { output: options.output, passphrase }), null, 2));
|
|
38
|
+
});
|
|
39
|
+
program.command('recover [snapshot]').description('Preview recovery first; apply only the reviewed fingerprint')
|
|
40
|
+
.option('--from <path>', 'Encrypted project handoff export')
|
|
41
|
+
.option('--apply', 'Apply the reviewed recovery and preserve the original')
|
|
42
|
+
.option('--expect <fingerprint>', 'Fingerprint returned by the recovery preview').action(async (snapshot, options) => {
|
|
43
|
+
const passphrase = options.from ? await recoveryPassphrase() : undefined;
|
|
44
|
+
const result = await recoverWork(project(), { ...options, snapshot, passphrase });
|
|
45
|
+
if (result.applied) await refreshWork(project());
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
});
|
|
17
48
|
program.command('view').description('Review and correct project memory in a local browser')
|
|
18
49
|
.option('--no-open', 'Print the local link without opening a browser').option('--port <number>', 'Local port; 0 chooses an available port', '0').action(async options => {
|
|
19
50
|
const { startWorkView } = await import('./view.js');
|
|
@@ -54,8 +85,8 @@ export async function workCli(argv) {
|
|
|
54
85
|
console.log(JSON.stringify(result, null, 2));
|
|
55
86
|
if (result.exit_code !== 0 || result.timed_out || !result.inputs_stable) process.exitCode = 1;
|
|
56
87
|
});
|
|
57
|
-
program.command('retract <id>').requiredOption('--revision <number>', 'Current record revision').option('--category <name>', 'record or check', 'record').action(async (id, options) => {
|
|
58
|
-
const result = await retractWork(project(), { id, expected_revision: Number(options.revision), category: options.category });
|
|
88
|
+
program.command('retract <id>').requiredOption('--revision <number>', 'Current record revision').option('--category <name>', 'record or check', 'record').option('--recovery <id>', 'Recovery generation returned by resume').action(async (id, options) => {
|
|
89
|
+
const result = await retractWork(project(), { id, expected_revision: Number(options.revision), category: options.category, expected_recovery: options.recovery });
|
|
59
90
|
await refreshWork(project());
|
|
60
91
|
console.log(JSON.stringify(result));
|
|
61
92
|
});
|