# dsh-checkpoint-diff

**File-diff visualization between checkpoint time nodes for
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).**

Every time a mutating tool runs,
[dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
captures a snapshot of your workspace — a checkpoint *time node*. This plugin
turns those nodes into a browsable timeline and shows you exactly what changed
**between any two of them**: a GUI panel with a per-file line diff, a `/diff`
command for headless use, and a small JSON API.

Read-only by design, with one explicit exception: **rollback**. You can restore
workspace files from any time node — the whole node or a single file — through
   the panel, `/rollback`, or the API. Rollback is *overwrite only*: it never
   deletes files created after the node, never touches `.git`/`.dsh`, and never
   writes to snapshot storage, git, or sessions. The single exception to "never
   deletes" is rollback's **single-shot undo** (`/rollback --undo`, panel button):
   it may remove a file only when the restore itself created it — and nothing
   else (in-process, lost on restart).

> [!IMPORTANT]
> **Install the upstream first.** This plugin is a *read-only consumer* — the
> checkpoints (snapshots) are produced by the upstream
> [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
> (the **snapshot producer**); this plugin itself **never produces snapshots**.
> **Installing only `dsh-checkpoint-diff` leaves the checkpoint timeline
> empty**: the Diff button, `/diff` and rollback have nothing to show (only the
> Trace (session log) scope works). See [Install](#install).

[![npm version](https://img.shields.io/npm/v/dsh-checkpoint-diff)](https://www.npmjs.com/package/dsh-checkpoint-diff)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![CI](https://github.com/tmpdot/dsh-checkpoint-diff/actions/workflows/ci.yml/badge.svg)](https://github.com/tmpdot/dsh-checkpoint-diff/actions/workflows/ci.yml)

> 中文版（默认）：[README.md](README.md)

## Why it exists: the idea

**The situation.** AI agents write code and modify files for you. Their
*thinking* is a black box, but their *actions* land in your workspace — and
with that come the questions: what did it change? When? And what if it broke
something? These questions must never be unanswerable.

**The stance.** Tools like this usually go one of two ways: **save you the
effort** — summarize, judge, filter on your behalf; or **let you rest easy** —
hand you the raw truth and guarantee you can always inspect it, trace it, and
roll it back. This plugin is unambiguously the second kind: no "read it for
you" summaries or filtering — it hands you the full power to **see clearly**
and **step back**.

- **You don't have to look — but you must be able to.** Ignore it day to day;
  but whenever you want, every mutating action maps to a fine-grained snapshot
  time node, and any two nodes can be diffed file by file, line by line.
- **When something goes wrong, you can trace it to the bottom.** Which step,
  which tool, which files — precisely reconstructed; and if it was a mistake,
  restore the workspace from any time node (overwrite-only, never deletes,
  undoable).
- **Peace of mind comes not from infallibility, but from traceability and
  reversibility.**

One line: **you may not look, but it must never be un-inspectable.** People
may choose not to look and not to investigate; what they must never lack is
the *means* to do both. That is why this plugin exists.

## Demo

### GUI panel

Real captures from a live harness session. Panel overview — scope switch,
from/to node selects (intent labels, `(HEAD)` prefix), tree file list
(A/M/D), per-line diff (red/green + ↑/↓ change-block jumping), and the
Restore workspace card:

![Panel overview](screenshots/01-overview.png)

**Trace (session log)** scope — session-log replay with the three-lane
drag-select timeline (Input / Model / Tools spans, turn boundaries; a drag
range maps to the nearest tool-call boundaries in from/to):

![Trace scope](screenshots/02-trace.png)

**This session** scope — the current session's checkpoint timeline.Select the version to roll back and click Preview Restore to preview the changes:

![This session scope](screenshots/03-this%20session.png)

**This project** scope — cross-session merge with the fork-lineage branch
dropdown:

![This project scope](screenshots/04-this%20project.png)

> Re-capture and update the README after any significant UI change — capture
> rules in [docs/screenshots/README.md](docs/screenshots/README.md).

### Command line (verbatim output format)

`/diff` lists this session's timeline — rewind leaves a snapshot time node
before every mutating tool run (format is the real output, data is
illustrative):

```text
diff: 3 checkpoint(s) for this session
  #a1b2c3d4  20m ago  turn 2 step 1  copy  3 file(s)  18 KiB  edit lib/engine.mjs
  #b2c3d4e5  10m ago  turn 3 step 1  copy  2 file(s)  12 KiB  edit README.md
  #c3d4e5f6   5m ago  turn 3 step 2  copy  4 file(s)  25 KiB  bash pnpm test
usage: /diff <from> <to>  (id prefix or "latest" for either side)
```

`/diff <from> <to>` between any two nodes — file summary + line diff (the GUI
panel shows the same red/green diff side by side):

```text
diff: #b2c3d4e5 (14:02 · edit README.md) → #c3d4e5f6 (14:07 · bash pnpm test)
  4 file(s) changed: +1 added, 2 modified, 1 deleted
  M  lib/engine.mjs
    - function lcs(a, b) {
    + function lcs(a, b, opts) {
  M  README.md
    - Read-only by design, one exception: rollback
    + Read-only by design, one explicit exception: rollback
  A  test/engine.test.mjs
  D  lib/legacy.mjs
```

## At a glance

- **What it is** — a DeepSeek Harness plugin that turns
  dsh-checkpoint-rewind snapshots into a browsable timeline with per-file,
  line-level diffs between any two time nodes (GUI panel / `/diff` / HTTP
  API), plus preview-first rollback (overwrite-only, never deletes, undoable).
- **Prerequisite** — **install the upstream
  [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
  (the snapshot producer) first**: this plugin only *reads* the checkpoints
  that rewind produces and **never creates snapshots itself**. Without it,
  the checkpoint timeline is empty and only the Trace (session log) scope
  works. See [Install](#install).
- **What it solves** — after an AI agent mutates your workspace: what changed,
  when, and how to trace and recover when things go wrong.
- **Who it's for** — developers using DeepSeek Harness with
  dsh-checkpoint-rewind installed.
- **Compatibility** — DSH 0.1.0-rc.5 / rc.6 · Node ^22.19 || >=24 ·
  Apache-2.0.
- **Entry points** — Diff button in the session header · `/diff` · `/rollback`
  · `/checkpoint-diff/api`.

## Features

- **Timeline** — every checkpoint as a selectable node (`#short-id`, time,
  turn/step, provider, trigger tool; `/rewind` guard checkpoints are marked).
- **Cross-session / same-project timelines** — switch to *This project* to
  merge checkpoints from every session sharing the workspace key, organized
  by `/rewind` fork lineage: branch dropdown (root + side branches, session
  titles), per-session tags, and fork / root-missing markers. Falls back to a
  flat merge when the optional `dsh-session-query` service is absent.
- **Intent labels** — each node is named from the session log: the `tool/call`
  events of its `(turn, step)` are looked up to produce labels like
  `edit README.md` or `bash pnpm test` (in the GUI, `/diff` output and the
  JSON API), falling back to the raw trigger tool when the log is unavailable.
  Cold sessions are resolved read-only through `sessionQuery.readSession`.
- **File summary** — changed files between two nodes with `A` (added) /
  `M` (modified) / `D` (deleted) badges.
- **Tree file view** — the changed-file list renders as a collapsible directory
  tree with per-directory A/M/D counts; click a file for its line diff.
- **Line diff** — a self-contained LCS engine (no git required for `copy`
  snapshots) renders `ctx`/`del`/`add` lines with aligned line numbers.
- **Trace replay (works without rewind)** — switch the panel scope to
  *Trace (session log)* or run `/diff --trace`: every `tool/call` boundary in
  the session log (`session.jsonl.zstd`) becomes a time node
  (`trace:<seq>`), and selecting any two nodes diffs that interval by
  **replaying `write`/`edit`/`str_replace_editor` arguments** (full file
  contents are in the tool parameters). No snapshot producer is needed — past
  sessions work out of the box, and rewind absence, quota eviction or `git gc`
  no longer silence the "what changed" question. Shell-command edits are
  invisible to the trace; any replay divergence is reported in `notes`
  (never silent). Data comes from `sessionQuery.readSession` first, with a
  direct zstd read (Node ≥ 23.5) as the last-resort fallback.
- **Two snapshot providers** — `git` (unreferenced stash/commit-tree objects,
  read via `git diff-tree`/`git show`) and `copy` (snapshot directories +
  manifests), dispatched per record; mixed-provider pairs are rejected loudly.
- **Rollback** — restore the workspace (or a single file) from any time node:
  preview first, then apply. Overwrite only — files created after the node are
  reported and left in place, never deleted. Works in session and project
  scope (cross-session nodes address the same workspace). Paths are validated
  (no traversal, no absolute paths, `.git`/`.dsh` never touched); the git
  provider only uses read-only git primitives and refuses to run unless the
  session cwd is the repository root.
- **Restore preview diff** — while previewing a restore, click any file in the
  plan to see the **current workspace → target snapshot** line diff right in
  the diff pane ("current → #target"), so you can see exactly what the
  restore would change before applying it.
- **Single-shot undo** — after applying a restore, one `↩ Undo this restore`
  action (panel button, `/rollback --undo`, `POST /api/rollback-undo`)
  reverts it: overwritten files get their pre-restore content back, and files
  the restore created are removed (the one "never deletes" exception).
  In-process only (lost on restart), no redo; files changed since the restore
  are skipped untouched.
- **Diff view niceties** — ↑/↓ buttons jump between **change blocks** of the
  current diff (a block is a maximal run of changed lines — a modification's
  red and green regions count as one block, a run of added lines as one);
  each jump centers the **block's middle line** in the viewport; at the last
  block ↓ shows a "Last change block (click again to wrap)" toast and only
  wraps on the next press (↑ symmetric at the first block); opening a file
  auto-scrolls to its **first** change block; a small "Last view" line under
  the diff pane jumps back to the previously viewed node pair (persisted in
  localStorage).
- **Position labels** — the newest node is tagged `(HEAD)` (uppercase,
  prefixed to the id: `(HEAD) #bbbbbbbb 20:26 · edit b.txt`) in the
  from/to/restore-target selects, in the summary range line and in the
  restore-preview tag. HEAD always means **the current snapshot** — the
  global newest node — so it never moves when you pick other versions or
  filter branches; refreshing the timeline moves it to a newer snapshot.
- **Separate restore section** — rollback lives in its own collapsible
  "Restore workspace" card below the toolbar, clearly separated from the
  node-diff area; its plan/result/undo status renders inside the card, and
  the restore preview (tagged "Restore preview: current workspace → …")
  is **independent of the from/to diff**: picking other versions never
  clears it, and changing the restore target reloads it against the new
  target. Preview-plan file rows are obvious links (`🔍 file`).
- **Graceful degradation** — git checkpoints whose snapshot object was
  reclaimed by `git gc` (or lost on re-clone) are marked `⚠ degraded` in the
  selects and excluded from the default selection; the timeline shows a
  "N checkpoint(s) degraded" banner, and any diff/rollback error names the
  exact dead node (`checkpoint #9312717a (to side) is missing from this
  repository (bad object …)`). No data is ever deleted; pick newer nodes or
  run rewind's `/rewind clear` to reset the timeline. Pruned checkpoints,
  missing records and missing files keep their existing clear errors.

## Alternatives compared

Through the lens of "an AI agent just modified my workspace", against the
common options (facts per each project's official docs):

| Tool | Snapshot granularity | Diff between any two nodes | Restore / rollback | Session awareness |
|---|---|---|---|---|
| **dsh-checkpoint-diff (this plugin)** | before every mutating tool run | **✅ yes, per file and per line** (GUI / `/diff` / API; intent labels, tree view, change-block jumping) | **preview-first → apply; never deletes; single-shot undo**; whole node or single file | **session + cross-session project scope** (fork lineage, branches) |
| dsh-checkpoint-rewind (upstream producer) | before every mutating tool run (0.5.0+: + per-turn / scheduled / manual) | ✅ settings-page pairwise compare (0.5.0+: file change-set + config line diff; no per-file workspace line diff) | `/rewind` preview → restore → fork (guard checkpoint; 0.5.0+: seed-replay session rewind) | single session |
| dsh-turn-rewind | per user message (coarser) | ❌ no (changeLedger service, no visual diff) | conversation + workspace rewind; web restore dialog | single session |
| dsh-snapshot | before each write/edit/delete (target file content) | ❌ no line-level diff | roll back a whole "conversation" (re-rollbackable) | single session (project-level quota) |
| Claude Code native checkpointing + `/rewind` | per conversation turn (automatic) | no node-to-node diff view in the docs (see official docs) | `/rewind` restores files (git-tracked) | inside Claude Code sessions |
| git baseline (`git diff`/`restore`, GitLens etc.) | per manual commit | ✅ any two commits, line-level (mature ecosystem) | `git restore`/`checkout`; no preview planning, no undo guard | none (bare repo) |
| IDE local history (JetBrains Local History / VS Code Timeline) | per edit/save (per file) | single-file revision level | restore any single-file revision | none |
| OS-level snapshots (Time Machine / Windows File History) | hourly/daily | ❌ no line-level diff | whole file/directory restore | none |

Note: the DSH ecosystem also has timeline-style plugins such as
`dsh-message-timeline`, `dsh-session-timeline`, `dsh-chat-timeline` and
`dsh-undo-savepoint` (DSH-config domain); this table focuses on the
diff-visualization + rollback dimension.

**Ecological position (verified 2026-08-17)**: upstream rewind 0.5.0 now ships
its own settings-page timeline with pairwise compare (file change-set + config
diff; source-verified: no per-file workspace line diff). This plugin
does not compete with the producer over "having a diff"; it stays a read-only
consumer + analysis layer — intent labels, cross-session fork lineage, non-git
workspaces (copy), preview-first rollback with single-shot undo, `/diff` and
the HTTP API — and aims to degrade gracefully even when rewind is absent or
snapshots are gone. Ecosystem assessment, compatibility facts (rewind 0.5.0
domain v2) and the differentiation roadmap:
[docs/competitive-analysis.md](docs/competitive-analysis.md).

## Install

> [!IMPORTANT]
> **Upstream first.** [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
> is the **snapshot producer** — it creates the checkpoints (snapshots) this
> plugin reads; this plugin is a *read-only consumer* and **never produces
> snapshots itself**. **Installing only `dsh-checkpoint-diff` leaves the
> timeline empty**: the Diff button, `/diff` and rollback have nothing to show
> (only the Trace (session log) scope works, see
> [Features](#features)). If you skipped the upstream, add it and restart the
> harness.

```bash
dsh plugin --profile web add dsh-checkpoint-rewind   # ⚠️ upstream snapshot producer — install first
dsh plugin --profile web add dsh-checkpoint-diff     # this plugin (consumer)
```

Both are bundle plugins: `dsh plugin add` appends them to the profile's
`dsh.profile.bundles`, and each package's `cordis.patch.yml` inserts its row.
**Restart the harness** for host-side changes; the browser bundle is served
with `cache-control: no-cache`, so a page refresh picks up client fixes.

> The bundle layers must mount **rewind before diff** (the diff plugin reuses
> rewind's `checkpoints` storage domain; the bundle order in
> `dsh.profile.bundles` decides this — verify it after `remove`/`add`
> cycles, which re-sort dependencies).

## Usage

### GUI

A **Diff** button appears in the session header action row. It opens a panel:

1. the toolbar has a scope switch — **This session** (current session only,
   default) or **This project** (every session sharing the workspace, with a
   **branch dropdown** when fork lineage is available: root branch + side
   branches, titles from `readTitle`);
2. pick two time nodes with the **from / to** selects (defaults: first →
   latest); each option shows the derived intent label when one is available
   (e.g. `#a1b2c3d4 14:02 · edit README.md`), prefixed with the owning
   session's short id in project scope;
3. the left pane lists changed files as a collapsible directory tree — directory
   rows show their A/M/D counts and toggle their subtree; file rows keep the
   A/M/D badge;
4. click a file for its line diff (red = deleted, green = added); the diff
   head carries ↑/↓ buttons that jump between the change blocks (block center
   aligned; at the last block ↓ first shows a toast and only wraps on the
   next press — ↑ symmetric), and the view opens at the first change block
   of the file.

**Rollback.** A collapsible **Restore workspace** card sits below the toolbar,
separate from the node-diff area: pick the node you want to restore to
(defaults to the latest node; every file row also carries a `↩` button to
restore just that file), press **Preview restore** to see the plan
(would-restore / unchanged / skipped files, plus files created after the node
that will be left in place), then **Apply restore**. The panel refreshes the
timeline afterwards.

While a restore plan is showing, click any *would-restore* file (shown as an
obvious `🔍 file` link) to see the **current workspace → target snapshot**
diff in the right pane, tagged "Restore preview: …" — the toolbar from/to
selects switch to a read-only "current workspace → target" pair so the
comparison direction is always obvious (click a file in the tree to exit
back to the normal from/to diff). The preview belongs to the restore section:
picking other from/to versions never clears it, and changing the restore
target reloads it against the new target. After an apply, the status area
lists what was restored without offering stale preview rows; an **↩ Undo this
restore** button reverts the restore in one shot (overwritten files get their
previous content back; files the restore created are removed; anything
changed since is left alone). The diff pane also remembers your last viewed
node pair: a small **Last view** line below it jumps back to it (even across
session/project scope).

Fork junctions crossed by the selected range are shown as `⤷ fork` markers;
an incomplete lineage renders an "earlier history unavailable" hint.

Click anywhere outside the panel to close it.

### Command

```
/diff                      list this session's checkpoint timeline
/diff <from> <to>          file summary + per-file line diffs between two nodes
/diff --project            list the project timeline (all sessions, branch header)
/diff --project <from> <to>  diff between two nodes across sessions
/rollback [--project] [--dry-run] <node> [<path>...]
                           restore workspace files from a time node
/rollback --undo           revert the most recent restore (single-shot)
```

`<from>` / `<to>` accept an id prefix or `latest`, e.g. `/diff a1b2c3d4 latest`.
In project scope, ambiguous prefixes prefer the current session's records.

`/rollback` restores every file of the node, or only the given paths; `latest`
is a valid node. `--dry-run` prints the plan without writing anything
(`would restore N file(s), M unchanged, K skipped` + the files that will be
left in place); `--project` addresses nodes from any session sharing this
workspace. Rollback never deletes files — contents are overwritten in place.
`/rollback --undo` reverts the most recent restore (in this process): files
return to their pre-restore content, and files the restore created are
removed; files changed since the restore are skipped (all skipped → error).

### HTTP API

Served by the harness `webServer` at the `/checkpoint-diff/api` prefix
(same-origin JSON). Read endpoints are GET-only and accept an optional
`scope=project` parameter to address records across sessions; `rollback` and
`rollback-undo` are POST endpoints (the only endpoints that write, and they
only write into the session workspace):

| Endpoint | Method | Params / Body | Returns |
|---|---|---|---|
| `/api/timeline` | GET | `session`, `scope?` | `{ok, records[]}` — newest-first timeline views; `scope=project` adds `branches[]` and `markers[]` |
| `/api/summary` | GET | `session, from, to, scope?` | `{ok, from, to, files[{path,status}], totalFiles, truncated}` |
| `/api/file-diff` | GET | `session, from, to, path, scope?` | `{ok, ops[{type:'ctx'\|'del'\|'add', text, a?, b?}], truncated, binary}` |
| `/api/preview-diff` | GET | `session, target, path, scope?` | `{ok, path, ops[], truncated, binary, present}` — current workspace → target snapshot diff (read-only) |
| `/api/rollback` | POST | JSON `{session, target, scope?, paths?, dryRun?}` | `{ok, dryRun, scope, target, files[{rel,action,reason?,mode?}], restored, unchanged, skipped, leftovers, notes}` |
| `/api/rollback-undo` | POST | JSON `{session}` | `{ok, target, time, restored, removed, skipped[]}` — reverts the most recent restore |

For `/api/rollback`: `target` is an id prefix or `latest`; `paths` limits the
restore to those files (omit = whole node); `dryRun: true` plans without
writing (`restored` then means *would restore*). `files[].action` is
`restore` \| `unchanged` \| `skip`; `leftovers` lists files created after the
node that were left in place (never deleted).

For `/api/preview-diff`: the diff direction is **current → snapshot** (`del` =
lines the restore would remove, `add` = lines it would bring back); `present`
is `both` or `workspace-missing` (file does not exist in the workspace yet).

For `/api/rollback-undo`: `restored` = files rewritten to their pre-restore
content, `removed` = files the restore created that were deleted (the one
"never deletes" exception), `skipped` = files left alone because they changed
since the restore (all skipped → `409`). Undo state lives in process memory
only — a harness restart clears it (then `404 nothing to undo`).

Errors: `{ok:false, error}` (400 bad params / mixed providers / bad scope /
unsafe paths, 404 unknown session, file absent from the node, or endpoint,
405 wrong method, 413 oversized body, 500 storage or git failures).

## Data contract

The public contract of this plugin's guarantees — consumption contract +
rollback safety contract: **[docs/contract.md](docs/contract.md)**.

- Reads the `checkpoints` storage domain (schema re-declared in
  `lib/domain.mjs`; the spec is not exported by rewind). Reuses the domain
  already opened by rewind (`storageDomain.get()` first, `open()` fallback
  with an `already-open` retry window).
- Records belong to a `(sessionId, cwd)` pair. `scope=session` (default)
  filters to the current session; `scope=project` merges every session
  sharing the workspace key and organizes them by `/rewind` fork lineage
  (`sessionQuery.traceSession`, optional service — absent = flat merge with
  no branch markers).
- **Intent labels** are derived read-only from the session log: `tool/call`
  events whose `(turn, step)` matches the checkpoint are indexed per request;
  a name matching the record's `triggerTool` wins, `fs/*-intent` triggers
  prefer the step's first mutating tool, otherwise the step's first call, and
  a missing log falls back to the raw trigger tool (`lib/labels.mjs`). Live
  sessions are read via `session.events`; cold sessions via
  `sessionQuery.readSession` (both fail soft).
- Snapshots are pre-mutation states: `/diff <from> <to>` compares the `from`
  snapshot against the `to` snapshot — `to` does not include changes made
  after it.
- **git snapshots cover tracked files only** (`git stash create` semantics):
  new/untracked files never enter a snapshot. To get them into the timeline
  and diff, stage them with `git add <path>` first (no commit needed — the
  index is enough).
- **Rollback restores a node's snapshot file set** into the current workspace:
  for `git` nodes that is the tracked file tree of the snapshot object, for
  `copy` nodes the manifest file list. Anything in the workspace that is not
  in the node's set is left in place and reported (`leftovers`). The git
  provider refuses to roll back when the session cwd is not the repository
  root (snapshot tree paths are root-relative). The most recent apply can be
  reverted once with `/rollback --undo` (in-process only).
- Unreferenced git objects are reaped by `git gc` (default ~2 weeks) and
  `maxSnapshots`/`maxSnapshotBytes` quotas prune old records — missing nodes
  degrade gracefully.

## Development

```bash
pnpm install
pnpm test                 # unit tests (diff engine, timeline, labels, tree, jsdom panel smoke tests)
pnpm test:integration     # assembled headless integration vs. real dsh-checkpoint-rewind
pnpm build:client         # rebuild the browser bundle -> lib/client.js
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for conventions and the
[ARCHITECTURE.md](ARCHITECTURE.md) for the design. The integration suite uses
the local harness deployment (rc.5 packages are not on the npm registry; see
`scripts/link-profile-deps.mjs`).

## Roadmap

- **Projection-based queries** — the `scope=project` timeline currently scans
  the whole checkpoints table per request; a `sessionProjections` unit could
  index by workspace key once the host event vocabulary covers
  `checkpoint/*` events.
- **Branch-aware diffing UI** — visual branch lines between time nodes (the
  panel currently shows fork markers in the summary area only).

## FAQ

**The Diff button / `/diff` shows no time nodes at all?**

You most likely skipped the upstream plugin
[dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
(the snapshot producer). This plugin never creates snapshots — it only reads
the checkpoints rewind leaves behind, so without the upstream the checkpoint
timeline is empty (only the Trace (session log) scope works). Add the
upstream and restart the harness; also make sure the bundle order puts
**rewind before diff** (see [Install](#install)).

**I changed workspace files but `checkpoint` says "nothing new was captured"?**

Two common causes: ① the edits touched **untracked files** — git snapshots
cover tracked files only (see [Data contract](#data-contract)); `git add
<path>` and checkpoint again. ② an automatic snapshot just captured the same
state — the dedup reply now notes "latest checkpoint (auto, seq …) already
records this exact workspace state", and the node is already on the timeline
for you to compare against.

## Acknowledgements

This plugin is built on top of
[dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind)
by [PerryLink](https://github.com/PerryLink) — the checkpoint *producer*
whose read-only storage domain (`checkpoints`) and snapshot layout this
project consumes. It is **not a fork** and shares no code with it: the two
integrate through the storage contract only. Third-party notices:
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

## License

Apache-2.0 — see [LICENSE](LICENSE). Third-party notices in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
