Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ release artifact. Current release versions are:

| Package | Version | Release role |
| --- | ---: | --- |
| `@openspec-ui/core` | 0.18.1 | Shared behavior and persistence contract |
| `openspec-ui-vscode` | 0.13.1 | VS Code delivery |
| `@openspec-ui/server` | 1.7.1 | Standalone server delivery |
| `@openspec-ui/core` | 0.19.0 | Shared behavior and persistence contract |
| `openspec-ui-vscode` | 0.14.0 | VS Code delivery |
| `@openspec-ui/server` | 1.8.0 | Standalone server delivery |
| `@openspec-ui/webui` | 1.8.1 | Shared browser UI |
| `@openspec-ui/cli` | 0.1.0 | CI merge-gate delivery |

Expand Down
105 changes: 105 additions & 0 deletions docs/adr/0008-change-scoped-rollback-and-retention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# 0008: Change-Scoped Rollback Instead of Task-Scoped, With Opt-In Retention

Status: Accepted

Date: 2026-08-18

## Context

Rollback (ADR 0004) restores exactly the files one process's checkpoint
touched, and is currently only offered per-process, only for active
changes (no code path checked archive status either way — it simply
never came up). Two related asks came up in review:

1. Should Rollback be scoped to individual `tasks.md` checklist items,
so undoing "just task 3" doesn't also undo tasks 1 and 2 from the same
run?
2. Should Rollback be available for archived changes too, and directly
from the change itself rather than only by finding the right entry in
the Processes list?

For (1), a concrete proposal was floated: a SQLite database recording a
diff per task, with a retention-period fork and a "restore with a
merge-risk warning" UX on rollback.

## Decision

1. **Rollback is scoped to the whole Change, not to individual tasks.**
`rollbackChangeCheckpoints` (`packages/core/src/checkpoint.ts`)
aggregates every finalized checkpoint recorded against a `changeName`:
each file's restore target is its content from the *earliest*
checkpoint that touched it (undo everything ever done under this
Change); the conflict check compares against each file's *latest*
known `afterHash`. Same fail-closed, all-or-nothing semantics as
single-process rollback — any conflict refuses the entire restore, not
just the conflicting file. No new storage: this is a pure aggregation
over checkpoints the system already captures.
2. **Rejected the SQLite/per-task-diff proposal.** The actually hard part
of task-scoped rollback is attribution (which files belong to which
task), and no storage technology changes that — it needs incremental,
per-checkbox-transition checkpoints, a materially bigger change to the
capture path than anything storage-related. Separately, a native
SQLite dependency in a VS Code extension carries real packaging risk
(native addon build-per-platform, or a slower WASM fallback) that
nothing else in this codebase's dependency-free, plain-JSON journal
architecture (ADR 0004, ADR 0006) has taken on. And "restore with a
merge-risk warning" is a materially more permissive philosophy than
the fail-closed rollback this codebase has used everywhere else.
Change-scoped rollback answers the same underlying want ("undo what I
did to this change") without any of the above.
3. **Rollback works identically for archived changes.** Archiving
(`archiveChange()`, a thin `openspec archive` CLI wrapper scoped to
`openspec/changes/`) never touches checkpoint/journal data — it was
already unblocked at the data layer before this change, just not
exposed as a command. `openspec-ui.rollbackChange` takes a
`ChangeTreeItem` from either the Changes or Archive tree, no
archived-state guard.
4. **Retention is opt-in, off by default.** `openspec-ui.checkpointRetentionDays`
(VS Code setting; `0` or negative — the default — keeps checkpoint/
process history forever, unchanged from every prior version). A
positive value prunes processes/checkpoints older than N days once, on
the next extension activation, via `WorkbenchProcessScheduler.removeBefore`.
Pruning a process makes Rollback (single-process and whole-Change)
permanently unavailable for it — this is disclosed in the setting's
own description and in the extension README, not silent.
5. **Scoped to the VS Code extension for this first pass.** The
automatic on-activation pruning has no standalone-app equivalent (the
standalone server has no settings/config system to read a retention
value from); `WorkbenchRecoveryService.rollbackChange` exists in core
and is exposed over REST for API completeness, but nothing calls it
automatically outside VS Code. Same pattern already used by
`repo-bootstrap-snippets`.

## Rejected Alternatives

### SQLite-backed per-task diff store

See point 2 above — rejected on both the attribution problem (unsolved
by a storage change) and the dependency-risk/philosophy mismatch with
the rest of this codebase.

### Warn-and-restore-anyway on rollback conflict

Rejected — every other rollback path in this codebase (`rollbackCheckpoint`,
and now `rollbackChangeCheckpoints`) refuses the entire restore on any
conflict. A permissive exception for this one path would be a surprising,
undocumented inconsistency.

### Automatic retention pruning in the standalone server too

Rejected for this pass — the standalone server has no persistent
settings mechanism to source a retention value from, and building one
is out of scope for what was actually asked (a VS Code setting).

## Consequences

- Rollback's mental model becomes "per-process" and "per-Change" (not
per-task) — simpler than task-scoped rollback would have been, at the
cost of not being able to undo a single task's contribution in
isolation when multiple tasks were touched by the same process run.
- No new dependency, no new persisted data shape — `rollbackChangeCheckpoints`
is pure aggregation over the existing checkpoint format.
- Checkpoint/process history still grows unboundedly by default, exactly
as before this change; operators who want bounded growth must opt in
explicitly via `openspec-ui.checkpointRetentionDays` and accept the
Rollback-availability trade-off that comes with it.
99 changes: 99 additions & 0 deletions openspec/changes/change-scoped-rollback/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## Why

