From d3c882345e45b205c1f5e22d9eb7de19dbd26a27 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 08:29:40 +0000 Subject: [PATCH 1/4] Remove the auto-push modes; full is the only behaviour The memory loop is reliable now, so the review report and the parked deletions only add friction. Every change under memories/, deletions included, is committed and pushed at the end of the turn. - Delete MEMORIES_AUTOPUSH_MODE: its prompt and settings component, the mode resolver, the review report and dedupe state, and the review-only PostToolUse announce hook. - Rename /approve-memories to /resolve-memories and refocus it on what retrying never fixes: merge rebase conflicts, rename files the guardrail rejects, restore a missing upstream, finish a rebase left in progress. - Pause both hooks while a rebase or merge is in progress, instead of staging conflict markers into the half-finished rebase. - Warn at SessionStart when the checkout has no upstream, which used to keep memories local without a word. - Commit as the git user with a subject that says what changed ("Ana: add learning_x", or counts plus a per-file body) instead of "auto: memories from ". The CI hostname check goes. - Goldens re-recorded from the TypeScript (UPDATE_GOLDENS=1), each diff reviewed; the locked-index golden keeps the macOS git wording. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Jcf1cp8tAMmCJBLywkDC1 --- .github/workflows/ci.yml | 9 - README.md | 123 ++-- commands/approve-memories.md | 31 - commands/resolve-memories.md | 64 ++ config/settings.json | 5 - runtime/announce.mts | 58 -- runtime/autopush.mts | 88 +-- runtime/lib/commit-message.mts | 68 +++ runtime/lib/git.mts | 23 + runtime/lib/hook-io.mts | 2 +- runtime/lib/mode.mts | 21 - runtime/lib/naming.mts | 2 - runtime/lib/pending.mts | 46 +- runtime/lib/push.mts | 4 +- runtime/lib/report.mts | 109 ---- runtime/pull.mts | 69 +-- scripts/configure-memories.ts | 11 +- techpack.yaml | 42 +- templates/instructions.md | 6 +- tests/differential.test.ts | 561 ++---------------- tests/epipe.test.ts | 18 +- .../announce-a-path-containing-a-quote.json | 6 - .../golden/announce-auto-mode-is-silent.json | 6 - ...announce-decision-prefix-also-matches.json | 6 - tests/golden/announce-empty-stdin.json | 6 - tests/golden/announce-malformed-stdin.json | 6 - tests/golden/announce-missing-file-path.json | 6 - ...nnounce-name-with-a-space-is-rejected.json | 6 - ...unce-nested-prefix-path-still-matches.json | 6 - ...announce-non-md-extension-is-rejected.json | 6 - .../golden/announce-non-string-file-path.json | 6 - ...h-outside-claude-memories-is-rejected.json | 6 - ...e-review-mode-well-named-memory-write.json | 6 - .../announce-unknown-mode-is-silent.json | 6 - .../golden/announce-unset-mode-is-silent.json | 6 - .../announce-wrong-prefix-is-rejected.json | 6 - ...st-line-is-skipped-like-bash-skips-it.json | 6 - ...ry-path-containing-a-space-and-a-hash.json | 6 - ...tion-counts-characters-not-code-units.json | 6 - ...characters-truncate-at-the-same-point.json | 6 - ...-auto-commits-and-pushes-a-new-memory.json | 6 - .../autopush-auto-parks-a-deletion.json | 6 - ...eletions-but-still-pushes-an-addition.json | 6 - .../autopush-auto-pushes-a-modification.json | 6 - ...opush-commits-and-pushes-a-new-memory.json | 6 + .../autopush-empty-stdin-still-pushes.json | 2 +- .../autopush-full-pushes-the-deletion.json | 6 - ...ll-without-deletions-omits-the-suffix.json | 6 - ...-guardrail-blocks-a-badly-named-file.json} | 4 +- ...autopush-guardrail-blocks-in-full-too.json | 6 - ...topush-guardrail-blocks-in-review-too.json | 6 - ...ush-guardrail-lists-several-offenders.json | 4 +- tests/golden/autopush-malformed-stdin.json | 2 +- tests/golden/autopush-missing-checkout.json | 2 +- .../autopush-no-upstream-configured.json | 2 +- .../golden/autopush-nothing-pending-auto.json | 6 - ...ing-pending-clears-stale-review-state.json | 6 - .../autopush-nothing-pending-review.json | 6 - tests/golden/autopush-nothing-pending.json | 6 + tests/golden/autopush-pushes-a-deletion.json | 6 + .../autopush-pushes-a-modification.json | 6 + ...marised-one-line-per-file-in-the-body.json | 6 + ...-names-the-git-author-not-the-machine.json | 6 + ...n-mode-falls-through-to-auto-silently.json | 6 - .../golden/autopush-unpushed-commit-only.json | 2 +- ...et-a-non-integer-falls-back-to-twelve.json | 2 +- ...-an-explicit-small-budget-is-honoured.json | 2 +- .../budget-unset-falls-back-to-twelve.json | 2 +- ...t-zero-is-clamped-to-a-single-attempt.json | 2 +- ...ctor-checkout-present-symlink-missing.json | 2 +- tests/golden/doctor-dangling-symlink.json | 2 +- tests/golden/doctor-healthy-setup-passes.json | 2 +- .../doctor-missing-checkout-and-symlink.json | 2 +- .../doctor-remote-no-checkout-skips.json | 2 +- ...doctor-remote-reachable-remote-passes.json | 2 +- ...octor-remote-unreachable-remote-warns.json | 2 +- ...lves-somewhere-that-is-not-a-checkout.json | 2 +- ...amed-deleted-file-trips-the-guardrail.json | 4 +- ...ure-is-reported-and-retried-next-stop.json | 2 +- ...mories-symlink-does-not-stop-the-push.json | 2 +- ...-with-an-unpushed-commit-stays-silent.json | 6 + ...the-turn-instead-of-reporting-nothing.json | 2 +- ...n-progress-is-flagged-at-sessionstart.json | 6 + ...left-in-progress-pauses-the-stop-hook.json | 6 + ...staged-rename-is-committed-and-pushed.json | 6 + ...-to-a-symlink-is-committed-and-pushed.json | 6 + ...-to-a-symlink-is-unclassified-pending.json | 6 - ...ast-result-does-not-silence-the-nudge.json | 6 - ...ops-a-false-file-path-from-the-stream.json | 6 - ...dge-announce-ignores-a-null-file-path.json | 6 - ...nt-when-a-scalar-follows-a-valid-path.json | 6 - ...is-silent-when-stdin-is-a-bare-scalar.json | 6 - ...-every-value-in-a-concatenated-stream.json | 6 - ...-non-string-file-path-the-way-jq-does.json | 6 - ...reports-a-path-after-a-leading-scalar.json | 6 - ...strips-a-trailing-newline-the-way-did.json | 6 - ...-only-the-second-value-carries-a-path.json | 6 - ...eady-staged-in-the-index-still-commit.json | 2 +- ...detached-head-with-an-unpushed-commit.json | 6 - ...staged-rename-as-unclassified-pending.json | 6 - ...nstart-survives-a-failed-fast-forward.json | 4 +- ...rd-alongside-our-own-untracked-memory.json | 4 +- ...rward-past-a-conflicting-local-commit.json | 4 +- ...art-fast-forwards-a-teammate-s-memory.json | 2 +- ...hook-rebases-onto-a-teammate-s-commit.json | 2 +- ...-does-not-trip-the-filename-guardrail.json | 2 +- ...c-a-dirty-root-file-stays-uncommitted.json | 6 + ...-leaves-a-dirty-root-file-uncommitted.json | 6 - ...-leaves-a-dirty-root-file-uncommitted.json | 6 - ...ode-does-not-report-a-dirty-root-file.json | 6 - ...ot-count-a-dirty-root-file-as-pending.json | 2 +- ...racked-memory-reports-lingering-state.json | 6 + ...de-leaves-the-dedupe-state-file-alone.json | 6 - tests/golden/pull-clean-tree-auto.json | 6 - tests/golden/pull-clean-tree-review.json | 6 - tests/golden/pull-clean-tree.json | 6 + ...-concatenated-json-stream-is-accepted.json | 4 +- ...pull-empty-mode-falls-through-to-auto.json | 6 - .../pull-empty-stdin-still-does-the-work.json | 4 +- tests/golden/pull-malformed-stdin.json | 2 +- tests/golden/pull-missing-checkout.json | 2 +- tests/golden/pull-no-upstream-configured.json | 4 +- ...iew-mode-clears-the-dedupe-state-file.json | 6 - ...acked-memories-pluralise-the-same-way.json | 4 +- ...ull-uncommitted-and-unpushed-together.json | 4 +- ...ode-warning-joins-the-pending-message.json | 6 - .../pull-unknown-mode-warns-clean-tree.json | 6 - tests/golden/pull-unpushed-commit-only.json | 4 +- ...d-memory-auto-reports-lingering-state.json | 6 - ...memory-review-reports-awaiting-review.json | 6 - ...empt-budget-falls-back-to-the-default.json | 2 +- ...h-a-rebase-conflict-pauses-and-aborts.json | 4 +- ...push-rebases-and-retries-into-success.json | 2 +- ...-zero-attempt-budget-still-tries-once.json | 2 +- ...chable-remote-reports-auth-or-network.json | 2 +- ...is-preserved-as-the-bash-preserves-it.json | 6 - ...letion-renders-its-relative-timestamp.json | 6 - ...review-a-modification-renders-numstat.json | 6 - ...w-a-new-file-renders-with-its-preview.json | 6 - ...iew-longer-than-80-chars-is-truncated.json | 6 - .../review-a-stale-stored-hash-reprints.json | 6 - .../review-a-tab-in-the-preview-survives.json | 6 - ...adding-a-file-after-a-report-reprints.json | 6 - ...w-an-empty-file-emits-no-preview-line.json | 6 - ...-in-a-memory-never-reach-the-terminal.json | 6 - ...iew-new-modified-and-deleted-together.json | 6 - ...pending-files-plus-an-unpushed-commit.json | 6 - ...ading-blank-and-whitespace-only-lines.json | 6 - ...the-same-pending-set-is-reported-once.json | 6 - ...-deletions-straddle-the-timestamp-cap.json | 6 - ...w-unpushed-commit-with-no-dirty-files.json | 6 - tests/harness.ts | 10 +- tests/manifest.test.ts | 10 +- tests/unit.test.ts | 126 ++-- 154 files changed, 522 insertions(+), 1614 deletions(-) delete mode 100644 commands/approve-memories.md create mode 100644 commands/resolve-memories.md delete mode 100644 config/settings.json delete mode 100755 runtime/announce.mts create mode 100644 runtime/lib/commit-message.mts delete mode 100644 runtime/lib/mode.mts delete mode 100644 runtime/lib/report.mts delete mode 100644 tests/golden/announce-a-path-containing-a-quote.json delete mode 100644 tests/golden/announce-auto-mode-is-silent.json delete mode 100644 tests/golden/announce-decision-prefix-also-matches.json delete mode 100644 tests/golden/announce-empty-stdin.json delete mode 100644 tests/golden/announce-malformed-stdin.json delete mode 100644 tests/golden/announce-missing-file-path.json delete mode 100644 tests/golden/announce-name-with-a-space-is-rejected.json delete mode 100644 tests/golden/announce-nested-prefix-path-still-matches.json delete mode 100644 tests/golden/announce-non-md-extension-is-rejected.json delete mode 100644 tests/golden/announce-non-string-file-path.json delete mode 100644 tests/golden/announce-path-outside-claude-memories-is-rejected.json delete mode 100644 tests/golden/announce-review-mode-well-named-memory-write.json delete mode 100644 tests/golden/announce-unknown-mode-is-silent.json delete mode 100644 tests/golden/announce-unset-mode-is-silent.json delete mode 100644 tests/golden/announce-wrong-prefix-is-rejected.json delete mode 100644 tests/golden/audit-a-crlf-blank-first-line-is-skipped-like-bash-skips-it.json delete mode 100644 tests/golden/audit-a-memory-path-containing-a-space-and-a-hash.json delete mode 100644 tests/golden/audit-an-80-character-truncation-counts-characters-not-code-units.json delete mode 100644 tests/golden/audit-astral-characters-truncate-at-the-same-point.json delete mode 100644 tests/golden/autopush-auto-commits-and-pushes-a-new-memory.json delete mode 100644 tests/golden/autopush-auto-parks-a-deletion.json delete mode 100644 tests/golden/autopush-auto-parks-deletions-but-still-pushes-an-addition.json delete mode 100644 tests/golden/autopush-auto-pushes-a-modification.json create mode 100644 tests/golden/autopush-commits-and-pushes-a-new-memory.json delete mode 100644 tests/golden/autopush-full-pushes-the-deletion.json delete mode 100644 tests/golden/autopush-full-without-deletions-omits-the-suffix.json rename tests/golden/{autopush-guardrail-blocks-a-badly-named-file-in-auto.json => autopush-guardrail-blocks-a-badly-named-file.json} (52%) delete mode 100644 tests/golden/autopush-guardrail-blocks-in-full-too.json delete mode 100644 tests/golden/autopush-guardrail-blocks-in-review-too.json delete mode 100644 tests/golden/autopush-nothing-pending-auto.json delete mode 100644 tests/golden/autopush-nothing-pending-clears-stale-review-state.json delete mode 100644 tests/golden/autopush-nothing-pending-review.json create mode 100644 tests/golden/autopush-nothing-pending.json create mode 100644 tests/golden/autopush-pushes-a-deletion.json create mode 100644 tests/golden/autopush-pushes-a-modification.json create mode 100644 tests/golden/autopush-several-changes-are-summarised-one-line-per-file-in-the-body.json create mode 100644 tests/golden/autopush-the-subject-names-the-git-author-not-the-machine.json delete mode 100644 tests/golden/autopush-unknown-mode-falls-through-to-auto-silently.json create mode 100644 tests/golden/edge-a-detached-head-with-an-unpushed-commit-stays-silent.json create mode 100644 tests/golden/edge-a-rebase-left-in-progress-is-flagged-at-sessionstart.json create mode 100644 tests/golden/edge-a-rebase-left-in-progress-pauses-the-stop-hook.json create mode 100644 tests/golden/edge-a-staged-rename-is-committed-and-pushed.json create mode 100644 tests/golden/edge-a-typechange-to-a-symlink-is-committed-and-pushed.json delete mode 100644 tests/golden/edge-a-typechange-to-a-symlink-is-unclassified-pending.json delete mode 100644 tests/golden/edge-an-empty-last-result-does-not-silence-the-nudge.json delete mode 100644 tests/golden/edge-announce-drops-a-false-file-path-from-the-stream.json delete mode 100644 tests/golden/edge-announce-ignores-a-null-file-path.json delete mode 100644 tests/golden/edge-announce-is-silent-when-a-scalar-follows-a-valid-path.json delete mode 100644 tests/golden/edge-announce-is-silent-when-stdin-is-a-bare-scalar.json delete mode 100644 tests/golden/edge-announce-reads-every-value-in-a-concatenated-stream.json delete mode 100644 tests/golden/edge-announce-renders-a-non-string-file-path-the-way-jq-does.json delete mode 100644 tests/golden/edge-announce-still-reports-a-path-after-a-leading-scalar.json delete mode 100644 tests/golden/edge-announce-strips-a-trailing-newline-the-way-did.json delete mode 100644 tests/golden/edge-announce-when-only-the-second-value-carries-a-path.json delete mode 100644 tests/golden/edge-review-on-a-detached-head-with-an-unpushed-commit.json delete mode 100644 tests/golden/edge-review-reports-a-staged-rename-as-unclassified-pending.json create mode 100644 tests/golden/pathspec-a-dirty-root-file-stays-uncommitted.json delete mode 100644 tests/golden/pathspec-auto-mode-leaves-a-dirty-root-file-uncommitted.json delete mode 100644 tests/golden/pathspec-full-mode-leaves-a-dirty-root-file-uncommitted.json delete mode 100644 tests/golden/pathspec-review-mode-does-not-report-a-dirty-root-file.json create mode 100644 tests/golden/pull-an-untracked-memory-reports-lingering-state.json delete mode 100644 tests/golden/pull-auto-mode-leaves-the-dedupe-state-file-alone.json delete mode 100644 tests/golden/pull-clean-tree-auto.json delete mode 100644 tests/golden/pull-clean-tree-review.json create mode 100644 tests/golden/pull-clean-tree.json delete mode 100644 tests/golden/pull-empty-mode-falls-through-to-auto.json delete mode 100644 tests/golden/pull-review-mode-clears-the-dedupe-state-file.json delete mode 100644 tests/golden/pull-unknown-mode-warning-joins-the-pending-message.json delete mode 100644 tests/golden/pull-unknown-mode-warns-clean-tree.json delete mode 100644 tests/golden/pull-untracked-memory-auto-reports-lingering-state.json delete mode 100644 tests/golden/pull-untracked-memory-review-reports-awaiting-review.json delete mode 100644 tests/golden/review-a-bare-carriage-return-is-preserved-as-the-bash-preserves-it.json delete mode 100644 tests/golden/review-a-deletion-renders-its-relative-timestamp.json delete mode 100644 tests/golden/review-a-modification-renders-numstat.json delete mode 100644 tests/golden/review-a-new-file-renders-with-its-preview.json delete mode 100644 tests/golden/review-a-preview-longer-than-80-chars-is-truncated.json delete mode 100644 tests/golden/review-a-stale-stored-hash-reprints.json delete mode 100644 tests/golden/review-a-tab-in-the-preview-survives.json delete mode 100644 tests/golden/review-adding-a-file-after-a-report-reprints.json delete mode 100644 tests/golden/review-an-empty-file-emits-no-preview-line.json delete mode 100644 tests/golden/review-ansi-escapes-in-a-memory-never-reach-the-terminal.json delete mode 100644 tests/golden/review-new-modified-and-deleted-together.json delete mode 100644 tests/golden/review-pending-files-plus-an-unpushed-commit.json delete mode 100644 tests/golden/review-preview-skips-leading-blank-and-whitespace-only-lines.json delete mode 100644 tests/golden/review-the-same-pending-set-is-reported-once.json delete mode 100644 tests/golden/review-twenty-one-deletions-straddle-the-timestamp-cap.json delete mode 100644 tests/golden/review-unpushed-commit-with-no-dirty-files.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7409b64..1a5bd1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,6 @@ jobs: - name: unit, contract and behaviour tests run: npm test - # A migration artifact: it pinned the premise that commit subjects were - # unchanged by the port. Cheap to keep, not load-bearing going forward. - - name: the hostname equivalence holds on this runner - run: | - from_shell=$(hostname -s) - from_node=$(node -e 'process.stdout.write(require("node:os").hostname().split(".")[0])') - echo "shell='$from_shell' node='$from_node'" - [ "$from_shell" = "$from_node" ] || { echo "::error::hostname -s and os.hostname() disagree"; exit 1; } - # The pack contains no shell at all. Hooks and pack scripts are TypeScript # executed through their shebang. - name: no shell crept back in diff --git a/README.md b/README.md index e725485..e8ebecb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ identifier: shared-memories requires: mcs >= 2026.9.3 ``` -**Contents** — [When is this useful?](#-when-is-this-useful) · [The problem](#-the-problem) · [How it works](#-how-it-works) · [What's included](#-whats-included) · [Installation](#-installation) · [Directory structure](#-directory-structure) · [Migration](#-migration-from-an-existing-local-memories-folder) · [Side branches](#-optional-push-to-a-side-branch) · [Auto-push modes](#-auto-push-modes) · [Deletions](#-intentional-deletion-workflow) · [Troubleshooting](#-troubleshooting) · [Development](#-development) +**Contents** — [When is this useful?](#-when-is-this-useful) · [The problem](#-the-problem) · [How it works](#-how-it-works) · [What's included](#-whats-included) · [Installation](#-installation) · [Directory structure](#-directory-structure) · [Migration](#-migration-from-an-existing-local-memories-folder) · [Side branches](#-optional-push-to-a-side-branch) · [Conflicts and stuck pushes](#-conflicts-and-stuck-pushes) · [Troubleshooting](#-troubleshooting) · [Development](#-development) --- @@ -58,10 +58,9 @@ Captures still come from [`mcs-cli/memory`](https://github.com/mcs-cli/memory). | Piece | What | How | |-------|------|-----| -| **SessionStart Hook** | Pulls the latest team memories at session start | `git pull --ff-only` against the shared checkout; also flags lingering uncommitted/unpushed state — a stuck auto-push in `auto` / `full` mode, or pending changes awaiting decision in `review` mode | -| **Stop Hook** | Handles new/modified memory files after each Claude turn per `MEMORIES_AUTOPUSH_MODE` (`auto` / `full` / `review`) | Runs async; filename guardrail blocks bad names in every mode; mode dictates whether deletions auto-push, whether anything is committed at all, and (in `review`) prints the per-turn pending-changes report | -| **PostToolUse Hook** | Tells Claude in-conversation that a memory was saved in `review` mode, so it can mention pending review and invoke `/approve-memories` when the user confirms | Fires sync after `Write` / `Edit` / `MultiEdit` to `.claude/memories/`; injects `additionalContext` into Claude's next decision step. Silent in `auto` and `full` | -| **/approve-memories Slash Command** | One shared approval surface both the user and Claude invoke identically — stages, commits, pulls `--rebase`, pushes everything pending under `memories/` | Re-runs the Stop-hook filename guardrail; takes an optional commit-message reason; works in every mode (primary use is `review`; also unblocks state stuck after a push failure in `auto` / `full`) | +| **SessionStart Hook** | Pulls the latest team memories at session start | `git pull --ff-only` against the shared checkout; also flags what would keep your memories from reaching the team: lingering uncommitted/unpushed state, a rebase left half-finished, or a checkout with no upstream | +| **Stop Hook** | Commits and pushes every memory change after each Claude turn — additions, edits and deletions alike | Runs async; the filename guardrail blocks bad names; commits as the git user with a subject that says what changed; rebases and retries with jitter when a teammate pushed first | +| **/resolve-memories Slash Command** | Fixes what the Stop hook can't fix by retrying: merges rebase conflicts, renames files the guardrail rejects, restores a missing upstream | Commits, rebases and pushes once the blocker is gone; diagnoses auth and git-identity failures, which need a human | | **Sparse Checkout + Symlink** | Keeps the shared repo invisible on disk | `.claude/.memories-repo/` is a blobless single-branch sparse clone; `.claude/memories` is a symlink Claude Code reads from | | **CLAUDE.local.md Section** | Tells Claude the memories folder is a shared checkout rather than local scratch, so it doesn't stage memories in the wrong repo, misread an empty `git log` as "no history", or tidy away a teammate's file | An mcs template section (`shared-memories.instructions`) composed into `CLAUDE.local.md` on every `mcs sync`, between `` markers; your own content outside the markers is preserved | @@ -69,15 +68,14 @@ Captures still come from [`mcs-cli/memory`](https://github.com/mcs-cli/memory). 1. **First `mcs sync`** — the configure script clones the shared repo sparsely into `.claude/.memories-repo/`, symlinks `.claude/memories` to it, and migrates any pre-existing local memories into the shared folder (conflicts are preserved for manual review) -2. **Session starts** — the SessionStart hook fast-forwards the shared checkout and warns about any lingering state (auth failure / rebase conflict / guardrail-rejected files in `auto` / `full`; pending review items in `review`) +2. **Session starts** — the SessionStart hook fast-forwards the shared checkout and warns about any lingering state (auth failure, rebase conflict, guardrail-rejected files, a rebase left in progress, no upstream) 3. **During work** — Claude uses the [`continuous-learning`](https://github.com/mcs-cli/memory) skill to write new `learning_*.md` / `decision_*.md` files -4. **Claude finishes a turn** — the Stop hook collects dirty files and dispatches by mode (`MEMORIES_AUTOPUSH_MODE`, see [Auto-Push Modes](#-auto-push-modes)): - - **Naming guardrail (all modes)** — any file failing `^memories/(learning|decision)_[a-zA-Z0-9_-]+\.md$` halts everything until renamed - - **`auto` (default)** — adds/mods auto-pushed; deletions parked in the working tree for manual review - - **`full`** — adds/mods AND deletions auto-pushed in one commit - - **`review`** — nothing auto; the hook prints a per-file report and the user (or Claude, via the PostToolUse nudge) invokes `/approve-memories` to push, or runs the discard commands shown in the report +4. **Claude finishes a turn** — the Stop hook commits everything under `memories/` and pushes it: + - **Naming guardrail** — any file failing `^memories/(learning|decision)_[a-zA-Z0-9_-]+\.md$` halts everything until renamed + - **Commit** — additions, edits and deletions in one commit, authored as you: `Ana Souza: add learning_swiftui_previews`, or `Ana Souza: 2 added, 1 removed` with one line per file in the body + - **Push** — `pull --rebase --autostash`, then push, retrying with jitter if a teammate pushed first. A conflict pauses the push until `/resolve-memories` merges it 5. **Next session** — teammates pull your new memories via SessionStart and the loop continues @@ -85,17 +83,15 @@ The Stop hook in full, since it is where all the policy lives: ```mermaid flowchart TD - A([Claude finishes a turn]) --> B{"anything uncommitted
or unpushed?"} + A([Claude finishes a turn]) --> P{"rebase or merge
in progress?"} + P -->|yes| Q["pause, ask for
/resolve-memories"] --> Z + P -->|no| B{"anything uncommitted
or unpushed?"} B -->|no| Z([exit 0]) B -->|yes| C{"every dirty file matches
memories/learning_*.md
or memories/decision_*.md?"} C -->|no| D["list the offenders,
push nothing"] --> Z - C -->|yes| E{MEMORIES_AUTOPUSH_MODE} - E -->|auto| F["stage adds and mods,
park deletions for review"] - E -->|full| G["stage everything,
deletions included"] - E -->|review| H["print the per-file report,
commit nothing"] - F --> I["commit · pull --rebase --autostash · push
retry with jitter if rejected"] - G --> I - H --> Z + C -->|yes| G["stage everything,
deletions included"] + G --> I["commit as the git user ·
pull --rebase --autostash · push
retry with jitter if rejected"] + I -->|rebase conflict| R["abort the rebase,
ask for /resolve-memories"] --> Z I --> Z ``` @@ -107,9 +103,8 @@ flowchart TD | Hook | Event | What It Does | |------|-------|-------------| -| **pull.mts** | `SessionStart` | Fast-forwards the shared memories checkout; emits a mode-aware warning if previous state is stuck (or, in `review` mode, summarises pending review) | -| **autopush.mts** | `Stop` (async) | Dispatches by `MEMORIES_AUTOPUSH_MODE` mode (`auto` / `full` / `review`); filename guardrail applies in every mode | -| **announce.mts** | `PostToolUse` (Write/Edit/MultiEdit) | In `review` mode only, surfaces the just-written memory to Claude's context so it mentions pending review in chat. Silent in `auto` and `full` | +| **pull.mts** | `SessionStart` | Fast-forwards the shared memories checkout; warns when previous state is stuck, a rebase is left in progress, or the checkout has no upstream | +| **autopush.mts** | `Stop` (async) | Commits every change under `memories/` as the git user, then rebases and pushes; the filename guardrail blocks bad names | Each runs as `node --experimental-strip-types --disable-warning=ExperimentalWarning `: mcs prefixes the interpreter the hook declares in `hookInterpreter`, and never looks at the shebang. They install to `.claude/hooks/shared-memories/`, with the library they import beside them in `lib/`. @@ -117,7 +112,7 @@ Each runs as `node --experimental-strip-types --disable-warning=ExperimentalWarn | Command | What It Does | |---------|-------------| -| **/approve-memories** | Stages, commits, pulls `--rebase`, and pushes everything pending under `memories/`. Primary entry point for `review` mode approval; also unblocks state stuck after a push failure in `auto` / `full` | +| **/resolve-memories** | Fixes what the Stop hook can't: merges rebase conflicts, renames files the guardrail rejects, restores a missing upstream, finishes a rebase left in progress — then commits and pushes. Diagnoses auth and git-identity failures. See [Conflicts and stuck pushes](#-conflicts-and-stuck-pushes) | ### Configuration Script @@ -129,7 +124,7 @@ Each runs as `node --experimental-strip-types --disable-warning=ExperimentalWarn | Section | What It Does | |---------|-------------| -| **shared-memories.instructions** | Four facts Claude cannot infer from the filesystem: writes and deletions are proposals to the team's repo; the memory set changes between sessions; the parent project's git doesn't track memories (so `git add` there is a silent no-op); and memory files must be addressed as `git -C .claude/.memories-repo -- memories/` | +| **shared-memories.instructions** | Four facts Claude cannot infer from the filesystem: writes and deletions reach the team's repo at the end of the turn; the memory set changes between sessions; the parent project's git doesn't track memories (so `git add` there is a silent no-op); and memory files must be addressed as `git -C .claude/.memories-repo -- memories/` | ### Doctor Checks @@ -183,13 +178,12 @@ During `mcs sync`, you'll be prompted for: |--------|-------------|---------| | **MEMORIES_REPO_URL** | Clone URL for the shared memories repo, e.g. `git@github.com:org/memories.git` | *(required)* | | **MEMORIES_BRANCH** | Branch that holds the memory files and this pack | `main` | -| **MEMORIES_AUTOPUSH_MODE** | Stop-hook behavior — `auto` (writes auto-pushed, deletions parked), `full` (writes + deletions auto-pushed), or `review` (nothing auto, per-turn report). See [Auto-Push Modes](#-auto-push-modes). | `auto` | > **Install per-project, not globally.** Run `mcs sync` from each project's root (the directory that contains `.claude/`, not `.claude/` itself) — do **not** install this pack into your user-level `~/.claude/` directory. > > The hooks anchor on their own on-disk location and operate on a sibling `.memories-repo/` working tree. A single global install would collapse every project onto one shared checkout, which breaks two things: > -> 1. **Concurrent sessions race on the same working tree.** If you run Claude in two projects at the same time, both Stop hooks try to stage, commit, rebase, and push against the same `.git` index — you'll see interleaved commits, half-applied rebases, and dedupe state (`review` mode's `.review-shown`) stomped between sessions. +> 1. **Concurrent sessions race on the same working tree.** If you run Claude in two projects at the same time, both Stop hooks try to stage, commit, rebase, and push against the same `.git` index — you'll see interleaved commits and half-applied rebases. > 2. **Project context bleeds across sessions.** Memories Claude writes while working on project A would be auto-pushed under project B's session if B happens to end its turn first. > > Per-project installs keep one independent clone per repo while still sharing the same remote, so the team-wide KB stays unified without the local race conditions. @@ -202,14 +196,11 @@ During `mcs sync`, you'll be prompted for: shared-memories/ ├── techpack.yaml # Manifest — defines all components ├── commands/ -│ └── approve-memories.md # Slash command for review-mode approval -├── config/ -│ └── settings.json # Templated env block — ships MEMORIES_AUTOPUSH_MODE +│ └── resolve-memories.md # Slash command for conflicts and stuck pushes ├── runtime/ # Installed to .claude/hooks/shared-memories/ │ ├── pull.mts # SessionStart: pull + stuck-state warning │ ├── autopush.mts # Stop: auto-commit + push (async) -│ ├── announce.mts # PostToolUse: review-mode nudge to Claude (sync) -│ └── lib/ # git, paths, naming, mode, pending, report, push +│ └── lib/ # git, paths, naming, pending, commit-message, push ├── scripts/ # Run in place from the pack checkout │ ├── configure-memories.ts # Sparse clone + symlink + migration │ ├── doctor-memories.ts # Setup health check @@ -230,7 +221,6 @@ On engineer disks, the pack materializes as: ├── hooks/shared-memories/ # run by mcs via each hook's declared hookInterpreter │ ├── pull.mts │ ├── autopush.mts -│ ├── announce.mts │ └── lib/ # imported as ./lib/… by the entries beside it ├── .memories-repo/ # sparse clone of MEMORIES_BRANCH │ ├── README.md, LICENSE, etc. # any root-level files your repo ships @@ -315,61 +305,34 @@ Free-push doesn't mean unprotected. Apply a repo-level ruleset to your side bran | `non_fast_forward` | `git push --force` | Prevents history rewrite that could erase content between reflog expiries | | `deletion` | `git push origin :` | Prevents catastrophic branch wipeout | -Normal commits (including ones that delete files via `memory-audit`) are unaffected, so the audit + manual-push flow still works. +Normal commits (including ones that delete files via `memory-audit`) are unaffected. --- -## 🚦 Auto-Push Modes +## 🧯 Conflicts and Stuck Pushes -The Stop hook's behavior is set during `mcs sync` via the `MEMORIES_AUTOPUSH_MODE` prompt. The chosen value is written to `.claude/settings.local.json`'s `env` block (per-user / project-local). To change modes later, re-run `mcs sync` and pick a different value, or edit `.claude/settings.local.json` directly. +Every change under `memories/` — additions, edits and deletions — is pushed at the end of the turn that made it. A memory removed by mistake comes back from history: -| Mode | Adds / Modifications | Deletions | When to use | -|------|----------------------|-----------|-------------| -| `auto` *(default)* | auto-pushed | parked for manual review | Safe default for most teams. The asymmetric trust matches how memories are typically created vs. removed. | -| `full` | auto-pushed | auto-pushed | You trust your workflow — `memory-audit` runs are deliberate, no fat-finger risk. Skips the Intentional Deletion Workflow below. | -| `review` | not pushed | not pushed | You want to inspect every change before it propagates to the team. The hook prints a per-file report each turn end. | - -Unset, empty, and unrecognized values fall through to `auto` (with a one-line warning for unrecognized values), so existing installs see zero behavior change. - -In `review` mode, the hook prints a report on each turn end: - -``` -Shared memories [review mode]: pending file(s) in memories/ and unpushed commit(s) -+ NEW - "" -~ MOD (+3 -1) - Diff: git -C .claude/.memories-repo diff -- memories/decision_bar.md -- DEL memories/learning_old.md (last modified 3 weeks ago) - Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_old.md - -Approve all: /approve-memories [optional commit reason] -Discard local changes: … +```bash +git -C .claude/.memories-repo log --diff-filter=D --oneline -- memories/ # find the commit that removed it +git -C .claude/.memories-repo checkout ^ -- memories/ # restore it; the next Stop pushes it ``` -`/approve-memories` (shipped by this pack) stages everything under `memories/`, commits as `review: ` (default: `approved memories from `), pulls `--rebase --autostash`, and pushes. It re-runs the Stop-hook filename guardrail, so a stray `wip.md` blocks the push. Both you and Claude invoke the same command — no copy-paste of multi-line git incantations. Also unblocks `auto` / `full` state stuck from a previous push failure. - -The same pending set is reported once per session — repeated turns within the session stay silent so the report doesn't spam every prompt. SessionStart resets the dedupe so unresolved changes re-surface in the next session instead of being buried forever. +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: -In `review` mode, Claude is also told about each memory write through a separate PostToolUse hook (`announce.mts`), so it can proactively mention pending review in the same turn and invoke `/approve-memories` when you confirm — without needing to wait for the terminal report. The terminal report and the in-conversation nudge are independent channels: the report goes to your terminal, the nudge goes to Claude's context. `auto` and `full` modes keep both channels silent. - -Pull is always automatic regardless of mode — incoming team memories arrive at session start. - ---- - -## 🧹 Intentional Deletion Workflow - -When you legitimately want to remove stale memories (typically after running the `memory-audit` skill from `mcs-cli/memory`), invoke the slash command: +| 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/` | +| **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` | ``` -/approve-memories audit cleanup +/resolve-memories [optional reason] ``` -That stages the deletions, commits as `review: audit cleanup`, pulls `--rebase`, and pushes — same recipe the slash command uses for any other approval, with the filename guardrail re-applied. - -The deletion block in `auto` mode is deliberate friction: audit is rare enough (monthly-ish) that requiring explicit human confirmation is cheap insurance against catastrophic local-delete-then-auto-push accidents. The slash command is that confirmation step in one keystroke. - -If your workflow makes the friction unnecessary, set `MEMORIES_AUTOPUSH_MODE=full` to skip the manual step entirely — `memory-audit`'s deletions will then auto-push alongside any other writes. - --- ## 🔧 Troubleshooting @@ -387,11 +350,9 @@ git -C .claude/.memories-repo status -- memories/ # dirty files git -C .claude/.memories-repo ls-files --others --exclude-standard -- memories/ # untracked ``` -Anything not matching `memories/(learning|decision)_*.md` needs renaming. +Anything not matching `memories/(learning|decision)_*.md` needs renaming — or run `/resolve-memories`. -**If SessionStart warns about lingering state**: -- In `auto` / `full` mode the previous push hit an auth/network issue (fix and wait for the next Stop, or run `/approve-memories` to retry immediately), or guardrail-rejected files are sitting dirty (rename them). `mcs doctor` will tell you which. -- In `review` mode the warning is expected — it lists pending changes awaiting your decision. End a turn to see the per-file report, then `/approve-memories` to push. +**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`. --- @@ -408,7 +369,9 @@ TypeScript run directly by Node: no build step, no runtime dependencies, no lock Fixtures carry an `expect` pattern asserted against the recording, so a fixture where nothing happens fails rather than passing vacuously. Machine- and day-dependent values — temp paths, hostname, dates, git's relative timestamps — are normalised; everything else is byte-exact. -CI runs on macOS across Node 22 and 24. Besides the typecheck and the suite it guards three things: the pack contains no shell at all, `hostname -s` still matches `os.hostname().split(".")[0]` (the commit subjects depend on it), and the suite leaves the working tree clean. +CI runs on macOS across Node 22 and 24. Besides the typecheck and the suite it guards two things: the pack contains no shell at all, and the suite leaves the working tree clean. + +To re-record after an intended behaviour change, run `UPDATE_GOLDENS=1 npm test` and review the diff of `tests/golden/` line by line: a recording is only as right as the behaviour it captured. --- diff --git a/commands/approve-memories.md b/commands/approve-memories.md deleted file mode 100644 index 083ddbf..0000000 --- a/commands/approve-memories.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: Approve and push pending shared memories ---- - -# Approve Shared Memories - -Stage, commit, pull --rebase, and push everything pending under `.claude/.memories-repo/memories/`. Works in any mode; primary use is `review`. - -Arguments: $ARGUMENTS (optional commit-message reason). If omitted, default to `approved memories from `. - -## 1. Check pending state - -Run `git -C .claude/.memories-repo status --porcelain -- memories/` and `git -C .claude/.memories-repo rev-list '@{u}..HEAD' --count` (treat missing upstream as 0). If both are empty, report **"Nothing to approve."** and stop. - -## 2. Filename guardrail - -Every dirty file under `memories/` must match `^memories/(learning|decision)_[a-zA-Z0-9_-]+\.md$` — the same pattern the Stop hook enforces, defined once in `runtime/lib/naming.mts`. If any file fails, list the offenders, instruct the user to rename them to `memories/learning__.md` or `memories/decision__.md`, and stop without committing. - -## 3. Stage, commit, push - -- `git -C .claude/.memories-repo add -A -- memories/` -- Compose the commit subject: `review: ` where `` is `$ARGUMENTS` if given, else `approved memories from $(hostname -s) $(date +%F)`. -- `git -C .claude/.memories-repo commit -m ""` — skip if nothing got staged (only unpushed commits pending). -- `git -C .claude/.memories-repo pull --rebase --autostash` -- `git -C .claude/.memories-repo push` - -If `pull --rebase` reports a conflict, run `git -C .claude/.memories-repo rebase --abort` and tell the user to resolve manually in `.claude/.memories-repo/memories`. Do not push. - -## 4. Report - -Tell the user the commit subject, the number of files committed, and that the push landed (or didn't, with the underlying error). diff --git a/commands/resolve-memories.md b/commands/resolve-memories.md new file mode 100644 index 0000000..5988788 --- /dev/null +++ b/commands/resolve-memories.md @@ -0,0 +1,64 @@ +--- +description: Fix what the shared-memories Stop hook can't fix on its own, then push +--- + +# Resolve Shared Memories + +The Stop hook commits and pushes every memory change at the end of each turn, and retries network failures and rejected pushes by itself. This command handles what retrying never fixes: a rebase conflict, a file the naming guardrail rejects, a checkout with no upstream, or a rebase someone left half-finished. Auth and git-identity failures need a human; diagnose them and say exactly what to run. + +Arguments: $ARGUMENTS (optional reason for the commit subject). + +All git commands run against the memories repo as `git -C .claude/.memories-repo …`, with `-- memories/` as the pathspec. Never `-C .claude/memories`: that puts git's cwd inside `memories/`, and the pathspec then matches nothing. + +## 1. Read the state + +- `git -C .claude/.memories-repo status --porcelain -- memories/`: dirty files +- `git -C .claude/.memories-repo rev-parse --abbrev-ref --symbolic-full-name '@{u}'`: the upstream (fails if none) +- `git -C .claude/.memories-repo rev-list '@{u}..HEAD' --count`: unpushed commits +- whether `git -C .claude/.memories-repo rev-parse --git-path rebase-merge` or `rebase-apply` exists: a rebase in progress + +If nothing is dirty, nothing is unpushed, an upstream exists and no rebase is in progress, report **"Nothing to resolve."** and stop. + +## 2. A rebase already in progress + +Go straight to step 6 and finish it. Don't commit anything new first. + +## 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: + +- read it and propose a conforming name: `learning__.md` for something learned, `decision__.md` for a decision taken; +- ask the user if the type is unclear, or if the file looks like scratch that shouldn't be shared at all; +- rename it with `git -C .claude/.memories-repo mv` if tracked, plain `mv` if untracked. + +## 4. No upstream + +Set it: `git -C .claude/.memories-repo branch --set-upstream-to=origin/`. If that remote branch doesn't exist, stop and tell the user. The branch is chosen during `mcs sync`. + +## 5. Commit + +- `git -C .claude/.memories-repo add -A -- memories/` +- Skip the commit if nothing got staged (only unpushed commits are pending). +- Subject: `: resolve — `. `` is the author git will use: the part of `git -C .claude/.memories-repo var GIT_AUTHOR_IDENT` before ` <`. `` is `$ARGUMENTS`, or a few words on what you fixed. +- If the commit fails because git has no identity, stop and tell the user to set `git config --global user.name` and `user.email`. + +## 6. Rebase onto the team's work, resolving conflicts + +`git -C .claude/.memories-repo pull --rebase --autostash`. For each conflicted file (`git -C .claude/.memories-repo diff --name-only --diff-filter=U`): + +- **Both sides edited a memory**: write one version that keeps every fact from both sides. Drop only exact duplicates, and keep the file's existing structure. Show the user the merged result. +- **One side deleted it, the other edited it**: ask the user whether to keep the edited version or let the deletion stand. +- **A conflict outside `memories/`**: `git -C .claude/.memories-repo rebase --abort`, tell the user, and stop. + +Then `git -C .claude/.memories-repo add -- ` and `GIT_EDITOR=true git -C .claude/.memories-repo rebase --continue`, repeating until the rebase completes. **Never finish this command with a rebase still in progress.** Either complete it or abort it. + +## 7. Push + +`git -C .claude/.memories-repo push`. + +- Rejected because a teammate pushed meanwhile: go back to step 6, once. +- Any other failure (auth, network, repository): stop. Suggest `ssh-add` for SSH remotes, a credential refresh for HTTPS, and `mcs doctor` to confirm remote access. Don't retry in a loop; the next Stop retries on its own. + +## 8. Report + +Tell the user what was wrong, what you changed (renames, conflict merges, upstream), the commit subject, and whether the push landed. diff --git a/config/settings.json b/config/settings.json deleted file mode 100644 index 9704f60..0000000 --- a/config/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "MEMORIES_AUTOPUSH_MODE": "__MEMORIES_AUTOPUSH_MODE__" - } -} diff --git a/runtime/announce.mts b/runtime/announce.mts deleted file mode 100755 index 119ee14..0000000 --- a/runtime/announce.mts +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning -import { additionalContext, failOpen, readStdin, streamValues } from "./lib/hook-io.mts"; -import { MEMORY_WRITE_PATTERN } from "./lib/naming.mts"; - -const isObject = (v: unknown): v is Record => - typeof v === "object" && v !== null && !Array.isArray(v); - -/** How `jq -r` renders one result: strings raw, everything else as pretty JSON. */ -const render = (v: unknown): string => (typeof v === "string" ? v : JSON.stringify(v, null, 2)); - -/** - * `jq -r '.tool_input.file_path // empty'` over every value on stdin, joined by - * newlines. jq does not stop at an erroring value — it skips it and carries on — - * and its exit status reflects only the LAST value. The bash's `|| exit 0` therefore - * fires only when the final value errored, which is what null means here. - */ -function filePaths(values: readonly unknown[]): string | null { - const out: string[] = []; - let lastErrored = false; - for (const value of values) { - lastErrored = false; - - let toolInput: unknown = null; - if (isObject(value)) toolInput = value["tool_input"] ?? null; - else if (value !== null) { - lastErrored = true; - continue; - } - - let filePath: unknown = null; - if (isObject(toolInput)) filePath = toolInput["file_path"] ?? null; - else if (toolInput !== null) { - lastErrored = true; - continue; - } - - if (filePath === null || filePath === false) continue; - out.push(render(filePath)); - } - // `$(...)` strips trailing newlines, and jq emits one per result. - return lastErrored ? null : out.join("\n").replace(/\n+$/, ""); -} - -failOpen("memories_announce", () => { - const values = streamValues(readStdin()); - if (values === null) return; - - const filePath = filePaths(values); - if (filePath === null || !MEMORY_WRITE_PATTERN.test(filePath)) return; - - // The bash matches the literal, not the resolved mode: unknown values are silent here. - if (process.env["MEMORIES_AUTOPUSH_MODE"] !== "review") return; - - additionalContext( - "PostToolUse", - `Memory file saved at ${filePath} (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.`, - ); -}); diff --git a/runtime/autopush.mts b/runtime/autopush.mts index 95c164d..443f55f 100755 --- a/runtime/autopush.mts +++ b/runtime/autopush.mts @@ -1,14 +1,10 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning -import { existsSync, renameSync, rmSync, writeFileSync } from "node:fs"; -import { hostname } from "node:os"; -import { join } from "node:path"; -import { git, gitPresent, isWorkTree, unpushedCount } from "./lib/git.mts"; +import { authorName, git, gitPresent, isWorkTree, operationInProgress, unpushedCount } from "./lib/git.mts"; +import { commitMessage, stagedChanges } from "./lib/commit-message.mts"; import { failOpen, isJsonStream, readStdin, say, warn } from "./lib/hook-io.mts"; -import { resolveMode } from "./lib/mode.mts"; -import { badNames, collect, headSha, modifiedPaths, uncommittedCount } from "./lib/pending.mts"; +import { badNames, uncommittedCount } from "./lib/pending.mts"; import { memoriesRepo, projectRoot } from "./lib/paths.mts"; import { RENAME_HINT } from "./lib/naming.mts"; -import { canonicalState, hashState, lastShownHash, renderReport } from "./lib/report.mts"; import { syncToRemote } from "./lib/push.mts"; const NAME = "memories_autopush"; @@ -19,12 +15,6 @@ const stage = (repo: string, args: readonly string[]): void => { if (!r.ok) throw new Error(`git ${args.join(" ")} failed (exit ${r.exit ?? r.failure})`); }; -const today = (): string => { - const d = new Date(); - const p = (n: number) => String(n).padStart(2, "0"); - return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; -}; - failOpen(NAME, () => { if (!gitPresent()) return warn(`${NAME}: git not found; skipping`); if (!isJsonStream(readStdin())) return warn(`${NAME}: stdin is not valid JSON; skipping`); @@ -35,89 +25,41 @@ failOpen(NAME, () => { return warn(`${NAME}: ${repo} is not a git worktree; skipping (project_root=${project})`); } - const { mode } = resolveMode(process.env["MEMORIES_AUTOPUSH_MODE"]); - const reviewState = join(repo, ".review-shown"); - - const uncommitted = uncommittedCount(repo); - let unpushed = unpushedCount(repo); - - if (uncommitted === 0 && unpushed === 0) { - if (mode === "review" && existsSync(reviewState)) rmSync(reviewState, { force: true }); + const op = operationInProgress(repo); + if (op !== null) { + say(`Shared memories: a ${op} is in progress in .claude/.memories-repo — auto-push paused. Run /resolve-memories to finish it.`); return; } - const pending = collect(repo, uncommitted, unpushed); + const uncommitted = uncommittedCount(repo); + let unpushed = unpushedCount(repo); + if (uncommitted === 0 && unpushed === 0) return; if (uncommitted > 0) { - const bad = badNames(repo, pending.untracked); + const bad = badNames(repo); if (bad.length > 0) { say("Shared memories: skipping auto-push — unconventional filename(s):"); for (const f of bad) say(` - ${f}`); say(RENAME_HINT); + say("Or run /resolve-memories to have Claude rename them."); return; } - } - if (mode === "review") { - const state = canonicalState(pending, pending.unpushed > 0 ? headSha(repo) : ""); - if (state === "") { - // Renames, typechanges, conflicts, or an unresolvable HEAD reach none of the filters. - if (uncommitted > 0) { - say(`Shared memories [review mode]: ${uncommitted} unclassified pending change(s) in memories/`); - say(" Inspect: git -C .claude/.memories-repo status -- memories/"); - } - if (unpushed > 0) { - say( - `Shared memories [review mode]: ${unpushed} unpushed commit(s) (HEAD unresolvable — repo may be detached or mid-rebase)`, - ); - say(" Inspect: git -C .claude/.memories-repo status"); - } - return; - } - - const hash = hashState(state); - if (lastShownHash(reviewState) === hash) return; - - for (const line of renderReport(repo, pending)) say(line); - - // Atomic, so a killed hook cannot leave an empty state file behind. - writeFileSync(`${reviewState}.tmp`, `${hash}\n`); - renameSync(`${reviewState}.tmp`, reviewState); - return; - } - - let committed = false; - - if (uncommitted > 0) { - if (mode === "auto") { - if (pending.deleted.length > 0) { - say(`Shared memories: ${pending.deleted.length} deleted memory file(s) left for manual review (not auto-pushed):`); - for (const f of pending.deleted) say(` - ${f}`); - say("If intentional (e.g. after memory-audit), approve them:"); - say(" /approve-memories audit cleanup"); - } - const stageable = [...new Set([...modifiedPaths(pending), ...pending.untracked])].filter(Boolean).sort(); - for (const f of stageable) stage(repo, ["add", "--", f]); - } else { - stage(repo, ["add", "-A", "--", "memories/"]); - } + stage(repo, ["add", "-A", "--", "memories/"]); if (!git(repo, ["diff", "--cached", "--quiet", "--", "memories/"]).ok) { - const suffix = mode === "full" && pending.deleted.length > 0 ? " (includes deletions)" : ""; - const msg = `auto: memories from ${hostname().split(".")[0]} ${today()}${suffix}`; + const msg = commitMessage(authorName(repo), stagedChanges(repo)); const commit = git(repo, ["commit", "-m", msg, "--quiet"]); - if (commit.ok) { - committed = true; - } else { + if (!commit.ok) { say("Shared memories: commit failed; will retry on next Stop."); const err = `${commit.stdout}${commit.stderr}`.replace(/\n$/, ""); if (err !== "") process.stdout.write(` ${err}\n`); return; } + unpushed = unpushedCount(repo); } } - if (committed) unpushed = unpushedCount(repo); if (unpushed === 0) return; syncToRemote(repo, process.env["MEMORIES_PUSH_ATTEMPTS"]); diff --git a/runtime/lib/commit-message.mts b/runtime/lib/commit-message.mts new file mode 100644 index 0000000..3c8f9d0 --- /dev/null +++ b/runtime/lib/commit-message.mts @@ -0,0 +1,68 @@ +import { gitLines } from "./git.mts"; + +export type Change = + | { readonly kind: "add" | "update" | "remove"; readonly path: string } + | { readonly kind: "rename"; readonly path: string; readonly from: string }; + +/** `memories/learning_a_b.md` → `learning_a_b`: the path prefix and extension carry nothing. */ +const short = (p: string): string => p.replace(/^memories\//, "").replace(/\.md$/, ""); + +/** One `git diff --name-status` line. Copies (C) are new files as far as the team is concerned. */ +export function parseNameStatus(line: string): Change | null { + const [status = "", a = "", b = ""] = line.split("\t"); + switch (status[0]) { + case "A": + case "C": + return a === "" ? null : { kind: "add", path: b || a }; + case "M": + case "T": + return a === "" ? null : { kind: "update", path: a }; + case "D": + return a === "" ? null : { kind: "remove", path: a }; + case "R": + return a === "" || b === "" ? null : { kind: "rename", path: b, from: a }; + default: + return null; + } +} + +/** What is staged under memories/, in the order git lists it. */ +export function stagedChanges(repo: string): Change[] { + return gitLines(repo, ["diff", "--cached", "--name-status", "-M", "--", "memories/"]) + .map(parseNameStatus) + .filter((c): c is Change => c !== null); +} + +const MARK = { add: "+", update: "~", remove: "-", rename: ">" } as const; + +const line = (c: Change): string => + c.kind === "rename" ? `${MARK.rename} ${short(c.from)} → ${short(c.path)}` : `${MARK[c.kind]} ${short(c.path)}`; + +/** + * `: add learning_x` for a single change; otherwise a count summary in the + * subject and one line per file in the body. Who and what, not which machine: + * the date is already in the commit. + */ +export function commitMessage(who: string, changes: readonly Change[]): string { + if (changes.length === 0) return `${who}: update memories`; + if (changes.length === 1) { + const [c] = changes as [Change]; + const what = c.kind === "rename" ? `rename ${short(c.from)} → ${short(c.path)}` : `${c.kind} ${short(c.path)}`; + return `${who}: ${what}`; + } + const count = (k: Change["kind"]) => changes.filter((c) => c.kind === k).length; + const parts = ( + [ + ["add", "added"], + ["update", "updated"], + ["rename", "renamed"], + ["remove", "removed"], + ] as const + ) + .map(([k, label]) => [count(k), label] as const) + .filter(([n]) => n > 0) + .map(([n, label]) => `${n} ${label}`); + const order = { add: 0, update: 1, rename: 2, remove: 3 } as const; + const body = [...changes].sort((x, y) => order[x.kind] - order[y.kind] || (x.path < y.path ? -1 : x.path > y.path ? 1 : 0)).map(line); + return `${who}: ${parts.join(", ")}\n\n${body.join("\n")}`; +} diff --git a/runtime/lib/git.mts b/runtime/lib/git.mts index d81ac2d..3997b5e 100644 --- a/runtime/lib/git.mts +++ b/runtime/lib/git.mts @@ -1,4 +1,6 @@ import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { hostname } from "node:os"; export type GitRun = { readonly ok: boolean; @@ -62,6 +64,27 @@ export function unpushedCount(dir: string): number { return Number.isNaN(n) ? 0 : n; } +/** + * A rebase or merge someone left half-finished. Staging then would mark the + * conflicted files resolved, markers and all, and commit into the detached HEAD. + */ +export function operationInProgress(dir: string): "rebase" | "merge" | null { + const exists = (name: string): boolean => { + const p = gitOut(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]); + return p !== "" && existsSync(p); + }; + if (exists("rebase-merge") || exists("rebase-apply")) return "rebase"; + if (exists("MERGE_HEAD")) return "merge"; + return null; +} + +/** The name git will author the next commit with, or the short hostname when git has none. */ +export function authorName(dir: string): string { + const ident = gitOut(dir, ["var", "GIT_AUTHOR_IDENT"]); + const name = /^(.*?) void): void { // A reader that goes away is not the hook's failure. Node raises EPIPE as an diff --git a/runtime/lib/mode.mts b/runtime/lib/mode.mts deleted file mode 100644 index 9de9190..0000000 --- a/runtime/lib/mode.mts +++ /dev/null @@ -1,21 +0,0 @@ -export const MODES = ["auto", "full", "review"] as const; -export type Mode = (typeof MODES)[number]; - -export type ModeResult = - | { readonly mode: Mode; readonly unrecognised: null } - | { readonly mode: "auto"; readonly unrecognised: string }; - -export function resolveMode(raw: string | undefined): ModeResult { - switch (raw) { - case undefined: - case "": - case "auto": - return { mode: "auto", unrecognised: null }; - case "full": - return { mode: "full", unrecognised: null }; - case "review": - return { mode: "review", unrecognised: null }; - default: - return { mode: "auto", unrecognised: raw }; - } -} diff --git a/runtime/lib/naming.mts b/runtime/lib/naming.mts index ffde79d..141b92d 100644 --- a/runtime/lib/naming.mts +++ b/runtime/lib/naming.mts @@ -1,7 +1,5 @@ /** The one definition. Bash carried four copies kept in step by comment. */ export const ALLOWED_PATTERN = /^memories\/(learning|decision)_[a-zA-Z0-9_-]+\.md$/; -export const MEMORY_WRITE_PATTERN = /(^|.*\/)\.claude\/memories\/(learning|decision)_[a-zA-Z0-9_-]+\.md$/; - export const RENAME_HINT = "Rename to memories/learning__.md or memories/decision__.md so the guardrail accepts them."; diff --git a/runtime/lib/pending.mts b/runtime/lib/pending.mts index 98ebc9a..d41e1b3 100644 --- a/runtime/lib/pending.mts +++ b/runtime/lib/pending.mts @@ -1,52 +1,14 @@ -import { gitLines, gitOut, unpushedCount } from "./git.mts"; +import { gitLines } from "./git.mts"; import { ALLOWED_PATTERN } from "./naming.mts"; -export type NumStat = { readonly added: string; readonly deleted: string; readonly path: string }; - -export type Pending = { - readonly uncommitted: number; - readonly unpushed: number; - readonly untracked: readonly string[]; - readonly numstats: readonly NumStat[]; - readonly deleted: readonly string[]; -}; - -/** The paths `git diff --numstat --diff-filter=AM` reported, derived rather than stored. */ -export const modifiedPaths = (p: Pending): string[] => p.numstats.map((n) => n.path).filter(Boolean); - export function uncommittedCount(repo: string): number { return gitLines(repo, ["status", "--porcelain", "--", "memories/"]).length; } -export function collect(repo: string, uncommitted: number, unpushed: number): Pending { - if (uncommitted === 0) { - return { uncommitted, unpushed, untracked: [], numstats: [], deleted: [] }; - } - const untracked = gitLines(repo, ["ls-files", "--others", "--exclude-standard", "--full-name", "--", "memories/"]); - const numstats = gitLines(repo, ["diff", "--numstat", "--diff-filter=AM", "HEAD", "--", "memories/"]) - .map((l) => l.split("\t")) - .filter((p): p is [string, string, string] => p.length === 3) - .map(([added, deleted, path]) => ({ added, deleted, path })); - return { - uncommitted, - unpushed, - untracked, - numstats, - deleted: gitLines(repo, ["diff", "--name-only", "--diff-filter=D", "HEAD", "--", "memories/"]), - }; -} - -/** `{ diff --name-only HEAD ; untracked } | sort -u`, then the guardrail. */ -export function badNames(repo: string, untracked: readonly string[]): string[] { +/** Dirty paths under memories/ that fail the naming rule: `{ diff --name-only HEAD ; untracked } | sort -u`. */ +export function badNames(repo: string): string[] { const tracked = gitLines(repo, ["diff", "--name-only", "HEAD", "--", "memories/"]); + const untracked = gitLines(repo, ["ls-files", "--others", "--exclude-standard", "--full-name", "--", "memories/"]); const dirty = [...new Set([...tracked, ...untracked])].filter((f) => f !== "").sort(); return dirty.filter((f) => !ALLOWED_PATTERN.test(f)); } - -export function recountUnpushed(repo: string): number { - return unpushedCount(repo); -} - -export function headSha(repo: string): string { - return gitOut(repo, ["rev-parse", "HEAD"]); -} diff --git a/runtime/lib/push.mts b/runtime/lib/push.mts index cfe3844..d62462c 100644 --- a/runtime/lib/push.mts +++ b/runtime/lib/push.mts @@ -36,11 +36,11 @@ export function syncToRemote(repo: string, attemptsRaw: string | undefined): voi 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. Resolve manually in .claude/.memories-repo/memories.", + "Shared memories: rebase conflict AND --abort failed — repo may be in a half-rebased state. Run /resolve-memories to finish it.", ); detail(abort.stderr.replace(/\n$/, "")); } else { - say("Shared memories: auto-push paused — rebase conflict. Resolve manually in .claude/.memories-repo/memories."); + say("Shared memories: auto-push paused — rebase conflict. Run /resolve-memories to merge it."); } } else { say("Shared memories: pull --rebase failed (likely auth or network). Will retry on next Stop."); diff --git a/runtime/lib/report.mts b/runtime/lib/report.mts deleted file mode 100644 index 9774c78..0000000 --- a/runtime/lib/report.mts +++ /dev/null @@ -1,109 +0,0 @@ -import { createHash } from "node:crypto"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { gitOut } from "./git.mts"; -import { modifiedPaths, type Pending } from "./pending.mts"; - -/** Only these three break terminal autolinking of file:// URLs; the bash leaves the rest. */ -export function urlEncodePath(p: string): string { - return p.replaceAll(" ", "%20").replaceAll("#", "%23").replaceAll("?", "%3F"); -} - -/** `tr -d '\000-\010\013\014\016-\037\177'`. Tab, LF and CR are deliberately NOT in that set. */ -const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g; - -export function preview(repo: string, file: string): string { - let text: string; - try { - text = readFileSync(join(repo, file), "utf8"); - } catch { - return ""; - } - // grep's [[:space:]] includes CR, so a CRLF file's blank line is blank here too. - const first = text.split("\n").find((l) => !/^[\t\n\v\f\r ]*$/.test(l)) ?? ""; - // bash ${var:0:80} counts characters, so an astral char is one, not two. - return Array.from(first.replace(CONTROL, "")).slice(0, 80).join(""); -} - -/** - * The hash the last review printed, or "" when it cannot be read. - * The bash was `tr -d '[:space:]' < "$f" 2>/dev/null || true`, so an unreadable - * state file meant no dedupe rather than no report. Reading it is racy besides: - * a concurrent session's Stop hook can remove it between the check and the read, - * and losing the whole report to that is the one outcome this pack avoids. - */ -export function lastShownHash(stateFile: string): string { - try { - return readFileSync(stateFile, "utf8").replace(/\s/g, ""); - } catch { - return ""; - } -} - -export function canonicalState(p: Pending, headSha: string): string { - const lines = [ - ...p.untracked.filter(Boolean).map((f) => `NEW\t${f}`), - ...modifiedPaths(p).map((f) => `MOD\t${f}`), - ...p.deleted.filter(Boolean).map((f) => `DEL\t${f}`), - ]; - if (p.unpushed > 0 && headSha !== "") lines.push(`UNPUSHED\t${headSha}\t${p.unpushed}`); - return lines.sort().join("\n"); -} - -export function hashState(state: string): string { - return createHash("sha256").update(state).digest("hex"); -} - -export function describe(fileCount: number, unpushed: number): string { - if (fileCount > 0 && unpushed > 0) { - return `${fileCount} pending file(s) in memories/ and ${unpushed} unpushed commit(s)`; - } - if (fileCount > 0) return `${fileCount} pending file(s) in memories/`; - return `${unpushed} unpushed commit(s)`; -} - -export function renderReport(repo: string, p: Pending): string[] { - const out: string[] = []; - const news = p.untracked.filter(Boolean); - const mods = modifiedPaths(p); - const dels = p.deleted.filter(Boolean); - out.push(`Shared memories [review mode]: ${describe(news.length + mods.length + dels.length, p.unpushed)}`); - - for (const f of news) { - out.push(`+ NEW `); - const pv = preview(repo, f); - if (pv !== "") out.push(` "${pv}"`); - } - - for (const { added, deleted, path } of p.numstats) { - if (path === "") continue; - out.push(`~ MOD (+${added} -${deleted})`); - out.push(` Diff: git -C .claude/.memories-repo diff -- ${path}`); - } - - // Per-file `git log` is one process each; the cap keeps a large audit predictable. - let idx = 0; - for (const f of dels) { - idx++; - if (idx <= 20) { - const last = gitOut(repo, ["log", "-1", "--format=%cr", "HEAD", "--", f]) || "?"; - out.push(`- DEL ${f} (last modified ${last})`); - } else { - out.push(`- DEL ${f}`); - } - out.push(` Recover: git -C .claude/.memories-repo checkout HEAD -- ${f}`); - } - - out.push(""); - if (p.unpushed > 0) { - out.push(` Note: ${p.unpushed} local commit(s) not yet on the remote.`); - out.push( - " Push: git -C .claude/.memories-repo pull --rebase --autostash && git -C .claude/.memories-repo push", - ); - out.push(""); - } - out.push("Approve all: /approve-memories [optional commit reason]"); - out.push("Discard local changes: git -C .claude/.memories-repo checkout -- memories/ \\"); - out.push(" && git -C .claude/.memories-repo clean -f -- memories/"); - return out; -} diff --git a/runtime/pull.mts b/runtime/pull.mts index 4bf23d5..6d467b1 100755 --- a/runtime/pull.mts +++ b/runtime/pull.mts @@ -1,9 +1,7 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning -import { rmSync } from "node:fs"; -import { join } from "node:path"; import { additionalContext, failOpen, isJsonStream, readStdin, warn } from "./lib/hook-io.mts"; -import { gitLines, gitPresent, isWorkTree, unpushedCount, git } from "./lib/git.mts"; -import { MODES, resolveMode } from "./lib/mode.mts"; +import { git, gitPresent, hasUpstream, isWorkTree, operationInProgress, unpushedCount } from "./lib/git.mts"; +import { uncommittedCount } from "./lib/pending.mts"; import { memoriesRepo, projectRoot } from "./lib/paths.mts"; const NAME = "memories_pull"; @@ -18,41 +16,38 @@ failOpen(NAME, () => { return warn(`${NAME}: ${repo} is not a git worktree; skipping (project_root=${project})`); } - const raw = process.env["MEMORIES_AUTOPUSH_MODE"]; - const { mode, unrecognised } = resolveMode(raw); - const modeWarning = - unrecognised === null - ? "" - : `Shared memories: unknown MEMORIES_AUTOPUSH_MODE='${unrecognised}' — falling back to auto. Fix the value in .claude/settings.local.json (valid: ${MODES.join(", ")}) and restart the session.`; - - git(repo, ["pull", "--ff-only", "--quiet"]); - - if (mode === "review") { - try { - rmSync(join(repo, ".review-shown"), { force: true }); - } catch { - /* the bash ignores this too */ - } + const op = operationInProgress(repo); + if (op !== null) { + additionalContext( + "SessionStart", + `Shared memories: a ${op} is in progress in .claude/.memories-repo, so auto-push is paused and teammates' memories were not pulled. Run /resolve-memories to finish it.`, + ); + return; } - const uncommitted = gitLines(repo, ["status", "--porcelain", "--", "memories/"]).length; - const unpushed = unpushedCount(repo); - - let pending = ""; - if (uncommitted > 0 || unpushed > 0) { - const joined = - uncommitted > 0 && unpushed > 0 - ? `${uncommitted} uncommitted file(s), ${unpushed} unpushed commit(s)` - : uncommitted > 0 - ? `${uncommitted} uncommitted file(s)` - : `${unpushed} unpushed commit(s)`; - pending = - mode === "review" - ? `Shared memories: ${joined} awaiting review (MEMORIES_AUTOPUSH_MODE=review). End a turn to see the per-file report with approve/discard commands.` - : `Shared memories have lingering state: ${joined}. The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.`; + // Without an upstream every Stop commits locally and nothing ever reaches the team. + if (!hasUpstream(repo)) { + additionalContext( + "SessionStart", + "Shared memories: the checkout in .claude/.memories-repo has no upstream branch, so memories are committed locally but never pushed or pulled. Run /resolve-memories to fix it.", + ); + return; } - const msg = modeWarning && pending ? `${modeWarning}\n\n${pending}` : modeWarning || pending; - if (msg === "") return; - additionalContext("SessionStart", msg); + git(repo, ["pull", "--ff-only", "--quiet"]); + + const uncommitted = uncommittedCount(repo); + const unpushed = unpushedCount(repo); + if (uncommitted === 0 && unpushed === 0) return; + + const joined = + uncommitted > 0 && unpushed > 0 + ? `${uncommitted} uncommitted file(s), ${unpushed} unpushed commit(s)` + : uncommitted > 0 + ? `${uncommitted} uncommitted file(s)` + : `${unpushed} unpushed commit(s)`; + additionalContext( + "SessionStart", + `Shared memories have lingering state: ${joined}. The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.`, + ); }); diff --git a/scripts/configure-memories.ts b/scripts/configure-memories.ts index c382de2..cf23309 100755 --- a/scripts/configure-memories.ts +++ b/scripts/configure-memories.ts @@ -1,8 +1,7 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync } from "node:fs"; -import { hostname } from "node:os"; import { join } from "node:path"; -import { git, gitLines, gitOut, gitPresent, isWorkTree } from "../runtime/lib/git.mts"; +import { authorName, git, gitLines, gitOut, gitPresent, isWorkTree } from "../runtime/lib/git.mts"; import { ALLOWED_PATTERN } from "../runtime/lib/naming.mts"; const out = (line: string) => process.stdout.write(`${line}\n`); @@ -245,9 +244,9 @@ function main(): void { if (!add.ok) process.exit(add.exit ?? 1); } // Unchecked, the push below succeeds vacuously and reports one that never happened. - const commit = git(repoDir, ["commit", "-m", `auto: migrate local memories from ${hostname().split(".")[0]}`, "--quiet"], { - inheritStderr: true, - }); + const body = good.map((f) => `+ ${f.replace(/^memories\//, "").replace(/\.md$/, "")}`).join("\n"); + const msg = `${authorName(repoDir)}: migrate ${good.length} local memor${good.length === 1 ? "y" : "ies"}\n\n${body}`; + const commit = git(repoDir, ["commit", "-m", msg, "--quiet"], { inheritStderr: true }); if (!commit.ok) process.exit(commit.exit ?? 1); if (git(repoDir, ["pull", "--rebase", "--autostash", "--quiet"]).ok) { if (git(repoDir, ["push", "--quiet"]).ok) { @@ -257,7 +256,7 @@ function main(): void { } } else { git(repoDir, ["rebase", "--abort"]); - out("Migrated memories committed locally; rebase conflict blocked the push. The next Stop hook will retry."); + out("Migrated memories committed locally; a rebase conflict blocked the push. Run /resolve-memories in a Claude session to merge it."); } } diff --git a/techpack.yaml b/techpack.yaml index 24ad479..677a161 100644 --- a/techpack.yaml +++ b/techpack.yaml @@ -26,18 +26,6 @@ prompts: label: "Branch name on the shared repo that holds memory files and this pack (e.g. main)" default: "main" - - key: MEMORIES_AUTOPUSH_MODE - type: select - label: "Stop-hook auto-push behavior" - options: - - value: auto - label: "auto — writes auto-pushed, deletions parked (default)" - - value: full - label: "full — writes AND deletions auto-pushed" - - value: review - label: "review — nothing auto, per-turn pending-changes report" - default: auto - components: # ── Dependencies ────────────────────────────────────────── - id: node @@ -62,7 +50,7 @@ components: # ── Hooks ───────────────────────────────────────────────── - id: hook-memories-pull displayName: Pull shared memories and flag stuck state - description: Fast-forwards the shared memories checkout on SessionStart. If a previous Stop hook's auto-push left uncommitted or unpushed work behind, emits a system-reminder warning so the engineer knows. + description: Fast-forwards the shared memories checkout on SessionStart. If a previous Stop hook's auto-push left uncommitted or unpushed work behind, a rebase is left half-finished, or the checkout has no upstream, emits a system-reminder so the engineer knows. hookEvent: SessionStart hookInterpreter: node --experimental-strip-types --disable-warning=ExperimentalWarning hookTimeout: 15 @@ -72,7 +60,7 @@ components: - id: hook-memories-autopush displayName: Sync shared memories - description: After Claude stops, handles new/modified memory files according to MEMORIES_AUTOPUSH_MODE (auto/full/review). Default auto matches today — writes auto-pushed, deletions parked for manual review. Unconventional filenames halt everything until fixed in all modes. See README for the mode reference. + description: After Claude stops, commits every change under memories/ — additions, edits and deletions — as the git user, then rebases and pushes. Unconventional filenames halt everything until renamed. hookEvent: Stop hookInterpreter: node --experimental-strip-types --disable-warning=ExperimentalWarning hookTimeout: 30 @@ -82,30 +70,14 @@ components: destination: autopush.mts # ── Commands ────────────────────────────────────────────── - - id: command-approve-memories - displayName: /approve-memories command - description: Slash command that stages, commits, pulls --rebase, and pushes all pending memory files. Primary use is `MEMORIES_AUTOPUSH_MODE=review` — the per-turn report tells both Claude and the user to invoke this to approve pending changes. Also unblocks state stuck from a previous push failure in auto/full mode. + - id: command-resolve-memories + displayName: /resolve-memories command + description: Slash command for what the Stop hook can't fix on its own — merges rebase conflicts, renames files the naming guardrail rejects, restores a missing upstream — then commits and pushes. Diagnoses auth and git-identity failures, which need a human. command: - source: commands/approve-memories.md - destination: approve-memories.md - - - id: hook-memories-announce - displayName: Surface review-mode memory writes to Claude - description: PostToolUse hook that injects additionalContext after a memory file is written, only when MEMORIES_AUTOPUSH_MODE=review. Lets Claude proactively mention pending memory review in conversation. Silent in auto/full modes. - hookEvent: PostToolUse - hookInterpreter: node --experimental-strip-types --disable-warning=ExperimentalWarning - hookMatcher: Write|Edit|MultiEdit - hookTimeout: 5 - hook: - source: runtime/announce.mts - destination: announce.mts + source: commands/resolve-memories.md + destination: resolve-memories.md # ── Configuration ───────────────────────────────────────── - - id: settings - displayName: Default auto-push mode - description: Writes the chosen MEMORIES_AUTOPUSH_MODE value into .claude/settings.local.json so the Stop hook reads it. Re-run `mcs sync` to change modes, or edit settings.local.json directly. - settingsFile: config/settings.json - - id: gitignore description: Globally ignore the memories symlink, the backing sparse checkout, and any migration backup dirs so they're never tracked by parent repos gitignore: diff --git a/templates/instructions.md b/templates/instructions.md index 40b4211..461ee32 100644 --- a/templates/instructions.md +++ b/templates/instructions.md @@ -3,9 +3,9 @@ `.claude/memories` → `.claude/.memories-repo/memories` — a symlink into a sparse checkout of a git repo the team shares. Treat it as shared state, not scratch. -- **Writes and deletions are proposals to the team's repo.** A Stop hook decides - when they propagate. Never delete or rewrite a memory file to tidy up unless - the user asked for it. +- **Writes and deletions reach the whole team.** The Stop hook commits and + pushes them, deletions included, at the end of every turn. Never delete or + rewrite a memory file to tidy up unless the user asked for it. - **The memory set changes from outside this session.** SessionStart pulls teammates' commits, so file counts and contents are not constants to cache. - **The parent project's git does not track memories.** `.claude/memories` and diff --git a/tests/differential.test.ts b/tests/differential.test.ts index c61cae9..d0a84d3 100644 --- a/tests/differential.test.ts +++ b/tests/differential.test.ts @@ -2,17 +2,21 @@ import { test, describe } from "node:test"; import assert from "node:assert/strict"; import { rmSync, symlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; -import { git, memory, readGolden, rejectAllPushes, runHook, type Fixture, type RunResult } from "./harness.ts"; - +import { git, memory, readGolden, rejectAllPushes, runHook, writeGolden, type Fixture, type RunResult } from "./harness.ts"; /** - * Behaviour is pinned to tests/golden/, recorded from the bash implementation - * this pack replaced and verified against it fixture by fixture before the bash - * was removed. A drift here is a change in what the pack does, not a stale test. + * Behaviour is pinned to tests/golden/. The goldens were first recorded from the + * bash implementation this pack replaced, and re-recorded from the TypeScript + * when the auto-push modes were removed, every diff reviewed by hand. A drift + * here is a change in what the pack does, not a stale test. + * + * `UPDATE_GOLDENS=1 npm test` re-records instead of asserting. Review the diff: + * the recording is only as right as the behaviour it captured. */ const ABORT = /^\w+: aborted.*$/m; function check(name: string, actual: RunResult, expect?: RegExp, abortsDiffer = false): void { + if (process.env["UPDATE_GOLDENS"] === "1") writeGolden(name, actual); const golden = readGolden(name); assert.ok(golden, `no golden recorded for "${name}"`); assert.equal(actual.stdout, golden.stdout, `stdout drifted from recorded behaviour for "${name}"`); @@ -33,123 +37,17 @@ function assertParity(fx: Fixture): void { check(fx.name, runHook(fx), fx.expect, fx.abortsDiffer); } -const announce: readonly Fixture[] = [ - { - name: "announce: review mode, well-named memory write", - expect: /"hookEventName": "PostToolUse"/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.md" } }), - }, - { - name: "announce: auto mode is silent", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.md" } }), - }, - { - name: "announce: unset mode is silent", - hook: "announce", - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.md" } }), - }, - { - name: "announce: unknown mode is silent", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "banana" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.md" } }), - }, - { - name: "announce: decision_ prefix also matches", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/decision_x_y.md" } }), - }, - { - name: "announce: name with a space is rejected", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_foo bar.md" } }), - }, - { - name: "announce: path outside .claude/memories is rejected", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/notes/learning_a_b.md" } }), - }, - { - name: "announce: nested prefix path still matches", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/deep/nest/.claude/memories/learning_a_b.md" } }), - }, - { - name: "announce: wrong prefix is rejected", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/scratch_a_b.md" } }), - }, - { - name: "announce: non-md extension is rejected", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.txt" } }), - }, - { - name: "announce: missing file_path", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: {} }), - }, - { - name: "announce: non-string file_path", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: 123 } }), - }, - { - name: "announce: empty stdin", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: "", - }, - { - name: "announce: malformed stdin", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: "not json at all", - }, - { - name: "announce: a path containing a quote", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: JSON.stringify({ tool_input: { file_path: '/p/"q"/.claude/memories/learning_a_b.md' } }), - }, -]; - -describe("announce — behaviour is pinned", () => { - for (const fx of announce) test(fx.name, () => assertParity(fx)); -}); - const pull: readonly Fixture[] = [ - { name: "pull: clean tree, auto", hook: "pull", env: { MEMORIES_AUTOPUSH_MODE: "auto" } }, - { name: "pull: clean tree, review", hook: "pull", env: { MEMORIES_AUTOPUSH_MODE: "review" } }, + { name: "pull: clean tree", hook: "pull" }, { - name: "pull: untracked memory, auto reports lingering state", + name: "pull: an untracked memory reports lingering state", expect: /lingering state/, hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, - setup: (repo) => memory(repo, "learning_pending_one.md"), - }, - { - name: "pull: untracked memory, review reports awaiting review", - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, setup: (repo) => memory(repo, "learning_pending_one.md"), }, { name: "pull: two untracked memories pluralise the same way", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_pending_one.md"); memory(repo, "learning_pending_two.md"); @@ -158,7 +56,6 @@ const pull: readonly Fixture[] = [ { name: "pull: unpushed commit only", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_committed_x.md"); git(repo, "add", "-A"); @@ -168,7 +65,6 @@ const pull: readonly Fixture[] = [ { name: "pull: uncommitted and unpushed together", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_committed_x.md"); git(repo, "add", "-A"); @@ -178,62 +74,29 @@ const pull: readonly Fixture[] = [ }, { name: "pull: no upstream configured", + expect: /no upstream branch/, hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { git(repo, "branch", "--unset-upstream"); memory(repo, "learning_pending_one.md"); }, }, - { - name: "pull: unknown mode warns, clean tree", - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "banana" }, - }, - { - name: "pull: unknown mode warning joins the pending message", - expect: /falling back to auto[^\n]*\\n\\nShared memories have lingering state/, - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "banana" }, - setup: (repo) => memory(repo, "learning_pending_one.md"), - }, - { - name: "pull: empty mode falls through to auto", - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "" }, - setup: (repo) => memory(repo, "learning_pending_one.md"), - }, - { - name: "pull: review mode clears the dedupe state file", - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => writeFileSync(join(repo, ".review-shown"), "deadbeef\n"), - }, - { - name: "pull: auto mode leaves the dedupe state file alone", - hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, - setup: (repo) => writeFileSync(join(repo, ".review-shown"), "deadbeef\n"), - }, { name: "pull: missing checkout", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => rmSync(repo, { recursive: true, force: true }), }, - { name: "pull: malformed stdin", hook: "pull", stdin: "not json", env: { MEMORIES_AUTOPUSH_MODE: "auto" } }, + { name: "pull: malformed stdin", hook: "pull", stdin: "not json" }, { name: "pull: empty stdin still does the work", hook: "pull", stdin: "", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => memory(repo, "learning_pending_one.md"), }, { name: "pull: concatenated JSON stream is accepted", hook: "pull", stdin: '{"a":1} {"b":2}', - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => memory(repo, "learning_pending_one.md"), }, ]; @@ -248,94 +111,61 @@ const commit = (repo: string, msg: string) => { }; const autopush: readonly Fixture[] = [ - { name: "autopush: nothing pending, auto", hook: "autopush", env: { MEMORIES_AUTOPUSH_MODE: "auto" } }, - { name: "autopush: nothing pending, review", hook: "autopush", env: { MEMORIES_AUTOPUSH_MODE: "review" } }, + { name: "autopush: nothing pending", hook: "autopush" }, { - name: "autopush: nothing pending clears stale review state", + name: "autopush: commits and pushes a new memory", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => writeFileSync(join(repo, ".review-shown"), "stale\n"), - }, - { - name: "autopush: auto commits and pushes a new memory", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => memory(repo, "learning_new_thing.md", "A brand new lesson.\n"), }, { - name: "autopush: auto pushes a modification", + name: "autopush: pushes a modification", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => memory(repo, "learning_seed_topic.md", "Rewritten seed.\n"), }, { - name: "autopush: auto parks a deletion", - expect: /deleted memory file\(s\) left for manual review/, + name: "autopush: pushes a deletion", + expect: /^$/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => rmSync(join(repo, "memories", "learning_seed_topic.md")), }, { - name: "autopush: auto parks deletions but still pushes an addition", + name: "autopush: several changes are summarised, one line per file in the body", + expect: /^$/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { - rmSync(join(repo, "memories", "learning_seed_topic.md")); - memory(repo, "learning_added_alongside.md"); + memory(repo, "learning_extra_topic.md", "Second seed.\n"); + commit(repo, "second seed"); + git(repo, "push", "-q"); + memory(repo, "learning_new_one.md"); + memory(repo, "decision_new_two.md"); + memory(repo, "learning_seed_topic.md", "Rewritten seed.\n"); + rmSync(join(repo, "memories", "learning_extra_topic.md")); }, }, { - name: "autopush: full pushes the deletion", - expect: /^$/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "full" }, - setup: (repo) => rmSync(join(repo, "memories", "learning_seed_topic.md")), - }, - { - name: "autopush: full without deletions omits the suffix", + name: "autopush: the subject names the git author, not the machine", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "full" }, + env: { GIT_AUTHOR_NAME: "Someone Else" }, setup: (repo) => memory(repo, "learning_new_thing.md"), }, { - name: "autopush: guardrail blocks a badly named file in auto", - expect: /^Shared memories: skipping auto-push — unconventional filename\(s\):\n - memories\/scratch\.md$/m, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, - setup: (repo) => memory(repo, "scratch.md"), - }, - { - name: "autopush: guardrail blocks in full too", + name: "autopush: guardrail blocks a badly named file", + expect: /^Shared memories: skipping auto-push — unconventional filename\(s\):\n - memories\/scratch\.md$[\s\S]*\/resolve-memories/m, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "full" }, - setup: (repo) => memory(repo, "scratch.md"), - }, - { - name: "autopush: guardrail blocks in review too", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, setup: (repo) => memory(repo, "scratch.md"), }, { name: "autopush: guardrail lists several offenders", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "scratch.md"); memory(repo, "wip notes.md"); memory(repo, "learning_ok_one.md"); }, }, - { - name: "autopush: unknown mode falls through to auto silently", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "banana" }, - setup: (repo) => memory(repo, "learning_new_thing.md"), - }, { name: "autopush: unpushed commit only", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_committed_x.md"); commit(repo, "local only"); @@ -344,7 +174,6 @@ const autopush: readonly Fixture[] = [ { name: "autopush: no upstream configured", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { git(repo, "branch", "--unset-upstream"); memory(repo, "learning_new_thing.md"); @@ -353,15 +182,13 @@ const autopush: readonly Fixture[] = [ { name: "autopush: missing checkout", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => rmSync(repo, { recursive: true, force: true }), }, - { name: "autopush: malformed stdin", hook: "autopush", stdin: "not json", env: { MEMORIES_AUTOPUSH_MODE: "auto" } }, + { name: "autopush: malformed stdin", hook: "autopush", stdin: "not json" }, { name: "autopush: empty stdin still pushes", hook: "autopush", stdin: "", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => memory(repo, "learning_new_thing.md"), }, ]; @@ -370,145 +197,6 @@ describe("autopush — behaviour is pinned", () => { for (const fx of autopush) test(fx.name, () => assertParity(fx)); }); -const ESC = String.fromCharCode(27); - -const review: readonly Fixture[] = [ - { - name: "review: a new file renders with its preview", - expect: /^\+ NEW \n {7}"First real line of the memory\."$/m, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "First real line of the memory.\nSecond line.\n"), - }, - { - name: "review: preview skips leading blank and whitespace-only lines", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "\n \n\t\nActual content here.\n"), - }, - { - name: "review: an empty file emits no preview line", - expect: /\+ NEW \n(?!ceva)(?! {7}")/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", ""), - }, - { - name: "review: a preview longer than 80 chars is truncated", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", `${"x".repeat(200)}\n`), - }, - { - name: "review: ANSI escapes in a memory never reach the terminal", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", `safe${ESC}[31mRED text\n`), - }, - { - name: "review: a bare carriage return is preserved, as the bash preserves it", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "before\rafter\n"), - }, - { - name: "review: a tab in the preview survives", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "before\tafter\n"), - }, - { - name: "review: a modification renders numstat", - expect: /^~ MOD \(\+3 -1\)$/m, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_seed_topic.md", "Line one.\nLine two.\nLine three.\n"), - }, - { - name: "review: a deletion renders its relative timestamp", - expect: /^- DEL memories\/learning_seed_topic\.md \(last modified .+\)$/m, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => rmSync(join(repo, "memories", "learning_seed_topic.md")), - }, - { - name: "review: new, modified and deleted together", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => { - memory(repo, "learning_extra_one.md", "Extra.\n"); - commit(repo, "add extra"); - git(repo, "push", "-q"); - memory(repo, "learning_brand_new.md", "Brand new.\n"); - memory(repo, "learning_seed_topic.md", "Modified seed.\n"); - rmSync(join(repo, "memories", "learning_extra_one.md")); - }, - }, - { - name: "review: pending files plus an unpushed commit", - expect: /pending file\(s\) in memories\/ and 1 unpushed commit\(s\)/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => { - memory(repo, "learning_committed_x.md"); - commit(repo, "local only"); - memory(repo, "learning_brand_new.md", "Brand new.\n"); - }, - }, - { - name: "review: unpushed commit with no dirty files", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => { - memory(repo, "learning_committed_x.md"); - commit(repo, "local only"); - }, - }, - { - name: "review: the same pending set is reported once", - expect: /^$/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - runs: 2, - setup: (repo) => memory(repo, "learning_new_thing.md", "A lesson.\n"), - }, - { - name: "review: a stale stored hash reprints", - expect: /Shared memories \[review mode\]/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "A lesson.\n"), - beforeEach: (repo) => writeFileSync(join(repo, ".review-shown"), "0000000000000000\n"), - }, - { - name: "review: twenty-one deletions straddle the timestamp cap", - expect: /^- DEL memories\/learning_bulk_20\.md$/m, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => { - for (let i = 0; i < 21; i++) memory(repo, `learning_bulk_${String(i).padStart(2, "0")}.md`, `Bulk ${i}.\n`); - commit(repo, "bulk"); - git(repo, "push", "-q"); - for (let i = 0; i < 21; i++) rmSync(join(repo, "memories", `learning_bulk_${String(i).padStart(2, "0")}.md`)); - }, - }, -]; - -const reviewChange: Fixture = { - name: "review: adding a file after a report reprints", - expect: /2 pending file\(s\)/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - runs: 2, - setup: (repo) => memory(repo, "learning_new_thing.md", "A lesson.\n"), - betweenRuns: (repo) => memory(repo, "learning_second_one.md", "Another lesson.\n"), -}; - -describe("review mode — behaviour is pinned", () => { - test(reviewChange.name, () => assertParity(reviewChange)); - for (const fx of review) test(fx.name, () => assertParity(fx)); -}); - /** A second clone that pushes first, so our push is rejected as non-fast-forward. */ const otherClonePushes = (repo: string, project: string, file: string, body: string) => { const work = dirname(project); @@ -527,7 +215,6 @@ const push: readonly Fixture[] = [ name: "push: a rejected push rebases and retries into success", expect: /^$/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { otherClonePushes(repo, project, "learning_theirs_one.md", "Theirs.\n"); memory(repo, "learning_ours_one.md", "Ours.\n"); @@ -535,9 +222,8 @@ const push: readonly Fixture[] = [ }, { name: "push: a rebase conflict pauses and aborts", - expect: /^Shared memories: auto-push paused — rebase conflict\. Resolve manually in \.claude\/\.memories-repo\/memories\.$/m, + expect: /^Shared memories: auto-push paused — rebase conflict\. Run \/resolve-memories to merge it\.$/m, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { otherClonePushes(repo, project, "learning_seed_topic.md", "Their version of the seed.\n"); memory(repo, "learning_seed_topic.md", "Our version of the seed.\n"); @@ -548,7 +234,6 @@ const push: readonly Fixture[] = [ name: "push: an unreachable remote reports auth or network", expect: /^Shared memories: pull --rebase failed \(likely auth or network\)\. Will retry on next Stop\.$/m, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_ours_one.md", "Ours.\n"); commit(repo, "local only"); @@ -558,7 +243,7 @@ const push: readonly Fixture[] = [ { name: "push: a zero attempt budget still tries once", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto", MEMORIES_PUSH_ATTEMPTS: "0" }, + env: { MEMORIES_PUSH_ATTEMPTS: "0" }, setup: (repo, project) => { otherClonePushes(repo, project, "learning_theirs_one.md", "Theirs.\n"); memory(repo, "learning_ours_one.md", "Ours.\n"); @@ -567,7 +252,7 @@ const push: readonly Fixture[] = [ { name: "push: a non-integer attempt budget falls back to the default", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto", MEMORIES_PUSH_ATTEMPTS: "banana" }, + env: { MEMORIES_PUSH_ATTEMPTS: "banana" }, setup: (repo, project) => { otherClonePushes(repo, project, "learning_theirs_one.md", "Theirs.\n"); memory(repo, "learning_ours_one.md", "Ours.\n"); @@ -584,9 +269,7 @@ const budgetFixture = (name: string, attempts: string | undefined, expect: RegEx name, expect, hook: "autopush", - env: attempts === undefined - ? { MEMORIES_AUTOPUSH_MODE: "auto" } - : { MEMORIES_AUTOPUSH_MODE: "auto", MEMORIES_PUSH_ATTEMPTS: attempts }, + ...(attempts === undefined ? {} : { env: { MEMORIES_PUSH_ATTEMPTS: attempts } }), setup: (repo, project) => { memory(repo, "learning_ours_one.md", "Ours.\n"); commit(repo, "local only"); @@ -605,40 +288,6 @@ describe("retry budget — behaviour is pinned", () => { for (const fx of budgets) test(fx.name, () => assertParity(fx)); }); -const audit: readonly Fixture[] = [ - { - // grep's [[:space:]] includes CR; a CRLF file's "blank" line is not blank to /^[\t ]*$/. - name: "audit: a CRLF blank first line is skipped like bash skips it", - expect: /"Real content\r"/, - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "\r\nReal content\r\n"), - }, - { - // bash ${var:0:80} counts characters; JS slice counts UTF-16 code units. - name: "audit: an 80-character truncation counts characters, not code units", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", `${"é".repeat(100)}\n`), - }, - { - name: "audit: astral characters truncate at the same point", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", `${"🌍".repeat(100)}\n`), - }, - { - name: "audit: a memory path containing a space and a hash", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => memory(repo, "learning_new_thing.md", "Content.\n"), - }, -]; - -describe("audit findings — behaviour is pinned", () => { - for (const fx of audit) test(fx.name, () => assertParity(fx)); -}); - /** The shared repo ships root-level files (README, LICENSE). Every git call is * scoped with `-- memories/` so they are visible but never swept into a commit. */ const withDirtyRootFile = (repo: string) => { @@ -650,19 +299,9 @@ const withDirtyRootFile = (repo: string) => { const pathspec: readonly Fixture[] = [ { - name: "pathspec: full mode leaves a dirty root file uncommitted", + name: "pathspec: a dirty root file stays uncommitted", expect: /^$/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "full" }, - setup: (repo) => { - withDirtyRootFile(repo); - memory(repo, "learning_new_thing.md", "A lesson.\n"); - }, - }, - { - name: "pathspec: auto mode leaves a dirty root file uncommitted", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { withDirtyRootFile(repo); memory(repo, "learning_new_thing.md", "A lesson.\n"); @@ -671,22 +310,11 @@ const pathspec: readonly Fixture[] = [ { name: "pathspec: a dirty root file does not trip the filename guardrail", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => withDirtyRootFile(repo), }, - { - name: "pathspec: review mode does not report a dirty root file", - hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - setup: (repo) => { - withDirtyRootFile(repo); - memory(repo, "learning_new_thing.md", "A lesson.\n"); - }, - }, { name: "pathspec: SessionStart does not count a dirty root file as pending", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => withDirtyRootFile(repo), }, ]; @@ -712,13 +340,11 @@ const incoming: readonly Fixture[] = [ { name: "incoming: SessionStart fast-forwards a teammate's memory", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => teammatePushes(repo, project, "learning_theirs_new.md"), }, { name: "incoming: a fast-forward alongside our own untracked memory", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { teammatePushes(repo, project, "learning_theirs_new.md"); memory(repo, "learning_ours_pending.md", "Ours, not yet pushed.\n"); @@ -727,7 +353,6 @@ const incoming: readonly Fixture[] = [ { name: "incoming: SessionStart does not fast-forward past a conflicting local commit", hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { teammatePushes(repo, project, "learning_theirs_new.md"); memory(repo, "learning_ours_own.md", "Ours.\n"); @@ -737,7 +362,6 @@ const incoming: readonly Fixture[] = [ { name: "incoming: the Stop hook rebases onto a teammate's commit", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { teammatePushes(repo, project, "learning_theirs_new.md"); memory(repo, "learning_ours_own.md", "Ours.\n"); @@ -749,101 +373,35 @@ describe("incoming memories — behaviour is pinned", () => { for (const fx of incoming) test(fx.name, () => assertParity(fx)); }); +/** Our edit conflicts with a teammate's, and the rebase is left mid-way with the file unmerged. */ +const rebaseLeftInProgress = (repo: string, project: string) => { + otherClonePushes(repo, project, "learning_seed_topic.md", "Their version of the seed.\n"); + memory(repo, "learning_seed_topic.md", "Our version of the seed.\n"); + commit(repo, "our conflicting edit"); + git(repo, "pull", "--rebase", "-q"); + memory(repo, "learning_new_thing.md", "Written while the rebase was stuck.\n"); +}; + const edges: readonly Fixture[] = [ - { - name: "edge: announce reads every value in a concatenated stream", - expect: /Memory file saved at \/p\/\.claude\/memories\/learning_a_b\.md\\n\/p\/\.claude\/memories\/learning_c_d\.md \(MEMORIES/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}} {"tool_input":{"file_path":"/p/.claude/memories/learning_c_d.md"}}', - }, - { - name: "edge: announce when only the second value carries a path", - expect: /learning_c_d\.md/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{}} {"tool_input":{"file_path":"/p/.claude/memories/learning_c_d.md"}}', - }, - { - // jq's `// empty` drops false, so it must not appear in the joined paths. - name: "edge: announce drops a false file_path from the stream", - expect: /Memory file saved at \/p\/\.claude\/memories\/learning_a_b\.md \(MEMORIES/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":false}} {"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}}', - }, - { - // jq -r renders a non-string result as pretty JSON, not as a JS-style cast. - name: "edge: announce renders a non-string file_path the way jq does", - expect: /Memory file saved at \{\\n \\"x\\": 1\\n\}\\n\/p\/\.claude/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":{"x":1}}} {"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}}', - }, - { - name: "edge: announce strips a trailing newline the way $() did", - expect: /Memory file saved at \/p\/\.claude\/memories\/learning_a_b\.md \(MEMORIES/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md\\n"}}', - }, - { - name: "edge: an empty last result does not silence the nudge", - expect: /Memory file saved at \/p\/\.claude\/memories\/learning_a_b\.md \(MEMORIES/, - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}} {"tool_input":{"file_path":""}}', - }, { name: "edge: a locked index aborts the turn instead of reporting nothing", abortsDiffer: true, expect: /Unable to create .*index\.lock/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_new_thing.md", "A lesson.\n"); writeFileSync(join(repo, ".git", "index.lock"), ""); }, }, { - name: "edge: announce ignores a null file_path", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":null}}', - }, - { - // jq neither aborts on an erroring value nor reports it once a later value - // succeeds, so the leading 42 is skipped and the path IS announced. The - // mirror-image fixture below is the silent one. - name: "edge: announce still reports a path after a leading scalar", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '42 {"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}}', - }, - { - // jq's status reflects the last value only, so a trailing scalar silences it. - name: "edge: announce is silent when a scalar follows a valid path", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: '{"tool_input":{"file_path":"/p/.claude/memories/learning_a_b.md"}} 42', - }, - { - name: "edge: announce is silent when stdin is a bare scalar", - hook: "announce", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, - stdin: "42", - }, - { - name: "edge: review reports a staged rename as unclassified pending", - expect: /unclassified pending change\(s\) in memories\//, + name: "edge: a staged rename is committed and pushed", + expect: /^$/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, setup: (repo) => git(repo, "mv", "memories/learning_seed_topic.md", "memories/learning_seed_renamed.md"), }, { - name: "edge: review on a detached HEAD with an unpushed commit", + name: "edge: a detached HEAD with an unpushed commit stays silent", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, setup: (repo) => { memory(repo, "learning_x_y.md"); commit(repo, "local"); @@ -851,9 +409,8 @@ const edges: readonly Fixture[] = [ }, }, { - name: "edge: a typechange to a symlink is unclassified pending", + name: "edge: a typechange to a symlink is committed and pushed", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "review" }, setup: (repo) => { const p = join(repo, "memories", "learning_seed_topic.md"); rmSync(p); @@ -865,7 +422,6 @@ const edges: readonly Fixture[] = [ expect: /Shared memories: commit failed; will retry on next Stop\./, hook: "autopush", env: { - MEMORIES_AUTOPUSH_MODE: "auto", GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null", GIT_AUTHOR_NAME: "", @@ -881,7 +437,6 @@ const edges: readonly Fixture[] = [ name: "edge: SessionStart survives a failed fast-forward", expect: /lingering state/, hook: "pull", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { teammatePushes(repo, project, "learning_theirs_a.md"); memory(repo, "learning_ours_b.md"); @@ -892,7 +447,6 @@ const edges: readonly Fixture[] = [ name: "edge: a badly named DELETED file trips the guardrail", expect: /unconventional filename\(s\):\n - memories\/scratch\.md/, hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { writeFileSync(join(repo, "memories", "scratch.md"), "x\n"); commit(repo, "add scratch"); @@ -903,7 +457,6 @@ const edges: readonly Fixture[] = [ { name: "edge: changes already staged in the index still commit", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo) => { memory(repo, "learning_x_y.md"); git(repo, "add", "-A"); @@ -912,13 +465,24 @@ const edges: readonly Fixture[] = [ { name: "edge: a dangling memories symlink does not stop the push", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "auto" }, setup: (repo, project) => { rmSync(join(project, ".claude", "memories")); symlinkSync(".memories-repo/nope", join(project, ".claude", "memories")); memory(repo, "learning_x_y.md"); }, }, + { + name: "edge: a rebase left in progress pauses the Stop hook", + expect: /^Shared memories: a rebase is in progress in \.claude\/\.memories-repo — auto-push paused\. Run \/resolve-memories to finish it\.$/m, + hook: "autopush", + setup: rebaseLeftInProgress, + }, + { + name: "edge: a rebase left in progress is flagged at SessionStart", + expect: /a rebase is in progress[^"]*\/resolve-memories/, + hook: "pull", + setup: rebaseLeftInProgress, + }, ]; describe("audited edges — behaviour is pinned", () => { @@ -941,7 +505,6 @@ describe("push failures are classified by exit code, not message text", () => { const r = runHook({ name: "internal: a broken pushurl is not a rejected update", hook: "autopush", - env: { MEMORIES_AUTOPUSH_MODE: "full" }, setup: (repo) => { // A valid fetch url with a broken pushurl: `pull --rebase` one line // earlier still exits 0, so the loop reaches the push, and the push diff --git a/tests/epipe.test.ts b/tests/epipe.test.ts index 80ca584..679d98d 100644 --- a/tests/epipe.test.ts +++ b/tests/epipe.test.ts @@ -4,13 +4,14 @@ import { spawn } from "node:child_process"; import { chmodSync, cpSync, mkdirSync, rmSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { makeProject } from "./harness.ts"; +import { makeProject, memory } from "./harness.ts"; const REPO = dirname(dirname(fileURLToPath(import.meta.url))); /** Runs a hook with its stdout reader gone before it writes: the EPIPE case. */ -function runWithNoReader(hook: string, env: Record, stdin: string) { - const { root, project } = makeProject(); +function runWithNoReader(hook: string, stdin: string, setup?: (repo: string) => void) { + const { root, project, repo } = makeProject(); + setup?.(repo); const hooks = join(project, ".claude", "hooks", "shared-memories"); mkdirSync(hooks, { recursive: true }); cpSync(join(REPO, "runtime", hook), join(hooks, hook)); @@ -20,7 +21,7 @@ function runWithNoReader(hook: string, env: Record, stdin: strin return new Promise<{ code: number | null; signal: string | null; stderr: string }>((resolve) => { const child = spawn(join(hooks, hook), [], { cwd: project, - env: { ...process.env, ...env }, + env: process.env, stdio: ["pipe", "pipe", "pipe"], }); let stderr = ""; @@ -36,12 +37,9 @@ function runWithNoReader(hook: string, env: Record, stdin: strin } describe("a hook whose reader has gone away", () => { - test("announce still exits 0 and reports no stack", async () => { - const r = await runWithNoReader( - "announce.mts", - { MEMORIES_AUTOPUSH_MODE: "review" }, - JSON.stringify({ tool_input: { file_path: "/p/.claude/memories/learning_a_b.md" } }), - ); + test("SessionStart still exits 0 and reports no stack", async () => { + // Lingering state makes pull write its additionalContext into the closed pipe. + const r = await runWithNoReader("pull.mts", "{}", (repo) => memory(repo, "learning_pending_one.md")); assert.equal(r.signal, null, "the hook must not die on SIGPIPE"); assert.equal(r.code, 0); assert.doesNotMatch(r.stderr, /EPIPE/, "EPIPE must not surface as a failure"); diff --git a/tests/golden/announce-a-path-containing-a-quote.json b/tests/golden/announce-a-path-containing-a-quote.json deleted file mode 100644 index b1a04d4..0000000 --- a/tests/golden/announce-a-path-containing-a-quote.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/\\\"q\\\"/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-auto-mode-is-silent.json b/tests/golden/announce-auto-mode-is-silent.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-auto-mode-is-silent.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-decision-prefix-also-matches.json b/tests/golden/announce-decision-prefix-also-matches.json deleted file mode 100644 index adeb14d..0000000 --- a/tests/golden/announce-decision-prefix-also-matches.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/decision_x_y.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-empty-stdin.json b/tests/golden/announce-empty-stdin.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-empty-stdin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-malformed-stdin.json b/tests/golden/announce-malformed-stdin.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-malformed-stdin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-missing-file-path.json b/tests/golden/announce-missing-file-path.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-missing-file-path.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-name-with-a-space-is-rejected.json b/tests/golden/announce-name-with-a-space-is-rejected.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-name-with-a-space-is-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-nested-prefix-path-still-matches.json b/tests/golden/announce-nested-prefix-path-still-matches.json deleted file mode 100644 index e8e6fb1..0000000 --- a/tests/golden/announce-nested-prefix-path-still-matches.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /deep/nest/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-non-md-extension-is-rejected.json b/tests/golden/announce-non-md-extension-is-rejected.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-non-md-extension-is-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-non-string-file-path.json b/tests/golden/announce-non-string-file-path.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-non-string-file-path.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-path-outside-claude-memories-is-rejected.json b/tests/golden/announce-path-outside-claude-memories-is-rejected.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-path-outside-claude-memories-is-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-review-mode-well-named-memory-write.json b/tests/golden/announce-review-mode-well-named-memory-write.json deleted file mode 100644 index 2da0719..0000000 --- a/tests/golden/announce-review-mode-well-named-memory-write.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-unknown-mode-is-silent.json b/tests/golden/announce-unknown-mode-is-silent.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-unknown-mode-is-silent.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-unset-mode-is-silent.json b/tests/golden/announce-unset-mode-is-silent.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-unset-mode-is-silent.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/announce-wrong-prefix-is-rejected.json b/tests/golden/announce-wrong-prefix-is-rejected.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/announce-wrong-prefix-is-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/audit-a-crlf-blank-first-line-is-skipped-like-bash-skips-it.json b/tests/golden/audit-a-crlf-blank-first-line-is-skipped-like-bash-skips-it.json deleted file mode 100644 index 6f0bc7c..0000000 --- a/tests/golden/audit-a-crlf-blank-first-line-is-skipped-like-bash-skips-it.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"Real content\r\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/audit-a-memory-path-containing-a-space-and-a-hash.json b/tests/golden/audit-a-memory-path-containing-a-space-and-a-hash.json deleted file mode 100644 index 0579998..0000000 --- a/tests/golden/audit-a-memory-path-containing-a-space-and-a-hash.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"Content.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/audit-an-80-character-truncation-counts-characters-not-code-units.json b/tests/golden/audit-an-80-character-truncation-counts-characters-not-code-units.json deleted file mode 100644 index 5411ab5..0000000 --- a/tests/golden/audit-an-80-character-truncation-counts-characters-not-code-units.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/audit-astral-characters-truncate-at-the-same-point.json b/tests/golden/audit-astral-characters-truncate-at-the-same-point.json deleted file mode 100644 index e31bc09..0000000 --- a/tests/golden/audit-astral-characters-truncate-at-the-same-point.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍🌍\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-auto-commits-and-pushes-a-new-memory.json b/tests/golden/autopush-auto-commits-and-pushes-a-new-memory.json deleted file mode 100644 index 7677c9d..0000000 --- a/tests/golden/autopush-auto-commits-and-pushes-a-new-memory.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-auto-parks-a-deletion.json b/tests/golden/autopush-auto-parks-a-deletion.json deleted file mode 100644 index 87e570e..0000000 --- a/tests/golden/autopush-auto-parks-a-deletion.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories: 1 deleted memory file(s) left for manual review (not auto-pushed):\n - memories/learning_seed_topic.md\nIf intentional (e.g. after memory-audit), approve them:\n /approve-memories audit cleanup\n", - "stderr": "", - "code": 0, - "git": "status-all: D memories/learning_seed_topic.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: D memories/learning_seed_topic.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: \nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-auto-parks-deletions-but-still-pushes-an-addition.json b/tests/golden/autopush-auto-parks-deletions-but-still-pushes-an-addition.json deleted file mode 100644 index 1f2fa48..0000000 --- a/tests/golden/autopush-auto-parks-deletions-but-still-pushes-an-addition.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories: 1 deleted memory file(s) left for manual review (not auto-pushed):\n - memories/learning_seed_topic.md\nIf intentional (e.g. after memory-audit), approve them:\n /approve-memories audit cleanup\n", - "stderr": "", - "code": 0, - "git": "status-all: D memories/learning_seed_topic.md\nlog-all: auto: memories from | seed\ntracked-all: memories/learning_added_alongside.md | memories/learning_seed_topic.md\nstatus: D memories/learning_seed_topic.md\nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_added_alongside.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_added_alongside.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_added_alongside.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-auto-pushes-a-modification.json b/tests/golden/autopush-auto-pushes-a-modification.json deleted file mode 100644 index a1d205a..0000000 --- a/tests/golden/autopush-auto-pushes-a-modification.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-commits-and-pushes-a-new-memory.json b/tests/golden/autopush-commits-and-pushes-a-new-memory.json new file mode 100644 index 0000000..9ca93d9 --- /dev/null +++ b/tests/golden/autopush-commits-and-pushes-a-new-memory.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: add learning_new_thing | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_new_thing | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: Fixture: add learning_new_thing | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" +} diff --git a/tests/golden/autopush-empty-stdin-still-pushes.json b/tests/golden/autopush-empty-stdin-still-pushes.json index 7677c9d..9ca93d9 100644 --- a/tests/golden/autopush-empty-stdin-still-pushes.json +++ b/tests/golden/autopush-empty-stdin-still-pushes.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: Fixture: add learning_new_thing | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_new_thing | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: Fixture: add learning_new_thing | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" } diff --git a/tests/golden/autopush-full-pushes-the-deletion.json b/tests/golden/autopush-full-pushes-the-deletion.json deleted file mode 100644 index 5689888..0000000 --- a/tests/golden/autopush-full-pushes-the-deletion.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: auto: memories from (includes deletions) | seed\ntracked-all: \nstatus: \nlog: auto: memories from (includes deletions) | seed\nupstream: origin/main\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: auto: memories from (includes deletions) | seed\nremote-files: " -} diff --git a/tests/golden/autopush-full-without-deletions-omits-the-suffix.json b/tests/golden/autopush-full-without-deletions-omits-the-suffix.json deleted file mode 100644 index 7677c9d..0000000 --- a/tests/golden/autopush-full-without-deletions-omits-the-suffix.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-guardrail-blocks-a-badly-named-file-in-auto.json b/tests/golden/autopush-guardrail-blocks-a-badly-named-file.json similarity index 52% rename from tests/golden/autopush-guardrail-blocks-a-badly-named-file-in-auto.json rename to tests/golden/autopush-guardrail-blocks-a-badly-named-file.json index cd13c30..ed76873 100644 --- a/tests/golden/autopush-guardrail-blocks-a-badly-named-file-in-auto.json +++ b/tests/golden/autopush-guardrail-blocks-a-badly-named-file.json @@ -1,6 +1,6 @@ { - "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\n", + "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\nOr run /resolve-memories to have Claude rename them.\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/scratch.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md scratch.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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/autopush-guardrail-blocks-in-full-too.json b/tests/golden/autopush-guardrail-blocks-in-full-too.json deleted file mode 100644 index cd13c30..0000000 --- a/tests/golden/autopush-guardrail-blocks-in-full-too.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/scratch.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md scratch.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-guardrail-blocks-in-review-too.json b/tests/golden/autopush-guardrail-blocks-in-review-too.json deleted file mode 100644 index cd13c30..0000000 --- a/tests/golden/autopush-guardrail-blocks-in-review-too.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/scratch.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md scratch.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-guardrail-lists-several-offenders.json b/tests/golden/autopush-guardrail-lists-several-offenders.json index cd08771..882abc4 100644 --- a/tests/golden/autopush-guardrail-lists-several-offenders.json +++ b/tests/golden/autopush-guardrail-lists-several-offenders.json @@ -1,6 +1,6 @@ { - "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\n - memories/wip notes.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\n", + "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\n - memories/wip notes.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\nOr run /resolve-memories to have Claude rename them.\n", "stderr": "", "code": 0, - "git": "status-all: ?? \"memories/wip notes.md\" | ?? memories/learning_ok_one.md | ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? \"memories/wip notes.md\" | ?? memories/learning_ok_one.md | ?? memories/scratch.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ok_one.md learning_seed_topic.md scratch.md wip notes.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? \"memories/wip notes.md\" | ?? memories/learning_ok_one.md | ?? memories/scratch.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? \"memories/wip notes.md\" | ?? memories/learning_ok_one.md | ?? memories/scratch.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_ok_one.md learning_seed_topic.md scratch.md wip notes.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/autopush-malformed-stdin.json b/tests/golden/autopush-malformed-stdin.json index 3b58e99..72032f6 100644 --- a/tests/golden/autopush-malformed-stdin.json +++ b/tests/golden/autopush-malformed-stdin.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "memories_autopush: stdin is not valid JSON; skipping\n", "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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/autopush-missing-checkout.json b/tests/golden/autopush-missing-checkout.json index e72e84b..e0a0cd9 100644 --- a/tests/golden/autopush-missing-checkout.json +++ b/tests/golden/autopush-missing-checkout.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "memories_autopush: /work/project/.claude/.memories-repo is not a git worktree; skipping (project_root=/work/project)\n", "code": 0, - "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nupstream: (none)\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: \nremote-files: " + "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: \nworktree: \nremote-log: \nremote-files: " } diff --git a/tests/golden/autopush-no-upstream-configured.json b/tests/golden/autopush-no-upstream-configured.json index 3c3c14d..f69aa62 100644 --- a/tests/golden/autopush-no-upstream-configured.json +++ b/tests/golden/autopush-no-upstream-configured.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: (none)\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: Fixture: add learning_new_thing | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_new_thing | seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/autopush-nothing-pending-auto.json b/tests/golden/autopush-nothing-pending-auto.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/autopush-nothing-pending-auto.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-nothing-pending-clears-stale-review-state.json b/tests/golden/autopush-nothing-pending-clears-stale-review-state.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/autopush-nothing-pending-clears-stale-review-state.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-nothing-pending-review.json b/tests/golden/autopush-nothing-pending-review.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/autopush-nothing-pending-review.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-nothing-pending.json b/tests/golden/autopush-nothing-pending.json new file mode 100644 index 0000000..03e9ccb --- /dev/null +++ b/tests/golden/autopush-nothing-pending.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "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/autopush-pushes-a-deletion.json b/tests/golden/autopush-pushes-a-deletion.json new file mode 100644 index 0000000..ea98f89 --- /dev/null +++ b/tests/golden/autopush-pushes-a-deletion.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: remove learning_seed_topic | seed\ntracked-all: \nstatus: \nlog: Fixture: remove learning_seed_topic | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: \nworktree: \nremote-log: Fixture: remove learning_seed_topic | seed\nremote-files: " +} diff --git a/tests/golden/autopush-pushes-a-modification.json b/tests/golden/autopush-pushes-a-modification.json new file mode 100644 index 0000000..b1834f7 --- /dev/null +++ b/tests/golden/autopush-pushes-a-modification.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: update learning_seed_topic | seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: Fixture: update learning_seed_topic | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: Fixture: update learning_seed_topic | seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/autopush-several-changes-are-summarised-one-line-per-file-in-the-body.json b/tests/golden/autopush-several-changes-are-summarised-one-line-per-file-in-the-body.json new file mode 100644 index 0000000..5f2aa22 --- /dev/null +++ b/tests/golden/autopush-several-changes-are-summarised-one-line-per-file-in-the-body.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: 2 added, 1 updated, 1 removed | second seed | seed\ntracked-all: memories/decision_new_two.md | memories/learning_new_one.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: 2 added, 1 updated, 1 removed | second seed | seed\nhead-body: + decision_new_two | + learning_new_one | ~ learning_seed_topic | - learning_extra_topic\nupstream: origin/main\nunpushed: 0\ntracked: memories/decision_new_two.md | memories/learning_new_one.md | memories/learning_seed_topic.md\nworktree: decision_new_two.md learning_new_one.md learning_seed_topic.md\nremote-log: Fixture: 2 added, 1 updated, 1 removed | second seed | seed\nremote-files: memories/decision_new_two.md | memories/learning_new_one.md | memories/learning_seed_topic.md" +} diff --git a/tests/golden/autopush-the-subject-names-the-git-author-not-the-machine.json b/tests/golden/autopush-the-subject-names-the-git-author-not-the-machine.json new file mode 100644 index 0000000..40a2377 --- /dev/null +++ b/tests/golden/autopush-the-subject-names-the-git-author-not-the-machine.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Someone Else: add learning_new_thing | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: Someone Else: add learning_new_thing | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: Someone Else: add learning_new_thing | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" +} diff --git a/tests/golden/autopush-unknown-mode-falls-through-to-auto-silently.json b/tests/golden/autopush-unknown-mode-falls-through-to-auto-silently.json deleted file mode 100644 index 7677c9d..0000000 --- a/tests/golden/autopush-unknown-mode-falls-through-to-auto-silently.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_new_thing.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/autopush-unpushed-commit-only.json b/tests/golden/autopush-unpushed-commit-only.json index 7bba178..0011465 100644 --- a/tests/golden/autopush-unpushed-commit-only.json +++ b/tests/golden/autopush-unpushed-commit-only.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_committed_x.md learning_seed_topic.md\nremote-log: local only | seed\nremote-files: memories/learning_committed_x.md | memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nworktree: learning_committed_x.md learning_seed_topic.md\nremote-log: local only | seed\nremote-files: memories/learning_committed_x.md | memories/learning_seed_topic.md" } diff --git a/tests/golden/budget-a-non-integer-falls-back-to-twelve.json b/tests/golden/budget-a-non-integer-falls-back-to-twelve.json index f22a647..98b3132 100644 --- a/tests/golden/budget-a-non-integer-falls-back-to-twelve.json +++ b/tests/golden/budget-a-non-integer-falls-back-to-twelve.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: auto-push failed after 12 attempt(s). Will retry on next Stop.\n To /work/remote.git\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to '/work/remote.git'\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/budget-an-explicit-small-budget-is-honoured.json b/tests/golden/budget-an-explicit-small-budget-is-honoured.json index cf43c31..706e657 100644 --- a/tests/golden/budget-an-explicit-small-budget-is-honoured.json +++ b/tests/golden/budget-an-explicit-small-budget-is-honoured.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: auto-push failed after 3 attempt(s). Will retry on next Stop.\n To /work/remote.git\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to '/work/remote.git'\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/budget-unset-falls-back-to-twelve.json b/tests/golden/budget-unset-falls-back-to-twelve.json index f22a647..98b3132 100644 --- a/tests/golden/budget-unset-falls-back-to-twelve.json +++ b/tests/golden/budget-unset-falls-back-to-twelve.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: auto-push failed after 12 attempt(s). Will retry on next Stop.\n To /work/remote.git\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to '/work/remote.git'\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/budget-zero-is-clamped-to-a-single-attempt.json b/tests/golden/budget-zero-is-clamped-to-a-single-attempt.json index 26800a3..615de04 100644 --- a/tests/golden/budget-zero-is-clamped-to-a-single-attempt.json +++ b/tests/golden/budget-zero-is-clamped-to-a-single-attempt.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: auto-push failed after 1 attempt(s). Will retry on next Stop.\n To /work/remote.git\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to '/work/remote.git'\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/doctor-checkout-present-symlink-missing.json b/tests/golden/doctor-checkout-present-symlink-missing.json index 29e1bc8..ff180f0 100644 --- a/tests/golden/doctor-checkout-present-symlink-missing.json +++ b/tests/golden/doctor-checkout-present-symlink-missing.json @@ -2,5 +2,5 @@ "stdout": "Shared memories setup is unhealthy:\n - symlink missing at /work/project/.claude/memories (run 'mcs sync' to relink)\n", "stderr": "", "code": 1, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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-dangling-symlink.json b/tests/golden/doctor-dangling-symlink.json index 02052a8..dc41e6c 100644 --- a/tests/golden/doctor-dangling-symlink.json +++ b/tests/golden/doctor-dangling-symlink.json @@ -2,5 +2,5 @@ "stdout": "Shared memories setup is unhealthy:\n - git checkout missing at /work/project/.claude/.memories-repo (run 'mcs sync' to re-clone)\n - symlink exists but doesn't resolve to a git checkout (run 'mcs sync' to repair)\n", "stderr": "", "code": 1, - "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nupstream: (none)\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: \nremote-files: " + "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: \nworktree: \nremote-log: \nremote-files: " } diff --git a/tests/golden/doctor-healthy-setup-passes.json b/tests/golden/doctor-healthy-setup-passes.json index 6e86273..03e9ccb 100644 --- a/tests/golden/doctor-healthy-setup-passes.json +++ b/tests/golden/doctor-healthy-setup-passes.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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-missing-checkout-and-symlink.json b/tests/golden/doctor-missing-checkout-and-symlink.json index 5d17934..b736348 100644 --- a/tests/golden/doctor-missing-checkout-and-symlink.json +++ b/tests/golden/doctor-missing-checkout-and-symlink.json @@ -2,5 +2,5 @@ "stdout": "Shared memories setup is unhealthy:\n - git checkout missing at /work/project/.claude/.memories-repo (run 'mcs sync' to re-clone)\n - symlink missing at /work/project/.claude/memories (run 'mcs sync' to relink)\n", "stderr": "", "code": 1, - "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nupstream: (none)\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: \nremote-files: " + "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: \nworktree: \nremote-log: \nremote-files: " } diff --git a/tests/golden/doctor-remote-no-checkout-skips.json b/tests/golden/doctor-remote-no-checkout-skips.json index daedf7f..5f3be86 100644 --- a/tests/golden/doctor-remote-no-checkout-skips.json +++ b/tests/golden/doctor-remote-no-checkout-skips.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 3, - "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nupstream: (none)\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: \nremote-files: " + "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: \nworktree: \nremote-log: \nremote-files: " } diff --git a/tests/golden/doctor-remote-reachable-remote-passes.json b/tests/golden/doctor-remote-reachable-remote-passes.json index 6e86273..03e9ccb 100644 --- a/tests/golden/doctor-remote-reachable-remote-passes.json +++ b/tests/golden/doctor-remote-reachable-remote-passes.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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-remote-unreachable-remote-warns.json b/tests/golden/doctor-remote-unreachable-remote-warns.json index b318f9a..599fbbf 100644 --- a/tests/golden/doctor-remote-unreachable-remote-warns.json +++ b/tests/golden/doctor-remote-unreachable-remote-warns.json @@ -2,5 +2,5 @@ "stdout": "Cannot reach the shared memories remote:\n remote: /nonexistent/definitely-not-here.git\n error: fatal: '/nonexistent/definitely-not-here.git' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n\nCommon causes:\n - SSH key not loaded — try: ssh-add ~/.ssh/\n - Network offline or VPN disconnected\n - Access revoked on the shared repo\n\nMemories still work locally; auto-push will retry on each Claude Stop once the remote is reachable.\n", "stderr": "", "code": 2, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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-symlink-resolves-somewhere-that-is-not-a-checkout.json b/tests/golden/doctor-symlink-resolves-somewhere-that-is-not-a-checkout.json index f69cad7..071ab9e 100644 --- a/tests/golden/doctor-symlink-resolves-somewhere-that-is-not-a-checkout.json +++ b/tests/golden/doctor-symlink-resolves-somewhere-that-is-not-a-checkout.json @@ -2,5 +2,5 @@ "stdout": "Shared memories setup is unhealthy:\n - symlink exists but doesn't resolve to a git checkout (run 'mcs sync' to repair)\n", "stderr": "", "code": 1, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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-badly-named-deleted-file-trips-the-guardrail.json b/tests/golden/edge-a-badly-named-deleted-file-trips-the-guardrail.json index 1f2b2b3..ba899b7 100644 --- a/tests/golden/edge-a-badly-named-deleted-file-trips-the-guardrail.json +++ b/tests/golden/edge-a-badly-named-deleted-file-trips-the-guardrail.json @@ -1,6 +1,6 @@ { - "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\n", + "stdout": "Shared memories: skipping auto-push — unconventional filename(s):\n - memories/scratch.md\nRename to memories/learning__.md or memories/decision__.md so the guardrail accepts them.\nOr run /resolve-memories to have Claude rename them.\n", "stderr": "", "code": 0, - "git": "status-all: D memories/scratch.md\nlog-all: add scratch | seed\ntracked-all: memories/learning_seed_topic.md | memories/scratch.md\nstatus: D memories/scratch.md\nlog: add scratch | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/scratch.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: add scratch | seed\nremote-files: memories/learning_seed_topic.md | memories/scratch.md" + "git": "status-all: D memories/scratch.md\nlog-all: add scratch | seed\ntracked-all: memories/learning_seed_topic.md | memories/scratch.md\nstatus: D memories/scratch.md\nlog: add scratch | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/scratch.md\nworktree: learning_seed_topic.md\nremote-log: add scratch | seed\nremote-files: memories/learning_seed_topic.md | memories/scratch.md" } diff --git a/tests/golden/edge-a-commit-failure-is-reported-and-retried-next-stop.json b/tests/golden/edge-a-commit-failure-is-reported-and-retried-next-stop.json index 5752f64..540a794 100644 --- a/tests/golden/edge-a-commit-failure-is-reported-and-retried-next-stop.json +++ b/tests/golden/edge-a-commit-failure-is-reported-and-retried-next-stop.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: commit failed; will retry on next Stop.\n fatal: empty ident name (for ) not allowed\n", "stderr": "", "code": 0, - "git": "status-all: A memories/learning_x_y.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: A memories/learning_x_y.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nreview-shown: absent\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: A memories/learning_x_y.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: A memories/learning_x_y.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/edge-a-dangling-memories-symlink-does-not-stop-the-push.json b/tests/golden/edge-a-dangling-memories-symlink-does-not-stop-the-push.json index 5186b47..856e689 100644 --- a/tests/golden/edge-a-dangling-memories-symlink-does-not-stop-the-push.json +++ b/tests/golden/edge-a-dangling-memories-symlink-does-not-stop-the-push.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nreview-shown: absent\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_x_y.md" + "git": "status-all: \nlog-all: Fixture: add learning_x_y | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: Fixture: add learning_x_y | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: Fixture: add learning_x_y | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_x_y.md" } diff --git a/tests/golden/edge-a-detached-head-with-an-unpushed-commit-stays-silent.json b/tests/golden/edge-a-detached-head-with-an-unpushed-commit-stays-silent.json new file mode 100644 index 0000000..85f3092 --- /dev/null +++ b/tests/golden/edge-a-detached-head-with-an-unpushed-commit-stays-silent.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: local | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: local | seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/edge-a-locked-index-aborts-the-turn-instead-of-reporting-nothing.json b/tests/golden/edge-a-locked-index-aborts-the-turn-instead-of-reporting-nothing.json index 929ed6f..890a73e 100644 --- a/tests/golden/edge-a-locked-index-aborts-the-turn-instead-of-reporting-nothing.json +++ b/tests/golden/edge-a-locked-index-aborts-the-turn-instead-of-reporting-nothing.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "fatal: Unable to create '/work/project/.claude/.memories-repo/.git/index.lock': File exists.\n\nAnother git process seems to be running in this repository, or the lock file may be stale\nmemories_autopush: aborted (rc=128) at line 299: git -C \"$memories_dir\" add -- \"$f\"\n", "code": 0, - "git": "status-all: ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/edge-a-rebase-left-in-progress-is-flagged-at-sessionstart.json b/tests/golden/edge-a-rebase-left-in-progress-is-flagged-at-sessionstart.json new file mode 100644 index 0000000..0538880 --- /dev/null +++ b/tests/golden/edge-a-rebase-left-in-progress-is-flagged-at-sessionstart.json @@ -0,0 +1,6 @@ +{ + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories: a rebase is in progress in .claude/.memories-repo, so auto-push is paused and teammates' memories were not pulled. Run /resolve-memories to finish it.\"\n }\n}\n", + "stderr": "", + "code": 0, + "git": "status-all: ?? memories/learning_new_thing.md | UU memories/learning_seed_topic.md\nlog-all: from the other clone | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_seed_topic.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md | UU memories/learning_seed_topic.md\nlog: from the other clone | seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_seed_topic.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: from the other clone | seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/edge-a-rebase-left-in-progress-pauses-the-stop-hook.json b/tests/golden/edge-a-rebase-left-in-progress-pauses-the-stop-hook.json new file mode 100644 index 0000000..3391852 --- /dev/null +++ b/tests/golden/edge-a-rebase-left-in-progress-pauses-the-stop-hook.json @@ -0,0 +1,6 @@ +{ + "stdout": "Shared memories: a rebase is in progress in .claude/.memories-repo — auto-push paused. Run /resolve-memories to finish it.\n", + "stderr": "", + "code": 0, + "git": "status-all: ?? memories/learning_new_thing.md | UU memories/learning_seed_topic.md\nlog-all: from the other clone | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_seed_topic.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md | UU memories/learning_seed_topic.md\nlog: from the other clone | seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_seed_topic.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: from the other clone | seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/edge-a-staged-rename-is-committed-and-pushed.json b/tests/golden/edge-a-staged-rename-is-committed-and-pushed.json new file mode 100644 index 0000000..aab496a --- /dev/null +++ b/tests/golden/edge-a-staged-rename-is-committed-and-pushed.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: rename learning_seed_topic → learning_seed_renamed | seed\ntracked-all: memories/learning_seed_renamed.md\nstatus: \nlog: Fixture: rename learning_seed_topic → learning_seed_renamed | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_renamed.md\nworktree: learning_seed_renamed.md\nremote-log: Fixture: rename learning_seed_topic → learning_seed_renamed | seed\nremote-files: memories/learning_seed_renamed.md" +} diff --git a/tests/golden/edge-a-typechange-to-a-symlink-is-committed-and-pushed.json b/tests/golden/edge-a-typechange-to-a-symlink-is-committed-and-pushed.json new file mode 100644 index 0000000..b1834f7 --- /dev/null +++ b/tests/golden/edge-a-typechange-to-a-symlink-is-committed-and-pushed.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: \nlog-all: Fixture: update learning_seed_topic | seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: Fixture: update learning_seed_topic | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: Fixture: update learning_seed_topic | seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/edge-a-typechange-to-a-symlink-is-unclassified-pending.json b/tests/golden/edge-a-typechange-to-a-symlink-is-unclassified-pending.json deleted file mode 100644 index 8168de3..0000000 --- a/tests/golden/edge-a-typechange-to-a-symlink-is-unclassified-pending.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 unclassified pending change(s) in memories/\n Inspect: git -C .claude/.memories-repo status -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: T memories/learning_seed_topic.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: T memories/learning_seed_topic.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-an-empty-last-result-does-not-silence-the-nudge.json b/tests/golden/edge-an-empty-last-result-does-not-silence-the-nudge.json deleted file mode 100644 index 2da0719..0000000 --- a/tests/golden/edge-an-empty-last-result-does-not-silence-the-nudge.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-drops-a-false-file-path-from-the-stream.json b/tests/golden/edge-announce-drops-a-false-file-path-from-the-stream.json deleted file mode 100644 index 2da0719..0000000 --- a/tests/golden/edge-announce-drops-a-false-file-path-from-the-stream.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-ignores-a-null-file-path.json b/tests/golden/edge-announce-ignores-a-null-file-path.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/edge-announce-ignores-a-null-file-path.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-is-silent-when-a-scalar-follows-a-valid-path.json b/tests/golden/edge-announce-is-silent-when-a-scalar-follows-a-valid-path.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/edge-announce-is-silent-when-a-scalar-follows-a-valid-path.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-is-silent-when-stdin-is-a-bare-scalar.json b/tests/golden/edge-announce-is-silent-when-stdin-is-a-bare-scalar.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/edge-announce-is-silent-when-stdin-is-a-bare-scalar.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-reads-every-value-in-a-concatenated-stream.json b/tests/golden/edge-announce-reads-every-value-in-a-concatenated-stream.json deleted file mode 100644 index c1fa673..0000000 --- a/tests/golden/edge-announce-reads-every-value-in-a-concatenated-stream.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md\\n/p/.claude/memories/learning_c_d.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-renders-a-non-string-file-path-the-way-jq-does.json b/tests/golden/edge-announce-renders-a-non-string-file-path-the-way-jq-does.json deleted file mode 100644 index dd81084..0000000 --- a/tests/golden/edge-announce-renders-a-non-string-file-path-the-way-jq-does.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at {\\n \\\"x\\\": 1\\n}\\n/p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-still-reports-a-path-after-a-leading-scalar.json b/tests/golden/edge-announce-still-reports-a-path-after-a-leading-scalar.json deleted file mode 100644 index 2da0719..0000000 --- a/tests/golden/edge-announce-still-reports-a-path-after-a-leading-scalar.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-strips-a-trailing-newline-the-way-did.json b/tests/golden/edge-announce-strips-a-trailing-newline-the-way-did.json deleted file mode 100644 index 2da0719..0000000 --- a/tests/golden/edge-announce-strips-a-trailing-newline-the-way-did.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_a_b.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-announce-when-only-the-second-value-carries-a-path.json b/tests/golden/edge-announce-when-only-the-second-value-carries-a-path.json deleted file mode 100644 index 9a1b741..0000000 --- a/tests/golden/edge-announce-when-only-the-second-value-carries-a-path.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"PostToolUse\",\n \"additionalContext\": \"Memory file saved at /p/.claude/memories/learning_c_d.md (MEMORIES_AUTOPUSH_MODE=review). This memory will not auto-push. Mention the pending memory to the user before ending your turn so they can decide whether to approve or discard. If they approve, run /approve-memories.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-changes-already-staged-in-the-index-still-commit.json b/tests/golden/edge-changes-already-staged-in-the-index-still-commit.json index 5186b47..856e689 100644 --- a/tests/golden/edge-changes-already-staged-in-the-index-still-commit.json +++ b/tests/golden/edge-changes-already-staged-in-the-index-still-commit.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nreview-shown: absent\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: auto: memories from | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_x_y.md" + "git": "status-all: \nlog-all: Fixture: add learning_x_y | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: Fixture: add learning_x_y | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: Fixture: add learning_x_y | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_x_y.md" } diff --git a/tests/golden/edge-review-on-a-detached-head-with-an-unpushed-commit.json b/tests/golden/edge-review-on-a-detached-head-with-an-unpushed-commit.json deleted file mode 100644 index 679b062..0000000 --- a/tests/golden/edge-review-on-a-detached-head-with-an-unpushed-commit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: local | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_x_y.md\nstatus: \nlog: local | seed\nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_x_y.md\nreview-shown: absent\nworktree: learning_seed_topic.md learning_x_y.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-review-reports-a-staged-rename-as-unclassified-pending.json b/tests/golden/edge-review-reports-a-staged-rename-as-unclassified-pending.json deleted file mode 100644 index d69a65d..0000000 --- a/tests/golden/edge-review-reports-a-staged-rename-as-unclassified-pending.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 unclassified pending change(s) in memories/\n Inspect: git -C .claude/.memories-repo status -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: R memories/learning_seed_topic.md -> memories/learning_seed_renamed.md\nlog-all: seed\ntracked-all: memories/learning_seed_renamed.md\nstatus: R memories/learning_seed_topic.md -> memories/learning_seed_renamed.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_renamed.md\nreview-shown: absent\nworktree: learning_seed_renamed.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/edge-sessionstart-survives-a-failed-fast-forward.json b/tests/golden/edge-sessionstart-survives-a-failed-fast-forward.json index f5f3271..55f9021 100644 --- a/tests/golden/edge-sessionstart-survives-a-failed-fast-forward.json +++ b/tests/golden/edge-sessionstart-survives-a-failed-fast-forward.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: ours | seed\ntracked-all: memories/learning_ours_b.md | memories/learning_seed_topic.md\nstatus: \nlog: ours | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_b.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_b.md learning_seed_topic.md\nremote-log: teammate memory | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_theirs_a.md" + "git": "status-all: \nlog-all: ours | seed\ntracked-all: memories/learning_ours_b.md | memories/learning_seed_topic.md\nstatus: \nlog: ours | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_b.md | memories/learning_seed_topic.md\nworktree: learning_ours_b.md learning_seed_topic.md\nremote-log: teammate memory | seed\nremote-files: memories/learning_seed_topic.md | memories/learning_theirs_a.md" } diff --git a/tests/golden/incoming-a-fast-forward-alongside-our-own-untracked-memory.json b/tests/golden/incoming-a-fast-forward-alongside-our-own-untracked-memory.json index 431bf0a..87f786e 100644 --- a/tests/golden/incoming-a-fast-forward-alongside-our-own-untracked-memory.json +++ b/tests/golden/incoming-a-fast-forward-alongside-our-own-untracked-memory.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_ours_pending.md\nlog-all: teammate memory | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: ?? memories/learning_ours_pending.md\nlog: teammate memory | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nreview-shown: absent\nworktree: learning_ours_pending.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" + "git": "status-all: ?? memories/learning_ours_pending.md\nlog-all: teammate memory | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: ?? memories/learning_ours_pending.md\nlog: teammate memory | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nworktree: learning_ours_pending.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/incoming-sessionstart-does-not-fast-forward-past-a-conflicting-local-commit.json b/tests/golden/incoming-sessionstart-does-not-fast-forward-past-a-conflicting-local-commit.json index e8a6f1a..da1154e 100644 --- a/tests/golden/incoming-sessionstart-does-not-fast-forward-past-a-conflicting-local-commit.json +++ b/tests/golden/incoming-sessionstart-does-not-fast-forward-past-a-conflicting-local-commit.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: our local commit | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md\nstatus: \nlog: our local commit | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_own.md | memories/learning_seed_topic.md\nreview-shown: absent\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" + "git": "status-all: \nlog-all: our local commit | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md\nstatus: \nlog: our local commit | 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/incoming-sessionstart-fast-forwards-a-teammate-s-memory.json b/tests/golden/incoming-sessionstart-fast-forwards-a-teammate-s-memory.json index 1bc6848..0675df9 100644 --- a/tests/golden/incoming-sessionstart-fast-forwards-a-teammate-s-memory.json +++ b/tests/golden/incoming-sessionstart-fast-forwards-a-teammate-s-memory.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: teammate memory | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: \nlog: teammate memory | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nreview-shown: absent\nworktree: 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" + "git": "status-all: \nlog-all: teammate memory | seed\ntracked-all: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: \nlog: teammate memory | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md | memories/learning_theirs_new.md\nworktree: 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/incoming-the-stop-hook-rebases-onto-a-teammate-s-commit.json b/tests/golden/incoming-the-stop-hook-rebases-onto-a-teammate-s-commit.json index be43e50..633aed1 100644 --- a/tests/golden/incoming-the-stop-hook-rebases-onto-a-teammate-s-commit.json +++ b/tests/golden/incoming-the-stop-hook-rebases-onto-a-teammate-s-commit.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | teammate memory | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: \nlog: auto: memories from | teammate memory | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_own.md | memories/learning_seed_topic.md | memories/learning_theirs_new.md\nreview-shown: absent\nworktree: learning_ours_own.md learning_seed_topic.md learning_theirs_new.md\nremote-log: auto: memories from | teammate memory | seed\nremote-files: memories/learning_ours_own.md | memories/learning_seed_topic.md | memories/learning_theirs_new.md" + "git": "status-all: \nlog-all: Fixture: add learning_ours_own | teammate memory | seed\ntracked-all: memories/learning_ours_own.md | memories/learning_seed_topic.md | memories/learning_theirs_new.md\nstatus: \nlog: Fixture: add learning_ours_own | teammate memory | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_own.md | 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: Fixture: add learning_ours_own | teammate memory | seed\nremote-files: memories/learning_ours_own.md | memories/learning_seed_topic.md | memories/learning_theirs_new.md" } diff --git a/tests/golden/pathspec-a-dirty-root-file-does-not-trip-the-filename-guardrail.json b/tests/golden/pathspec-a-dirty-root-file-does-not-trip-the-filename-guardrail.json index c321b1d..f89730c 100644 --- a/tests/golden/pathspec-a-dirty-root-file-does-not-trip-the-filename-guardrail.json +++ b/tests/golden/pathspec-a-dirty-root-file-does-not-trip-the-filename-guardrail.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: M README.md\nlog-all: add a root README | seed\ntracked-all: README.md | memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: add a root README | seed\nremote-files: README.md | memories/learning_seed_topic.md" + "git": "status-all: M README.md\nlog-all: add a root README | seed\ntracked-all: README.md | 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: add a root README | seed\nremote-files: README.md | memories/learning_seed_topic.md" } diff --git a/tests/golden/pathspec-a-dirty-root-file-stays-uncommitted.json b/tests/golden/pathspec-a-dirty-root-file-stays-uncommitted.json new file mode 100644 index 0000000..ab9f7cd --- /dev/null +++ b/tests/golden/pathspec-a-dirty-root-file-stays-uncommitted.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "git": "status-all: M README.md\nlog-all: Fixture: add learning_new_thing | add a root README | seed\ntracked-all: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: Fixture: add learning_new_thing | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: Fixture: add learning_new_thing | add a root README | seed\nremote-files: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md" +} diff --git a/tests/golden/pathspec-auto-mode-leaves-a-dirty-root-file-uncommitted.json b/tests/golden/pathspec-auto-mode-leaves-a-dirty-root-file-uncommitted.json deleted file mode 100644 index 718d33d..0000000 --- a/tests/golden/pathspec-auto-mode-leaves-a-dirty-root-file-uncommitted.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: M README.md\nlog-all: auto: memories from | add a root README | seed\ntracked-all: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | add a root README | seed\nremote-files: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/pathspec-full-mode-leaves-a-dirty-root-file-uncommitted.json b/tests/golden/pathspec-full-mode-leaves-a-dirty-root-file-uncommitted.json deleted file mode 100644 index 718d33d..0000000 --- a/tests/golden/pathspec-full-mode-leaves-a-dirty-root-file-uncommitted.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: M README.md\nlog-all: auto: memories from | add a root README | seed\ntracked-all: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md\nstatus: \nlog: auto: memories from | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_new_thing.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: auto: memories from | add a root README | seed\nremote-files: README.md | memories/learning_new_thing.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/pathspec-review-mode-does-not-report-a-dirty-root-file.json b/tests/golden/pathspec-review-mode-does-not-report-a-dirty-root-file.json deleted file mode 100644 index 3b3d276..0000000 --- a/tests/golden/pathspec-review-mode-does-not-report-a-dirty-root-file.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"A lesson.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md | M README.md\nlog-all: add a root README | seed\ntracked-all: README.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: add a root README | seed\nremote-files: README.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/pathspec-sessionstart-does-not-count-a-dirty-root-file-as-pending.json b/tests/golden/pathspec-sessionstart-does-not-count-a-dirty-root-file-as-pending.json index c321b1d..f89730c 100644 --- a/tests/golden/pathspec-sessionstart-does-not-count-a-dirty-root-file-as-pending.json +++ b/tests/golden/pathspec-sessionstart-does-not-count-a-dirty-root-file-as-pending.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: M README.md\nlog-all: add a root README | seed\ntracked-all: README.md | memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: add a root README | seed\nremote-files: README.md | memories/learning_seed_topic.md" + "git": "status-all: M README.md\nlog-all: add a root README | seed\ntracked-all: README.md | 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: add a root README | seed\nremote-files: README.md | memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-an-untracked-memory-reports-lingering-state.json b/tests/golden/pull-an-untracked-memory-reports-lingering-state.json new file mode 100644 index 0000000..58c5709 --- /dev/null +++ b/tests/golden/pull-an-untracked-memory-reports-lingering-state.json @@ -0,0 +1,6 @@ +{ + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", + "stderr": "", + "code": 0, + "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" +} diff --git a/tests/golden/pull-auto-mode-leaves-the-dedupe-state-file-alone.json b/tests/golden/pull-auto-mode-leaves-the-dedupe-state-file-alone.json deleted file mode 100644 index 9f3aeb8..0000000 --- a/tests/golden/pull-auto-mode-leaves-the-dedupe-state-file-alone.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-clean-tree-auto.json b/tests/golden/pull-clean-tree-auto.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/pull-clean-tree-auto.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-clean-tree-review.json b/tests/golden/pull-clean-tree-review.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/pull-clean-tree-review.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-clean-tree.json b/tests/golden/pull-clean-tree.json new file mode 100644 index 0000000..03e9ccb --- /dev/null +++ b/tests/golden/pull-clean-tree.json @@ -0,0 +1,6 @@ +{ + "stdout": "", + "stderr": "", + "code": 0, + "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/pull-concatenated-json-stream-is-accepted.json b/tests/golden/pull-concatenated-json-stream-is-accepted.json index 431554d..58c5709 100644 --- a/tests/golden/pull-concatenated-json-stream-is-accepted.json +++ b/tests/golden/pull-concatenated-json-stream-is-accepted.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-empty-mode-falls-through-to-auto.json b/tests/golden/pull-empty-mode-falls-through-to-auto.json deleted file mode 100644 index 431554d..0000000 --- a/tests/golden/pull-empty-mode-falls-through-to-auto.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-empty-stdin-still-does-the-work.json b/tests/golden/pull-empty-stdin-still-does-the-work.json index 431554d..58c5709 100644 --- a/tests/golden/pull-empty-stdin-still-does-the-work.json +++ b/tests/golden/pull-empty-stdin-still-does-the-work.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-malformed-stdin.json b/tests/golden/pull-malformed-stdin.json index 0823d92..74dfbf7 100644 --- a/tests/golden/pull-malformed-stdin.json +++ b/tests/golden/pull-malformed-stdin.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "memories_pull: stdin is not valid JSON; skipping\n", "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "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/pull-missing-checkout.json b/tests/golden/pull-missing-checkout.json index 7abad6e..e60a10e 100644 --- a/tests/golden/pull-missing-checkout.json +++ b/tests/golden/pull-missing-checkout.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "memories_pull: /work/project/.claude/.memories-repo is not a git worktree; skipping (project_root=/work/project)\n", "code": 0, - "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nupstream: (none)\nunpushed: 0\ntracked: \nreview-shown: absent\nworktree: \nremote-log: \nremote-files: " + "git": "status-all: \nlog-all: \ntracked-all: \nstatus: \nlog: \nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: \nworktree: \nremote-log: \nremote-files: " } diff --git a/tests/golden/pull-no-upstream-configured.json b/tests/golden/pull-no-upstream-configured.json index 761e21b..b14c8ba 100644 --- a/tests/golden/pull-no-upstream-configured.json +++ b/tests/golden/pull-no-upstream-configured.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories: the checkout in .claude/.memories-repo has no upstream branch, so memories are committed locally but never pushed or pulled. Run /resolve-memories to fix it.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nhead-body: \nupstream: (none)\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-review-mode-clears-the-dedupe-state-file.json b/tests/golden/pull-review-mode-clears-the-dedupe-state-file.json deleted file mode 100644 index 6e86273..0000000 --- a/tests/golden/pull-review-mode-clears-the-dedupe-state-file.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-two-untracked-memories-pluralise-the-same-way.json b/tests/golden/pull-two-untracked-memories-pluralise-the-same-way.json index e921fe8..3ba424b 100644 --- a/tests/golden/pull-two-untracked-memories-pluralise-the-same-way.json +++ b/tests/golden/pull-two-untracked-memories-pluralise-the-same-way.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 2 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 2 uncommitted file(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md | ?? memories/learning_pending_two.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md | ?? memories/learning_pending_two.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_pending_two.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_pending_one.md | ?? memories/learning_pending_two.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md | ?? memories/learning_pending_two.md\nlog: seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nworktree: learning_pending_one.md learning_pending_two.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-uncommitted-and-unpushed-together.json b/tests/golden/pull-uncommitted-and-unpushed-together.json index 5f2dfef..6b7d639 100644 --- a/tests/golden/pull-uncommitted-and-unpushed-together.json +++ b/tests/golden/pull-uncommitted-and-unpushed-together.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s), 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s), 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: ?? memories/learning_dirty_y.md\nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_dirty_y.md\nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_committed_x.md learning_dirty_y.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: ?? memories/learning_dirty_y.md\nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_dirty_y.md\nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nworktree: learning_committed_x.md learning_dirty_y.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-unknown-mode-warning-joins-the-pending-message.json b/tests/golden/pull-unknown-mode-warning-joins-the-pending-message.json deleted file mode 100644 index 0977a7b..0000000 --- a/tests/golden/pull-unknown-mode-warning-joins-the-pending-message.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories: unknown MEMORIES_AUTOPUSH_MODE='banana' — falling back to auto. Fix the value in .claude/settings.local.json (valid: auto, full, review) and restart the session.\\n\\nShared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-unknown-mode-warns-clean-tree.json b/tests/golden/pull-unknown-mode-warns-clean-tree.json deleted file mode 100644 index 60c5f21..0000000 --- a/tests/golden/pull-unknown-mode-warns-clean-tree.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories: unknown MEMORIES_AUTOPUSH_MODE='banana' — falling back to auto. Fix the value in .claude/settings.local.json (valid: auto, full, review) and restart the session.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: \nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-unpushed-commit-only.json b/tests/golden/pull-unpushed-commit-only.json index d00a048..ac4cdf8 100644 --- a/tests/golden/pull-unpushed-commit-only.json +++ b/tests/golden/pull-unpushed-commit-only.json @@ -1,6 +1,6 @@ { - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", + "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 unpushed commit(s). The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.\"\n }\n}\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_committed_x.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nworktree: learning_committed_x.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/pull-untracked-memory-auto-reports-lingering-state.json b/tests/golden/pull-untracked-memory-auto-reports-lingering-state.json deleted file mode 100644 index 431554d..0000000 --- a/tests/golden/pull-untracked-memory-auto-reports-lingering-state.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories have lingering state: 1 uncommitted file(s). The previous Stop hook's auto-push didn't complete — check SSH auth (ssh-add), network, or file naming (must match memories/{learning,decision}_.md). The next Stop will retry automatically.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/pull-untracked-memory-review-reports-awaiting-review.json b/tests/golden/pull-untracked-memory-review-reports-awaiting-review.json deleted file mode 100644 index 2a37fa5..0000000 --- a/tests/golden/pull-untracked-memory-review-reports-awaiting-review.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "{\n \"hookSpecificOutput\": {\n \"hookEventName\": \"SessionStart\",\n \"additionalContext\": \"Shared memories: 1 uncommitted file(s) awaiting review (MEMORIES_AUTOPUSH_MODE=review). End a turn to see the per-file report with approve/discard commands.\"\n }\n}\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? memories/learning_pending_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_pending_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_pending_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/push-a-non-integer-attempt-budget-falls-back-to-the-default.json b/tests/golden/push-a-non-integer-attempt-budget-falls-back-to-the-default.json index 731b2e0..5a3069a 100644 --- a/tests/golden/push-a-non-integer-attempt-budget-falls-back-to-the-default.json +++ b/tests/golden/push-a-non-integer-attempt-budget-falls-back-to-the-default.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: auto: memories from | from the other clone | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: auto: memories from | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" + "git": "status-all: \nlog-all: Fixture: add learning_ours_one | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: Fixture: add learning_ours_one | from the other clone | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: Fixture: add learning_ours_one | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" } diff --git a/tests/golden/push-a-rebase-conflict-pauses-and-aborts.json b/tests/golden/push-a-rebase-conflict-pauses-and-aborts.json index 5a9d184..d68190a 100644 --- a/tests/golden/push-a-rebase-conflict-pauses-and-aborts.json +++ b/tests/golden/push-a-rebase-conflict-pauses-and-aborts.json @@ -1,6 +1,6 @@ { - "stdout": "Shared memories: auto-push paused — rebase conflict. Resolve manually in .claude/.memories-repo/memories.\n", + "stdout": "Shared memories: auto-push paused — rebase conflict. Run /resolve-memories to merge it.\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: our conflicting edit | seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: our conflicting edit | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_seed_topic.md\nremote-log: from the other clone | seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: our conflicting edit | seed\ntracked-all: memories/learning_seed_topic.md\nstatus: \nlog: our conflicting edit | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_seed_topic.md\nworktree: learning_seed_topic.md\nremote-log: from the other clone | seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/push-a-rejected-push-rebases-and-retries-into-success.json b/tests/golden/push-a-rejected-push-rebases-and-retries-into-success.json index 731b2e0..5a3069a 100644 --- a/tests/golden/push-a-rejected-push-rebases-and-retries-into-success.json +++ b/tests/golden/push-a-rejected-push-rebases-and-retries-into-success.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: auto: memories from | from the other clone | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: auto: memories from | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" + "git": "status-all: \nlog-all: Fixture: add learning_ours_one | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: Fixture: add learning_ours_one | from the other clone | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: Fixture: add learning_ours_one | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" } diff --git a/tests/golden/push-a-zero-attempt-budget-still-tries-once.json b/tests/golden/push-a-zero-attempt-budget-still-tries-once.json index 731b2e0..5a3069a 100644 --- a/tests/golden/push-a-zero-attempt-budget-still-tries-once.json +++ b/tests/golden/push-a-zero-attempt-budget-still-tries-once.json @@ -2,5 +2,5 @@ "stdout": "", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: auto: memories from | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: auto: memories from | from the other clone | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: auto: memories from | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" + "git": "status-all: \nlog-all: Fixture: add learning_ours_one | from the other clone | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nstatus: \nlog: Fixture: add learning_ours_one | from the other clone | seed\nhead-body: \nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md\nworktree: learning_ours_one.md learning_seed_topic.md learning_theirs_one.md\nremote-log: Fixture: add learning_ours_one | from the other clone | seed\nremote-files: memories/learning_ours_one.md | memories/learning_seed_topic.md | memories/learning_theirs_one.md" } diff --git a/tests/golden/push-an-unreachable-remote-reports-auth-or-network.json b/tests/golden/push-an-unreachable-remote-reports-auth-or-network.json index f59c625..79fbed7 100644 --- a/tests/golden/push-an-unreachable-remote-reports-auth-or-network.json +++ b/tests/golden/push-an-unreachable-remote-reports-auth-or-network.json @@ -2,5 +2,5 @@ "stdout": "Shared memories: pull --rebase failed (likely auth or network). Will retry on next Stop.\n fatal: '/nonexistent/definitely-not-here.git' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n", "stderr": "", "code": 0, - "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nreview-shown: absent\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" + "git": "status-all: \nlog-all: local only | seed\ntracked-all: memories/learning_ours_one.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nhead-body: \nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_ours_one.md | memories/learning_seed_topic.md\nworktree: learning_ours_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" } diff --git a/tests/golden/review-a-bare-carriage-return-is-preserved-as-the-bash-preserves-it.json b/tests/golden/review-a-bare-carriage-return-is-preserved-as-the-bash-preserves-it.json deleted file mode 100644 index c470036..0000000 --- a/tests/golden/review-a-bare-carriage-return-is-preserved-as-the-bash-preserves-it.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"before\rafter\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-deletion-renders-its-relative-timestamp.json b/tests/golden/review-a-deletion-renders-its-relative-timestamp.json deleted file mode 100644 index 2f8bdc7..0000000 --- a/tests/golden/review-a-deletion-renders-its-relative-timestamp.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n- DEL memories/learning_seed_topic.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_seed_topic.md\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | D memories/learning_seed_topic.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: D memories/learning_seed_topic.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: \nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-modification-renders-numstat.json b/tests/golden/review-a-modification-renders-numstat.json deleted file mode 100644 index ad35ae0..0000000 --- a/tests/golden/review-a-modification-renders-numstat.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n~ MOD /work/project/.claude/.memories-repo/memories/learning_seed_topic.md> (+3 -1)\n Diff: git -C .claude/.memories-repo diff -- memories/learning_seed_topic.md\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | M memories/learning_seed_topic.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: M memories/learning_seed_topic.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-new-file-renders-with-its-preview.json b/tests/golden/review-a-new-file-renders-with-its-preview.json deleted file mode 100644 index f004b5d..0000000 --- a/tests/golden/review-a-new-file-renders-with-its-preview.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"First real line of the memory.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-preview-longer-than-80-chars-is-truncated.json b/tests/golden/review-a-preview-longer-than-80-chars-is-truncated.json deleted file mode 100644 index 34ce403..0000000 --- a/tests/golden/review-a-preview-longer-than-80-chars-is-truncated.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-stale-stored-hash-reprints.json b/tests/golden/review-a-stale-stored-hash-reprints.json deleted file mode 100644 index 3de8879..0000000 --- a/tests/golden/review-a-stale-stored-hash-reprints.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"A lesson.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-a-tab-in-the-preview-survives.json b/tests/golden/review-a-tab-in-the-preview-survives.json deleted file mode 100644 index b34836d..0000000 --- a/tests/golden/review-a-tab-in-the-preview-survives.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"before\tafter\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-adding-a-file-after-a-report-reprints.json b/tests/golden/review-adding-a-file-after-a-report-reprints.json deleted file mode 100644 index ccabaf5..0000000 --- a/tests/golden/review-adding-a-file-after-a-report-reprints.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 2 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"A lesson.\"\n+ NEW /work/project/.claude/.memories-repo/memories/learning_second_one.md>\n \"Another lesson.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md | ?? memories/learning_second_one.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md | ?? memories/learning_second_one.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_second_one.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-an-empty-file-emits-no-preview-line.json b/tests/golden/review-an-empty-file-emits-no-preview-line.json deleted file mode 100644 index 0a08b67..0000000 --- a/tests/golden/review-an-empty-file-emits-no-preview-line.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-ansi-escapes-in-a-memory-never-reach-the-terminal.json b/tests/golden/review-ansi-escapes-in-a-memory-never-reach-the-terminal.json deleted file mode 100644 index be4a37d..0000000 --- a/tests/golden/review-ansi-escapes-in-a-memory-never-reach-the-terminal.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"safe[31mRED text\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-new-modified-and-deleted-together.json b/tests/golden/review-new-modified-and-deleted-together.json deleted file mode 100644 index 8b479a8..0000000 --- a/tests/golden/review-new-modified-and-deleted-together.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 3 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_brand_new.md>\n \"Brand new.\"\n~ MOD /work/project/.claude/.memories-repo/memories/learning_seed_topic.md> (+1 -1)\n Diff: git -C .claude/.memories-repo diff -- memories/learning_seed_topic.md\n- DEL memories/learning_extra_one.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_extra_one.md\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: M memories/learning_seed_topic.md | ?? .review-shown | ?? memories/learning_brand_new.md | D memories/learning_extra_one.md\nlog-all: add extra | seed\ntracked-all: memories/learning_extra_one.md | memories/learning_seed_topic.md\nstatus: M memories/learning_seed_topic.md | ?? memories/learning_brand_new.md | D memories/learning_extra_one.md\nlog: add extra | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_extra_one.md | memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_brand_new.md learning_seed_topic.md\nremote-log: add extra | seed\nremote-files: memories/learning_extra_one.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-pending-files-plus-an-unpushed-commit.json b/tests/golden/review-pending-files-plus-an-unpushed-commit.json deleted file mode 100644 index 50729b6..0000000 --- a/tests/golden/review-pending-files-plus-an-unpushed-commit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/ and 1 unpushed commit(s)\n+ NEW /work/project/.claude/.memories-repo/memories/learning_brand_new.md>\n \"Brand new.\"\n\n Note: 1 local commit(s) not yet on the remote.\n Push: git -C .claude/.memories-repo pull --rebase --autostash && git -C .claude/.memories-repo push\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_brand_new.md\nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: ?? memories/learning_brand_new.md\nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_brand_new.md learning_committed_x.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-preview-skips-leading-blank-and-whitespace-only-lines.json b/tests/golden/review-preview-skips-leading-blank-and-whitespace-only-lines.json deleted file mode 100644 index 20cf0ea..0000000 --- a/tests/golden/review-preview-skips-leading-blank-and-whitespace-only-lines.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 pending file(s) in memories/\n+ NEW /work/project/.claude/.memories-repo/memories/learning_new_thing.md>\n \"Actual content here.\"\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-the-same-pending-set-is-reported-once.json b/tests/golden/review-the-same-pending-set-is-reported-once.json deleted file mode 100644 index 0014f7b..0000000 --- a/tests/golden/review-the-same-pending-set-is-reported-once.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown | ?? memories/learning_new_thing.md\nlog-all: seed\ntracked-all: memories/learning_seed_topic.md\nstatus: ?? memories/learning_new_thing.md\nlog: seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_new_thing.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-twenty-one-deletions-straddle-the-timestamp-cap.json b/tests/golden/review-twenty-one-deletions-straddle-the-timestamp-cap.json deleted file mode 100644 index 233de1a..0000000 --- a/tests/golden/review-twenty-one-deletions-straddle-the-timestamp-cap.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 21 pending file(s) in memories/\n- DEL memories/learning_bulk_00.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_00.md\n- DEL memories/learning_bulk_01.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_01.md\n- DEL memories/learning_bulk_02.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_02.md\n- DEL memories/learning_bulk_03.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_03.md\n- DEL memories/learning_bulk_04.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_04.md\n- DEL memories/learning_bulk_05.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_05.md\n- DEL memories/learning_bulk_06.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_06.md\n- DEL memories/learning_bulk_07.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_07.md\n- DEL memories/learning_bulk_08.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_08.md\n- DEL memories/learning_bulk_09.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_09.md\n- DEL memories/learning_bulk_10.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_10.md\n- DEL memories/learning_bulk_11.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_11.md\n- DEL memories/learning_bulk_12.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_12.md\n- DEL memories/learning_bulk_13.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_13.md\n- DEL memories/learning_bulk_14.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_14.md\n- DEL memories/learning_bulk_15.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_15.md\n- DEL memories/learning_bulk_16.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_16.md\n- DEL memories/learning_bulk_17.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_17.md\n- DEL memories/learning_bulk_18.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_18.md\n- DEL memories/learning_bulk_19.md (last modified )\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_19.md\n- DEL memories/learning_bulk_20.md\n Recover: git -C .claude/.memories-repo checkout HEAD -- memories/learning_bulk_20.md\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: D memories/learning_bulk_01.md | D memories/learning_bulk_02.md | D memories/learning_bulk_03.md | D memories/learning_bulk_04.md | D memories/learning_bulk_05.md | D memories/learning_bulk_06.md | D memories/learning_bulk_07.md | D memories/learning_bulk_08.md | D memories/learning_bulk_09.md | D memories/learning_bulk_10.md | D memories/learning_bulk_11.md | D memories/learning_bulk_12.md | D memories/learning_bulk_13.md | D memories/learning_bulk_14.md | D memories/learning_bulk_15.md | D memories/learning_bulk_16.md | D memories/learning_bulk_17.md | D memories/learning_bulk_18.md | D memories/learning_bulk_19.md | D memories/learning_bulk_20.md | ?? .review-shown | D memories/learning_bulk_00.md\nlog-all: bulk | seed\ntracked-all: memories/learning_bulk_00.md | memories/learning_bulk_01.md | memories/learning_bulk_02.md | memories/learning_bulk_03.md | memories/learning_bulk_04.md | memories/learning_bulk_05.md | memories/learning_bulk_06.md | memories/learning_bulk_07.md | memories/learning_bulk_08.md | memories/learning_bulk_09.md | memories/learning_bulk_10.md | memories/learning_bulk_11.md | memories/learning_bulk_12.md | memories/learning_bulk_13.md | memories/learning_bulk_14.md | memories/learning_bulk_15.md | memories/learning_bulk_16.md | memories/learning_bulk_17.md | memories/learning_bulk_18.md | memories/learning_bulk_19.md | memories/learning_bulk_20.md | memories/learning_seed_topic.md\nstatus: D memories/learning_bulk_01.md | D memories/learning_bulk_02.md | D memories/learning_bulk_03.md | D memories/learning_bulk_04.md | D memories/learning_bulk_05.md | D memories/learning_bulk_06.md | D memories/learning_bulk_07.md | D memories/learning_bulk_08.md | D memories/learning_bulk_09.md | D memories/learning_bulk_10.md | D memories/learning_bulk_11.md | D memories/learning_bulk_12.md | D memories/learning_bulk_13.md | D memories/learning_bulk_14.md | D memories/learning_bulk_15.md | D memories/learning_bulk_16.md | D memories/learning_bulk_17.md | D memories/learning_bulk_18.md | D memories/learning_bulk_19.md | D memories/learning_bulk_20.md | D memories/learning_bulk_00.md\nlog: bulk | seed\nupstream: origin/main\nunpushed: 0\ntracked: memories/learning_bulk_00.md | memories/learning_bulk_01.md | memories/learning_bulk_02.md | memories/learning_bulk_03.md | memories/learning_bulk_04.md | memories/learning_bulk_05.md | memories/learning_bulk_06.md | memories/learning_bulk_07.md | memories/learning_bulk_08.md | memories/learning_bulk_09.md | memories/learning_bulk_10.md | memories/learning_bulk_11.md | memories/learning_bulk_12.md | memories/learning_bulk_13.md | memories/learning_bulk_14.md | memories/learning_bulk_15.md | memories/learning_bulk_16.md | memories/learning_bulk_17.md | memories/learning_bulk_18.md | memories/learning_bulk_19.md | memories/learning_bulk_20.md | memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_seed_topic.md\nremote-log: bulk | seed\nremote-files: memories/learning_bulk_00.md | memories/learning_bulk_01.md | memories/learning_bulk_02.md | memories/learning_bulk_03.md | memories/learning_bulk_04.md | memories/learning_bulk_05.md | memories/learning_bulk_06.md | memories/learning_bulk_07.md | memories/learning_bulk_08.md | memories/learning_bulk_09.md | memories/learning_bulk_10.md | memories/learning_bulk_11.md | memories/learning_bulk_12.md | memories/learning_bulk_13.md | memories/learning_bulk_14.md | memories/learning_bulk_15.md | memories/learning_bulk_16.md | memories/learning_bulk_17.md | memories/learning_bulk_18.md | memories/learning_bulk_19.md | memories/learning_bulk_20.md | memories/learning_seed_topic.md" -} diff --git a/tests/golden/review-unpushed-commit-with-no-dirty-files.json b/tests/golden/review-unpushed-commit-with-no-dirty-files.json deleted file mode 100644 index e70c72d..0000000 --- a/tests/golden/review-unpushed-commit-with-no-dirty-files.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stdout": "Shared memories [review mode]: 1 unpushed commit(s)\n\n Note: 1 local commit(s) not yet on the remote.\n Push: git -C .claude/.memories-repo pull --rebase --autostash && git -C .claude/.memories-repo push\n\nApprove all: /approve-memories [optional commit reason]\nDiscard local changes: git -C .claude/.memories-repo checkout -- memories/ \\\n && git -C .claude/.memories-repo clean -f -- memories/\n", - "stderr": "", - "code": 0, - "git": "status-all: ?? .review-shown\nlog-all: local only | seed\ntracked-all: memories/learning_committed_x.md | memories/learning_seed_topic.md\nstatus: \nlog: local only | seed\nupstream: origin/main\nunpushed: 1\ntracked: memories/learning_committed_x.md | memories/learning_seed_topic.md\nreview-shown: present\nworktree: learning_committed_x.md learning_seed_topic.md\nremote-log: seed\nremote-files: memories/learning_seed_topic.md" -} diff --git a/tests/harness.ts b/tests/harness.ts index ede7f36..7e85017 100644 --- a/tests/harness.ts +++ b/tests/harness.ts @@ -1,23 +1,21 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { hostname, tmpdir, userInfo } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const REPO = dirname(dirname(fileURLToPath(import.meta.url))); -export type HookName = "pull" | "autopush" | "announce"; +export type HookName = "pull" | "autopush"; const HOOK_FILE: Record = { pull: "pull.mts", autopush: "autopush.mts", - announce: "announce.mts", }; const EVENT: Record = { pull: "SessionStart", autopush: "Stop", - announce: "PostToolUse", }; const GOLDEN_DIR = join(REPO, "tests", "golden"); @@ -36,7 +34,6 @@ export function normalizeRun(r: RunResult, root: string): RunResult { .split(root) .join("") .replace(/\.memories-migration-\d{8}-\d{6}/g, ".memories-migration-") - .replace(/\(last modified [^)]*\)/g, "(last modified )") .replace(/\d{4}-\d{2}-\d{2}/g, "") .replace(/(deleted by )[0-9a-f]{7,40}/g, "$1") .split(host) @@ -278,10 +275,11 @@ export function gitState(repo: string): string { `tracked-all: ${git(repo, "ls-files").split("\n").filter(Boolean).sort().join(" | ")}`, `status: ${git(repo, "status", "--porcelain", "--", "memories/").split("\n").filter(Boolean).sort().join(" | ")}`, `log: ${git(repo, "log", "--format=%s", "--", "memories/").split("\n").filter(Boolean).join(" | ")}`, + // The subject alone hides the per-file list a multi-change commit carries. + `head-body: ${git(repo, "log", "-1", "--format=%b").split("\n").filter(Boolean).join(" | ")}`, `upstream: ${upstream || "(none)"}`, `unpushed: ${upstream ? git(repo, "rev-list", "@{u}..HEAD", "--count") : "0"}`, `tracked: ${git(repo, "ls-files", "--", "memories/").split("\n").filter(Boolean).sort().join(" | ")}`, - `review-shown: ${existsSync(join(repo, ".review-shown")) ? "present" : "absent"}`, `worktree: ${entries(join(repo, "memories")).join(" ")}`, `remote-log: ${git(repo, "log", "--format=%s", "origin/main").split("\n").filter(Boolean).join(" | ")}`, `remote-files: ${git(repo, "ls-tree", "-r", "--name-only", "origin/main").split("\n").filter(Boolean).sort().join(" | ")}`, diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 3d3a10e..566bf11 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -24,7 +24,7 @@ describe("techpack manifest", () => { test("every hook declares the interpreter mcs runs it with", () => { // `.mts` is in mcs's ambiguousExtensions, so without this it falls back to bash. const declared = manifest.match(/hookInterpreter: node --experimental-strip-types/g) ?? []; - assert.equal(declared.length, 3, "each of the three hooks needs its own hookInterpreter"); + assert.equal(declared.length, 2, "each of the two hooks needs its own hookInterpreter"); assert.match(manifest, /minMCSVersion: "2026\.9\.3"/, "the interpreter field needs the release that honours it"); }); @@ -50,8 +50,8 @@ describe("techpack manifest", () => { describe("hook install contract", () => { const dests = [...manifest.matchAll(/destination:\s*(\S+\.mts)/g)].map((m) => m[1] as string); - test("three hooks are registered, all TypeScript", () => { - assert.equal(dests.length, 3, "expected three registered hook entry points"); + test("two hooks are registered, all TypeScript", () => { + assert.equal(dests.length, 2, "expected two registered hook entry points"); for (const d of dests) assert.ok(existsSync(join(REPO, "runtime", d)), `${d} is registered but not shipped`); }); @@ -119,9 +119,9 @@ describe("hook install contract", () => { describe("the naming rule has one definition", () => { test("the slash command documents the pattern the code enforces", () => { - const cmd = readFileSync(join(REPO, "commands", "approve-memories.md"), "utf8"); + const cmd = readFileSync(join(REPO, "commands", "resolve-memories.md"), "utf8"); const m = /\^memories\/\(learning\|decision\)_\[a-zA-Z0-9_-\]\+\\\.md\$/.exec(cmd); - assert.ok(m, "approve-memories.md no longer documents the guardrail pattern"); + assert.ok(m, "resolve-memories.md no longer documents the guardrail pattern"); assert.equal(ALLOWED_PATTERN.source, "^memories\\/(learning|decision)_[a-zA-Z0-9_-]+\\.md$"); }); diff --git a/tests/unit.test.ts b/tests/unit.test.ts index 1ac7f32..46e7806 100644 --- a/tests/unit.test.ts +++ b/tests/unit.test.ts @@ -1,29 +1,13 @@ import { test, describe } from "node:test"; import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { stripJsonComments } from "./harness.ts"; import { isJsonStream } from "../runtime/lib/hook-io.mts"; -import { resolveMode } from "../runtime/lib/mode.mts"; -import { ALLOWED_PATTERN, MEMORY_WRITE_PATTERN } from "../runtime/lib/naming.mts"; +import { ALLOWED_PATTERN } from "../runtime/lib/naming.mts"; +import { commitMessage, parseNameStatus, type Change } from "../runtime/lib/commit-message.mts"; import { jitterMs, pushAttempts } from "../runtime/lib/push.mts"; -import { canonicalState, describe as describePending, hashState, lastShownHash, urlEncodePath } from "../runtime/lib/report.mts"; -import type { Pending } from "../runtime/lib/pending.mts"; - -describe("mode resolution", () => { - for (const [raw, mode] of [[undefined, "auto"], ["", "auto"], ["auto", "auto"], ["full", "full"], ["review", "review"]] as const) { - test(`${JSON.stringify(raw)} resolves to ${mode}`, () => { - const r = resolveMode(raw); - assert.equal(r.mode, mode); - assert.equal(r.unrecognised, null); - }); - } - test("an unrecognised value falls back to auto and is reported", () => { - assert.deepEqual(resolveMode("banana"), { mode: "auto", unrecognised: "banana" }); - }); -}); describe("the naming guardrail", () => { const ok = ["memories/learning_a_b.md", "memories/decision_x-y_z.md", "memories/learning_A1_b2.md"]; @@ -37,12 +21,6 @@ describe("the naming guardrail", () => { ]; for (const f of ok) test(`accepts ${f}`, () => assert.ok(ALLOWED_PATTERN.test(f))); for (const f of bad) test(`rejects ${f}`, () => assert.ok(!ALLOWED_PATTERN.test(f))); - - test("the write pattern anchors on .claude/memories at any depth", () => { - assert.ok(MEMORY_WRITE_PATTERN.test("/a/b/.claude/memories/learning_a_b.md")); - assert.ok(MEMORY_WRITE_PATTERN.test(".claude/memories/decision_a_b.md")); - assert.ok(!MEMORY_WRITE_PATTERN.test("/a/notes/learning_a_b.md")); - }); }); describe("the retry budget", () => { @@ -58,55 +36,44 @@ describe("the retry budget", () => { }); }); -describe("file:// encoding", () => { - test("encodes only what breaks terminal autolinking", () => { - assert.equal(urlEncodePath("/a b/c#d?e"), "/a%20b/c%23d%3Fe"); - }); - test("leaves other reserved characters alone", () => { - assert.equal(urlEncodePath("/a(b)/c&d'e"), "/a(b)/c&d'e"); - }); -}); - -const pending = (over: Partial = {}): Pending => ({ - uncommitted: 0, - unpushed: 0, - untracked: [], - numstats: [], - deleted: [], - ...over, -}); +describe("commit messages", () => { + const add = (p: string): Change => ({ kind: "add", path: `memories/${p}.md` }); + const upd = (p: string): Change => ({ kind: "update", path: `memories/${p}.md` }); + const rm = (p: string): Change => ({ kind: "remove", path: `memories/${p}.md` }); -describe("review state", () => { - test("is stable regardless of the order files are discovered in", () => { - const a = canonicalState(pending({ untracked: ["memories/b.md", "memories/a.md"] }), ""); - const b = canonicalState(pending({ untracked: ["memories/a.md", "memories/b.md"] }), ""); - assert.equal(hashState(a), hashState(b)); + test("a single change names the memory, without path or extension", () => { + assert.equal(commitMessage("Ana", [add("learning_a_b")]), "Ana: add learning_a_b"); + assert.equal(commitMessage("Ana", [upd("learning_a_b")]), "Ana: update learning_a_b"); + assert.equal(commitMessage("Ana", [rm("learning_a_b")]), "Ana: remove learning_a_b"); }); - test("distinguishes a new file from a modified one", () => { - const a = canonicalState(pending({ untracked: ["memories/a.md"] }), ""); - const b = canonicalState(pending({ numstats: [{ added: "1", deleted: "0", path: "memories/a.md" }] }), ""); - assert.notEqual(hashState(a), hashState(b)); - }); - test("ignores content, so re-editing the same file does not reprint", () => { - assert.equal( - canonicalState(pending({ untracked: ["memories/a.md"] }), ""), - canonicalState(pending({ untracked: ["memories/a.md"] }), ""), - ); + + test("a single rename shows both names", () => { + const c: Change = { kind: "rename", from: "memories/learning_old.md", path: "memories/learning_new.md" }; + assert.equal(commitMessage("Ana", [c]), "Ana: rename learning_old → learning_new"); }); - test("an unpushed commit contributes only when HEAD resolves", () => { - assert.equal(canonicalState(pending({ unpushed: 2 }), ""), ""); - assert.match(canonicalState(pending({ unpushed: 2 }), "abc123"), /^UNPUSHED\tabc123\t2$/); + + test("several changes count by kind, leave out empty kinds, and list each file in the body", () => { + const msg = commitMessage("Ana", [rm("learning_z"), add("learning_b"), upd("decision_c"), add("decision_a")]); + assert.equal(msg, "Ana: 2 added, 1 updated, 1 removed\n\n+ decision_a\n+ learning_b\n~ decision_c\n- learning_z"); }); - test("the digest is sha256, matching `shasum -a 256`", () => { - assert.equal(hashState(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + + test("nothing staged still yields a usable subject", () => { + assert.equal(commitMessage("Ana", []), "Ana: update memories"); }); -}); -describe("the pending description", () => { - test("names files and commits separately", () => { - assert.equal(describePending(2, 3), "2 pending file(s) in memories/ and 3 unpushed commit(s)"); - assert.equal(describePending(2, 0), "2 pending file(s) in memories/"); - assert.equal(describePending(0, 3), "3 unpushed commit(s)"); + test("git's name-status lines map onto change kinds", () => { + assert.deepEqual(parseNameStatus("A\tmemories/learning_a.md"), { kind: "add", path: "memories/learning_a.md" }); + assert.deepEqual(parseNameStatus("M\tmemories/learning_a.md"), { kind: "update", path: "memories/learning_a.md" }); + assert.deepEqual(parseNameStatus("T\tmemories/learning_a.md"), { kind: "update", path: "memories/learning_a.md" }); + assert.deepEqual(parseNameStatus("D\tmemories/learning_a.md"), { kind: "remove", path: "memories/learning_a.md" }); + assert.deepEqual(parseNameStatus("R097\tmemories/learning_a.md\tmemories/learning_b.md"), { + kind: "rename", + from: "memories/learning_a.md", + path: "memories/learning_b.md", + }); + assert.deepEqual(parseNameStatus("C100\tmemories/learning_a.md\tmemories/learning_b.md"), { kind: "add", path: "memories/learning_b.md" }); + assert.equal(parseNameStatus("U\tmemories/learning_a.md"), null); + assert.equal(parseNameStatus(""), null); }); }); @@ -119,29 +86,6 @@ describe("the jq stdin gate", () => { } }); -describe("the review dedupe state fails open", () => { - test("an unreadable state file means no dedupe rather than a lost report", () => { - const dir = mkdtempSync(join(tmpdir(), "sm-dedupe-")); - try { - // A directory where the file belongs: readFileSync throws EISDIR, which used - // to escape into failOpen and swallow the entire review report. The bash read - // it as `tr ... 2>/dev/null || true` and simply printed. - const asDir = join(dir, "state-is-a-directory"); - mkdirSync(asDir); - assert.equal(lastShownHash(asDir), ""); - - // The racy case: a concurrent Stop hook removed it after the existsSync. - assert.equal(lastShownHash(join(dir, "never-existed")), ""); - - // And it still reads a real one, `tr -d '[:space:]'` and all. - writeFileSync(join(dir, "shown"), " deadbeef\n"); - assert.equal(lastShownHash(join(dir, "shown")), "deadbeef"); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); - describe("reading tsconfig.json as the JSONC it is by convention", () => { const parse = (t: string): unknown => JSON.parse(stripJsonComments(t)); From 97bbe04e9ebd0c0b5a007d0e198d22e26e91ad64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 08:34:20 +0000 Subject: [PATCH 2/4] Simplify: one name for /resolve-memories, one table per change kind - RESOLVE_COMMAND next to RENAME_HINT; the hooks, push loop and configure script interpolate it instead of spelling the command out eight times. - commit-message.mts: one KINDS table instead of three parallel ones, the rename arrow written once; configure builds its migration body with the shared changeLine instead of its own copy of the name stripping. - Fewer git spawns: operationInProgress asks rev-parse for all three paths at once, pull stops checking the upstream twice, and the Stop hook reads the staged set once instead of probing with diff --quiet first. - authorName drops the hostname fallback: without an identity the commit fails anyway, so the fallback only ever named a commit that never landed. - hook-io: drop the unused parseJson, keep streamValues and emitJson private, and share detail() with the push loop. No behaviour change: every golden still matches. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Jcf1cp8tAMmCJBLywkDC1 --- runtime/autopush.mts | 17 +++++----- runtime/lib/commit-message.mts | 60 ++++++++++++++++++---------------- runtime/lib/git.mts | 40 ++++++++++++++--------- runtime/lib/hook-io.mts | 21 ++++-------- runtime/lib/naming.mts | 3 ++ runtime/lib/pending.mts | 2 +- runtime/lib/push.mts | 12 +++---- runtime/pull.mts | 23 ++++++------- scripts/configure-memories.ts | 7 ++-- tests/differential.test.ts | 17 +++++----- 10 files changed, 104 insertions(+), 98 deletions(-) diff --git a/runtime/autopush.mts b/runtime/autopush.mts index 443f55f..9346354 100755 --- a/runtime/autopush.mts +++ b/runtime/autopush.mts @@ -1,10 +1,10 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning import { authorName, git, gitPresent, isWorkTree, operationInProgress, unpushedCount } from "./lib/git.mts"; import { commitMessage, stagedChanges } from "./lib/commit-message.mts"; -import { failOpen, isJsonStream, readStdin, say, warn } from "./lib/hook-io.mts"; +import { detail, failOpen, isJsonStream, readStdin, say, warn } from "./lib/hook-io.mts"; import { badNames, uncommittedCount } from "./lib/pending.mts"; import { memoriesRepo, projectRoot } from "./lib/paths.mts"; -import { RENAME_HINT } from "./lib/naming.mts"; +import { RENAME_HINT, RESOLVE_COMMAND } from "./lib/naming.mts"; import { syncToRemote } from "./lib/push.mts"; const NAME = "memories_autopush"; @@ -27,7 +27,7 @@ failOpen(NAME, () => { const op = operationInProgress(repo); if (op !== null) { - say(`Shared memories: a ${op} is in progress in .claude/.memories-repo — auto-push paused. Run /resolve-memories to finish it.`); + say(`Shared memories: a ${op} is in progress in .claude/.memories-repo — auto-push paused. Run ${RESOLVE_COMMAND} to finish it.`); return; } @@ -41,19 +41,18 @@ failOpen(NAME, () => { say("Shared memories: skipping auto-push — unconventional filename(s):"); for (const f of bad) say(` - ${f}`); say(RENAME_HINT); - say("Or run /resolve-memories to have Claude rename them."); + say(`Or run ${RESOLVE_COMMAND} to have Claude rename them.`); return; } stage(repo, ["add", "-A", "--", "memories/"]); - if (!git(repo, ["diff", "--cached", "--quiet", "--", "memories/"]).ok) { - const msg = commitMessage(authorName(repo), stagedChanges(repo)); - const commit = git(repo, ["commit", "-m", msg, "--quiet"]); + const changes = stagedChanges(repo); + if (changes.length > 0) { + const commit = git(repo, ["commit", "-m", commitMessage(authorName(repo), changes), "--quiet"]); if (!commit.ok) { say("Shared memories: commit failed; will retry on next Stop."); - const err = `${commit.stdout}${commit.stderr}`.replace(/\n$/, ""); - if (err !== "") process.stdout.write(` ${err}\n`); + detail(`${commit.stdout}${commit.stderr}`.replace(/\n$/, "")); return; } unpushed = unpushedCount(repo); diff --git a/runtime/lib/commit-message.mts b/runtime/lib/commit-message.mts index 3c8f9d0..a1af694 100644 --- a/runtime/lib/commit-message.mts +++ b/runtime/lib/commit-message.mts @@ -4,23 +4,38 @@ export type Change = | { readonly kind: "add" | "update" | "remove"; readonly path: string } | { readonly kind: "rename"; readonly path: string; readonly from: string }; +/** Every kind once: its body mark and summary word, in the order the body lists them. */ +const KINDS = { + add: { mark: "+", past: "added" }, + update: { mark: "~", past: "updated" }, + rename: { mark: ">", past: "renamed" }, + remove: { mark: "-", past: "removed" }, +} as const; +const ORDER = Object.keys(KINDS) as Change["kind"][]; + /** `memories/learning_a_b.md` → `learning_a_b`: the path prefix and extension carry nothing. */ const short = (p: string): string => p.replace(/^memories\//, "").replace(/\.md$/, ""); +const names = (c: Change): string => (c.kind === "rename" ? `${short(c.from)} → ${short(c.path)}` : short(c.path)); + +/** One body line per change, `+ learning_a_b`. */ +export const changeLine = (c: Change): string => `${KINDS[c.kind].mark} ${names(c)}`; + /** One `git diff --name-status` line. Copies (C) are new files as far as the team is concerned. */ export function parseNameStatus(line: string): Change | null { const [status = "", a = "", b = ""] = line.split("\t"); + if (a === "") return null; switch (status[0]) { case "A": case "C": - return a === "" ? null : { kind: "add", path: b || a }; + return { kind: "add", path: b || a }; case "M": case "T": - return a === "" ? null : { kind: "update", path: a }; + return { kind: "update", path: a }; case "D": - return a === "" ? null : { kind: "remove", path: a }; + return { kind: "remove", path: a }; case "R": - return a === "" || b === "" ? null : { kind: "rename", path: b, from: a }; + return b === "" ? null : { kind: "rename", path: b, from: a }; default: return null; } @@ -33,10 +48,8 @@ export function stagedChanges(repo: string): Change[] { .filter((c): c is Change => c !== null); } -const MARK = { add: "+", update: "~", remove: "-", rename: ">" } as const; - -const line = (c: Change): string => - c.kind === "rename" ? `${MARK.rename} ${short(c.from)} → ${short(c.path)}` : `${MARK[c.kind]} ${short(c.path)}`; +const byKindThenPath = (x: Change, y: Change): number => + ORDER.indexOf(x.kind) - ORDER.indexOf(y.kind) || (x.path < y.path ? -1 : x.path > y.path ? 1 : 0); /** * `: add learning_x` for a single change; otherwise a count summary in the @@ -44,25 +57,14 @@ const line = (c: Change): string => * the date is already in the commit. */ export function commitMessage(who: string, changes: readonly Change[]): string { - if (changes.length === 0) return `${who}: update memories`; - if (changes.length === 1) { - const [c] = changes as [Change]; - const what = c.kind === "rename" ? `rename ${short(c.from)} → ${short(c.path)}` : `${c.kind} ${short(c.path)}`; - return `${who}: ${what}`; - } - const count = (k: Change["kind"]) => changes.filter((c) => c.kind === k).length; - const parts = ( - [ - ["add", "added"], - ["update", "updated"], - ["rename", "renamed"], - ["remove", "removed"], - ] as const - ) - .map(([k, label]) => [count(k), label] as const) - .filter(([n]) => n > 0) - .map(([n, label]) => `${n} ${label}`); - const order = { add: 0, update: 1, rename: 2, remove: 3 } as const; - const body = [...changes].sort((x, y) => order[x.kind] - order[y.kind] || (x.path < y.path ? -1 : x.path > y.path ? 1 : 0)).map(line); - return `${who}: ${parts.join(", ")}\n\n${body.join("\n")}`; + const [only] = changes; + if (only === undefined) return `${who}: update memories`; + if (changes.length === 1) return `${who}: ${only.kind} ${names(only)}`; + + const summary = ORDER.flatMap((k) => { + const n = changes.filter((c) => c.kind === k).length; + return n > 0 ? [`${n} ${KINDS[k].past}`] : []; + }); + const body = [...changes].sort(byKindThenPath).map(changeLine); + return `${who}: ${summary.join(", ")}\n\n${body.join("\n")}`; } diff --git a/runtime/lib/git.mts b/runtime/lib/git.mts index 3997b5e..cabd406 100644 --- a/runtime/lib/git.mts +++ b/runtime/lib/git.mts @@ -1,6 +1,5 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { hostname } from "node:os"; export type GitRun = { readonly ok: boolean; @@ -57,32 +56,43 @@ export function hasUpstream(dir: string): boolean { return git(dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).ok; } -export function unpushedCount(dir: string): number { - if (!hasUpstream(dir)) return 0; - const out = gitOut(dir, ["rev-list", "@{u}..HEAD", "--count"]); - const n = Number.parseInt(out, 10); +/** Commits on HEAD the upstream lacks. Assumes an upstream; `unpushedCount` checks first. */ +export function aheadOfUpstream(dir: string): number { + const n = Number.parseInt(gitOut(dir, ["rev-list", "@{u}..HEAD", "--count"]), 10); return Number.isNaN(n) ? 0 : n; } +export function unpushedCount(dir: string): number { + return hasUpstream(dir) ? aheadOfUpstream(dir) : 0; +} + /** * A rebase or merge someone left half-finished. Staging then would mark the * conflicted files resolved, markers and all, and commit into the detached HEAD. */ export function operationInProgress(dir: string): "rebase" | "merge" | null { - const exists = (name: string): boolean => { - const p = gitOut(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]); - return p !== "" && existsSync(p); - }; - if (exists("rebase-merge") || exists("rebase-apply")) return "rebase"; - if (exists("MERGE_HEAD")) return "merge"; + const paths = gitLines(dir, [ + "rev-parse", + "--path-format=absolute", + "--git-path", + "rebase-merge", + "--git-path", + "rebase-apply", + "--git-path", + "MERGE_HEAD", + ]); + const [rebaseMerge, rebaseApply, mergeHead] = paths.map((p) => existsSync(p)); + if (rebaseMerge === true || rebaseApply === true) return "rebase"; + if (mergeHead === true) return "merge"; return null; } -/** The name git will author the next commit with, or the short hostname when git has none. */ +/** + * The name git will author the next commit with. When git has no identity the + * commit itself fails, so the placeholder never reaches a subject. + */ export function authorName(dir: string): string { - const ident = gitOut(dir, ["var", "GIT_AUTHOR_IDENT"]); - const name = /^(.*?) _.md or memories/decision__.md so the guardrail accepts them."; + +/** The slash command that fixes what retrying never will. Every hint names it through this. */ +export const RESOLVE_COMMAND = "/resolve-memories"; diff --git a/runtime/lib/pending.mts b/runtime/lib/pending.mts index d41e1b3..9768949 100644 --- a/runtime/lib/pending.mts +++ b/runtime/lib/pending.mts @@ -9,6 +9,6 @@ export function uncommittedCount(repo: string): number { export function badNames(repo: string): string[] { const tracked = gitLines(repo, ["diff", "--name-only", "HEAD", "--", "memories/"]); const untracked = gitLines(repo, ["ls-files", "--others", "--exclude-standard", "--full-name", "--", "memories/"]); - const dirty = [...new Set([...tracked, ...untracked])].filter((f) => f !== "").sort(); + const dirty = [...new Set([...tracked, ...untracked])].sort(); return dirty.filter((f) => !ALLOWED_PATTERN.test(f)); } diff --git a/runtime/lib/push.mts b/runtime/lib/push.mts index d62462c..d164af1 100644 --- a/runtime/lib/push.mts +++ b/runtime/lib/push.mts @@ -1,5 +1,6 @@ import { git } from "./git.mts"; -import { say } from "./hook-io.mts"; +import { detail, say } from "./hook-io.mts"; +import { RESOLVE_COMMAND } from "./naming.mts"; export function pushAttempts(raw: string | undefined): number { if (raw === undefined || raw === "") return 12; @@ -20,11 +21,6 @@ function sleep(ms: number): void { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } -/** `printf ' %s\n' "$err"` indents the first line only. */ -function detail(err: string): void { - if (err !== "") process.stdout.write(` ${err}\n`); -} - export function syncToRemote(repo: string, attemptsRaw: string | undefined): void { const attempts = pushAttempts(attemptsRaw); for (let attempt = 1; ; attempt++) { @@ -36,11 +32,11 @@ export function syncToRemote(repo: string, attemptsRaw: string | undefined): voi 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-memories to finish it.", + `Shared memories: rebase conflict AND --abort failed — repo may be in a half-rebased state. Run ${RESOLVE_COMMAND} to finish it.`, ); detail(abort.stderr.replace(/\n$/, "")); } else { - say("Shared memories: auto-push paused — rebase conflict. Run /resolve-memories to merge it."); + say(`Shared memories: auto-push paused — rebase conflict. Run ${RESOLVE_COMMAND} to merge it.`); } } else { say("Shared memories: pull --rebase failed (likely auth or network). Will retry on next Stop."); diff --git a/runtime/pull.mts b/runtime/pull.mts index 6d467b1..d1d1558 100755 --- a/runtime/pull.mts +++ b/runtime/pull.mts @@ -1,6 +1,7 @@ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning import { additionalContext, failOpen, isJsonStream, readStdin, warn } from "./lib/hook-io.mts"; -import { git, gitPresent, hasUpstream, isWorkTree, operationInProgress, unpushedCount } from "./lib/git.mts"; +import { aheadOfUpstream, git, gitPresent, hasUpstream, isWorkTree, operationInProgress } from "./lib/git.mts"; +import { RESOLVE_COMMAND } from "./lib/naming.mts"; import { uncommittedCount } from "./lib/pending.mts"; import { memoriesRepo, projectRoot } from "./lib/paths.mts"; @@ -20,7 +21,7 @@ failOpen(NAME, () => { if (op !== null) { additionalContext( "SessionStart", - `Shared memories: a ${op} is in progress in .claude/.memories-repo, so auto-push is paused and teammates' memories were not pulled. Run /resolve-memories to finish it.`, + `Shared memories: a ${op} is in progress in .claude/.memories-repo, so auto-push is paused and teammates' memories were not pulled. Run ${RESOLVE_COMMAND} to finish it.`, ); return; } @@ -29,7 +30,7 @@ failOpen(NAME, () => { if (!hasUpstream(repo)) { additionalContext( "SessionStart", - "Shared memories: the checkout in .claude/.memories-repo has no upstream branch, so memories are committed locally but never pushed or pulled. Run /resolve-memories to fix it.", + `Shared memories: the checkout in .claude/.memories-repo has no upstream branch, so memories are committed locally but never pushed or pulled. Run ${RESOLVE_COMMAND} to fix it.`, ); return; } @@ -37,17 +38,17 @@ failOpen(NAME, () => { git(repo, ["pull", "--ff-only", "--quiet"]); const uncommitted = uncommittedCount(repo); - const unpushed = unpushedCount(repo); + const unpushed = aheadOfUpstream(repo); if (uncommitted === 0 && unpushed === 0) return; - const joined = - uncommitted > 0 && unpushed > 0 - ? `${uncommitted} uncommitted file(s), ${unpushed} unpushed commit(s)` - : uncommitted > 0 - ? `${uncommitted} uncommitted file(s)` - : `${unpushed} unpushed commit(s)`; + const joined = [ + uncommitted > 0 ? `${uncommitted} uncommitted file(s)` : "", + unpushed > 0 ? `${unpushed} unpushed commit(s)` : "", + ] + .filter(Boolean) + .join(", "); additionalContext( "SessionStart", - `Shared memories have lingering state: ${joined}. The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run /resolve-memories for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.`, + `Shared memories have lingering state: ${joined}. The previous Stop hook's auto-push didn't complete. The next Stop retries automatically. If it keeps failing: run ${RESOLVE_COMMAND} for a rebase conflict or misnamed files (must match memories/{learning,decision}_.md); for auth, check ssh-add or run mcs doctor.`, ); }); diff --git a/scripts/configure-memories.ts b/scripts/configure-memories.ts index cf23309..dfa73b3 100755 --- a/scripts/configure-memories.ts +++ b/scripts/configure-memories.ts @@ -2,7 +2,8 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { authorName, git, gitLines, gitOut, gitPresent, isWorkTree } from "../runtime/lib/git.mts"; -import { ALLOWED_PATTERN } from "../runtime/lib/naming.mts"; +import { ALLOWED_PATTERN, RESOLVE_COMMAND } from "../runtime/lib/naming.mts"; +import { changeLine } from "../runtime/lib/commit-message.mts"; const out = (line: string) => process.stdout.write(`${line}\n`); const err = (line: string) => process.stderr.write(`${line}\n`); @@ -244,7 +245,7 @@ function main(): void { if (!add.ok) process.exit(add.exit ?? 1); } // Unchecked, the push below succeeds vacuously and reports one that never happened. - const body = good.map((f) => `+ ${f.replace(/^memories\//, "").replace(/\.md$/, "")}`).join("\n"); + const body = good.map((path) => changeLine({ kind: "add", path })).join("\n"); const msg = `${authorName(repoDir)}: migrate ${good.length} local memor${good.length === 1 ? "y" : "ies"}\n\n${body}`; const commit = git(repoDir, ["commit", "-m", msg, "--quiet"], { inheritStderr: true }); if (!commit.ok) process.exit(commit.exit ?? 1); @@ -256,7 +257,7 @@ function main(): void { } } else { git(repoDir, ["rebase", "--abort"]); - out("Migrated memories committed locally; a rebase conflict blocked the push. Run /resolve-memories in a Claude session to merge it."); + out(`Migrated memories committed locally; a rebase conflict blocked the push. Run ${RESOLVE_COMMAND} in a Claude session to merge it.`); } } diff --git a/tests/differential.test.ts b/tests/differential.test.ts index d0a84d3..a582d6c 100644 --- a/tests/differential.test.ts +++ b/tests/differential.test.ts @@ -210,6 +210,13 @@ const otherClonePushes = (repo: string, project: string, file: string, body: str git(other, "push", "-q"); }; +/** A teammate pushed an edit to the seed memory, and we committed a different one. */ +const conflictingEdit = (repo: string, project: string) => { + otherClonePushes(repo, project, "learning_seed_topic.md", "Their version of the seed.\n"); + memory(repo, "learning_seed_topic.md", "Our version of the seed.\n"); + commit(repo, "our conflicting edit"); +}; + const push: readonly Fixture[] = [ { name: "push: a rejected push rebases and retries into success", @@ -224,11 +231,7 @@ const push: readonly Fixture[] = [ name: "push: a rebase conflict pauses and aborts", expect: /^Shared memories: auto-push paused — rebase conflict\. Run \/resolve-memories to merge it\.$/m, hook: "autopush", - setup: (repo, project) => { - otherClonePushes(repo, project, "learning_seed_topic.md", "Their version of the seed.\n"); - memory(repo, "learning_seed_topic.md", "Our version of the seed.\n"); - commit(repo, "our conflicting edit"); - }, + setup: conflictingEdit, }, { name: "push: an unreachable remote reports auth or network", @@ -375,9 +378,7 @@ describe("incoming memories — behaviour is pinned", () => { /** Our edit conflicts with a teammate's, and the rebase is left mid-way with the file unmerged. */ const rebaseLeftInProgress = (repo: string, project: string) => { - otherClonePushes(repo, project, "learning_seed_topic.md", "Their version of the seed.\n"); - memory(repo, "learning_seed_topic.md", "Our version of the seed.\n"); - commit(repo, "our conflicting edit"); + conflictingEdit(repo, project); git(repo, "pull", "--rebase", "-q"); memory(repo, "learning_new_thing.md", "Written while the rebase was stuck.\n"); }; From 4f1aed0991a5846202e095ae719859486f08f226 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 19:20:43 +0000 Subject: [PATCH 3/4] Gate the commit on git, and give /resolve-memories absolute paths - autopush: decide whether to commit with `diff --cached --quiet` again, and use the parsed name-status list only to word the message. An empty parse (a failed diff, or an unmerged path after a failed autostash) used to skip the commit silently on every Stop. - resolve-memories: ask rev-parse for absolute --git-path results, as the hooks do. A relative path resolves against the project root, so the check looked in the project's own .git and could report "Nothing to resolve." mid-rebase. MERGE_HEAD is checked too, and step 2 says how to finish a merge. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Jcf1cp8tAMmCJBLywkDC1 --- commands/resolve-memories.md | 8 ++++---- runtime/autopush.mts | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/commands/resolve-memories.md b/commands/resolve-memories.md index 5988788..68e57da 100644 --- a/commands/resolve-memories.md +++ b/commands/resolve-memories.md @@ -15,13 +15,13 @@ All git commands run against the memories repo as `git -C .claude/.memories-repo - `git -C .claude/.memories-repo status --porcelain -- memories/`: dirty files - `git -C .claude/.memories-repo rev-parse --abbrev-ref --symbolic-full-name '@{u}'`: the upstream (fails if none) - `git -C .claude/.memories-repo rev-list '@{u}..HEAD' --count`: unpushed commits -- whether `git -C .claude/.memories-repo rev-parse --git-path rebase-merge` or `rebase-apply` exists: a rebase in progress +- `git -C .claude/.memories-repo rev-parse --path-format=absolute --git-path rebase-merge --git-path rebase-apply --git-path MERGE_HEAD`: if any of the three printed paths exists, a rebase (or merge) is in progress. The paths must be absolute: a relative one resolves against the project root and checks the wrong `.git` -If nothing is dirty, nothing is unpushed, an upstream exists and no rebase is in progress, report **"Nothing to resolve."** and stop. +If nothing is dirty, nothing is unpushed, an upstream exists and no rebase or merge is in progress, report **"Nothing to resolve."** and stop. -## 2. A rebase already in progress +## 2. A rebase or merge already in progress -Go straight to step 6 and finish it. Don't commit anything new first. +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`. ## 3. Filename guardrail diff --git a/runtime/autopush.mts b/runtime/autopush.mts index 9346354..9e7ce3e 100755 --- a/runtime/autopush.mts +++ b/runtime/autopush.mts @@ -47,9 +47,11 @@ failOpen(NAME, () => { stage(repo, ["add", "-A", "--", "memories/"]); - const changes = stagedChanges(repo); - if (changes.length > 0) { - const commit = git(repo, ["commit", "-m", commitMessage(authorName(repo), changes), "--quiet"]); + // Whether to commit is git's call; the parsed list only words the message, so a + // status it can't classify still commits (or fails loudly) instead of skipping. + if (!git(repo, ["diff", "--cached", "--quiet", "--", "memories/"]).ok) { + const msg = commitMessage(authorName(repo), stagedChanges(repo)); + const commit = git(repo, ["commit", "-m", msg, "--quiet"]); if (!commit.ok) { say("Shared memories: commit failed; will retry on next Stop."); detail(`${commit.stdout}${commit.stderr}`.replace(/\n$/, "")); From f804360c16305a596676d693785137c5b7150865 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 20:02:37 +0000 Subject: [PATCH 4/4] Make /resolve-memories user-invoked only, with an argument hint Commands now load like skills, so Claude could run this one on its own when a hook message names it. Merging conflicts and pushing to the team's repo should be the user's call: disable-model-invocation keeps it manual, and argument-hint shows the optional commit reason while typing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Jcf1cp8tAMmCJBLywkDC1 --- commands/resolve-memories.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands/resolve-memories.md b/commands/resolve-memories.md index 68e57da..0b2e017 100644 --- a/commands/resolve-memories.md +++ b/commands/resolve-memories.md @@ -1,5 +1,7 @@ --- description: Fix what the shared-memories Stop hook can't fix on its own, then push +argument-hint: "[reason]" +disable-model-invocation: true --- # Resolve Shared Memories