Skip to content

feat(engine): propose deterministic spec-merge engine (part 1) (#1288)#1279

Closed
clay-good wants to merge 8 commits into
Fission-AI:mainfrom
clay-good:add-unarchive-command
Closed

feat(engine): propose deterministic spec-merge engine (part 1) (#1288)#1279
clay-good wants to merge 8 commits into
Fission-AI:mainfrom
clay-good:add-unarchive-command

Conversation

@clay-good

@clay-good clay-good commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Proposal PR (OpenSpec change, no code). Dogfoods OpenSpec: the deliverable is a change folder under openspec/changes/add-deterministic-spec-sync/ (proposal + design + tasks + 3 spec deltas), validated with openspec validate --strict. Companion to #1278; same principle as #1277 (deterministic CLI, thin skills).

Status

Open, proposal-only. Validates clean under openspec validate add-deterministic-spec-sync --strict.

Narrowed per @alfred-openspec's review. The earlier revision spanned 8 capabilities; alfred agreed the applied-delta baseline is the right spine but asked to "narrow the first slice to deterministic merge/baseline, sync check/fix, archive using that path, and CLI-delegated /opsx:sync, then split unarchive, format, and diff into follow-ups once the primitive is proven." Done:

  • This PR = that first slice (engine + baseline + sync + archive integration + /opsx:sync delegation).
  • Follow-up: feat(engine): propose deterministic spec-merge engine (part 2) (#1279)  #1288 (add-spec-tooling-suite) carries unarchive, format, diff, and the unified check gate — everything that builds on the baseline.
  • The change was renamed add-deterministic-sync-and-unarchiveadd-deterministic-spec-sync to match.

What's missing (the motivation)

The spec merge (delta → specs/) is the load-bearing op in OpenSpec, reachable today only inside archive or via the agent-driven /opsx:sync — which is non-deterministic, and whose scenario-merge is the mechanism behind #1246's silent scenario drops. The deterministic engine applySpecs() has zero callers. So today you cannot gate spec drift in CI without a model, cleanly re-merge a revised delta, or catch merge conflicts before archive.

What it does (first slice)

One primitive — a per-change applied-delta baseline (pre-image + applied-result digest + per-edit provenance, recorded whenever deltas merge into specs/) — plus one engine, exposed as:

  • cli-sync (new) — deterministic, idempotent openspec sync [change]; --fix/default regenerates specs/ (byte-identical for the same delta+base; a revised delta regenerates with no crumbs); --check is a model-free drift gate for CI + pre-commit that also surfaces conflicts early (delta-vs-base and cross-change), verifies delta↔spec correspondence, and runs incrementally (incremental verdict == full verdict).
  • cli-archive (modified) — routes its merge through the shared engine and records the baseline before moving, so archiving becomes deterministically reversible (unlocking the follow-up unarchive). Forward-only, no output change.
  • specs-sync-skill (modified)/opsx:sync delegates to openspec sync instead of editing specs in the agent → fixes the Archiving two changes that MODIFY the same requirement silently drops scenarios (distinct from #1112) #1246 scenario-drop class by construction.

Rejected, and it shapes the design (design.md): removing archive / folding the merge into apply (breaks the specs/ = shipped-reality invariant, Decision 5); editing specs/ in place with a sidecar reasoning log (breaks proposed-vs-shipped isolation, Decision 11). Their valid kernels are adopted within the delta model — early conflict detection and provenance/correspondence via sync --check (Decisions 11–12).

Proof (for a proposal)

Notes / scope

🤖 Generated with Claude Code

…c unarchive

Add an OpenSpec change proposing `openspec unarchive [change-name]` and a
thin `/opsx:unarchive` skill: the deterministic inverse of `openspec archive`.

Core finding driving the design: the archived delta is not self-inverting.
ADDED/RENAMED are reversible from the delta; REMOVED/MODIFIED are not (the
forward merge discards the pre-image, confirmed in specs-apply.ts and Fission-AI#1246).
So archive gains a forward-only reversal snapshot (pre-image + post-merge
digest) that makes reversal byte-exact, drift-guarded, and atomic, with
graceful degradation for pre-snapshot archives and a --keep-specs escape
hatch.

Also folds in the CI-determinism recommendation: expose the deterministic
merge engine (today applySpecs() has zero callers, reachable only inside
archive) as a model-free `openspec sync --check` drift gate — captured as a
separable companion, de-risked by this PR's round-trip determinism.

Dogfoods OpenSpec: proposal.md, design.md (7 decisions), tasks.md, and spec
deltas for cli-unarchive (new), opsx-unarchive-skill (new), cli-archive
(reversal snapshot). Validates with `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good requested a review from TabishB as a code owner June 29, 2026 21:19
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new OpenSpec change set that defines deterministic sync, reversible unarchive, shared format, and CLI-delegated /opsx workflows, plus baseline capture, drift checks, and phased rollout notes.

Changes

Deterministic sync, unarchive, and format

Layer / File(s) Summary
Foundation and rollout framing
openspec/changes/add-deterministic-sync-and-unarchive/.openspec.yaml, .../proposal.md, .../design.md, .../tasks.md
Introduces the change metadata, the deterministic baseline primitive, the shared merge-engine framing, and the phased rollout context.
Sync, archive, and format contracts
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md, .../cli-archive/spec.md, .../cli-format/spec.md, .../design.md, .../tasks.md
Defines deterministic sync and archive behavior, including baseline capture, drift checks, provenance, conflict detection, incremental checking, and shared canonical formatting.
Unarchive contract and skill delegation
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md, .../specs/opsx-unarchive-skill/spec.md, .../specs/specs-sync-skill/spec.md
Defines archive reversal, drift guarding, atomic restore behavior, pre-baseline compatibility, stable diagnostics, and the /opsx:unarchive and /opsx:sync skill requirements.
Phased wiring, docs, and verification
openspec/changes/add-deterministic-sync-and-unarchive/proposal.md, .../design.md, .../tasks.md
Specifies the rollout phases for workflow delegation, documentation updates, CI and pre-commit guidance, and end-to-end verification across platforms.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #1246: Both changes address the same class of lossiness around sequential MODIFIED archives and drift-sensitive merge behavior.
  • #1112: Related because both cover earlier detection of delta applicability mismatches during archive/sync flows.

Possibly related PRs

  • Fission-AI/OpenSpec#632: Related to the /opsx:sync invocation path and workflow/template delegation changes.
  • Fission-AI/OpenSpec#1030: Related to generating and running /opsx:sync by default, which overlaps with the sync-skill and delegation changes here.

Suggested reviewers

  • TabishB

Poem

A rabbit read specs in moonlit rows,
Then synced them straight with careful toes.
Unarchive back, no crumbs to chase,
Format made each line find its place.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The proposal adds baseline-driven drift checks and conflict detection to prevent silent overwrites when sequentially archiving MODIFIED requirements.
Out of Scope Changes check ✅ Passed The changes stay within the proposed deterministic spec-tooling scope and add no unrelated implementation work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a proposal for a deterministic spec-merge engine.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@clay-good clay-good self-assigned this Jun 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
openspec/changes/add-unarchive-command/design.md (1)

73-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reword the contradictory "zero callers" claim.

"has zero callers" is literally false if the engine is "reachable only inside archive" — archive itself is a caller. Rephrase to "has no standalone CLI exposure" or "is not invoked by any command other than openspec archive" to match the intended meaning.

See matching suggestion in proposal.md line 46.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/design.md` at line 73, The wording
around applySpecs() is contradictory: it is not true that it has “zero callers”
if archive invokes it. Update the design text to say that applySpecs() has no
standalone CLI exposure or is only invoked by openspec archive, and keep the
rest of the point about exposing a separate openspec sync command and --check
drift gate intact.
openspec/changes/add-unarchive-command/proposal.md (1)

46-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the "zero callers" claim.

The phrase "has zero callers" contradicts the following clause "it is reachable only inside openspec archive" — if archive can reach it, archive is a caller. The intended point is that no standalone CLI command or external caller invokes applySpecs(); it is only reachable bundled inside openspec archive.

Reword to avoid the contradiction, e.g.: "has no standalone CLI exposure — it is reachable only inside openspec archive, bundled with the folder move."

Same wording appears in design.md line 73.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/proposal.md` at line 46, Reword the
claim about applySpecs() so it does not say “zero callers” while also saying it
is reachable inside openspec archive, since that is contradictory. Update the
proposal text around applySpecs() to state that there is no standalone CLI
exposure or external caller, and that the function is only reachable as part of
openspec archive’s bundled folder move; apply the same wording consistently in
design.md as well.
openspec/changes/add-unarchive-command/tasks.md (1)

37-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconsider the hidden --skip-specs alias for --keep-specs.

In the archive command, --skip-specs means "skip spec updates" (don't merge specs into specs/). Using --skip-specs as a hidden alias for --keep-specs in unarchive semantically means "skip spec reversal" — the opposite effect. This naming symmetry is likely to confuse users who muscle-memory archive's flag and expect it to behave the same way. Consider removing the alias or renaming it to something unambiguous like --no-spec-restore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/tasks.md` at line 37, The `unarchive`
CLI registration in `src/cli/index.ts` should not reuse `--skip-specs` as a
hidden alias for `--keep-specs`, because that flag name implies the opposite
behavior from archive and will confuse users. Update the `unarchive
[change-name]` command wiring to remove the ambiguous alias or replace it with a
clearly named option such as `--no-spec-restore`, while keeping the existing
`UnarchiveCommand().execute(...)` action and the other flags intact.
openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md (3)

91-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add explicit missing-file drift scenario.

The drift guard should explicitly handle when an affected spec no longer exists (was deleted after archiving). Treating absence as drift is implied by "current content no longer matches," but an explicit scenario prevents implementers from overlooking it:

#### Scenario: Drift on missing spec

- **WHEN** an affected spec was deleted after archiving
- **THEN** the command treats it as drifted (the current state is absent, which does not match the recorded post-merge digest)
- **AND** it refuses to reverse and reports the spec as drifted
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md` around
lines 91 - 104, The drift guard in the unarchive spec needs an explicit
missing-file case so implementers do not miss deleted specs. Update the CLI
unarchive spec around the drift scenarios to add a “drift on missing spec” case
that states an affected spec deleted after archiving is treated as drift,
reported as such, and causes the reversal to refuse; keep the wording aligned
with the existing reversal snapshot and post-merge digest checks in the drift
section.

22-24: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Clarify non-interactive detection beyond --json.

"Or otherwise non-interactively" is ambiguous. Specify the conditions that trigger non-interactive mode (e.g., --json flag, CI=true, or stdin not being a TTY). Without this, implementers may inconsistently handle piped input or environment-based automation.

- **WHEN** the command is run with `--json`, or when `stdin` is not a TTY and no `--interactive` flag is forced
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md` around
lines 22 - 24, Clarify the non-interactive requirement in the unarchive CLI spec
by updating the acceptance criteria around the command path that requires a
change name. In the CLI-unarchive spec, replace the vague “otherwise
non-interactively” wording with explicit triggers such as `--json`, `CI=true`,
or `stdin` not being a TTY unless `--interactive` is forced, so implementers can
consistently detect when to fail with the machine-readable missing-name
diagnostic and non-zero exit.

28-28: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Clarify resolution priority between full id and bare name.

The spec does not state whether resolution tries an exact full-id match first before falling back to bare-name stripping. If a user accidentally passes a string that happens to be both a valid full id and a bare name with multiple matches, the behavior could be surprising. Explicitly require:

The command SHALL first attempt an exact match against the full directory id; only if that fails SHALL it apply prefix-stripping bare-name resolution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md` at line
28, Clarify the unarchive target resolution order in the CLI spec by updating
the resolution rule in the cli-unarchive spec to state that the command first
tries an exact match against the full archived directory id, and only if that
fails does it fall back to bare-name prefix stripping. Keep the existing
no-ambiguous-selection requirement, and make sure the wording around the target
resolution behavior in the spec is unambiguous about the priority between full
id and bare name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openspec/changes/add-unarchive-command/specs/cli-archive/spec.md`:
- Line 5: Clarify the reversal snapshot contract in the archive spec by defining
the exact storage location and schema used by the archive flow. Update the spec
text around the archive operation and `openspec unarchive` expectations to name
the snapshot path relative to the change folder, specify that it is a versioned
JSON format, and define the per-spec fields and encoding so implementers can
read it consistently.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md`:
- Around line 121-136: The atomicity requirement for the unarchive flow is too
vague in the CLI unarchive spec; tighten the sequence and guarantee in the
atomic operation scenario. Update the requirement to explicitly define the order
in the unarchive command: validate the destination and drift-free specs first,
stage reversed specs in a temporary location, atomically swap them into
openspec/specs via the unarchive command’s main reversal flow, then move the
change folder last. Make clear that the implementation must use an atomic
rename/swap approach for the spec update, and that any failure before the final
folder move must leave openspec/specs unchanged with staged data discarded.
- Around line 137-161: Clarify and lock down mixed-delta behavior in the
unarchive spec so the success/failure model is unambiguous. Update the `openspec
unarchive` backward-compatibility requirement to choose either the conservative
path (fail the whole command if any requirement is MODIFIED/REMOVED) or the
partial path (reverse only ADDED/RENAMED items and leave the rest untouched). If
you choose partial reversal, also update the `--json` contract to report
per-spec outcomes using separate reversed/skipped or reversed/failed collections
instead of a single all-or-nothing error result. Make the `Reverse the
self-invertible operations` and `Refuse to guess irreversible operations`
scenarios consistent with the chosen model.

---

Nitpick comments:
In `@openspec/changes/add-unarchive-command/design.md`:
- Line 73: The wording around applySpecs() is contradictory: it is not true that
it has “zero callers” if archive invokes it. Update the design text to say that
applySpecs() has no standalone CLI exposure or is only invoked by openspec
archive, and keep the rest of the point about exposing a separate openspec sync
command and --check drift gate intact.

In `@openspec/changes/add-unarchive-command/proposal.md`:
- Line 46: Reword the claim about applySpecs() so it does not say “zero callers”
while also saying it is reachable inside openspec archive, since that is
contradictory. Update the proposal text around applySpecs() to state that there
is no standalone CLI exposure or external caller, and that the function is only
reachable as part of openspec archive’s bundled folder move; apply the same
wording consistently in design.md as well.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md`:
- Around line 91-104: The drift guard in the unarchive spec needs an explicit
missing-file case so implementers do not miss deleted specs. Update the CLI
unarchive spec around the drift scenarios to add a “drift on missing spec” case
that states an affected spec deleted after archiving is treated as drift,
reported as such, and causes the reversal to refuse; keep the wording aligned
with the existing reversal snapshot and post-merge digest checks in the drift
section.
- Around line 22-24: Clarify the non-interactive requirement in the unarchive
CLI spec by updating the acceptance criteria around the command path that
requires a change name. In the CLI-unarchive spec, replace the vague “otherwise
non-interactively” wording with explicit triggers such as `--json`, `CI=true`,
or `stdin` not being a TTY unless `--interactive` is forced, so implementers can
consistently detect when to fail with the machine-readable missing-name
diagnostic and non-zero exit.
- Line 28: Clarify the unarchive target resolution order in the CLI spec by
updating the resolution rule in the cli-unarchive spec to state that the command
first tries an exact match against the full archived directory id, and only if
that fails does it fall back to bare-name prefix stripping. Keep the existing
no-ambiguous-selection requirement, and make sure the wording around the target
resolution behavior in the spec is unambiguous about the priority between full
id and bare name.

In `@openspec/changes/add-unarchive-command/tasks.md`:
- Line 37: The `unarchive` CLI registration in `src/cli/index.ts` should not
reuse `--skip-specs` as a hidden alias for `--keep-specs`, because that flag
name implies the opposite behavior from archive and will confuse users. Update
the `unarchive [change-name]` command wiring to remove the ambiguous alias or
replace it with a clearly named option such as `--no-spec-restore`, while
keeping the existing `UnarchiveCommand().execute(...)` action and the other
flags intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 012c9beb-9647-40d4-a50c-25483db70f85

📥 Commits

Reviewing files that changed from the base of the PR and between 546224e and 0d93736.

📒 Files selected for processing (7)
  • openspec/changes/add-unarchive-command/.openspec.yaml
  • openspec/changes/add-unarchive-command/design.md
  • openspec/changes/add-unarchive-command/proposal.md
  • openspec/changes/add-unarchive-command/specs/cli-archive/spec.md
  • openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md
  • openspec/changes/add-unarchive-command/specs/opsx-unarchive-skill/spec.md
  • openspec/changes/add-unarchive-command/tasks.md


### Requirement: Reversal Snapshot Capture

When the archive operation rewrites main specs, it SHALL capture a self-contained reversal snapshot inside the change folder so that the operation can later be reversed deterministically by `openspec unarchive`. The snapshot SHALL be forward-only and SHALL NOT alter how archive merges, moves, validates, or what it outputs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Clarify snapshot storage location and format.

The spec states the snapshot is "forward-only" and "inside the change folder," but does not specify the file name, format, or structure. Without this, implementers may choose incompatible representations (e.g., JSON vs. binary, single file vs. per-spec files), and future tooling cannot reliably read it. Specify at minimum the file path relative to the change folder and whether the format is versioned.

Consider:

- The snapshot SHALL be written as `<change-folder>/.openspec/reversal-snapshot.json` containing a versioned schema with per-spec entries of `{ "path": "...", "preMergeBytes": "base64...", "postMergeDigest": "sha256:..." }`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/specs/cli-archive/spec.md` at line 5,
Clarify the reversal snapshot contract in the archive spec by defining the exact
storage location and schema used by the archive flow. Update the spec text
around the archive operation and `openspec unarchive` expectations to name the
snapshot path relative to the change folder, specify that it is a versioned JSON
format, and define the per-spec fields and encoding so implementers can read it
consistently.

Comment thread openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md Outdated
Comment on lines +137 to +161
### Requirement: Backward Compatibility For Pre-Snapshot Archives

For changes archived before reversal snapshots existed, the command SHALL reverse the operations it can invert from the archived delta alone and SHALL refuse to guess the operations it cannot, never producing an incorrect spec.

#### Scenario: Reverse the self-invertible operations

- **WHEN** an archived change has no reversal snapshot
- **AND** its delta contains only ADDED and/or RENAMED requirements
- **THEN** the command reverses them by delta inversion (removing added requirements, renaming renamed requirements back)
- **AND** restores `openspec/specs/` accordingly

#### Scenario: Refuse to guess irreversible operations

- **WHEN** an archived change has no reversal snapshot
- **AND** its delta contains MODIFIED or REMOVED requirements (whose pre-image is not recoverable from the delta)
- **THEN** the command does not modify those specs
- **AND** it reports which requirements cannot be safely reversed
- **AND** it directs the user to `--keep-specs` (and optionally to git-based recovery)

#### Scenario: Keep-specs always available

- **WHEN** an archived change has no reversal snapshot
- **AND** the user runs `openspec unarchive <name> --keep-specs`
- **THEN** the command restores the folder without touching specs, regardless of which delta operations the change contains

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clarify partial reversal semantics for mixed deltas.

When a pre-snapshot change contains both self-invertible and irreversible operations across different specs, the spec implies partial reversal but does not define the success/failure reporting model. Specify whether the command:

  • Option A (conservative): Refuses entirely if any spec contains MODIFIED/REMOVED, requiring --keep-specs.
  • Option B (partial): Reverses what it can, leaves irreversible specs untouched, and reports partial success with clear per-spec status.

If Option B, the --json output contract in lines 171-176 must accommodate partial success (e.g., reversed: [...], failed: [...] rather than a single error code).

#### Scenario: Mixed invertibility

- **WHEN** a pre-snapshot change has some specs with only ADDED/RENAMED operations and others with MODIFIED/REMOVED
- **THEN** the command SHALL either:
  - (conservative) refuse to modify any specs and require `--keep-specs`; or
  - (partial) reverse the self-invertible specs, leave the irreversible specs untouched, and report which specs were reversed and which were skipped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-unarchive-command/specs/cli-unarchive/spec.md` around
lines 137 - 161, Clarify and lock down mixed-delta behavior in the unarchive
spec so the success/failure model is unambiguous. Update the `openspec
unarchive` backward-compatibility requirement to choose either the conservative
path (fail the whole command if any requirement is MODIFIED/REMOVED) or the
partial path (reverse only ADDED/RENAMED items and leave the rest untouched). If
you choose partial reversal, also update the `--json` contract to report
per-spec outcomes using separate reversed/skipped or reversed/failed collections
instead of a single all-or-nothing error result. Make the `Reverse the
self-invertible operations` and `Refuse to guess irreversible operations`
scenarios consistent with the chosen model.

…chive)

Re-scope the unarchive proposal around the long-term direction the full
Discord thread converged on: the delta->specs merge should be a deterministic,
idempotent, REVERSIBLE pure-code engine, exposed as first-class commands.
Unarchive becomes one payoff of that engine, alongside a standalone
`openspec sync`, a model-free CI/pre-commit drift gate, and crumb-free
re-merge.

Key insight: one primitive — a per-change applied-delta baseline (pre-image +
applied-result digest) — unifies three asks the chat raised separately:
deterministic reverse (unarchive), idempotent "no crumbs" re-sync, and
hash-based drift detection. Frozen into the archive, it is the unarchive
reversal snapshot.

Adopted from the discussion:
- deterministic, code-only `openspec sync` + `--check` (no model in CI)
- `sync --fix` as eslint-style pre-commit auto-fixer
- skills (/opsx:sync, /opsx:unarchive) delegate to the CLI; agent never merges
  (fixes the Fission-AI#1246 "intelligent merge drops scenarios" failure mode)
Rejected (documented): removing archive / folding merge into apply — preserves
the specs/ = shipped-reality invariant (design Decision 5).

Rename change add-unarchive-command -> add-deterministic-sync-and-unarchive.
Spec deltas: cli-sync (new), cli-unarchive (new), opsx-unarchive-skill (new),
cli-archive (baseline + shared engine), specs-sync-skill (CLI delegation).
Validates with `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: add-unarchive-command — deterministic openspec unarchive + /opsx:unarchive Propose: deterministic spec-merge engine — openspec sync + openspec unarchive (+ /opsx:unarchive) Jun 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md (1)

162-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include archive-directory-missing in the --json error code enumeration or explicitly map it to not-found.

Line 168-170 defines a distinct "missing archive directory" error condition, but Line 173's explicit --json blocked-condition list (not found, ambiguous, drift, destination exists, or pre-baseline irreversibility) omits it. Clarify whether archive-directory-missing shares the not-found code or has its own stable code so CLI consumers can distinguish "no archive directory" from "no matching change."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md`
around lines 162 - 176, The error-condition spec for the CLI unarchive command
is missing a clear JSON mapping for the “missing archive directory” case. Update
the `Error Conditions` and `JSON diagnostics` text in the unarchive spec so
`archive-directory-missing` is either added as its own stable code or explicitly
mapped to `not-found`, and make the `--json` blocked-condition list in the
`Scenario: JSON diagnostics` section match that decision consistently.
🧹 Nitpick comments (1)
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md (1)

75-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Distinguish error codes for the two --check failure modes.

The two --check scenarios describe different root causes: (1) deltas cannot be cleanly applied to base specs, and (2) committed specs have drifted from previously generated output. For machine-readable CI gating and diagnostics, consider requiring distinct stable error codes in the JSON output so callers can differentiate a broken delta from an accidental manual edit without parsing human-readable messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`
around lines 75 - 97, Update the `Drift Check Mode` requirement in
`cli-sync/spec.md` to distinguish the two `--check` failure paths with stable
machine-readable error codes in JSON output: one for unappliable deltas and
another for drift between committed specs and regenerated output. Adjust the
`openspec sync <name> --check` scenarios so callers can reliably tell which
condition occurred without parsing the human message, while still preserving the
non-zero exit and no-file-modification behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md`:
- Around line 162-176: The error-condition spec for the CLI unarchive command is
missing a clear JSON mapping for the “missing archive directory” case. Update
the `Error Conditions` and `JSON diagnostics` text in the unarchive spec so
`archive-directory-missing` is either added as its own stable code or explicitly
mapped to `not-found`, and make the `--json` blocked-condition list in the
`Scenario: JSON diagnostics` section match that decision consistently.

---

Nitpick comments:
In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`:
- Around line 75-97: Update the `Drift Check Mode` requirement in
`cli-sync/spec.md` to distinguish the two `--check` failure paths with stable
machine-readable error codes in JSON output: one for unappliable deltas and
another for drift between committed specs and regenerated output. Adjust the
`openspec sync <name> --check` scenarios so callers can reliably tell which
condition occurred without parsing the human message, while still preserving the
non-zero exit and no-file-modification behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fab22a2-751f-463e-9673-a894de3557d8

📥 Commits

Reviewing files that changed from the base of the PR and between 0d93736 and babf94e.

📒 Files selected for processing (9)
  • openspec/changes/add-deterministic-sync-and-unarchive/.openspec.yaml
  • openspec/changes/add-deterministic-sync-and-unarchive/design.md
  • openspec/changes/add-deterministic-sync-and-unarchive/proposal.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-archive/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-unarchive/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/opsx-unarchive-skill/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/specs-sync-skill/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md
💤 Files with no reviewable changes (2)
  • openspec/changes/add-deterministic-sync-and-unarchive/.openspec.yaml
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/opsx-unarchive-skill/spec.md
✅ Files skipped from review due to trivial changes (4)
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md
  • openspec/changes/add-deterministic-sync-and-unarchive/design.md
  • openspec/changes/add-deterministic-sync-and-unarchive/proposal.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-archive/spec.md

clay-good and others added 2 commits June 29, 2026 16:51
…; scrub names

Continue folding the design discussion into PR Fission-AI#1279, within OpenSpec's scope.

Adopt (in scope):
- Early conflict resolution — sync --check surfaces delta-vs-base and
  cross-change conflicts at commit/PR time, not at archive. Gets the
  "resolve conflicts immediately" benefit without abandoning deltas.
- Provenance — the merge records which change/delta produced each spec edit;
  enables a deterministic delta<->spec correspondence check (orphan edit /
  unapplied delta -> fail) and a future spec-aware diff driver.
- 3 new cli-sync requirements: Change Provenance, Delta-Spec Correspondence,
  Cross-Change Conflict Detection. New design Decisions 11-12.

Engage and reject, with reasoning (documented):
- Edit specs/ in place; treat git diff as the delta; semantic deltas in a
  sidecar log. Rejected: the delta layer is what isolates proposed from
  shipped and makes parallel changes + single-change reverse possible. Its
  valid kernel (early conflict resolution, reasoning-with-the-change) is
  adopted via the items above. (Decision 11)

Defer (delineated): the spec-aware git diff driver — provenance data is in
scope; the diff-driver presentation is a follow-up (Decision 12).

Also: scrub contributor names from all PR content (neutral attributions).
Validates with `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uth vision

Continue folding the design discussion into PR Fission-AI#1279, within OpenSpec's scope.

Adopt (in scope, new):
- Incremental checking — `sync --check` uses the baseline per-spec digests to
  skip unchanged specs and re-check only what moved ("use hashes to optimize
  when checks are needed"). Correctness invariant: a skip is allowed only on an
  exact digest match; mismatch/missing/unknown-scheme forces a full check, so
  the incremental verdict always equals the full verdict. New cli-sync
  requirement + design Decision 13 + tasks.

Acknowledge and defer:
- Spec linter/formatter ("how the spec looks/reads/organized"). This PR makes
  OUTPUT specs canonical and byte-stable; an authoring-side formatter is a
  separable follow-up adjacent to openspec-conventions/validate.

Engage and reject, with reasoning (documented, Decision 13):
- Spec-as-source-of-truth: edit in place + commit only the spec, treating
  proposal/design/tasks as disposable. Rejected — those artifacts are the
  reviewable, resumable product (agreement before code); the delta layer keeps
  proposed vs shipped isolated. Valid kernel (lightweight changes needn't carry
  every artifact) is already served by schema flexibility, outside this PR.

No human names anywhere in PR content (verified).
Validates with `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md (1)

61-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing scenario for re-sync drift due to another change.

The spec covers idempotent re-run (lines 49–53) and drift between committed specs and generated output (lines 86–91), but omits the critical edge case from the design: when sync re-applies a revised delta and discovers that specs/ has drifted from the recorded baseline because a later change touched the same requirement. Per design.md Decision 3, the engine must refuse to silently reverse-then-apply over someone else's edit and instead report drift.

Add a scenario under Applied-Delta Baseline or Drift Check Mode covering this case:

#### Scenario: Baseline drift on re-sync

- **WHEN** sync re-applies a revised delta and the current `openspec/specs/` content for an affected spec differs from the recorded baseline pre-image
- **THEN** the command reports baseline drift for that spec
- **AND** it exits with a non-zero status code and modifies no files
- **AND** it does not proceed with the merge
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`
around lines 61 - 107, Add a missing scenario for the re-sync drift edge case in
the sync spec: when the sync engine re-applies a revised delta and the current
openspec/specs/ content no longer matches the recorded applied-delta baseline
pre-image, it must report baseline drift instead of proceeding. Update the
relevant requirement block around Applied-Delta Baseline / Drift Check Mode to
cover that the sync command exits non-zero, modifies no files, and does not
continue the merge when this happens, using the same sync terminology already
present in the spec.
🧹 Nitpick comments (4)
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md (2)

132-166: 📐 Maintainability & Code Quality | 🔵 Trivial

"Committed" implies git workflow; use "current" or "synced" for generality.

Lines 149 and 153 refer to "committed openspec/specs/ content" and "committed openspec/specs/" for the correspondence check. The check should apply to the current state of specs/ whether or not changes are committed. Consider "current openspec/specs/ content" or "synced openspec/specs/ content" to avoid tying the requirement to a specific VCS workflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`
around lines 132 - 166, The wording in the Delta–Spec Correspondence requirement
uses “committed” in a way that unnecessarily ties the behavior to git state.
Update the requirement text in the sync spec to refer to the current or synced
openspec/specs/ content instead, while keeping the same check semantics in the
related scenarios. Preserve the meaning in the Requirement: Delta–Spec
Correspondence and its scenarios, but replace any “committed” phrasing with
neutral terms so the behavior applies regardless of workflow.

117-131: 📐 Maintainability & Code Quality | 🔵 Trivial

"root context" is undefined; consider clarifying or referencing.

Line 124 mentions emitting "the root context as JSON" without defining what that includes. If this is a term from existing CLI conventions, add a cross-reference; otherwise, specify the fields (e.g., change name, spec paths affected, baseline status).

Additionally, the blocked-path scenario (lines 128–131) mentions "a machine-readable diagnostic with a stable code" but does not enumerate the codes. For a spec, consider listing the expected diagnostic codes (e.g., NO_DELTA, DELTA_NOT_APPLICABLE, CONFLICT, DRIFT, CORRESPONDENCE_FAILURE) so implementations and consumers can rely on them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`
around lines 117 - 131, Clarify the JSON sync spec by defining what “root
context” means in the sync command’s JSON output and reference the existing CLI
concept or explicitly list its fields (for example, change name, affected spec
paths, and baseline status) in the `JSON success` scenario. Also update the
`JSON blocked path` scenario in `spec.md` to enumerate the stable diagnostic
codes expected from the sync flow so implementations can emit consistent
machine-readable errors; use the `openspec sync` JSON scenarios and the
requirement text as the place to make these definitions explicit.
openspec/changes/add-deterministic-sync-and-unarchive/design.md (2)

81-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Disambiguate "commit order" from git commits.

"Commit order" in this context means the sequence of file operations, not git commits. Rephrase to "execution order" or "apply order" to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/design.md` around lines
81 - 85, The phrase “commit order” in the Decision 7 text is ambiguous and can
be read as git commits instead of file-operation sequencing. Update the wording
in the deterministic sync/unarchive design section to use “execution order” or
“apply order” when describing the sequence around moveDirectory and the
restore/delete steps, so the intent is clear and consistent with the reversal
flow.

76-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify "opaque up to <name>" phrasing.

The intent is clear (strip date/sequence prefix, match on the name portion), but "opaque up to" is awkward. Consider: "treats the leading prefix as strippable up to <name>" or "prefix is ignored up to <name>".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/design.md` around lines
76 - 79, Revise the wording in the `Decision` section to replace the awkward
“opaque up to <name>” phrasing with clearer language like “strippable” or
“ignored up to <name>”. Keep the meaning the same in `design.md`: `unarchive
[change-name]` should match by removing the leading archive prefix and using the
remaining name portion, while preserving the rest of the behavior described
there.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`:
- Around line 168-187: The cross-change conflict detection requirement is
missing the `--all` mode needed to inspect more than one active change at once.
Update the `openspec sync` spec to add a scenario for `--check --all` (or
equivalent `--all`) that checks every active change for overlapping
requirements, reports conflicts, and returns non-zero. Make sure the `sync`
command definition and the conflict scenarios clearly distinguish single-change
behavior from the new all-changes behavior.

---

Outside diff comments:
In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`:
- Around line 61-107: Add a missing scenario for the re-sync drift edge case in
the sync spec: when the sync engine re-applies a revised delta and the current
openspec/specs/ content no longer matches the recorded applied-delta baseline
pre-image, it must report baseline drift instead of proceeding. Update the
relevant requirement block around Applied-Delta Baseline / Drift Check Mode to
cover that the sync command exits non-zero, modifies no files, and does not
continue the merge when this happens, using the same sync terminology already
present in the spec.

---

Nitpick comments:
In `@openspec/changes/add-deterministic-sync-and-unarchive/design.md`:
- Around line 81-85: The phrase “commit order” in the Decision 7 text is
ambiguous and can be read as git commits instead of file-operation sequencing.
Update the wording in the deterministic sync/unarchive design section to use
“execution order” or “apply order” when describing the sequence around
moveDirectory and the restore/delete steps, so the intent is clear and
consistent with the reversal flow.
- Around line 76-79: Revise the wording in the `Decision` section to replace the
awkward “opaque up to <name>” phrasing with clearer language like “strippable”
or “ignored up to <name>”. Keep the meaning the same in `design.md`: `unarchive
[change-name]` should match by removing the leading archive prefix and using the
remaining name portion, while preserving the rest of the behavior described
there.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md`:
- Around line 132-166: The wording in the Delta–Spec Correspondence requirement
uses “committed” in a way that unnecessarily ties the behavior to git state.
Update the requirement text in the sync spec to refer to the current or synced
openspec/specs/ content instead, while keeping the same check semantics in the
related scenarios. Preserve the meaning in the Requirement: Delta–Spec
Correspondence and its scenarios, but replace any “committed” phrasing with
neutral terms so the behavior applies regardless of workflow.
- Around line 117-131: Clarify the JSON sync spec by defining what “root
context” means in the sync command’s JSON output and reference the existing CLI
concept or explicitly list its fields (for example, change name, affected spec
paths, and baseline status) in the `JSON success` scenario. Also update the
`JSON blocked path` scenario in `spec.md` to enumerate the stable diagnostic
codes expected from the sync flow so implementations can emit consistent
machine-readable errors; use the `openspec sync` JSON scenarios and the
requirement text as the place to make these definitions explicit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ab512a2-6f9b-4090-9454-e632d8e87fc2

📥 Commits

Reviewing files that changed from the base of the PR and between babf94e and 137b6c0.

📒 Files selected for processing (4)
  • openspec/changes/add-deterministic-sync-and-unarchive/design.md
  • openspec/changes/add-deterministic-sync-and-unarchive/proposal.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-sync/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md
✅ Files skipped from review due to trivial changes (1)
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md

Comment thread openspec/changes/add-deterministic-spec-sync/specs/cli-sync/spec.md
…nspec format)

Per request, promote the spec linter/formatter from a deferred follow-up to a
first-class capability in this PR.

New capability cli-format: `openspec format [target]` — a deterministic,
behavior-preserving spec/delta formatter with a `--check` gate, sharing ONE
canonicalizer with the merge engine (sync/archive) so their output cannot
diverge: synced/archived specs pass `format --check` by construction.

- Behavior-preserving: normalizes only presentation (whitespace, blank lines,
  list markers, heading spacing, canonical delta-section headers); never
  reorders requirements/scenarios or rewrites prose (parse-before==parse-after).
- `--check` is a second model-free gate (CI + pre-commit) alongside sync --check;
  `--fix`/default writes; incremental via digests; --json.
- Pure text → no agent, no /opsx:format skill. Not validate (semantic) and not
  code-vs-spec verify (inference) — those compose around it.

Extract a shared src/core/spec-canonical.ts so merge engine + formatter use one
canonicalizer. New design Decision 14; Decision 13's deferral removed. New tasks
phase 2A + canonicalizer extraction (1.1a). Updated proposal, goals/non-goals.

Validates with `openspec validate --strict`. No human names in PR content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: deterministic spec-merge engine — openspec sync + openspec unarchive (+ /opsx:unarchive) Propose: deterministic spec tooling — openspec sync + unarchive + format (+ /opsx:unarchive) Jun 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (6)
openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md (3)

38-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Specify the canonical line ending format.

The scenario states that "the canonical output uses normalized line endings regardless of the platform" but does not specify whether the canonical form is LF, CRLF, or otherwise. Without this, implementers may choose differently on different platforms, violating the byte-identical requirement on line 26.

Add an explicit assertion that the canonical line ending is LF (or your chosen convention).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md`
around lines 38 - 42, The “Line endings normalized” scenario in
cli-format/spec.md is underspecified because it says normalized line endings
without naming the canonical form. Update the scenario to explicitly state the
canonical output uses LF line endings, and make sure the wording aligns with the
existing byte-identical requirement so implementers can follow the same
convention consistently.

116-118: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Clarify digest storage and invalidation for incremental checking.

The incremental checking requirement mentions "recorded content digests" but does not specify:

  • Where digests are recorded (a cache file? .git? .openspec/?)
  • How the digest is keyed (by path? by content hash?)
  • Invalidation semantics (e.g., when the canonicalizer version changes)

Without this, implementers may build incompatible caching strategies that silently skip re-checking after formatter logic updates.

Add a scenario or note defining the digest storage location and invalidation conditions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md`
around lines 116 - 118, Clarify the Incremental Checking requirement by defining
where recorded content digests live, how they are keyed, and when they must be
invalidated. Update the spec around the format check behavior to reference the
incremental cache explicitly (for example, the cache owned by the CLI format
implementation) and state that skips are only valid when both the file content
and the canonicalizer/formatter version are unchanged. Add a note or scenario
near the Incremental Checking requirement that names the digest store, the
path/content-hash keying strategy, and the version-based invalidation rule so
implementations stay compatible.

66-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reference where "conventions" are defined.

The scenario refers to "the canonical spacing defined by the conventions" without linking to where those conventions are specified. This creates an undefined reference that could lead to inconsistent implementation.

Add a cross-reference to the conventions document or inline the canonical spacing rules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md`
around lines 66 - 69, The scenario text in the CLI format spec uses an undefined
reference to “the conventions,” so update the canonical heading/spacing wording
in the affected scenario to point to the actual conventions source. Use the spec
section’s existing scenario title (“Canonical headings and spacing”) and either
add a cross-reference to the conventions document or inline the exact spacing
rules so the formatter behavior is unambiguous.
openspec/changes/add-deterministic-sync-and-unarchive/proposal.md (3)

53-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Flags --store and --json appear without prior introduction.

--store (sync, unarchive) and --json (format, sync, unarchive) are mentioned here but not introduced in the "What Changes" overview (lines 31-41). Consider adding them to the initial command descriptions for completeness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/proposal.md` around
lines 53 - 56, The command summaries in proposal.md mention `--store` and
`--json` before those flags are introduced in the “What Changes” overview.
Update the earlier descriptions for the affected entries (`cli-sync`,
`cli-unarchive`, and `cli-format`) to explicitly include these flags so the
option set is complete and consistent throughout the document.

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three successive sentences begin with "It." Consider varying the openings for readability.

Current: "It joins... It is not... It is not..."
Suggested: "It joins... This is not... Nor is it..."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/proposal.md` at line
41, The paragraph in the `openspec format` section has repetitive sentence
openings starting with “It,” which hurts readability. Revise the wording in that
description so the three consecutive sentences use varied openings, keeping the
same meaning while changing the later “It is not” sentences to alternatives such
as “This is not” and “Nor is it.”

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

"Three modes" is imprecise — only two are described.

The text lists: (1) default / --fix (treated as one), and (2) --check. That's two modes, not three. Either split default and --fix as separate (if they differ), or correct the count to "Two modes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/add-deterministic-sync-and-unarchive/proposal.md` around
lines 31 - 33, The “Three modes” description in the `openspec sync [change]`
section is inaccurate because only two behaviors are defined: default/`--fix`
and `--check`. Update the wording in the proposal text to either say “Two modes”
or clearly separate default and `--fix` only if they are intended to be
distinct, keeping the `cli-sync` description consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openspec/changes/add-deterministic-sync-and-unarchive/proposal.md`:
- Around line 53-56: The command summaries in proposal.md mention `--store` and
`--json` before those flags are introduced in the “What Changes” overview.
Update the earlier descriptions for the affected entries (`cli-sync`,
`cli-unarchive`, and `cli-format`) to explicitly include these flags so the
option set is complete and consistent throughout the document.
- Line 41: The paragraph in the `openspec format` section has repetitive
sentence openings starting with “It,” which hurts readability. Revise the
wording in that description so the three consecutive sentences use varied
openings, keeping the same meaning while changing the later “It is not”
sentences to alternatives such as “This is not” and “Nor is it.”
- Around line 31-33: The “Three modes” description in the `openspec sync
[change]` section is inaccurate because only two behaviors are defined:
default/`--fix` and `--check`. Update the wording in the proposal text to either
say “Two modes” or clearly separate default and `--fix` only if they are
intended to be distinct, keeping the `cli-sync` description consistent.

In
`@openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md`:
- Around line 38-42: The “Line endings normalized” scenario in
cli-format/spec.md is underspecified because it says normalized line endings
without naming the canonical form. Update the scenario to explicitly state the
canonical output uses LF line endings, and make sure the wording aligns with the
existing byte-identical requirement so implementers can follow the same
convention consistently.
- Around line 116-118: Clarify the Incremental Checking requirement by defining
where recorded content digests live, how they are keyed, and when they must be
invalidated. Update the spec around the format check behavior to reference the
incremental cache explicitly (for example, the cache owned by the CLI format
implementation) and state that skips are only valid when both the file content
and the canonicalizer/formatter version are unchanged. Add a note or scenario
near the Incremental Checking requirement that names the digest store, the
path/content-hash keying strategy, and the version-based invalidation rule so
implementations stay compatible.
- Around line 66-69: The scenario text in the CLI format spec uses an undefined
reference to “the conventions,” so update the canonical heading/spacing wording
in the affected scenario to point to the actual conventions source. Use the spec
section’s existing scenario title (“Canonical headings and spacing”) and either
add a cross-reference to the conventions document or inline the exact spacing
rules so the formatter behavior is unambiguous.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5fec863-4430-4161-8379-b68e3136e141

📥 Commits

Reviewing files that changed from the base of the PR and between 041387f and d2525df.

📒 Files selected for processing (4)
  • openspec/changes/add-deterministic-sync-and-unarchive/design.md
  • openspec/changes/add-deterministic-sync-and-unarchive/proposal.md
  • openspec/changes/add-deterministic-sync-and-unarchive/specs/cli-format/spec.md
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md
✅ Files skipped from review due to trivial changes (2)
  • openspec/changes/add-deterministic-sync-and-unarchive/tasks.md
  • openspec/changes/add-deterministic-sync-and-unarchive/design.md

…cope-complete

Promote the spec-aware diff driver from a deferred follow-up to a first-class
capability, completing the discussion's "review = read spec deltas + reasoning
inline" vision deterministically.

New capability cli-diff: `openspec diff [target] [--base <ref>]` — a
deterministic, pure-code renderer that splices each changed requirement's
provenance and rationale inline (what changed and why, together). Usable
standalone or, opt-in, as a git diff driver via .gitattributes (never alters
git config without consent).

Key insight: OpenSpec ALREADY has the "log" — the why lives in proposal.md, the
what/where is the applied-delta provenance this PR records — so diff joins
existing artifacts rather than building the sidecar reasoning database the
discussion reached for. No inference; honest about unattributable changes; not
semantic review (that's the verify direction).

New design Decision 15 (+ Decision 12 deferral removed) and a "Scope
completeness" note: the proposal now covers every deterministic, in-scope idea
from the discussion, so later sessions should shift from expansion to
sequencing/refinement for owner review. New tasks phase 2B. PR now 7
capabilities.

Validates with `openspec validate --strict`. No human names in PR content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: deterministic spec tooling — openspec sync + unarchive + format (+ /opsx:unarchive) Propose: deterministic spec tooling — sync · unarchive · format · diff (+ /opsx:unarchive) Jun 29, 2026

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The baseline primitive is the right spine, but this proposal has grown too broad for one implementation pass. I’d narrow the first slice to deterministic merge/baseline, sync check/fix, archive using that path, and CLI-delegated /opsx:sync, then split unarchive, format, and diff into follow-ups once the primitive is proven.

… gate (pre-commit + CI)

Address all four CodeRabbit review findings on PR Fission-AI#1279:
- cli-archive: pin the applied-delta baseline storage location + versioned
  schema (.openspec/merge-baseline.json + .openspec/pre-merge/<cap>/spec.md;
  unknown schema version => treated as no baseline).
- cli-unarchive: specify the atomic validate->stage->commit sequence and the
  per-step failure/rollback semantics.
- cli-unarchive: make pre-baseline reversal all-or-nothing (refuse entirely on
  any MODIFIED/REMOVED rather than partially reverse) for atomicity.
- cli-sync: add the --all flag for cross-change conflict detection.

Build the deferred CI/hook items (they serve the deterministic linter): new
capability cli-check — `openspec check [--fix] [--all]`, the unified
deterministic linter that runs format --check + sync --check + validate in one
gate. Answers "pre-commit OR CI?": BOTH — same binary, identical verdict; a
drift gate is a pure function of the committed files. Ships a runner-agnostic
opt-in hook installer and a copy-paste CI step (no model, no API keys). --fix
only does mechanical remediation; conflicts still fail.

design Decision 16 (+ Decision 4 updated from "staged" to "built"). Now 8
capabilities. Validates with `openspec validate --strict`. No human names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: deterministic spec tooling — sync · unarchive · format · diff (+ /opsx:unarchive) Propose: deterministic spec tooling — sync · unarchive · format · diff · check (+ /opsx:unarchive) Jun 29, 2026
@clay-good

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — all four findings are addressed in the latest push (the change folder was renamed to add-deterministic-sync-and-unarchive, so two comments pointed at the pre-rename paths; the underlying issues still applied and are fixed):

  1. Applied-delta baseline storage/format (cli-archive) — pinned: a versioned manifest at <change>/.openspec/merge-baseline.json plus captured pre-merge content under <change>/.openspec/pre-merge/<capability>/spec.md; per-spec entries carry {capability, preImage|null, appliedDigest}; an unrecognized schema version is treated as "no baseline" rather than misread. cli-sync references the same storage.
  2. Unarchive atomicity (cli-unarchive) — specified the validate → stage → commit sequence with a temporary staging area and per-step failure semantics (failure before the move leaves specs untouched; failure of the move rolls specs back from the baseline; if rollback can't complete, report partial state + exact recovery).
  3. Pre-baseline mixed invertibility (cli-unarchive) — defined as all-or-nothing (the conservative option): if any MODIFIED/REMOVED is present, refuse the entire spec reversal and direct to --keep-specs, rather than partially reverse — consistent with the atomic guarantee.
  4. --all flag for cross-change conflict detection (cli-sync) — added the flag and a "check all active changes" scenario.

Separately, this push also builds the previously-deferred CI/pre-commit items as a new cli-check capability — openspec check is the unified deterministic linter that runs identically as a pre-commit hook and a CI gate (same binary, same verdict), with an opt-in hook installer and a CI template.

@alfred-openspec

Copy link
Copy Markdown
Collaborator

Thanks Clay. I verified the latest head validates and the four concrete review findings are addressed: baseline storage/schema, unarchive atomicity, pre-baseline all-or-nothing behavior, and cross-change --all are all now specified.\n\nI’m going to keep my changes-requested position though. The new openspec check material is directionally good, but it makes the proposal even broader rather than narrowing the first implementation slice. My recommendation is still to land the primitive first: deterministic merge/baseline, sync --check/--fix, archive using that path, and CLI-delegated /opsx:sync. Then split unarchive, format, diff, and the unified check/hook/CI surface into follow-up proposals once the primitive is proven.\n\nSmall stale nit while you’re in there: the proposal still says applySpecs() has “zero callers” while also saying archive reaches it. That should be reworded to “no standalone CLI exposure” or similar.

@clay-good clay-good marked this pull request as draft June 30, 2026 20:01
@clay-good clay-good marked this pull request as ready for review June 30, 2026 21:42
@clay-good clay-good changed the title Propose: deterministic spec tooling — sync · unarchive · format · diff · check (+ /opsx:unarchive) [Feature] Propose deterministic spec tooling — sync · format · diff · check · unarchive (two primitives, one toolchain) Jul 1, 2026
…slice)

Per @alfred-openspec's review on Fission-AI#1279 ("too broad for one implementation
pass"), narrow this proposal to the load-bearing first slice and split the
rest into a follow-up:

Keep (this PR): the applied-delta baseline + deterministic merge engine,
`openspec sync` (+ `--check`/`--fix`), `archive` routed through the engine,
and `/opsx:sync` delegating to the CLI. Capabilities: cli-sync (new),
cli-archive (modified), specs-sync-skill (modified). Design Decisions 1-5,
11-13.

Move to follow-up (add-spec-tooling-suite): unarchive, format, diff, and the
unified `check` gate. Capabilities cli-unarchive, opsx-unarchive-skill,
cli-format, cli-diff, cli-check; design Decisions 6-10, 14-16.

Rename the change add-deterministic-sync-and-unarchive -> add-deterministic-
spec-sync to match the narrowed scope. Validates clean under
`openspec validate add-deterministic-spec-sync --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title [Feature] Propose deterministic spec tooling — sync · format · diff · check · unarchive (two primitives, one toolchain) [Feature] Propose the deterministic spec-merge engine — sync + applied-delta baseline (first slice; follow-up #1288) Jul 1, 2026

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. This now matches the first-slice scope I was pushing for: deterministic merge/baseline, sync check/fix, archive through the same path, and CLI-delegated /opsx:sync, with unarchive/format/diff/check split into follow-up #1288.\n\nI re-validated the renamed change () and checked the current diff/CI. The scope is now tight enough to merge as a proposal.

@alfred-openspec

Copy link
Copy Markdown
Collaborator

Tiny correction to my approval note: the validated renamed change is add-deterministic-spec-sync.

@TabishB TabishB left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned previously, deterministic merging is something we already had and would go wrong for a variety of reasons. It requires a level of determinism when produce the delta itself that is hard to control. Especially when further edits are made to the delta.


## Context

OpenSpec's spec merge (delta → `specs/`) is reached two ways today: deterministically but only *inside* `openspec archive` (fused to the folder move), or by an AI agent in `/opsx:sync` that "directly edits main specs." The standalone deterministic apply (`applySpecs()`, [src/core/specs-apply.ts:391](../../../src/core/specs-apply.ts)) has zero callers. The design discussion (2026-06) worked from "add unarchive" to a single root cause: **the merge must be deterministic, idempotent, and reversible pure code, exposed as commands** — then a code-only CI drift gate, a `sync --fix` pre-commit hook, crumb-free re-merge, and (later) reverse all fall out of one primitive.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether the merge must be deterministic and idempotent, or if it should be a pure code operation. I think we should just like leave that to Git where possible. Like I don't know how I feel about us trying to make this like a deterministic thing on our end.

Like I'm just not sure of like the complexity involved here, but yeah, I think like the sync command or like the sync skill in general is like ver relatively confusing to users, so I'm not sure, but I'll read through more of this.

## Goals / Non-Goals

**Goals**
- A deterministic merge: same (delta + base) → byte-identical `specs/`, in pure code, on every platform.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, similar to the archive command itself, I think the tricky part here is the merge step.

The archive command basically does two things: it merges the spec changes, then moves the folder. As discussed in the other PRs, the reason we stopped using it directly is that the deterministic merge was too brittle. It would fail in weird edge cases, and sometimes a purely mechanical merge does not really make sense anyway.

For example, during the merge you might want to rewrite things, simplify them, or make the final spec read more coherently. So I’m not sure this should be treated as a purely deterministic operation.

@clay-good clay-good changed the title [Feature] Propose the deterministic spec-merge engine — sync + applied-delta baseline (first slice; follow-up #1288) feat(engine): propose deterministic spec-merge engine (part 1) (#1288) Jul 2, 2026
@TabishB

TabishB commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@clay-good Let me know if we can close this or there's some other action item you want to take from here.

@clay-good clay-good closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants