Change — snapshot transactions
begin → commit or rollback. Atomic, snapshot-backed edits so agents can experiment without leaving the repo in a half-written state.
On this page
Change — snapshot transactions
The Change system is how Mandu lets agents (and humans) make multi-file edits without fear. You open a transaction, you do your work, and you either commit the result or roll back to the exact state that existed before the transaction began. No "git stash", no manual backups, no half-edited specs.
It is implemented in @mandujs/core/change and surfaces at two layers: the
mandu change CLI and the mandu.tx.* MCP tools.
When to use it
Reach for a transaction whenever a single logical change touches more than one file or when the operation might be reverted later:
- Negotiated scaffolds (
mandu.negotiate.scaffoldwrites many files). mandu.generateruns, which may rewrite routes, manifests, and resources.- Applying an ATE heal suggestion (
ate.apply_healtakes a snapshot by default). - Guard auto-corrections (
guard.healapplying fixes).
For a one-line edit that you are sure about, you do not need a transaction. Everything else benefits from one.
CLI surface
bunx mandu change begin --message "feature: add payment scaffolds"
bunx mandu change status
bunx mandu change commit
bunx mandu change rollback # rolls back the currently active change
bunx mandu change rollback <changeId> # rolls back a committed change by id
bunx mandu change list
bunx mandu change prune # reclaim snapshots from old records
status reports the active change (if any) with its id, snapshot id, started
timestamp, and message. list shows the full history with stats:
📊 Stats:
Total records: 12
Active: 0 | Committed: 10 | Rolled back: 2
Snapshots: 12
📜 Change history:
✅ 20260401-143010-a1b feature: scaffold payment
↩️ 20260331-091822-c2d negotiated auth (reverted)
MCP surface
Every CLI operation has a matching tool under the transaction category:
| MCP tool | Shape |
|---|---|
mandu.tx.begin |
{ message?, sessionId? } → { changeId, lockId, snapshotId, ... } |
mandu.tx.commit |
{ lockId? } → { changeId, message } |
mandu.tx.rollback |
{ changeId?, lockId? } → { changeId, restored: { filesRestored, filesFailed, errors } } |
mandu.tx.status |
{} → { hasActiveTransaction, changeId?, snapshotId?, lock } |
Legacy aliases mandu_begin, mandu_commit, mandu_rollback,
mandu_tx_status still resolve to the same handlers, but new integrations
should use the dotted names.
The MCP layer adds a concurrency lock on top of the core transaction
state: mandu.tx.begin acquires a lockId bound to an optional
sessionId, and commit / rollback expect that lockId back. That keeps
two agents from stepping on each other inside the same project.
What gets snapshotted
createSnapshot captures:
.mandu/routes.manifest.json(required).- Every
*.tsfile underspec/slots/**. - The Mandu lockfile and MCP config section.
- Validated
mandu.configsnapshot.
Snapshots live at .mandu/history/snapshots/<snapshotId>.snapshot.json.
Transaction records live at .mandu/history/changes.json. The single active
transaction pointer is .mandu/history/active.json. Everything is plain JSON,
so you can inspect — or, in emergencies, restore — by hand.
Typical flow
From the agent side, the sequence around a scaffold looks like this:
// 1. Open a transaction
const begin = await mcp.call("mandu.tx.begin", {
message: "scaffold: payment module",
sessionId: "agent-1",
});
// 2. Do the work — scaffolds, generates, guard fixes, etc.
try {
await mcp.call("mandu.negotiate.scaffold", { plan });
await mcp.call("mandu.generate", { dryRun: false });
await mcp.call("mandu.guard.check", {});
// 3a. All good → commit
await mcp.call("mandu.tx.commit", { lockId: begin.lockId });
} catch (err) {
// 3b. Any step failed → rollback to the opening snapshot
await mcp.call("mandu.tx.rollback", { lockId: begin.lockId });
throw err;
}
From the CLI side, the same flow is three commands bracketing whatever you did in between:
bunx mandu change begin --message "negotiate auth"
# ... edits / generators ...
bunx mandu change commit # or: bunx mandu change rollback
Rolling back a committed change
Rollback is not limited to the currently active transaction. Pass a specific
changeId to restore any committed snapshot:
bunx mandu change rollback 20260401-143010-a1b
The record is marked rolled_back but kept in history, so the action itself
is auditable.
Pruning
Snapshots accumulate. bunx mandu change prune removes snapshot files whose
records are already marked committed beyond a retention window. It never
removes records themselves — changes.json remains the full log.
Do not do this
- Do not open a second transaction while one is active.
beginChangethrows with the existingchangeIdand message — that is the signal to finish the current work, not to force it.- Do not edit
.mandu/history/*.jsonby hand during an active transaction. The on-disk state is the source of truth for the MCP lock and the CLI.- Do not rely on
commitundoing anything. Commit only marks a record as final; it does not restore files. If you want the state from before, rollback is the only path.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/build-with-agents/change to my project.
Summary of the page:
The Change system is in @mandujs/core/change. CLI: bunx mandu change begin|commit|rollback|status|list|prune. MCP: mandu.tx.begin|commit|rollback|status. State lives under .mandu/history with snapshots, changes.json, and a single active.json lock.
Required invariants — must hold after your changes:
- Only one transaction can be active at a time per project (enforced via .mandu/history/active.json)
- Every begin takes a full snapshot of the manifest and spec/slots before any write
- rollback restores files from the captured snapshot; commit only marks the record committed
- Change IDs use YYYYMMDD-HHmmss-xxx and never collide with snapshot IDs
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
For Agents
The Change system is in @mandujs/core/change. CLI: bunx mandu change begin|commit|rollback|status|list|prune. MCP: mandu.tx.begin|commit|rollback|status. State lives under .mandu/history with snapshots, changes.json, and a single active.json lock.
- Only one transaction can be active at a time per project (enforced via .mandu/history/active.json)
- Every begin takes a full snapshot of the manifest and spec/slots before any write
- rollback restores files from the captured snapshot; commit only marks the record committed
- Change IDs use YYYYMMDD-HHmmss-xxx and never collide with snapshot IDs