Repository navigation
feat(cli): reach upstream's archive and sync surfaces - #64
Merged
Merged
Conversation
Artifacts for roadmap R7 (rows 38-42) and the R7 obligations: archive --no-validate, archive's upstream JSON envelopes on every refusal, the shared verbatim-view scenario-preservation helper, respelled relays, namespace-folder refusal, new-spec-non-added on REMOVED, and the new cospec sync-specs command run through the binary's archive on a scratch copy. validate --strict clean; apply exit 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Append the archive-and-sync-parity builders to test/contract/fixtures.ts: feat v2 changes (done tasks, resolved verification, the composed schema copied in) for ADDED on a new capability, MODIFIED, REMOVED, RENAMED and a retired capability; the three verbatim-view fixtures; the bare-verification, incomplete-task, scenario-drop and revalidation-only variants; a namespace folder; ADDED+REMOVED, REMOVED-only and REMOVED-only-with-marker on a new capability; a symlinked capability alias; a short carried Purpose; a no-delta feat, a skip_specs feat and a chore change; and the mode-000 archive case. A smoke row per builder pins how `openspec validate --strict` and `cospec validate --strict` read it. Two cospec rows are test.failing on this tree (new-added-removed, new-removed-only-marked): new-spec-non-added still fires on REMOVED until task 2.1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Write the contract rows of verification groups 1-3, 4.1, 5.1, 6.1-6.4, 7.1, 7.2, 8 and 2.3 in archive-no-validate.test.ts, each reading the pinned binary's answer at test time through the upstream oracle, and the archive document rows through support/key-oracle.ts. The key oracle gains a path class (archive.path/root.path compared relative to each tool's own root, since cospec and the binary archive separate copies) and the tasks-gate fix as a named collision: cospec's --force-incomplete wins because --yes, a no-op under cospec, does not lift its stricter gate. Failing on this tree: 35 rows marked test.failing (1.1-1.6, 2.1, 2.2 x4, 2.3 x7, 2.4, 2.6, 4.1, 5.1, 6.1-6.3, 7.1 x3, 7.2 x5, 8.1, 8.2 sweep), plus the 2 cospec smoke rows from 1.1. Passing already and left plain: 3.1-3.3 (the gate already reads the verbatim view, design fact 1), 6.4 x2 and the 8.2 pass-through row. mise run test:contract: 2620 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Write the contract rows of verification groups 9-12 and 5.2 in sync-specs.test.ts, plus the sync-specs halves of 8.1 and 8.2. Every byte-equality row compares the main specs against `openspec archive c1 -y` on a copy (file list and sha256 per file, and the directory list so a pruned capability directory shows). Each cospec run gets a private TMPDIR, so a row proves the scratch tree is gone afterwards, or, with TMPDIR unwritable, that the command never made one. All 33 rows are test.failing: `cospec sync-specs` does not exist yet, and every refusal row asserts its own message or code, not just exit 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
archive/new-spec-non-added now skips REMOVED as well as ADDED on a
capability with no living spec: the binary's merge ignores that REMOVED
with a warning ("nothing to remove") and applies the rest. MODIFIED and
RENAMED stay ERRORs. A REMOVED-only delta without retire_capabilities is
still refused, by archive/rebuilt-spec-invalid: once the precondition no
longer suppresses it, the rebuilt check already fires on the skeleton spec
(no requirement), so no extension was needed; under the marker it is the
binary's skip and reports nothing.
Unit table 6.5 plus the two REMOVED-only cases. Flips rows 6.1 and 6.3 and
the two cospec smoke rows; 6.4 stays green. Row 6.2 also asserts cospec's
"Specs: already in sync" line, which is task 3.5's, so it flips there. The
archive-parity fixture `new-spec-non-added` (REMOVED-only, no marker) now
names archive/rebuilt-spec-invalid as the rule that refuses it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Move changeDeltaOps and the archive/scenario-preservation step out of
commands/archive.ts into core/scenario-gate.ts, unchanged in behavior:
scenarioGate(base, caps) reads each living spec through parseLivingSpec and
returns {drops, livingCaps}, and scenarioRefusal(command, drops) prints the
refusal archive printed before. The inputs stay typed on the verbatim view.
Unit row 3.4: a type-level test refuses an AdvisoryDeltaOp argument, and no
command calls findScenarioDrops directly. Rows 3.1-3.3 still green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
core/archive-output.ts builds the failure document every `cospec archive`
refusal prints under --json: cospec's change/type/archived/reason, then the
binary's archive: null, root and status[0], each diagnostic in the binary's
code and message (ported from dist/core/archive.js) where the binary
refuses the same input, its message and fix respelled through the
allowlist. archive.ts routes every refusal through one `refuse`, exports
jsonFailurePayload = { archive: null } for the resolver document, refuses
an invalid change name with archive_change_name_invalid before the lookup,
and lists every change directory in the not-found message as the binary
does. The revalidation document keeps every key of its report; a delegated
failure carries archive_error with the binary's last reason line and keeps
openspecExit.
Unit enumeration 2.5: one case per refusal reason archive.ts names (the
three post-delegation ones with the binary stubbed), plus a source check
that no refusal returns outside `refuse`. Flips rows 2.3 (unknown change,
invalid name, revalidation, incomplete tasks, taken slot, scenario drop),
2.4 and 2.6; the oracle rows pair status[] entries by code. 2.3's
namespace-folder row flips with task 3.3.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`cospec archive <folder>` runs findNestedChangesIn right after the change resolves and before revalidation, as the binary does, and refuses it with the binary's message (`Cannot archive '<name>': <explanation>`) and fix on stderr, or as one archive_change_is_namespace_folder document under --json, exit 1, with nothing moved and no meta/nested-change report. The refusal enumeration gains its namespace-folder case. Flips rows 5.1 and 2.3's namespace-folder row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`cospec archive` now runs the binary's two checks at the binary's two points, on the same runtime, so it diverges per OS exactly as the binary does (design D7): first, before the change resolves, the binary's assertPathWithin over changes/, changes/archive/ and specs/ through realpathSync.native (glob.ts's port, now exported), refused as archive_path_outside_root; then, at the slot check, an lstat of the slot whose non-ENOENT errno is archive_error in the runtime's own words. Between them the archive directory is read the degraded way (readValidateContext's empty index plus a warning naming it, the self-blocker check on an empty map, basenames listing nothing), so nothing crashes. buildValidateContext, now unused, is removed. Flips row 4.1 (macOS: archive_path_outside_root, one stderr line). Row 4.2 ran in oven/bun:1.3.14 as uid 1000 with the worktree read-only at /w: both tools answer archive_error, EACCES statx on the slot, nothing moved, no lock; recorded in the ledger. The refusal enumeration gains its archive-unreadable case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The success document gains the binary's
archive: {change, archivedAs, path, specsUpdated, totals?, warnings?} and
root, with every cospec key unchanged. core/archive-output.ts reads the
binary's own fixed lines from the human-mode archive it wrapped: its
`Totals:` line, `Specs updated successfully.` / `Specs already in sync; no
files changed.`, and its spec-merge warnings plus each retirement's note,
which is the list its own --json carries (recorded in design D5, where the
binary wins over the original wording). totals is left out under
--skip-specs or when no Totals line was printed, and specsUpdated then
falls back to whether any main-spec file's bytes changed.
The Specs line names why no sync ran (`skipped (--skip-specs)`,
`none (the <type> schema has no specs artifact)`, `none (no delta specs,
so no spec sync)`), with specsSkipReason flag/schema/no-deltas in JSON
beside the unchanged specs: "skipped", and reads `already in sync` when
the binary said so.
Unit table 7.3. Flips rows 2.1, 2.2 (x4), 7.1 (x3), 7.2 (x5), and 6.2,
whose in-sync Specs line this task adds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every relay of the wrapped archive's output now goes through respellRemedies: the captured block of the aborted and half-state reports, the relayed warnings in both modes (the Warning: lines and JSON warnings), archive.warnings, and the failure document's message and fix. The allowlist rewrites only upstream's exact sentences, so a path, a change name or spec text passes through byte-for-byte; remedy-sources.ts already classifies these core/archive.js lines as relayed and needs no new entry. Flips rows 8.1 (archive) and 8.2's sweep of archive outputs, which now sweeps only what `cospec archive` itself printed in each row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`cospec archive <change> --no-validate` skips cospec's revalidation and forwards --no-validate to the wrapped `archive -y`, so the binary skips its own validation as an openspec user asked (design D3). The namespace-folder check, tasks gate, archive/verification-incomplete, slot check, archive/scenario-preservation, on-disk verification and spot-check all still run, and a banner on stderr (both modes) says so before the first of them. The flag moves from pending to handled in core/command-table.ts and its parity-pending.yaml entry is deleted. The unit tests that used it as their pending-flag example now use `init --language`; archive's help and completion list it. Flips rows 1.1-1.6. archive-no-validate.test.ts has no test.failing left. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
core/scratch-root.ts (design D11): syncThroughScratch copies what the binary's archive reads — config.yaml/config.yml, schemas/, specs/, the one change and an empty changes/archive/ — into a mkdtemp directory under the OS temp directory (links copied verbatim), runs an injected runner there, and confirms the run archived the scratch copy (exit 0, no abort, the change gone, one archive entry holding .openspec.yaml). It then diffs the scratch specs/ against its pre-run copy, refuses if the real specs/ changed while the run worked, copies back each written file atomically (an existing file's mode kept), unlinks each deleted one, prunes directories the run emptied, and re-reads the real tree to verify exactly that landed. A symbolic link that leads outside the copied paths is refused before any copy, naming it. The scratch directory is removed in a finally, so a claim file or a partial write can only ever exist there. Unit rows 11.2 (a runner that claims, writes partially and fails leaves the real tree byte-identical and the scratch gone) and 11.5 (the real specs changing mid-run refuses and writes nothing), plus the copy-back, retirement-prune and link cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`cospec sync-specs <change>` merges a change's delta specs into the main specs and leaves the change active (design D11). It resolves the change as archive does (an invalid name, an unknown change and a namespace folder, `Cannot sync '<name>'`, refused with archive's codes), says there is nothing to sync for a schema with no specs artifact, skip_specs: true or no delta files, runs archive's revalidation and the shared scenario-preservation gate, then runs the pinned binary's own `archive -y` on the scratch copy (expected exits 0 and 1, the scratch post-condition in core/scratch-root.ts) and copies back what it changed. Text prints a `Synced:` line per file and the binary's Totals, or that the specs were already in sync, with relayed warnings respelled; --json prints one document, and every refusal archive's failure document with synced: false. The command-table row (json and store accepted, one required `change` positional) and its cli.ts dispatch entry land here; scenario-gate's importers are now archive.ts and sync-specs.ts. Flips rows 9.1-9.4, 10.1, 10.2, 11.1, 11.3, 11.4, 12.1-12.4, 5.2 and the sync-specs halves of 8.1 and 8.2: sync-specs.test.ts has no test.failing left. Reachability green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sync-specs workflow now does upstream's `sync`: select the change, preview the merge (`cospec validate <slug>`, the delta files, which main specs it will create, change or delete, any retirement and its marker), run `cospec sync-specs <slug>`, and relay the result. The "no supported mid-flight sync" text is gone; the retirement section stays. The archive workflow names the early-sync no-op (both hard gates still run), and both bodies stop saying a REMOVED on a new capability is refused. The sync-specs description in harness.yaml becomes "Merge a change's delta specs into the main specs without archiving it, exactly as archive would", keeping its trigger phrases (design D13). `mise run generate` regenerated every harness; the render goldens and snapshots are rewritten for the two bodies only. Row 13.1's canon render test checks every harness body; generate:check reports no drift. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bring every page design D14 names to the shipped behavior, each fact on the page that owns it: commands.md gets archive's --no-validate, the Specs line reasons, the archive and sync-specs JSON documents and codes, and a sync-specs row; apply-and-archive.md gets the namespace-folder refusal, --no-validate's gates, early-synced no-op archives and REMOVED on a new capability; docs/apply-archive.md the archive step order around the binary's own directory checks and the scratch-tree design; validation-rules.md archive/new-spec-non-added's trigger; harness-setup.md the sync <-> sync-specs sentence. docs/harness-integration.md, which still said a mid-flight sync is unsupported, now describes the shipped workflow. mise run docs:build is green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
.agents/shared.md's workflow step 6 names `cospec sync-specs <slug>` as the way to land a change's main specs early (archive's own merge on a scratch copy, the change left active, its later archive a no-op merge with both hard gates). The "JSON documents are additive" paragraph lists archive as a mirrored command instead of "next", says where archive's upstream keys come from, and names the tasks-gate `status[].fix` in NAMED_COLLISIONS with why cospec's value wins. `mise run agents:sync` propagated it to CLAUDE.md and AGENTS.md; agents:check is green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Record the observed result after `->` on every verification row the agent owns (4.2's container run was recorded with task 3.4), confirm row 14.1 (no test.failing or test.todo left in archive-no-validate.test.ts or sync-specs.test.ts) and row 14.2 (`mise run check` green: unit 2004, integration 193, contract 2653, bench 343, release-test 14, all 0 fail; `mise run test:pack` 2 pass). `cospec validate archive-and-sync-parity --strict` is clean. Row 13.4 stays `[ ]`: it is the human-held DeepSeek eval (task 8.2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`cospec archive` never reads `skip_specs:` on a change that has delta files: its revalidation refuses the conflict (deltas/skip-specs-conflict), exit 1. `cospec sync-specs` checked the marker first and answered "Nothing to sync" with exit 0, a false clear of a change archive refuses. The new row (failing on this tree) runs both on the conflicting fixture, with TMPDIR unwritable so no scratch run can start. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sync-specs now reads `skip_specs: true` only for a change with no delta files, as archive does. Beside delta files the marker is a conflict the archive-precondition revalidation refuses (deltas/skip-specs-conflict), so sync-specs falls through to that revalidation and refuses with archive's report and exit 1 instead of answering "Nothing to sync" with exit 0. Flips the row pinned in the previous commit; ledger row 10.2 records it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rows 1.1 and 14.2 named pre-rebase commits that no longer exist on the PR branch; point them at the post-rebase shas and the check run actually observed on the rebased tree (70c4e65). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A delta kept in a file the merge never reads (specs/spec.md, specs/<capability>.md, a note beside spec.md) is no delta to the capability walk, so sync-specs answered "Nothing to sync: c1 has no delta specs" with exit 0 while cospec archive and the binary's archive refuse the same change (deltas/unread-file). sync-specs now runs archive's revalidation before the no-deltas and skip_specs answers; only a schema with no specs artifact still answers without running anything. Contract rows 10.2 pin the three shapes against archive's own report. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
archive's specsUpdated fallback hashed every file under openspec/specs/, so one unrelated spec this process cannot read (mode 000) crashed `cospec archive --json` with a raw EACCES and empty stdout, where the binary's archive reads only each delta's target and archives. The snapshot now covers just the living spec.md each delta capability targets (absent when missing; an unreadable one by its metadata, left to the binary to answer). Contract rows 1.7 pin the --no-validate parity and the one-document answer without the flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scratch copy kept links verbatim, so an absolute in-tree alias
(openspec/specs/alias -> <root>/openspec/specs/widgets) still pointed at
the real tree: the binary merged straight into the real widgets/spec.md,
then sync-specs refused with "nothing was written". Each copied path is
now read at its real path and every link inside it re-pointed at the
scratch copy of its target; a symlinked specs/ is walked for escapes.
The copy-back diffed only files, so a retired capability whose spec.md
is a link kept the link and its directory ("already in sync") while the
binary's archive deletes both. Every entry kind is now diffed: a removed
link is deleted, a link replaced by a file is written, and a link the
run created or re-pointed is a breach thrown before any write.
An unrelated spec this process cannot read crashed the copy and the
fingerprint with a raw EACCES; it is now copied as an empty placeholder
of the same mode and fingerprinted by its metadata. Rows 9.5, 11.4,
11.6 and six scratch-root unit cases pin each shape.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The unread-delta, absolute-alias and linked-spec builders join R7_FIXTURES, so the smoke rows confirm both validators read each one as its flags say (72 pass). The mode-000 builder stays out: the binary's validate answers it per OS, as its doc comment now says. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ticks tasks.md 8.3 (rebase onto main 80ee385, bun install --frozen-lockfile, mise run check) and refreshes verification row 14.2's stale evidence (70c4e65/unit 2019 from a prior close-out) with this merge-stage run on tree 6380655: unit 2025, integration 193, contract 2678 (1391.9s), bench 343, release-test 14, 0 fail; test:pack 2 pass; docs:build and cospec validate --strict both clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
replygirl
force-pushed
the
worktree-archive-and-sync-parity
branch
from
October 5, 2026 13:50
9f03aa9 to
2ef08a0
Compare
replygirl
marked this pull request as ready for review
October 5, 2026 13:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cospec archiveand/cospec:sync-specswere the last two places where anopenspecinvocation behaved differently undercospec. This PR closes bothgaps:
cospec archive --no-validateis accepted (forwarded to the wrappedarchive -y) instead of refused as pending, while the tasks gate,archive/verification-incomplete,archive/scenario-preservation, thenamespace-folder refusal, the slot check and on-disk verification still run.
cospec archive --jsonnow answers with upstream's keys(
archive/root/status) on both success and every refusal, instead ofstderr-only prose for gate refusals.
remedies.tsallowlist.Specs:line names why a sync was skipped (--skip-specs, no specsartifact, no delta specs) instead of collapsing everything into
skipped.not a validation report.
archive/new-spec-non-addedno longer falsely refuses REMOVED operations ona capability with no living spec.
cospec sync-specs <change>(and/cospec:sync-specs) merges a change'sdelta specs into the main specs early — via the binary's own
archive -yrunon a scratch copy — without archiving the change, so upstream's
syncworkflow has a cospec counterpart. A later archive of an early-synced change
is the binary's own no-op merge.
BREAKING
(verbatim from the archived proposal)
cospec archive --jsonrefusals now print a JSON document on stdout. Theyused to print only stderr prose (gate refusals, unknown change, slot
collision). The validation refusal's document is no longer
validate's barereport: it keeps that report's keys and adds
archive,root,status,change,type,archivedandreason.cospec archive <namespace folder>reportsarchive_change_is_namespace_folder(the binary's message and fix) insteadof a
meta/nested-changevalidation report. A script grepping the validateoutput for this case needs the new text.
cospec archive --no-validatearchives where it exited 1 as pending, andskips the binary's validation too (so the binary also retires no capability,
as it does under the flag).
Specs:line of a no-delta change readsnoneand names the reason,where it read
skipped./cospec:sync-specswrites main specs. It used to only preview.cospec validatestops reportingarchive/new-spec-non-addedon REMOVEDoperations.
Review findings fixed
Four findings from review were fixed on this branch:
sync-specs's scratch-root handlingArchive
mise run cospec -- archive archive-and-sync-parityran clean with no flags,moving the change to
openspec/changes/archive/2026-10-05-archive-and-sync-parity/as the finalcommit on this branch:
🤖 Generated with Claude Code