Skip to content

feat(cli): reach upstream's list, status and validate surfaces - #59

Merged
replygirl merged 67 commits into
mainfrom
worktree-cli-surface-parity
Oct 5, 2026
Merged

replygirl merged 67 commits into
mainfrom
worktree-cli-surface-parity

Conversation

@replygirl

@replygirl replygirl commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

cospec list, cospec status and cospec validate had drifted from what the
wrapped OpenSpec 1.13.1 binary answers for the same invocation. This PR closes
that gap:

  • a native nested-change detector: namespace folders under openspec/changes/
    are reported for what they are, and are not treated as empty changes. Schema
    outputs are matched with the binary's own glob semantics (core/glob.ts over
    fast-glob, pinned to the version the pinned openspec resolves).
  • a next step on every status entry.
  • upstream's own JSON keys merged additively into the list, status and
    validate documents, checked by a contract key oracle.
  • delegated status for schemas cospec doesn't type: a fork, spec-driven, or
    one that resolves nowhere.
  • validate's item resolution: --type, the ambiguity and unknown-name
    refusals, bulk-flag precedence, --report full|findings and --concurrency.
  • the schemas and archived-changes completion sources.
  • one JSON document on every apply early exit and every --json read
    failure.
  • a linear-time fix for the archive/target-invalid dedupe, which had
    regressed to double-reporting a quoted requirement header.

Breaking

  • cospec list now orders by most recent change first. Pass --sort name for
    the old order.
  • cospec list outside an OpenSpec root answers OpenSpec's own
    no_openspec_root refusal, exit 1, where it printed No active changes.
  • cospec validate <name> --all|--changes|--specs validates the bulk scope,
    not the one item.
  • An ambiguous validate name is refused, and an unknown one prints the
    binary's message.
  • A namespace folder makes status --change and status --all exit 1 and
    validate fail. validate, apply and archive (which share
    validateChange) now report it as meta/nested-change, not
    meta/openspec-yaml — a script grepping the old rule id for this case needs
    the new one.
  • status --json on a schema cospec doesn't type exits 1 when the binary
    does.
  • status types a change directory without .openspec.yaml by config.yaml.
    validate, apply and archive still refuse it.
  • The root key of the status --all and no-active-changes documents is an
    object, not a path string.
  • A cospec-typed change whose schema OpenSpec can't load, or whose
    .openspec.yaml OpenSpec refuses, makes status exit 1, in text and
    --json (rounds 4; text mode used to render cospec's own table at exit 0 in
    both cases).

Judgment calls

  • Commit scopes: tasks.md named subjects with the scopes status, list
    and completion. commitlint's scope-enum doesn't allow them, so those
    commits use the allowed cli scope.
  • status --schema overrides the schema; it doesn't filter. This was probed
    against the pinned binary: every change is reported as that schema.
  • Unknown validate names get the binary's nearestMatches (the five
    nearest by edit distance), not cospec's own closest().
  • resolveNext's order follows the verification spec's table. Design's
    prose disagreed with it. The order:
    1. the first ready required artifact;
    2. else cospec apply <id>, once every required one is done (a skipped
      specs counts as done);
    3. else the first ready artifact of any kind;
    4. else nothing.
  • Rule-id change: validate, apply and archive share
    validateChange, so all three now report a namespace folder as
    meta/nested-change instead of meta/openspec-yaml. Rule ids are documented
    as stable public API, so this is in the Breaking list above.
  • items[].type collision (ruled by the roadmap): items[].type keeps
    cospec's meaning, the change's schema. OpenSpec's value is carried as
    kind. This is the oracle's one named collision besides version, and it is
    documented on the validate page's JSON section.
  • Verification row 8.4 (unknown --store) stays [~] defer. cospec's
    message text is root-resolution-parity's documented wording, accepted at the
    round-1 review. The row compares code, target, fix, payload and exit, and
    they all match.
  • Round 2:
    • fast-glob is an exact-pinned devDependency, like yaml. It is not loaded
      from the wrapped package at run time, because the standalone binary's
      embedded openspec bundle has no copy to load. glob.test.ts fails if an
      OpenSpec pin bump moves it.
    • On an unreadable tasks.md, the binary refuses under Bun on macOS but
      lists the change on Linux. cospec relays whichever answer the binary gives
      on that OS.