Two related gaps raised in review: (1) Rollback only targets one
process's checkpoint at a time — undoing a Change that took several
implement runs means finding and rolling back each one individually, in
the right order; (2) nothing in the UI offers Rollback for an already-
archived Change, even though archiving never actually touches
checkpoint/journal data (`archiveChange()` is a thin `openspec archive`
CLI wrapper scoped to `openspec/changes/`) — it was already unblocked at
the data layer, just never exposed as a command.

A task-scoped alternative (rolling back a single `tasks.md` checklist
item) was discussed and explicitly rejected — see ADR 0008 for the full
reasoning (attribution problem unsolved by storage choice; SQLite
dependency risk in a VS Code extension; fail-closed-vs-warn-and-proceed
philosophy mismatch).

Also closes a related, previously undiscussed gap: checkpoint/process
history has never had any retention mechanism in the VS Code extension
(the core `WorkbenchRecoveryService.cleanupBefore` exists but is only
reachable through the standalone server's REST API, which the extension
doesn't use in its primary direct-import mode) — now that Rollback
reaches further back in a Change's history, unbounded growth is worth
addressing with an explicit, opt-in setting.

## What Changes

- `packages/core/src/checkpoint.ts`: new `rollbackChangeCheckpoints()`
aggregates every finalized checkpoint for a Change into one restore —
each file's target is its content from the *earliest* checkpoint that
touched it, conflict-checked against each file's *latest* known
post-run hash. Same fail-closed, all-or-nothing semantics as
single-process rollback. Refactored `rollbackCheckpoint()` to share the
new `restoreFiles()` primitive instead of duplicating the conflict-
check/restore loop (behavior-preserving).
- `packages/core/src/workbench-recovery.ts` (`WorkbenchRecoveryService`,
used by the server/standalone) and `packages/extension/src/
implementation-sessions.ts` (`ImplementationSessionManager`, used by
the extension's primary direct-import mode) both gain
`changeRollbackDetails(changeName)` (preview: process/file counts, for
a confirmation prompt) and `rollbackChange(changeName)` (perform it),
built on the shared core aggregation function.
- New `openspec-ui.rollbackChange` command, offered on a `ChangeTreeItem`
from **either** the Changes or Archive tree (no archived-state guard —
archiving never touched this data).
- New `openspec-ui.checkpointRetentionDays` VS Code setting (default `0`
= keep forever, unchanged from every prior version). A positive value
prunes processes/checkpoints older than N days once, on the next
extension activation. `WorkbenchProcessScheduler` gains `removeBefore()`
(in-place mutation, unlike `WorkbenchRecoveryService.cleanupBefore`'s
whole-instance replacement — chosen because the extension's scheduler
instance is shared by reference across several tree providers and the
chat participant, so replacing it wholesale would require a bigger,
unrelated refactor). Pruning makes Rollback permanently unavailable for
the pruned process(es) — disclosed in the setting description and
README, not silent.
- `packages/server/src/recovery-rest.ts`: `POST /api/processes/rollback-
change` for API completeness/parity with the extension's capability —
no new standalone UI button for it in this pass (see "Impact").

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `persistent-workbench-runs`: Rollback can now target an entire Change,
not just one process; adds an opt-in, VS Code-only retention setting.
- `vscode-extension`: Changes and Archive trees offer a "Rollback Change"
action.

## Impact

- `packages/core/src/checkpoint.ts` (`restoreFiles`, `RestoreEntry`,
`rollbackChangeCheckpoints`), `checkpoint.test.ts`.
- `packages/core/src/workbench-recovery.ts` (`changeRollbackDetails`,
`rollbackChange`), `workbench-recovery.test.ts`.
- `packages/core/src/process-scheduler.ts` (`removeBefore`),
`process-scheduler.test.ts`.
- `packages/extension/src/implementation-sessions.ts`
(`changeRollbackDetails`, `rollbackChange`, `dropSessions`),
`implementation-sessions.test.ts`.
- `packages/extension/src/commands.ts` (`openspec-ui.rollbackChange`),
`commands.test.ts`.
- `packages/extension/src/extension.ts` (retention pruning at
activation, reading the new setting).
- `packages/extension/package.json` (command, context-menu entries on
both `openspec-ui.activeChange`/`openspec-ui.archivedChange`,
`openspec-ui.checkpointRetentionDays` configuration property).
- `packages/server/src/recovery-rest.ts`, `server.ts`,
`recovery-rest.test.ts` (or `server.test.ts`, wherever the existing
process-rollback endpoint is tested) — REST parity only, no automatic
retention pruning on the server (no settings/config system to source a
value from) and no new standalone webui button — deliberate scope
decision for this pass, same pattern `repo-bootstrap-snippets` already
used ("VS Code only for this first pass").
- `docs/adr/0008-change-scoped-rollback-and-retention.md` (new).
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## ADDED Requirements

### Requirement: Rollback can target an entire Change

The core SHALL offer, alongside per-process rollback, a rollback that
aggregates every finalized checkpoint recorded against a Change name
into a single restore. Each file's restore target SHALL be its content
from the earliest checkpoint that touched it; the conflict check SHALL
compare current file content against each file's latest known post-run
hash. Any conflict SHALL refuse the entire restore, not just the
conflicting file — the same fail-closed, all-or-nothing semantics as
single-process rollback. This capability SHALL be available identically
whether the Change is currently active or already archived.

#### Scenario: Restoring a Change with multiple implement runs

- **WHEN** a Change was implemented across two sequential runs and the
user requests a Change-level rollback
- **THEN** every file either run touched is restored to its content from
before the earlier of the two runs

#### Scenario: A file changed outside any known run

- **WHEN** a Change-level rollback is requested and a file's current
content does not match its latest known post-run hash
- **THEN** the service reports a conflict and restores no files at all

#### Scenario: Rollback for an archived Change

- **WHEN** a Change-level rollback is requested for a Change that has
since been archived
- **THEN** the restore proceeds identically to an active Change — archive
status plays no part in eligibility

### Requirement: Opt-in checkpoint retention via extension setting

The VS Code extension SHALL expose a setting controlling automatic
checkpoint/process retention, defaulting to keeping history forever
(unchanged from prior behavior). When configured with a positive number
of days, the extension SHALL prune processes and their checkpoint
sessions older than that cutoff once, at extension activation. Pruning a
process SHALL make both single-process and Change-level rollback
permanently unavailable for it, and this consequence SHALL be disclosed
in the setting's own description.

#### Scenario: Default behavior is unchanged

- **WHEN** the retention setting is left at its default
- **THEN** no process or checkpoint data is ever automatically pruned

#### Scenario: A positive retention period prunes old history

- **WHEN** the setting is configured to a positive number of days and the
extension activates
- **THEN** processes created before the cutoff, and their checkpoint
sessions, are removed together
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## ADDED Requirements

### Requirement: Changes and Archive trees offer whole-Change rollback

A "Rollback Change" action SHALL be available on a Change item in either
the "Changes" or "Archive" tree view. Selecting it, when at least one
rollback-eligible process exists for that Change, SHALL show a
confirmation naming the affected file and process counts before
proceeding; when no rollback-eligible process exists, the system SHALL
report that instead of prompting for confirmation.

#### Scenario: Rollback from the Changes tree

- **WHEN** the user selects "Rollback Change" on an active Change with
rollback-eligible processes
- **THEN** a confirmation shows the affected file and process counts
- **AND** confirming restores those files and refreshes the trees

#### Scenario: Rollback from the Archive tree

- **WHEN** the user selects "Rollback Change" on an archived Change with
rollback-eligible processes
- **THEN** the same confirmation and restore behavior applies, unmodified
by archive status

#### Scenario: No rollback-eligible processes

- **WHEN** the user selects "Rollback Change" on a Change with no
rollback-eligible processes
- **THEN** the system reports this without showing a confirmation dialog
Loading
Loading