From 3736bf1d960d75e56fe999e742c1d5790f62a6b3 Mon Sep 17 00:00:00 2001 From: Bruno Guidolim Date: Thu, 29 Oct 2026 14:55:33 +0100 Subject: [PATCH] Recover from rebases the Stop hook leaves behind - Roll back a pull that fails mid-rebase without a conflict, and name `mcs sync` when the commit-graph caused it - Turn off core.commitGraph per clone on every sync; doctor warns when it's on or auto-push is stuck - /resolve-memories clears an unfinishable rebase with --quit, keeping edits made since it started --- README.md | 8 +-- commands/resolve-memories.md | 2 + runtime/lib/git.mts | 5 ++ runtime/lib/push.mts | 24 +++++++-- scripts/configure-memories.ts | 19 +++++-- scripts/doctor-memories.ts | 26 +++++++-- tests/configure.test.ts | 21 +++++++- tests/differential.test.ts | 53 ++++++++++++++++++- ...no-memories-tree-exits-2-without-done.json | 2 +- ...-symlink-with-no-checkout-is-replaced.json | 2 +- ...d-md-file-migrates-but-is-not-counted.json | 2 +- ...n-the-migration-backup-is-left-behind.json | 2 +- ...ry-beside-a-live-checkout-is-relinked.json | 2 +- ...e-project-path-is-not-normalised-away.json | 2 +- ...deleted-files-straddle-the-reason-cap.json | 2 +- ...an-already-linked-setup-is-left-alone.json | 2 +- ...named-migrated-file-is-left-untracked.json | 2 +- ...kdown-file-is-not-counted-as-a-memory.json | 2 +- ...emories-directory-migrates-and-pushes.json | 2 +- ...reviously-deleted-memory-is-held-back.json | 2 +- ...n-empty-memories-directory-is-removed.json | 2 +- ...-shared-copy-wins-a-filename-conflict.json | 2 +- ...-checkout-with-no-symlink-is-relinked.json | 2 +- .../case-5-fresh-setup-clones-and-links.json | 2 +- ...that-already-has-it-off-is-left-alone.json | 6 +++ ...tor-a-checkout-with-no-upstream-warns.json | 6 +++ .../doctor-a-commit-graph-left-on-warns.json | 6 +++ ...or-a-file-the-guardrail-rejects-warns.json | 6 +++ ...octor-a-rebase-left-in-progress-warns.json | 6 +++ ...ure-names-mcs-sync-instead-of-a-retry.json | 6 +++ ...e-abort-also-fails-pauses-for-resolve.json | 6 +++ ...ase-without-a-conflict-is-rolled-back.json | 6 +++ ...dangling-symlink-is-skipped-not-fatal.json | 2 +- ...tion-commit-is-not-reported-as-a-push.json | 2 +- ...eady-has-the-union-rule-is-left-alone.json | 2 +- ...-is-merged-and-left-for-the-next-stop.json | 2 +- ...-file-is-appended-onto-the-shared-one.json | 2 +- ...oved-in-when-the-shared-repo-has-none.json | 2 +- ...nion-rule-beside-its-other-attributes.json | 2 +- tests/harness.ts | 1 + tests/scripts.test.ts | 26 ++++++++- 41 files changed, 240 insertions(+), 39 deletions(-) create mode 100644 tests/golden/commit-graph-a-clone-that-already-has-it-off-is-left-alone.json create mode 100644 tests/golden/doctor-a-checkout-with-no-upstream-warns.json create mode 100644 tests/golden/doctor-a-commit-graph-left-on-warns.json create mode 100644 tests/golden/doctor-a-file-the-guardrail-rejects-warns.json create mode 100644 tests/golden/doctor-a-rebase-left-in-progress-warns.json create mode 100644 tests/golden/edge-a-commit-graph-failure-names-mcs-sync-instead-of-a-retry.json create mode 100644 tests/golden/edge-a-mid-rebase-failure-whose-abort-also-fails-pauses-for-resolve.json create mode 100644 tests/golden/edge-a-pull-that-fails-mid-rebase-without-a-conflict-is-rolled-back.json diff --git a/README.md b/README.md index 7d7b2d1..dae88e2 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Each runs as `node --experimental-strip-types --disable-warning=ExperimentalWarn | Check | What It Verifies | |-------|-----------------| -| **Shared memories setup** | Sparse checkout exists and the symlink resolves to a live git repo; warns when the usage file's `merge=union` rule is missing (auto-fixable via `mcs sync`) | +| **Shared memories setup** | Sparse checkout exists and the symlink resolves to a live git repo; warns when the usage file's `merge=union` rule is missing or `core.commitGraph` is on (both auto-fixable via `mcs sync`), and when auto-push is stuck on a rebase or merge in progress, a missing upstream, or a misnamed file (fix with `/resolve-memories`) | | **Shared memories remote access** | Auth + network reachability via `git ls-remote origin`; warns (doesn't fail) on issues since local reads still work | ### Dependencies @@ -319,13 +319,13 @@ git -C .claude/.memories-repo log --diff-filter=D --oneline -- memories/ git -C .claude/.memories-repo checkout ^ -- memories/ # restore it; the next Stop pushes it ``` -The Stop hook retries on every turn, so a network blip or a teammate pushing first sorts itself out. A few things never clear up by retrying. SessionStart and the Stop hook name them when they happen, and `/resolve-memories` handles the ones Claude can fix: +The Stop hook retries on every turn, so a network blip or a teammate pushing first sorts itself out. A pull that fails partway through its rebase without a conflict is rolled back on the spot, so it never leaves the checkout stuck. A few things never clear up by retrying. SessionStart and the Stop hook name them when they happen, and `/resolve-memories` handles the ones Claude can fix: | Blocker | What `/resolve-memories` does | |---------|-------------------------------| | **Rebase conflict** — you and a teammate edited the same memory | Merges both versions so neither side's facts are lost, and shows you the result. Asks you when one side deleted the file and the other edited it | | **Unconventional filename** — the guardrail halts every push | Proposes a `learning_…` / `decision_…` name and renames the file. Asks you when the type is unclear | -| **Rebase left in progress** — the hooks pause rather than commit conflict markers | Finishes it, or aborts it if the conflict is outside `memories/` | +| **Rebase left in progress** — the hooks pause rather than commit conflict markers | Finishes it, or aborts it if the conflict is outside `memories/`. A rebase that can't be finished is cleared with `--quit`, which keeps edits made since it started | | **No upstream** — commits stay local without a word | Sets the upstream to `origin/` | | **Auth** (SSH key not loaded, expired token) | Diagnoses only: run `ssh-add`, refresh credentials, check with `mcs doctor` | | **No git identity** — the commit itself fails | Diagnoses only: set `git config --global user.name` / `user.email` | @@ -368,6 +368,8 @@ Anything not matching `memories/(learning|decision)_*.md` needs renaming — or **If SessionStart warns about lingering state**, the previous push hit one of the [blockers](#-conflicts-and-stuck-pushes). A network or auth issue clears on the next Stop once it's fixed (`mcs doctor` checks remote access); a rebase conflict or a misnamed file needs `/resolve-memories`. +**If pulls fail with "in the commit graph file but not in the object database"**, the blobless checkout can't lazy-fetch with git's commit-graph on. `mcs sync` turns it off for the checkout (`core.commitGraph=false`), and `mcs doctor` warns when it's still on. + --- ## 🧪 Development diff --git a/commands/resolve-memories.md b/commands/resolve-memories.md index 19ce332..d84f3e2 100644 --- a/commands/resolve-memories.md +++ b/commands/resolve-memories.md @@ -25,6 +25,8 @@ If nothing is dirty, nothing is unpushed, an upstream exists and no rebase or me Go straight to step 6 and finish it. Don't commit anything new first. For a merge (`MERGE_HEAD`), resolve the conflicts the same way, then finish with `git -C .claude/.memories-repo commit --no-edit`. +If the rebase can't be finished (its todo file is missing, or `rebase --continue` fails with no conflicted file), or memories were edited after it started, clear it with `git -C .claude/.memories-repo rebase --quit`. Never `--abort` or reset: both overwrite the working tree, and edits made after the rebase started are in no stash. Check that `git -C .claude/.memories-repo symbolic-ref -q HEAD` names the branch again, stop and tell the user if it doesn't, then continue at step 5. + ## 3. Filename guardrail Every dirty file under `memories/` must match `^memories/(learning|decision)_[a-zA-Z0-9_-]+\.md$`, the pattern the Stop hook enforces, defined once in `runtime/lib/naming.mts`. For each offender: diff --git a/runtime/lib/git.mts b/runtime/lib/git.mts index df83c57..815e455 100644 --- a/runtime/lib/git.mts +++ b/runtime/lib/git.mts @@ -103,6 +103,11 @@ export function hasUnionRule(dir: string): boolean { return top !== "" && gitOut(top, ["check-attr", "merge", "--", USAGE_FILE]).endsWith(": merge: union"); } +/** Whether the clone has the commit-graph turned off, which configure sets on every sync. */ +export function commitGraphOff(dir: string): boolean { + return gitOut(dir, ["config", "--type=bool", "--get", "core.commitGraph"]) === "false"; +} + /** Whether the binary exists, the question `command -v git` asked. */ export function gitPresent(): boolean { const r = spawnSync("git", ["--version"], { stdio: "ignore" }); diff --git a/runtime/lib/push.mts b/runtime/lib/push.mts index f34a8aa..f38702b 100644 --- a/runtime/lib/push.mts +++ b/runtime/lib/push.mts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { git, gitLines } from "./git.mts"; +import { git, gitLines, operationInProgress } from "./git.mts"; import { detail, say } from "./hook-io.mts"; import { RESOLVE_COMMAND, USAGE_FILE } from "./naming.mts"; @@ -56,13 +56,26 @@ function finishUsageOnlyRebase(repo: string): string | null { function abortRebase(repo: string, why: string): void { const abort = git(repo, ["rebase", "--abort"]); if (!abort.ok) { - say(`Shared memories: rebase conflict AND --abort failed — repo may be in a half-rebased state. Run ${RESOLVE_COMMAND} to finish it.`); + say( + `Shared memories: the rebase failed AND --abort failed — repo may be in a half-rebased state. Run ${RESOLVE_COMMAND} to finish it.`, + ); detail(abort.stderr.replace(/\n$/, "")); } else { say(why); } } +// `err` must be git's LC_ALL=C stderr. +function pullFailure(err: string, midRebase: boolean): string { + // Retrying never clears this one, and every Stop would hit it again until the setting is fixed. + if (/commit.graph/i.test(err)) { + return "Shared memories: pull --rebase failed on the checkout's commit-graph. Run `mcs sync` to turn it off; until then every push fails the same way."; + } + return midRebase + ? "Shared memories: pull --rebase failed partway and was rolled back. Will retry on next Stop." + : "Shared memories: pull --rebase failed (likely auth or network). Will retry on next Stop."; +} + export function syncToRemote(repo: string, attemptsRaw: string | undefined): void { const attempts = pushAttempts(attemptsRaw); for (let attempt = 1; ; attempt++) { @@ -71,7 +84,12 @@ export function syncToRemote(repo: string, attemptsRaw: string | undefined): voi if (!pull.ok) { const err = pull.stderr.replace(/\n$/, ""); if (!/conflict/i.test(err)) { - say("Shared memories: pull --rebase failed (likely auth or network). Will retry on next Stop."); + // A failure mid-pick (a missing object, say) leaves the rebase behind, and that pauses every later Stop. + // Aborting now is safe: everything is committed or in the autostash. + const midRebase = operationInProgress(repo) === "rebase"; + const message = pullFailure(err, midRebase); + if (midRebase) abortRebase(repo, message); + else say(message); detail(err); return; } diff --git a/scripts/configure-memories.ts b/scripts/configure-memories.ts index 8c5edd0..2669842 100755 --- a/scripts/configure-memories.ts +++ b/scripts/configure-memories.ts @@ -15,7 +15,7 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { changeLine } from "../runtime/lib/commit-message.mts"; -import { authorName, git, gitLines, gitOut, gitPresent, hasUnionRule, isWorkTree } from "../runtime/lib/git.mts"; +import { authorName, commitGraphOff, git, gitLines, gitOut, gitPresent, hasUnionRule, isWorkTree } from "../runtime/lib/git.mts"; import { ALLOWED_PATTERN, isAllowedPath, RESOLVE_COMMAND, USAGE_ATTRIBUTE, USAGE_FILE } from "../runtime/lib/naming.mts"; const out = (line: string) => process.stdout.write(`${line}\n`); @@ -113,6 +113,19 @@ function ensureUnionAttribute(checkout: string): void { out(`Added the merge=union rule for ${USAGE_FILE} to ${path}.`); } +// In a blobless clone the commit-graph can make git refuse to lazy-fetch objects +// it needs mid-rebase, leaving the Stop hook's rebase half-applied. +function ensureCommitGraphOff(checkout: string): void { + if (commitGraphOff(checkout)) return; + if (git(checkout, ["config", "--local", "core.commitGraph", "false"]).ok) out("Turned off core.commitGraph for the memories checkout."); +} + +// Every sync, both paths: an already-linked clone predates whatever setting was added last. +function configureClone(checkout: string): void { + ensureUnionAttribute(checkout); + ensureCommitGraphOff(checkout); +} + function stamp(): string { const d = new Date(); const p = (n: number) => String(n).padStart(2, "0"); @@ -135,7 +148,7 @@ function main(): void { // Case 1: healthy. if (isSymlink(link) && isWorkTree(link)) { - ensureUnionAttribute(link); + configureClone(link); out("Shared memories already linked — leaving as-is."); process.exit(0); } @@ -209,7 +222,7 @@ function main(): void { mkdirSync(join(repoDir, "memories"), { recursive: true }); linkMemories(); } - ensureUnionAttribute(repoDir); + configureClone(repoDir); let imported = 0; let skipped = 0; diff --git a/scripts/doctor-memories.ts b/scripts/doctor-memories.ts index 894e0e1..dba5416 100755 --- a/scripts/doctor-memories.ts +++ b/scripts/doctor-memories.ts @@ -1,8 +1,9 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning import { existsSync, lstatSync } from "node:fs"; import { join } from "node:path"; -import { hasUnionRule, isWorkTree } from "../runtime/lib/git.mts"; -import { USAGE_FILE } from "../runtime/lib/naming.mts"; +import { commitGraphOff, hasUnionRule, hasUpstream, isWorkTree, operationInProgress } from "../runtime/lib/git.mts"; +import { RESOLVE_COMMAND, USAGE_FILE } from "../runtime/lib/naming.mts"; +import { badNames } from "../runtime/lib/pending.mts"; // mcs shellScript doctor contract: 0 pass, 1 fail, 2 warn, 3 skip. const project = process.env.MCS_PROJECT_PATH || process.cwd(); @@ -29,9 +30,28 @@ if (!isSymlink(link)) { // A warning only: without the rule the Stop hook still merges usage conflicts itself. const warnings: string[] = []; -if (existsSync(join(repo, ".git")) && !hasUnionRule(repo)) { +const checkout = isWorkTree(repo); +if (checkout && !hasUnionRule(repo)) { warnings.push(`no merge=union rule for ${USAGE_FILE} in the checkout (run 'mcs sync' to add it)`); } +if (checkout && !commitGraphOff(repo)) { + warnings.push("core.commitGraph is not off in the checkout (run 'mcs sync' to set it)"); +} + +// Stuck states that pause auto-push. Warnings, since reads still work and mcs sync can't fix them. +// A rebase detaches HEAD, which would also read as a missing upstream. +if (checkout) { + const op = operationInProgress(repo); + if (op !== null) { + warnings.push(`a ${op} is in progress in the checkout, so auto-push is paused (run ${RESOLVE_COMMAND})`); + } else if (!hasUpstream(repo)) { + warnings.push(`the checkout has no upstream branch, so memories never leave this machine (run ${RESOLVE_COMMAND})`); + } + const bad = badNames(repo); + if (bad.length > 0) { + warnings.push(`${bad.length} file(s) the naming guardrail rejects block every push: ${bad.join(", ")} (run ${RESOLVE_COMMAND})`); + } +} if (problems.length + warnings.length > 0) { process.stdout.write(`Shared memories setup ${problems.length > 0 ? "is unhealthy" : "needs attention"}:\n`); diff --git a/tests/configure.test.ts b/tests/configure.test.ts index 1b2e195..a795f79 100644 --- a/tests/configure.test.ts +++ b/tests/configure.test.ts @@ -50,7 +50,10 @@ const fixtures: readonly ConfigureFixture[] = [ name: "case 5: fresh setup clones and links", expectCode: 0, expect: /Cloning shared memories \(branch 'main', sparse\)[\s\S]*Done\. 1 memory file\(s\) available/, - inspect: claude => assert.equal(attributes(claude), "memories/.usage.tsv merge=union\n"), + inspect: claude => { + assert.equal(attributes(claude), "memories/.usage.tsv merge=union\n"); + assert.equal(commitGraph(claude), "false"); + }, }, { name: "case 1: an already-linked setup is left alone", @@ -61,6 +64,7 @@ const fixtures: readonly ConfigureFixture[] = [ git(claude, "clone", "-q", join(project, "..", "remote.git"), join(claude, ".memories-repo")); symlinkSync(".memories-repo/memories", join(claude, "memories")); }, + inspect: claude => assert.equal(commitGraph(claude), "false"), }, { name: "case 2: an empty memories directory is removed", @@ -366,3 +370,18 @@ const usage: readonly ConfigureFixture[] = [ describe("the usage file — behaviour is pinned", () => { for (const fx of usage) test(fx.name, () => assertParity(fx)); }); + +const commitGraph = (claude: string): string => git(join(claude, ".memories-repo"), "config", "--get", "core.commitGraph"); + +describe("the commit-graph setting — behaviour is pinned", () => { + const fx: ConfigureFixture = { + name: "commit-graph: a clone that already has it off is left alone", + expectCode: 0, + pre: (project, claude) => { + linkedCheckout(project, claude); + git(join(claude, ".memories-repo"), "config", "core.commitGraph", "false"); + }, + inspect: claude => assert.equal(commitGraph(claude), "false"), + }; + test(fx.name, () => assertParity(fx)); +}); diff --git a/tests/differential.test.ts b/tests/differential.test.ts index 25adb68..8b6c19a 100644 --- a/tests/differential.test.ts +++ b/tests/differential.test.ts @@ -1,9 +1,10 @@ import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { chmodSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { describe, test } from "node:test"; +import { operationInProgress } from "../runtime/lib/git.mts"; import { commit, dropUnionRule, @@ -355,6 +356,13 @@ const rebaseLeftInProgress = (repo: string, project: string) => { memory(repo, "learning_new_thing.md", "Written while the rebase was stuck.\n"); }; +/** Runs `cmd` once the rebase has started, so the pick fails with no conflict to report. */ +const failMidRebase = (repo: string, cmd: string) => { + const hook = join(repo, ".git", "hooks", "post-checkout"); + writeFileSync(hook, `#!/bin/sh\n[ -e .git/rebase-merge ] && ${cmd}\nexit 0\n`); + chmodSync(hook, 0o755); +}; + const edges: readonly Fixture[] = [ { name: "edge: a locked index aborts the turn instead of reporting nothing", @@ -457,6 +465,47 @@ const edges: readonly Fixture[] = [ hook: "pull", setup: rebaseLeftInProgress, }, + { + name: "edge: a pull that fails mid-rebase without a conflict is rolled back", + expect: /^Shared memories: pull --rebase failed partway and was rolled back\. Will retry on next Stop\.$/m, + hook: "autopush", + setup: (repo, project) => { + teammatePushes(project, "learning_theirs_new.md"); + memory(repo, "learning_ours_own.md", "Ours.\n"); + // Picking our commit has to write a new tree; the abort only reads. + failMidRebase(repo, "chmod -R a-w .git/objects"); + }, + inspect: repo => { + execFileSync("chmod", ["-R", "u+w", join(repo, ".git", "objects")]); + assert.equal(operationInProgress(repo), null, "the rebase was left behind"); + }, + }, + { + name: "edge: a commit-graph failure names mcs sync instead of a retry", + expect: /commit-graph\. Run `mcs sync` to turn it off/, + hook: "autopush", + setup: (repo, project) => { + teammatePushes(project, "learning_theirs_new.md"); + memory(repo, "learning_ours_own.md", "Ours.\n"); + // git's wording from a blobless clone on 2.50; no current git reproduces the failure itself. + failMidRebase( + repo, + "echo 'fatal: commit 0123abc exists in commit-graph but not in the object database' >&2; chmod -R a-w .git/objects", + ); + }, + inspect: repo => execFileSync("chmod", ["-R", "u+w", join(repo, ".git", "objects")]), + }, + { + name: "edge: a mid-rebase failure whose abort also fails pauses for resolve", + expect: /^Shared memories: the rebase failed AND --abort failed .* Run \/resolve-memories to finish it\.$/m, + hook: "autopush", + setup: (repo, project) => { + teammatePushes(project, "learning_theirs_new.md"); + memory(repo, "learning_ours_own.md", "Ours.\n"); + // An untracked file where our commit adds one blocks both the pick and the reset back. + failMidRebase(repo, "echo stray > memories/learning_ours_own.md"); + }, + }, ]; describe("audited edges — behaviour is pinned", () => { diff --git a/tests/golden/audited-a-bootstrap-branch-with-no-memories-tree-exits-2-without-done.json b/tests/golden/audited-a-bootstrap-branch-with-no-memories-tree-exits-2-without-done.json index 13fea35..50f605f 100644 --- a/tests/golden/audited-a-bootstrap-branch-with-no-memories-tree-exits-2-without-done.json +++ b/tests/golden/audited-a-bootstrap-branch-with-no-memories-tree-exits-2-without-done.json @@ -1,5 +1,5 @@ { - "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\n", + "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 2, "git": ".memories-repo memories" diff --git a/tests/golden/audited-a-dangling-symlink-with-no-checkout-is-replaced.json b/tests/golden/audited-a-dangling-symlink-with-no-checkout-is-replaced.json index 68d8fa5..98aac5a 100644 --- a/tests/golden/audited-a-dangling-symlink-with-no-checkout-is-replaced.json +++ b/tests/golden/audited-a-dangling-symlink-with-no-checkout-is-replaced.json @@ -1,5 +1,5 @@ { - "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/audited-a-dotted-md-file-migrates-but-is-not-counted.json b/tests/golden/audited-a-dotted-md-file-migrates-but-is-not-counted.json index 9a6760d..e712062 100644 --- a/tests/golden/audited-a-dotted-md-file-migrates-but-is-not-counted.json +++ b/tests/golden/audited-a-dotted-md-file-migrates-but-is-not-counted.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 3 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 3 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/.hidden\n - memories/.hidden.md\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 3 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 3 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/.hidden\n - memories/.hidden.md\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\n.hidden .hidden.md learning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/audited-a-subdirectory-in-the-migration-backup-is-left-behind.json b/tests/golden/audited-a-subdirectory-in-the-migration-backup-is-left-behind.json index f6f5548..3eb86ff 100644 --- a/tests/golden/audited-a-subdirectory-in-the-migration-backup-is-left-behind.json +++ b/tests/golden/audited-a-subdirectory-in-the-migration-backup-is-left-behind.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-migration- .memories-repo memories\nlearning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/audited-a-symlink-to-a-non-git-directory-beside-a-live-checkout-is-relinked.json b/tests/golden/audited-a-symlink-to-a-non-git-directory-beside-a-live-checkout-is-relinked.json index fa293aa..bfbe58d 100644 --- a/tests/golden/audited-a-symlink-to-a-non-git-directory-beside-a-live-checkout-is-relinked.json +++ b/tests/golden/audited-a-symlink-to-a-non-git-directory-beside-a-live-checkout-is-relinked.json @@ -1,5 +1,5 @@ { - "stdout": "Memories checkout present, (re)linking /work/project/.claude/memories → .memories-repo/memories...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Memories checkout present, (re)linking /work/project/.claude/memories → .memories-repo/memories...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/audited-a-trailing-slash-on-the-project-path-is-not-normalised-away.json b/tests/golden/audited-a-trailing-slash-on-the-project-path-is-not-normalised-away.json index dbd831f..299e8d5 100644 --- a/tests/golden/audited-a-trailing-slash-on-the-project-path-is-not-normalised-away.json +++ b/tests/golden/audited-a-trailing-slash-on-the-project-path-is-not-normalised-away.json @@ -1,5 +1,5 @@ { - "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project//.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project//.claude/memories.\n", + "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project//.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project//.claude/memories.\n", "stderr": "Cloning into '/work/project//.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/audited-twenty-one-previously-deleted-files-straddle-the-reason-cap.json b/tests/golden/audited-twenty-one-previously-deleted-files-straddle-the-reason-cap.json index 2c2a0f6..8ea25b8 100644 --- a/tests/golden/audited-twenty-one-previously-deleted-files-straddle-the-reason-cap.json +++ b/tests/golden/audited-twenty-one-previously-deleted-files-straddle-the-reason-cap.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 21 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept); 21 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\n\nHeld back 21 file(s) previously deleted from the shared repo:\n - learning_gone_00.md\n deleted by remove many\n - learning_gone_01.md\n deleted by remove many\n - learning_gone_02.md\n deleted by remove many\n - learning_gone_03.md\n deleted by remove many\n - learning_gone_04.md\n deleted by remove many\n - learning_gone_05.md\n deleted by remove many\n - learning_gone_06.md\n deleted by remove many\n - learning_gone_07.md\n deleted by remove many\n - learning_gone_08.md\n deleted by remove many\n - learning_gone_09.md\n deleted by remove many\n - learning_gone_10.md\n deleted by remove many\n - learning_gone_11.md\n deleted by remove many\n - learning_gone_12.md\n deleted by remove many\n - learning_gone_13.md\n deleted by remove many\n - learning_gone_14.md\n deleted by remove many\n - learning_gone_15.md\n deleted by remove many\n - learning_gone_16.md\n deleted by remove many\n - learning_gone_17.md\n deleted by remove many\n - learning_gone_18.md\n deleted by remove many\n - learning_gone_19.md\n deleted by remove many\n - learning_gone_20.md\n\nSomeone removed these deliberately, so they were not re-imported or pushed.\nTo re-add one anyway: cp '/work/project/.claude/.memories-migration-/' '/work/project/.claude/memories/'\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 21 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept); 21 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\n\nHeld back 21 file(s) previously deleted from the shared repo:\n - learning_gone_00.md\n deleted by remove many\n - learning_gone_01.md\n deleted by remove many\n - learning_gone_02.md\n deleted by remove many\n - learning_gone_03.md\n deleted by remove many\n - learning_gone_04.md\n deleted by remove many\n - learning_gone_05.md\n deleted by remove many\n - learning_gone_06.md\n deleted by remove many\n - learning_gone_07.md\n deleted by remove many\n - learning_gone_08.md\n deleted by remove many\n - learning_gone_09.md\n deleted by remove many\n - learning_gone_10.md\n deleted by remove many\n - learning_gone_11.md\n deleted by remove many\n - learning_gone_12.md\n deleted by remove many\n - learning_gone_13.md\n deleted by remove many\n - learning_gone_14.md\n deleted by remove many\n - learning_gone_15.md\n deleted by remove many\n - learning_gone_16.md\n deleted by remove many\n - learning_gone_17.md\n deleted by remove many\n - learning_gone_18.md\n deleted by remove many\n - learning_gone_19.md\n deleted by remove many\n - learning_gone_20.md\n\nSomeone removed these deliberately, so they were not re-imported or pushed.\nTo re-add one anyway: cp '/work/project/.claude/.memories-migration-/' '/work/project/.claude/memories/'\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-migration- .memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-1-an-already-linked-setup-is-left-alone.json b/tests/golden/case-1-an-already-linked-setup-is-left-alone.json index 8f79e81..7d85b9e 100644 --- a/tests/golden/case-1-an-already-linked-setup-is-left-alone.json +++ b/tests/golden/case-1-an-already-linked-setup-is-left-alone.json @@ -1,5 +1,5 @@ { - "stdout": "Added the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nShared memories already linked — leaving as-is.\n", + "stdout": "Added the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nShared memories already linked — leaving as-is.\n", "stderr": "", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-2-a-badly-named-migrated-file-is-left-untracked.json b/tests/golden/case-2-a-badly-named-migrated-file-is-left-untracked.json index 5d2e348..9e16642 100644 --- a/tests/golden/case-2-a-badly-named-migrated-file-is-left-untracked.json +++ b/tests/golden/case-2-a-badly-named-migrated-file-is-left-untracked.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 2 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/scratch.md\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 3 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 2 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/scratch.md\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 3 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_mine_own.md learning_shared_seed.md scratch.md" diff --git a/tests/golden/case-2-a-migrated-non-markdown-file-is-not-counted-as-a-memory.json b/tests/golden/case-2-a-migrated-non-markdown-file-is-not-counted-as-a-memory.json index 473f29e..070c8cf 100644 --- a/tests/golden/case-2-a-migrated-non-markdown-file-is-not-counted-as-a-memory.json +++ b/tests/golden/case-2-a-migrated-non-markdown-file-is-not-counted-as-a-memory.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 2 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/notes.txt\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 2 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nNote: these migrated files don't match the naming convention and were left untracked:\n - memories/notes.txt\nRename them to memories/learning__.md or memories/decision__.md so they can be shared with the team.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_mine_own.md learning_shared_seed.md notes.txt" diff --git a/tests/golden/case-2-a-populated-memories-directory-migrates-and-pushes.json b/tests/golden/case-2-a-populated-memories-directory-migrates-and-pushes.json index 3ec09e7..8b9da6c 100644 --- a/tests/golden/case-2-a-populated-memories-directory-migrates-and-pushes.json +++ b/tests/golden/case-2-a-populated-memories-directory-migrates-and-pushes.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/case-2-a-previously-deleted-memory-is-held-back.json b/tests/golden/case-2-a-previously-deleted-memory-is-held-back.json index d5d1dad..9f6c415 100644 --- a/tests/golden/case-2-a-previously-deleted-memory-is-held-back.json +++ b/tests/golden/case-2-a-previously-deleted-memory-is-held-back.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept); 1 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\n\nHeld back 1 file(s) previously deleted from the shared repo:\n - learning_curated_away.md\n deleted by remove it\n\nSomeone removed these deliberately, so they were not re-imported or pushed.\nTo re-add one anyway: cp '/work/project/.claude/.memories-migration-/' '/work/project/.claude/memories/'\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept); 1 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\n\nHeld back 1 file(s) previously deleted from the shared repo:\n - learning_curated_away.md\n deleted by remove it\n\nSomeone removed these deliberately, so they were not re-imported or pushed.\nTo re-add one anyway: cp '/work/project/.claude/.memories-migration-/' '/work/project/.claude/memories/'\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-migration- .memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-2-an-empty-memories-directory-is-removed.json b/tests/golden/case-2-an-empty-memories-directory-is-removed.json index 24a7e66..1fb9527 100644 --- a/tests/golden/case-2-an-empty-memories-directory-is-removed.json +++ b/tests/golden/case-2-an-empty-memories-directory-is-removed.json @@ -1,5 +1,5 @@ { - "stdout": "Found empty /work/project/.claude/memories — removing and proceeding with fresh setup.\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found empty /work/project/.claude/memories — removing and proceeding with fresh setup.\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-2-the-shared-copy-wins-a-filename-conflict.json b/tests/golden/case-2-the-shared-copy-wins-a-filename-conflict.json index 60e1c34..9117caa 100644 --- a/tests/golden/case-2-the-shared-copy-wins-a-filename-conflict.json +++ b/tests/golden/case-2-the-shared-copy-wins-a-filename-conflict.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 0 local memory file(s); 1 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 0 local memory file(s); 1 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-migration- .memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-3-a-live-checkout-with-no-symlink-is-relinked.json b/tests/golden/case-3-a-live-checkout-with-no-symlink-is-relinked.json index fa293aa..bfbe58d 100644 --- a/tests/golden/case-3-a-live-checkout-with-no-symlink-is-relinked.json +++ b/tests/golden/case-3-a-live-checkout-with-no-symlink-is-relinked.json @@ -1,5 +1,5 @@ { - "stdout": "Memories checkout present, (re)linking /work/project/.claude/memories → .memories-repo/memories...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Memories checkout present, (re)linking /work/project/.claude/memories → .memories-repo/memories...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/case-5-fresh-setup-clones-and-links.json b/tests/golden/case-5-fresh-setup-clones-and-links.json index 68d8fa5..98aac5a 100644 --- a/tests/golden/case-5-fresh-setup-clones-and-links.json +++ b/tests/golden/case-5-fresh-setup-clones-and-links.json @@ -1,5 +1,5 @@ { - "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Cloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/commit-graph-a-clone-that-already-has-it-off-is-left-alone.json b/tests/golden/commit-graph-a-clone-that-already-has-it-off-is-left-alone.json new file mode 100644 index 0000000..8f79e81 --- /dev/null +++ b/tests/golden/commit-graph-a-clone-that-already-has-it-off-is-left-alone.json @@ -0,0 +1,6 @@ +{ + "stdout": "Added the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nShared memories already linked — leaving as-is.\n", + "stderr": "", + "code": 0, + "git": ".memories-repo memories\nlearning_shared_seed.md" +} diff --git a/tests/golden/doctor-a-checkout-with-no-upstream-warns.json b/tests/golden/doctor-a-checkout-with-no-upstream-warns.json new file mode 100644 index 0000000..8b67570 --- /dev/null +++ b/tests/golden/doctor-a-checkout-with-no-upstream-warns.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories setup needs attention:\n - the checkout has no upstream branch, so memories never leave this machine (run /resolve-memories)\n", + "stderr": "", + "code": 2, + "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/doctor-a-commit-graph-left-on-warns.json b/tests/golden/doctor-a-commit-graph-left-on-warns.json new file mode 100644 index 0000000..4607d59 --- /dev/null +++ b/tests/golden/doctor-a-commit-graph-left-on-warns.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories setup needs attention:\n - core.commitGraph is not off in the checkout (run 'mcs sync' to set it)\n", + "stderr": "", + "code": 2, + "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/doctor-a-file-the-guardrail-rejects-warns.json b/tests/golden/doctor-a-file-the-guardrail-rejects-warns.json new file mode 100644 index 0000000..fb4acf2 --- /dev/null +++ b/tests/golden/doctor-a-file-the-guardrail-rejects-warns.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories setup needs attention:\n - 1 file(s) the naming guardrail rejects block every push: memories/scratch.md (run /resolve-memories)\n", + "stderr": "", + "code": 2, + "git": "status-all: ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/scratch.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md scratch.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/doctor-a-rebase-left-in-progress-warns.json b/tests/golden/doctor-a-rebase-left-in-progress-warns.json new file mode 100644 index 0000000..95b8954 --- /dev/null +++ b/tests/golden/doctor-a-rebase-left-in-progress-warns.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories setup needs attention:\n - a rebase is in progress in the checkout, so auto-push is paused (run /resolve-memories)\n", + "stderr": "", + "code": 2, + "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/edge-a-commit-graph-failure-names-mcs-sync-instead-of-a-retry.json b/tests/golden/edge-a-commit-graph-failure-names-mcs-sync-instead-of-a-retry.json new file mode 100644 index 0000000..325d962 --- /dev/null +++ b/tests/golden/edge-a-commit-graph-failure-names-mcs-sync-instead-of-a-retry.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories: pull --rebase failed on the checkout's commit-graph. Run `mcs sync` to turn it off; until then every push fails the same way.\n fatal: commit 0123abc exists in commit-graph but not in the object database\nerror: insufficient permission for adding an object to repository database .git/objects\nhint: Could not execute the todo command\nhint:\nhint: pick b3e0beb3b72b923b22644ae35562e2769c182b29 # Fixture: add learning_ours_own\nhint:\nhint: It has been rescheduled; To edit the command before continuing, please\nhint: edit the todo list first:\nhint:\nhint: git rebase --edit-todo\nhint: git rebase --continue\n", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: add learning_ours_own | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_ours_own | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_own.md | memories/learning_seed_topic.md\nworktree: learning_ours_own.md learning_seed_topic.md\nremote-log: teammate memory | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_theirs_new.md" +} diff --git a/tests/golden/edge-a-mid-rebase-failure-whose-abort-also-fails-pauses-for-resolve.json b/tests/golden/edge-a-mid-rebase-failure-whose-abort-also-fails-pauses-for-resolve.json new file mode 100644 index 0000000..5da65ca --- /dev/null +++ b/tests/golden/edge-a-mid-rebase-failure-whose-abort-also-fails-pauses-for-resolve.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories: the rebase failed AND --abort failed — repo may be in a half-rebased state. Run /resolve-memories to finish it.\n error: The following untracked working tree files would be overwritten by reset:\n\tmemories/learning_ours_own.md\nPlease move or remove them before you reset.\nAborting\nfatal: could not move back to b3e0beb3b72b923b22644ae35562e2769c182b29\n error: The following untracked working tree files would be overwritten by merge:\n\tmemories/learning_ours_own.md\nPlease move or remove them before you merge.\nAborting\nhint: Could not execute the todo command\nhint:\nhint: pick b3e0beb3b72b923b22644ae35562e2769c182b29 # Fixture: add learning_ours_own\nhint:\nhint: It has been rescheduled; To edit the command before continuing, please\nhint: edit the todo list first:\nhint:\nhint: git rebase --edit-todo\nhint: git rebase --continue\n", + "stderr": "", + "code": 0, + "git": "status-all: ?? memories/learning_ours_own.md\nlog-all: teammate memory | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: ?? memories/learning_ours_own.md\nlog: teammate memory | seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nworktree: learning_ours_own.md learning_seed_topic.md learning_theirs_new.md\nremote-log: teammate memory | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_theirs_new.md" +} diff --git a/tests/golden/edge-a-pull-that-fails-mid-rebase-without-a-conflict-is-rolled-back.json b/tests/golden/edge-a-pull-that-fails-mid-rebase-without-a-conflict-is-rolled-back.json new file mode 100644 index 0000000..879d5cb --- /dev/null +++ b/tests/golden/edge-a-pull-that-fails-mid-rebase-without-a-conflict-is-rolled-back.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories: pull --rebase failed partway and was rolled back. Will retry on next Stop.\n error: insufficient permission for adding an object to repository database .git/objects\nhint: Could not execute the todo command\nhint:\nhint: pick b3e0beb3b72b923b22644ae35562e2769c182b29 # Fixture: add learning_ours_own\nhint:\nhint: It has been rescheduled; To edit the command before continuing, please\nhint: edit the todo list first:\nhint:\nhint: git rebase --edit-todo\nhint: git rebase --continue\n", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: add learning_ours_own | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_ours_own | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_own.md | memories/learning_seed_topic.md\nworktree: learning_ours_own.md learning_seed_topic.md\nremote-log: teammate memory | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_theirs_new.md" +} diff --git a/tests/golden/unchecked-a-dangling-symlink-is-skipped-not-fatal.json b/tests/golden/unchecked-a-dangling-symlink-is-skipped-not-fatal.json index f6f5548..3eb86ff 100644 --- a/tests/golden/unchecked-a-dangling-symlink-is-skipped-not-fatal.json +++ b/tests/golden/unchecked-a-dangling-symlink-is-skipped-not-fatal.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept); 0 previously-deleted file(s) held back.\n Kept at /work/project/.claude/.memories-migration- for review.\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-migration- .memories-repo memories\nlearning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/unchecked-a-failed-migration-commit-is-not-reported-as-a-push.json b/tests/golden/unchecked-a-failed-migration-commit-is-not-reported-as-a-push.json index e364ad7..07d2c17 100644 --- a/tests/golden/unchecked-a-failed-migration-commit-is-not-reported-as-a-push.json +++ b/tests/golden/unchecked-a-failed-migration-commit-is-not-reported-as-a-push.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\nfatal: empty ident name (for <>) not allowed\n", "code": 128, "git": ".memories-repo memories\nlearning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/usage-a-clone-that-already-has-the-union-rule-is-left-alone.json b/tests/golden/usage-a-clone-that-already-has-the-union-rule-is-left-alone.json index 7f7892d..73cf175 100644 --- a/tests/golden/usage-a-clone-that-already-has-the-union-rule-is-left-alone.json +++ b/tests/golden/usage-a-clone-that-already-has-the-union-rule-is-left-alone.json @@ -1,5 +1,5 @@ { - "stdout": "Shared memories already linked — leaving as-is.\n", + "stdout": "Turned off core.commitGraph for the memories checkout.\nShared memories already linked — leaving as-is.\n", "stderr": "", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/golden/usage-a-local-usage-file-alone-is-merged-and-left-for-the-next-stop.json b/tests/golden/usage-a-local-usage-file-alone-is-merged-and-left-for-the-next-stop.json index 2f9a541..43e826d 100644 --- a/tests/golden/usage-a-local-usage-file-alone-is-merged-and-left-for-the-next-stop.json +++ b/tests/golden/usage-a-local-usage-file-alone-is-merged-and-left-for-the-next-stop.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept).\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 1 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 0 local memory file(s); 0 conflict(s) (shared version kept).\nDone. 1 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\n.usage.tsv learning_shared_seed.md" diff --git a/tests/golden/usage-a-local-usage-file-is-appended-onto-the-shared-one.json b/tests/golden/usage-a-local-usage-file-is-appended-onto-the-shared-one.json index 70b686d..d966959 100644 --- a/tests/golden/usage-a-local-usage-file-is-appended-onto-the-shared-one.json +++ b/tests/golden/usage-a-local-usage-file-is-appended-onto-the-shared-one.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\n.usage.tsv learning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/usage-a-local-usage-file-is-moved-in-when-the-shared-repo-has-none.json b/tests/golden/usage-a-local-usage-file-is-moved-in-when-the-shared-repo-has-none.json index 70b686d..d966959 100644 --- a/tests/golden/usage-a-local-usage-file-is-moved-in-when-the-shared-repo-has-none.json +++ b/tests/golden/usage-a-local-usage-file-is-moved-in-when-the-shared-repo-has-none.json @@ -1,5 +1,5 @@ { - "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", + "stdout": "Found pre-existing /work/project/.claude/memories with 2 entr(y/ies) — staging for migration...\n → backing up to /work/project/.claude/.memories-migration-\nCloning shared memories (branch 'main', sparse) into /work/project/.claude/.memories-repo...\nAdded the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nMigration: imported 1 local memory file(s); 0 conflict(s) (shared version kept).\nPushed migrated memories to the shared branch.\nDone. 2 memory file(s) available at /work/project/.claude/memories.\n", "stderr": "Cloning into '/work/project/.claude/.memories-repo'...\nwarning: --filter is ignored in local clones; use file:// instead.\ndone.\n", "code": 0, "git": ".memories-repo memories\n.usage.tsv learning_mine_own.md learning_shared_seed.md" diff --git a/tests/golden/usage-an-existing-clone-gets-the-union-rule-beside-its-other-attributes.json b/tests/golden/usage-an-existing-clone-gets-the-union-rule-beside-its-other-attributes.json index 8f79e81..7d85b9e 100644 --- a/tests/golden/usage-an-existing-clone-gets-the-union-rule-beside-its-other-attributes.json +++ b/tests/golden/usage-an-existing-clone-gets-the-union-rule-beside-its-other-attributes.json @@ -1,5 +1,5 @@ { - "stdout": "Added the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nShared memories already linked — leaving as-is.\n", + "stdout": "Added the merge=union rule for memories/.usage.tsv to /work/project/.claude/.memories-repo/.git/info/attributes.\nTurned off core.commitGraph for the memories checkout.\nShared memories already linked — leaving as-is.\n", "stderr": "", "code": 0, "git": ".memories-repo memories\nlearning_shared_seed.md" diff --git a/tests/harness.ts b/tests/harness.ts index 4642504..3d850a8 100644 --- a/tests/harness.ts +++ b/tests/harness.ts @@ -249,6 +249,7 @@ export function makeProject(): { root: string; work: string; project: string; re git(repo, "push", "-q", "-u", "origin", "main"); // What configure-memories writes to every clone. writeFileSync(attributesFile(repo), `${USAGE_ATTRIBUTE}\n`); + git(repo, "config", "core.commitGraph", "false"); // The ancestor Node resolves module type from. "commonjs" is the hostile case. writeFileSync(join(project, "package.json"), '{"name":"consumer","type":"commonjs"}\n'); diff --git a/tests/scripts.test.ts b/tests/scripts.test.ts index 9e6d5d2..18e89e3 100644 --- a/tests/scripts.test.ts +++ b/tests/scripts.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdirSync, rmSync, symlinkSync, unlinkSync } from "node:fs"; +import { mkdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, test } from "node:test"; import { dropUnionRule, git, type RunResult, readGolden, runScript, type ScriptFixture, writeGolden } from "./harness.ts"; @@ -81,6 +81,30 @@ const fixtures: readonly ScriptFixture[] = [ unlinkSync(join(claude(project), "memories")); }, }, + { + name: "doctor: a commit-graph left on warns", + script: "doctor-memories", + expectCode: 2, + setup: repo => git(repo, "config", "--unset", "core.commitGraph"), + }, + { + name: "doctor: a rebase left in progress warns", + script: "doctor-memories", + expectCode: 2, + setup: repo => mkdirSync(join(repo, ".git", "rebase-merge")), + }, + { + name: "doctor: a checkout with no upstream warns", + script: "doctor-memories", + expectCode: 2, + setup: repo => git(repo, "branch", "--unset-upstream"), + }, + { + name: "doctor: a file the guardrail rejects warns", + script: "doctor-memories", + expectCode: 2, + setup: repo => writeFileSync(join(repo, "memories", "scratch.md"), "x\n"), + }, { name: "doctor-remote: reachable remote passes", script: "doctor-memories-remote", expectCode: 0 }, { name: "doctor-remote: no checkout skips",