Round-2 review fixes (tasks 11.1–11.13)

  • 11.1: the round-2 rows were written first, as failing tests.
  • 11.2: validate <id> --type spec on a spec that discovery skips (a
    dot-directory, a linked capability) validates that file, where it used to
    print an empty passing report.
  • 11.3: the namespace-folder detector matches generates with the binary's
    glob semantics (braces, ranges, extglobs, negation) through core/glob.ts.
  • 11.4: bench parseSchemaConformanceJson returns null for a refused or
    incomplete validate document.
  • 11.5: status takes a custom schema's status from its own artifacts.
  • 11.6: validate --archived relays the binary's failure document. It no longer
    says "needs >=1.9.0".
  • 11.7: an unreadable openspec/changes/archive/ leaves validate and apply
    answering, with an archive_unreadable warning.
  • 11.8: validate --json outside a root prints the one no_openspec_root
    document.
  • 11.9: a failed list --specs relays the binary's document, or its message and
    fix in text.
  • 11.10: an unreadable living spec.md is one meta/unreadable-artifact ERROR,
    and every other spec is reported as it is alone.
  • 11.11: evidence is recorded on every group-15 row, and the docs pages that own
    each fact are updated.
  • 11.12: an unreadable tasks.md is answered as the binary answers it on each OS
    (CI was red on Linux before this fix).
  • 11.13: CodeQL js/redos alert fix(cli): report the embedded openspec pin from cospec doctor #15 is closed. The test no longer compiles an
    exponential reference regex; a star-height guard flags it instead.

Round-3 review fixes (tasks 12.1–12.13)

  • 12.1: the round-3 rows were written first, as failing tests.
  • 12.2: every delegated validation names its kind (--type change/--type spec); a refusal the binary answers with a status[] document is the
    item's openspec/validate ERROR, never an empty passing report; --strict
    fails a warning-only spec.
  • 12.3: the living spec a delta targets is read through the change's reader, so
    an unreadable one is meta/unreadable-artifact naming the file.
  • 12.4: an errno failure that escapes validate, status or apply (an
    unreadable openspec/changes/, openspec/specs/ or capability directory) is
    one --json document with the binary's per-command code and payload.
  • 12.5: validate <name> with no openspec/ directory resolves the name as
    unknown_item, as the binary does.
  • 12.6: a change is looked up as the binary's validateChangeExists looks it
    up.
  • 12.7: every diagnostic status relays from the binary is spelled through the
    remedy allowlist, in text and under --json.
  • 12.8: an in-progress cospec-typed entry keeps artifacts: [] under --json,
    singly and in the sweep.
  • 12.9: status refuses a change the binary refuses, including in text mode
    for a change cospec cannot read.
  • 12.10: apply relays the binary's failure document when instructions apply
    refuses after the gate clears.
  • 12.11: an unreadable directory no artifact lives in leaves the change's
    answer unchanged.
  • 12.12: rows 15.4, 15.6 and 15.7 are held to what their ledger promises.
  • 12.13: evidence recorded on every group-16 row; docs updated.

Round-4 review fixes (tasks 13.1–13.3)

  • 13.1: text-mode status asks the binary for a cospec-typed change whose
    schema it cannot load (missing, unreadable, unparsable or invalid), singly
    and in the --all sweep, and relays its refusal as --json already did.
    Before, text mode rendered cospec's own table at exit 0 where --json and
    the binary both refused at exit 1.
  • 13.2: text-mode status asks the binary for a cospec-typed change whose
    .openspec.yaml the binary's readChangeMetadata refuses (unreadable, not
    YAML, an unlisted schema, or failing ChangeMetadataSchema), the same way.
  • 13.3: changeMetadataRefused's own listSchemas call (13.2's mechanism) can
    itself fail with an errno — the project openspec/schemas unreadable or not
    a directory, distinct from the per-schema listing 13.1 already guards. That
    escaped binaryDecides uncaught in both the --all sweep's upstream-fetch
    decision and the --change lookup's own decision, neither inside a
    per-change try/catch, crashing the whole command with a bare top-level
    cospec: ENOTDIR … line instead of the per-change refusal --json already
    answered correctly. changeMetadataRefused now catches it like every other
    read failure in that function. Found and fixed in post-round-4 re-review.

