lifecycle: status — state as data, sync --check gate - #1
Conversation
ixxie
left a comment
There was a problem hiding this comment.
Self-review of layer 1: upstream-ready. Typecheck clean, targeted suites green (225 tests incl. the full archive suite).
What holds up well: fail-closed gate semantics (unreadable metadata → conflict, never skipped), folded-ness decided by byte-identical regeneration rather than bookkeeping, symmetric mode-refusal messages in both directions, born-proposed at creation, and the deliberate lifecycle-vs-task-status naming in the list JSON payload. Tests cover idempotency and both refusal directions.
Minor notes (none blocking):
- The proposal text pitches
sync --check --changedas the scoped pre-commit tier, but only the optional change-name positional exists. Close the gap in one direction before peeling — implement--changedor soften the doc. Related: the gate regenerates every shipped change's folds on each run, O(all history) where archive's scan was O(active); fine now, but--changed/incremental is the eventual answer and worth a sentence in the proposal. openspec list --status bogussilently prints "No changes with status 'bogus'" — validate against the closed set the completions registry already advertises.- Named
sync <x> --checkwith unreadable metadata throws out ofevaluate, while the no-arg gate reports the same state as aconflictentry; both exit non-zero, but the shapes differ.
| const changePath = path.join(changesDir, changeDir); | ||
| const lastModified = await getLastModified(changePath); | ||
| const lifecycle = readLifecycleStatus(changePath); | ||
| if (options.status && lifecycle !== options.status) { |
There was a problem hiding this comment.
--status bogus falls through to an empty list rather than an error. Validating against LIFECYCLE_STATES (the registry already advertises proposed/shipped) would catch the typo instead of reporting a plausible-looking "no changes".
|
|
||
| // An explicitly named change must be shipped before its deltas may touch | ||
| // specs/. In check mode a non-shipped change is simply not gated. | ||
| const metadata = readChangeMetadata(changeDir, projectRoot); |
There was a problem hiding this comment.
In --check mode this readChangeMetadata throw escapes for a named change, while the no-arg sweep converts the same condition into a conflict report entry. Both exit non-zero, so low stakes — but wrapping here to emit the same conflict shape would make the gate's output uniform for CI parsers.
|
Review findings addressed in fd9e069:
The |
… sync --check gate Opt-in via `lifecycle: status` in openspec/config.yaml (default 'archive' unchanged). Under the mode: a change's lifecycle state lives in its .openspec.yaml `status` field (proposed | applied | shipped) and nothing ever moves to changes/archive/. New `openspec sync` folds shipped changes' deltas into specs/ as a standalone idempotent step through the existing specs-apply engine; `sync --check` verifies by regeneration (rebuilt output byte-identical to the current spec) and exits 1 on a shipped-but-unfolded change, making the gate a pure function of the tree — runnable at pre-commit, pre-push and CI. archive refuses under the mode and points at the status workflow; list gains a status column and --status filter; new changes are born `status: proposed`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restores archive's declare+fold atomicity as a convenience instead of a
mandate: ship sets `status: shipped` in the change's .openspec.yaml and
runs the same idempotent sync fold, emitting one working-tree diff so the
commit that declares shipped is the commit whose tree satisfies the
shipped ⇒ folded predicate. Sugar over the field edit + sync, never the
only way. Also neutralizes the folded-spec skeleton wording ("created
from change X") — the old text assumed the archive workflow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…comment Drops 'applied'. The closed set exists because tooling attaches consequences to each state; applied carried none (sync gates shipped, overlap reasoning keys on proposed), and implementation progress is already recorded by tasks.md checkboxes — a duplicate that can drift. The next state to earn a slot is whichever arrives with consequences (abandoned — releasing the live claim — is the obvious candidate). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edge Programmatic callers read the returned SyncReport instead of sniffing process.exitCode; a new silent option suppresses output for reuse as a gate. Unreadable metadata on a named sync now yields the same conflict entry the no-arg sweep reports, so CI parses one shape either way, and list rejects an unknown --status value instead of printing an empty list that reads as success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fd9e069 to
9315379
Compare
The repo tracks its own features as OpenSpec changes; this adds the proposal, design note, tasks and capability spec for lifecycle: status, plus the release changeset. The design note records why the state set is closed at two, why folded-ness is decided by regeneration rather than bookkeeping, and why archive refuses under the mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ailures stay JSON Review findings from the PR: - sync swallowed every readdir error and reported no shipped changes, so an unreadable changes/ dir passed a gate whose whole claim is failing closed. Only ENOENT means 'no changes' now; anything else propagates. - the sync and ship CLI handlers dropped the --json flag on the error path, breaking the agent contract that a --json failure leaves exactly one JSON document on stdout. - the capability spec claimed ship is a no-op under archive mode; it refuses, as the implementation and its test have always done. Added the fail-closed requirement the first fix creates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Changes under `lifecycle: status` shard as changes/YYYY/MM/DD-<name>/ — assigned at birth, immutable, so location encodes only the creation date and nothing ever moves. A shared discovery module enumerates both layouts (YYYY/MM dirs are shards to walk into; anything else is a change; the DD- prefix strips from the id), list/sync/ship resolve ids through it, and createChange shards new changes. Duplicate ids across shard dates are rejected at creation and on lookup. `openspec migrate` converts a legacy project one way: archived changes become changes/YYYY/MM/DD-<name>/ with status: shipped (the folder date's meaning shifts from archival to creation — the closest surviving record), active changes shard by their created date as proposed, config gains lifecycle: status. Metadata edits are tolerant raw-YAML key writes, never strict-schema round-trips — a migration that drops fields it does not understand destroys history. --dry-run prints the plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reversal moves only bookkeeping, like the forward direction: shipped changes return to changes/archive/YYYY-MM-DD-<name>/ (dates from the shard path), proposed changes return flat, the status key is stripped (under archive mode, location is the state), empty shard dirs prune, and the config line disappears. No spec text changes in either direction — archive-mode specs/ is folded shipped reality, which is exactly what status-mode maintains, so the round-trip is a pure relayout (covered by a round-trip test). Refuses while any shipped change has unfolded deltas: the archive layout asserts folds that must actually exist. One honest asymmetry, printed on completion: changes shipped under status mode carry their creation date into the archive folder name, where convention reads an archival date. This is the exit ramp the experimental flag's exit criteria require — if the mode is ever removed rather than graduated, --to archive is how projects return to supported ground. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mbiguity and survives interruption Review fixes on the sharded layout: - getActiveChangeIds, getAvailableChanges and the view dashboard enumerate through discoverChanges, so a migrated tree no longer reports the year shard as a change named '2026'; show, validate, status and instructions resolve sharded dirs via resolveChangeDir (with the flat join kept as fallback behind the traversal guard). - resolveChangeDir returns null for ids discovery could never produce (separators, dot segments), so hostile ids cannot address anything outside changes/; discovery skips hidden dirs like the flat scan did. - migrate pre-flights id ambiguity: a legacy name reused across archive eras — idiomatic under archive mode — would shard into two dirs no bare id can address, so the plan is refused with the collisions named before the first rename (dry-run included). Duplicate targets refuse likewise instead of clobbering. - an interrupted migration now resumes: the flat scan skips year shards left by a partial run instead of renaming changes/YYYY into itself. - the reverse-migration gate reads SyncCommand's returned report via the new silent option instead of monkey-patching console.log and sniffing process.exitCode. - metadata stamping edits the YAML document in place, preserving comments and key order legacy files may carry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live QA on the demo caught show --json reporting a sharded change as '2026': both extractNameFromPath copies took the segment after 'changes/', which in the sharded layout is the year. They now recognize changes/YYYY/MM/DD-<name>/ and return the de-prefixed name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… name derivation share one source of truth Second-pass review fixes: - resolveChangeDir refuses 'archive' and year-shaped ids, so the resolver and discovery agree on the addressable namespace — the flat-first stat no longer hands out changes/2026 as a change. - openspec new change derives its success message from the dir createChange actually made instead of a flat join, so under lifecycle: status it prints the sharded path that exists. - the two byte-identical extractNameFromPath copies delegate to a shared itemNameFromPath in change-discovery, where the shard rule lives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/ under status mode Live QA caught openspec new change scaffolding changes/archive/ into a status-mode tree — the one directory the mode abolishes. The root-completion scaffold is now mode-aware; root health never required the dir (a missing archive/ raises no diagnostic). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layout and migration are part of the same vision, so the dogfooded change describes all of it: two new capability specs for layout discovery and bidirectional migration, the design note's reasoning for sharding by an immutable creation date and for reversal being a pure relayout, and an explicit note that Fission-AI#1367's domain discovery is the better mechanism and should supersede the sharding if it lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Superseded by the upstream PR Fission-AI#1684. |
Layer 1 of 2 (stack:
lifecycle-status→lifecycle-sharding). Fork-internal review stack for the upstream proposal drafted in QUA-1574; layers will peel serially to Fission-AI/OpenSpec.Experimental opt-in
lifecycle: statusmode: a change's lifecycle state is astatus: proposed | shippedfield in its.openspec.yaml, never a directory move.openspec syncfolds shipped changes' deltas idempotently;sync --checkis the deterministic tree-level gate (shipped ⇒ folded) runnable at pre-commit, pre-push and CI;openspec shipflips and folds as one diff; archive refuses under the mode and points at the status workflow.Working demo (installs this stack from the fork): https://github.com/ixxie/openspec-status-demo — see PR #2 there for the gate going red/green live.
🤖 Generated with Claude Code