Merge stage

  • Rebased onto main (67f20c5d, through #61), 64 commits, zero conflicts;
    bun install --frozen-lockfile unchanged.
  • mise run check green on the rebased tree: unit 1858, contract 2521,
    integration 176, bench 343, release-test 14 — 0 fail. mise run docs:build
    green separately. mise run cospec -- validate cli-surface-parity --strict
    clean.
  • mise run cospec -- archive cli-surface-parity, no --force* flag of any
    kind. The archive left two of its own newly-created capabilities
    (json-document-parity, nested-change-detection) with its generic
    "TBD - created by archiving" ## Purpose placeholder, which fails
    validate --all --strict; both got a real Purpose paragraph in the same
    archive commit.
  • git show --stat on the archive commit (f53fdab3): 20 files, all under
    openspec/ — the 11-file rename into
    openspec/changes/archive/2026-10-05-cli-surface-parity/ and 9 merged/created
    spec files under openspec/specs/.

🤖 Generated with Claude Code

Comment thread apps/cli/test/unit/commands/validate.test.ts Fixed
@replygirl
replygirl force-pushed the worktree-cli-surface-parity branch from 7f9a900 to c70a7c2 Compare September 29, 2026 09:09
replygirl and others added 28 commits October 5, 2026 00:43
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
35 rows fail on this tree and are marked test.failing; each flips in
the task that implements it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Port the binary's nested-change detector into core/change.ts, drop
dot-directories from listChanges, and carry each namespace folder's
nested ids on its list row so the detector matrix row compares them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
resolveNext is the one decision behind the JSON next key and the human
next-step line. Row 5.6 already holds on this tree, so it is a plain
test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One delegated status --json call per invocation is merged into
cospec's documents by identity through core/upstream-keys.ts; root is
the binary's {path, source} object and nextSteps is spelled cospec.
Rows 1.4 and 3.2 stay failing until the namespace-folder entry (4.5)
and the spec-driven entry (4.3) land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A forked, spec-driven, unknown-schema or hand-made change is answered
from the binary's status --json document: a port of its text printer
with cospec's next step, the merged JSON entry, and the binary's exit
code. A directory without .openspec.yaml takes config.yaml's schema.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The override is forwarded to the delegated call and replaces every
change's schema; an unknown name is refused with the binary's message
before the sweep enumerates or the named change is reported.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A namespace folder is refused under --change and carried as a failure
entry in the sweep; an unreadable archive leaves the gate computed from
an empty index with a warning; any other read failure is a change_error
document; a resolver failure under --json is one document with the
binary's payload and, for a raw failure, change_error. Rows 6.2, 6.3,
8.1 and 8.4 are split per command so each part flips with its task.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
list makes one delegated list --json call: the binary's rows set the
order and membership, each keeps cospec's columns by name with the
binary's keys merged in, and --sort name is forwarded. list --specs
carries the delegated root. A resolver failure under --json is one
document with list's payload; row 1.1 flips with the namespace
marking (5.2).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A namespace folder's row reads not a change with state not-a-change and
its nested ids, and the binary's warnings follow the table; the
binary's own failure document is relayed; an unreadable archive lists
with a warning; an unreadable blocking-changes.md fails only its row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--type forces the kind; a name that is both a change and a spec is
refused as ambiguous, one that is neither gets the binary's nearest
matches, a path-shaped forced name is invalid_item and a forced kind
naming nothing on disk is one meta/item-missing ERROR. A bulk flag
beside a name runs the bulk scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The binary's four request refusals are answered before any root is
resolved; findings keeps only the items with issues under the
binary's report object inside cospec's version 1 envelope, with full's
exit code. Row 1.6 flips with the summary keys (6.4).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Change validations run through a pool bounded by --concurrency, else
OPENSPEC_CONCURRENCY, else 6, a value that is not a positive integer
ignored as the binary ignores it; results keep input order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
validate --json carries root, items[].durationMs, summary.totals and
summary.byType for the kinds in scope, keeping version 1 and each
change item's schema as its type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A change is read through one errno-recording reader: an artifact that
cannot be read is a meta/unreadable-artifact ERROR and nothing else runs
or is delegated for the change; a namespace folder is one
meta/nested-change ERROR. Delegated messages and the --archived
fallback relay are spelled through the remedy allowlist, and a raw
resolver failure under --json is validate_error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The archive/target-invalid twin is matched line by line: the fixed head
once, then each defect line on its own with the quoted header as .* up
to its fixed suffix, so a header holding a quote is reported once, by
cospec. The ReDoS guard now runs the pre-fix pattern on a quote-heavy
message with a non-matching tail and bounds both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Row 36.1 archives a delta whose only mis-depth scenario sits inside an
HTML comment through the binary, shows cospec raises no
deltas/scenario-depth for it while the verbatim view would, and is
cited by name from views.ts and views.test.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
__complete gains schemas (from schemas --json, in the binary's order)
and archived-changes (a walk of the resolved root's archive), and a
source name is matched case-insensitively.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every --schema a table row declares and the first positional of schema
which|validate|fork complete from cospec __complete schemas in the
bash, zsh and fish scripts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under --json, no openspec/ root, an unknown change (its suggestion in
the message), a failed legacy delegation, a failed step-5 call and a
resolver failure are each one change_error document on stdout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
commands.md's list, status, validate and __complete rows name the new
flags, keys, the named collision, next, and each BREAKING item;
validation-rules.md adds the three meta rules and the --json and
findings shapes; how-it-relates-to-openspec.md states the native
namespace-folder detection and the --schema override;
docs/architecture.md describes the additive merge, the key oracle and
the detector's home; docs/validation.md states the standing rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
list's rows come from the binary's list --json, which refuses a
directory with no OpenSpec root; the text relay now prints the binary's
Fix: line too, a contract row pins the no-root answer against the
binary, the pack smoke lists after init, and commands.md names the
change among list's BREAKING items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
instructions apply --change routes to apply, whose --json refusal is
now one change_error document; the namespace-folder close-out row flips
to meta/nested-change as its deferred marker said; the ReDoS guard's
bound tightens to 100ms, clear of JSC's backtrack cap on the pre-fix
pattern.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
replygirl and others added 24 commits October 5, 2026 00:43
An unreadable living spec.md is one meta/unreadable-artifact ERROR on
that spec, read nothing and delegated nothing, and every other spec is
reported as it is alone: when one is unreadable each readable spec is
asked of the binary by itself, since its --specs sweep may refuse the
whole run (Bun's realpath on macOS). Discovery lists such a regular
file as unreadable under reportUnreadable, validate only (row 15.9).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodeQL alert #15 (js/redos): validate.test.ts compiled the pre-fix
target-invalid pattern, a repeated group over unbounded quantifiers,
to prove it backtracked. That pattern now survives only as text, and
a star-height guard flags it and the textbook shapes while clearing
both TARGET_INVALID patterns, now exported. The narrowed-span check
runs per line, with no repeated group (task 11.13, row 15.13).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Record observed evidence on verification group 15, re-observe 14.1-14.4
and 11.2, and document the round-2 behavior on the pages that own it:
validate's forced --type spec, archive_unreadable, no_openspec_root and
unreadable living specs, list --specs failure relay, apply's warning,
and the glob port. shared.md notes the fast-glob pin rides the OpenSpec
pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rows 16.1-16.12 in cli-surface.test.ts, each test.failing until its fix
lands, and the key oracle's `kept` class: a key whose value is cospec's
own pre-existing one, compared by presence and type, with its own
self-test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task group 12 and verification group 16: one task and one row per
round-3 review finding, each fix verified by its own row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The per-change delegated call never passed --type change, so a change
sharing a living spec's name drew the binary's ambiguous_item refusal,
which delegate() turned into no items: a false pass. A named spec rode
the whole --specs sweep, which a sibling's mode could refuse. And a
spec's valid ignored --strict.

delegate() now runs under the wrapped-call discipline and returns the
binary's items or its refusal; a refusal is the item's openspec/validate
ERROR. Each change passes --type change, a named spec --type spec, and
specReport applies --strict as the binary's createReport does. Row 15.9
now admits the refusal macOS's realpath makes on every other spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
loadChange read the living spec a delta targets with a bare
readFileSync, so a mode-000 spec threw out of validate <change>, the
whole --all pool and apply, with no --json document. It is now read
through the change's reader: an unreadable one is the change's
meta/unreadable-artifact ERROR on the delta's path, naming the living
file, and nothing is delegated for it. A change whose validation still
throws an errno failure is that change's ERROR in the bulk pool, as the
binary's queue records a failed item.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An unreadable openspec/changes/, openspec/specs/ or capability directory
threw out of validate, status and apply to the top-level handler, which
printed prose even under --json. Each command now answers an errno
failure it lets escape as the binary's failWithError does: one
{status: [{severity, code, message}]} document with validate_error or
change_error, status --all's carrying its {changes: [], root: null}
null-shape. Text mode and every other error keep propagating.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no openspec/ directory every validate invocation answered
no_openspec_root. The binary resolves a named item against its implicit
root instead, where nothing matches it: unknown_item, "Unknown item
'<name>'." in text. A name alone now falls through to item resolution;
the bulk scopes, --archived and a bare validate keep the refusal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
resolveChange took any path that existed, so a regular file under
openspec/changes/ became a typed empty change status reported with exit
0, and refused every name outside the kebab grammar, so status
--change Add_Auth failed (suggesting Add_Auth) where the binary, status
--all and list report it. It now requires a directory and refuses only
what the binary's validateChangeLookupName refuses: a relative path
segment, a separator, a NUL, a leading dot, or `archive`. Kebab-case
stays validate's meta/name-kebab and the slug grammar cospec new
creates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
status relayed the binary's failure messages raw: the single-change and
sweep text lines, the sweep's failure entries, the unknown --schema
refusal, and status[].message under --json, so an allowlisted sentence
such as "Create one with: openspec new change <name>" reached cospec's
output with a bare openspec command. upstreamFailure and the relayed
document now spell every diagnostic's message and fix through the
remedy allowlist, as the tasks.md refusal already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A cospec-typed change with no artifacts emits artifacts: [], and the
additive merge appended every binary artifact object into it, so status
--change e1 --json (and its sweep entry) carried the binary's six
objects without cospec's done/required/ready keys where main printed [].
The binary's entry for an in-progress change now leaves its artifacts
out of the merge. D3 and the key-oracle requirement name the exception;
the oracle compares the key as `kept`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A cospec-typed change counted the binary's refusal only when its
tasks.md was unreadable: otherwise the binary's change_error was merged
into a success entry and status --json, --all --json and the text forms
exited 0 on a change the binary refuses (a mode-000 directory, or on
macOS a mode-000 proposal.md). Any error in the delegated document is
now the answer: its document under --json, `cospec status: <message>`
in text, and a failure entry in the sweep. A change cospec cannot read
every entry of asks the binary in text mode too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the wrapped instructions apply refused a change whose gate had
cleared (a project schema it cannot read, say), runJson's exitCodes [0]
threw a violation naming only the exit code, so apply printed "the
wrapped OpenSpec call ... exited 1 (expected 0)" and dropped the
binary's reason and fix. openspecApplyInstructions now accepts exit 1
with the binary's failure document as an answer, and apply relays it as
list, status and validate --archived relay theirs: the document under
--json, `cospec apply: <message>` and its `Fix:` line in text, each
spelled through the remedy allowlist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChangeReader.files() recorded every unreadable directory under a
change, so a mode-000 .cache/ or specs/.h/ was the change's lone
meta/unreadable-artifact ERROR, suppressing every other rule and
blocking apply, where the binary (which skips dot-entries) passes it.
Only a directory an artifact can live in is recorded now: the change
itself and specs/ outside every dot-directory. Any other could only
ever hold meta/unexpected-file advisories, so the walk passes it by.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Row 15.4 now runs the key oracle on the --all --json sweep, compares the
text sweep's exit with the binary's, and checks no sweep next or
nextSteps names proposal. Row 15.6 holds each text form's exit to its
--json form and, with the archive readable again, its stdout and exit
to the locked run's. Row 15.7 holds bare validate --json to the scoped
forms' whole document. Row 15.3's evidence records the test file's own
counts: 21 tests over 8 project schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The instructions-apply gate row asserted apply's old "unknown change
'1foo'" refusal, which the kebab lookup guard produced. apply now looks
1foo up as the binary does, which reads it, and its gate refuses the
name with meta/name-kebab: the row asserts that answer, and still that
instructions apply --change 1foo answers exactly as apply 1foo does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The validate, status and apply rows of the commands page and the
validation-rules page state each round-3 fact on the page that owns it:
the kind every delegated validation names and a refusal becoming the
item's ERROR, --strict on specs, the unreadable target living spec and
the directories passed by, a named item outside any root, the binary's
change lookup, refusals relayed by status and apply, the errno document,
and an empty change's artifacts: []. Every group-16 row records its
observed evidence on macOS and, for the mode-000 rows, in a non-root
Linux container; rows 14.1, 14.2 and 14.4 are re-observed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text-mode status asked the binary only for a change it could not read, so a
cospec-typed change whose project schema was removed, unreadable, unparsable
or invalid rendered cospec's own table with gate clear and exited 0, while
--json and the binary refused it. binaryDecides now also asks the binary when
loadSchema fails, for --change and the --all sweep, and relays its refusal.
Row 17.1 covers all four breakages in text and --json; docs, design, the
delta spec and both BREAKING lists say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text-mode status sent a cospec-typed change to the binary only when cospec
could not read one of its entries or could not load its schema. A change whose
.openspec.yaml the binary's readChangeMetadata refuses (a malformed created,
an empty goal, a non-boolean skip_specs, a bad initiative, ...) therefore
rendered gate clear and exited 0, while --json and the binary exited 1 with
Invalid metadata. binaryDecides now also asks the binary when
changeMetadataRefused, a port of that read, refuses the file. It applies to
--change and to the --all sweep. Rows 17.3 and 17.4 cover this. The docs, the
design, the delta spec and both BREAKING lists say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
changeMetadataRefused's listed() call (listSchemas) could throw an
unguarded errno (e.g. ENOTDIR when the project's openspec/schemas is
a regular file), escaping binaryDecides uncaught in both status's
--all sweep decision and its --change lookup decision -- crashing the
whole command with a bare top-level message instead of the per-change
refusal --json already answers correctly. Catch the errno and treat
it as refused, like every other read failure in this function.

Tick task 13.3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A fast unit test for changeMetadataRefused's listSchemas() errno catch
(task 13.3), beside the existing contract row 17.5. design.md gains the
paragraph documenting that fix.

Task 1.1: rebased onto main (67f20c5); bun install --frozen-lockfile
and mise run check both green on the rebased tree (unit 1858, contract
2521, integration 176, bench 343, release 14, 0 fail).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tick task 10.2 — performed by the next commit (the archive), verified
by its git show --stat — and record the merge-stage mise run check
observation on row 14.4 (unit 1858, contract 2521, integration 176,
bench 343, release-test 14, 0 fail).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mise run cospec -- archive cli-surface-parity, no --force* flag:
validated, merged specs (+21 ~4 -0, 0 removed), moved to
openspec/changes/archive/2026-10-05-cli-surface-parity/, and wrote a
real Purpose paragraph for the two capabilities the archive created
(json-document-parity, nested-change-detection) in place of its
"TBD - created by archiving" placeholder, which failed validate --strict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@replygirl
replygirl force-pushed the worktree-cli-surface-parity branch from 2c5ac5b to f53fdab Compare October 5, 2026 06:14
@replygirl
replygirl marked this pull request as ready for review October 5, 2026 06:15
Copilot AI balanced review requested due to automatic review settings October 5, 2026 06:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@replygirl
replygirl merged commit 57bc903 into main Oct 5, 2026
12 checks passed
@replygirl
replygirl deleted the worktree-cli-surface-parity branch October 5, 2026 06:40
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