Repository navigation
feat(cli): reach upstream's list, status and validate surfaces - #59
Merged
Merged
Conversation
replygirl
force-pushed
the
worktree-cli-surface-parity
branch
from
September 29, 2026 09:09
7f9a900 to
c70a7c2
Compare
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>
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
force-pushed
the
worktree-cli-surface-parity
branch
from
October 5, 2026 06:14
2c5ac5b to
f53fdab
Compare
replygirl
marked this pull request as ready for review
October 5, 2026 06:15
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 list,cospec statusandcospec validatehad drifted from what thewrapped OpenSpec 1.13.1 binary answers for the same invocation. This PR closes
that gap:
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.tsoverfast-glob, pinned to the version the pinned openspec resolves).nextstep on everystatusentry.list,statusandvalidatedocuments, checked by a contract key oracle.spec-driven, orone that resolves nowhere.
validate's item resolution:--type, the ambiguity and unknown-namerefusals, bulk-flag precedence,
--report full|findingsand--concurrency.schemasandarchived-changescompletion sources.applyearly exit and every--jsonreadfailure.
archive/target-invaliddedupe, which hadregressed to double-reporting a quoted requirement header.
Breaking
cospec listnow orders by most recent change first. Pass--sort nameforthe old order.
cospec listoutside an OpenSpec root answers OpenSpec's ownno_openspec_rootrefusal, exit 1, where it printedNo active changes.cospec validate <name> --all|--changes|--specsvalidates the bulk scope,not the one item.
validatename is refused, and an unknown one prints thebinary's message.
status --changeandstatus --allexit 1 andvalidatefail.validate,applyandarchive(which sharevalidateChange) now report it asmeta/nested-change, notmeta/openspec-yaml— a script grepping the old rule id for this case needsthe new one.
status --jsonon a schema cospec doesn't type exits 1 when the binarydoes.
statustypes a change directory without.openspec.yamlbyconfig.yaml.validate,applyandarchivestill refuse it.rootkey of thestatus --alland no-active-changes documents is anobject, not a path string.
.openspec.yamlOpenSpec refuses, makesstatusexit 1, in text and--json(rounds 4; text mode used to render cospec's own table at exit 0 inboth cases).
Judgment calls
tasks.mdnamed subjects with the scopesstatus,listand
completion. commitlint'sscope-enumdoesn't allow them, so thosecommits use the allowed
cliscope.status --schemaoverrides the schema; it doesn't filter. This was probedagainst the pinned binary: every change is reported as that schema.
validatenames get the binary'snearestMatches(the fivenearest by edit distance), not cospec's own
closest().resolveNext's order follows the verification spec's table. Design'sprose disagreed with it. The order:
cospec apply <id>, once every required one is done (a skippedspecscounts as done);validate,applyandarchivesharevalidateChange, so all three now report a namespace folder asmeta/nested-changeinstead ofmeta/openspec-yaml. Rule ids are documentedas stable public API, so this is in the Breaking list above.
items[].typecollision (ruled by the roadmap):items[].typekeepscospec's meaning, the change's schema. OpenSpec's value is carried as
kind. This is the oracle's one named collision besidesversion, and it isdocumented on the validate page's JSON section.
--store) stays[~] defer. cospec'smessage text is
root-resolution-parity's documented wording, accepted at theround-1 review. The row compares code, target, fix, payload and exit, and
they all match.
fast-globis an exact-pinned devDependency, likeyaml. It is not loadedfrom the wrapped package at run time, because the standalone binary's
embedded openspec bundle has no copy to load.
glob.test.tsfails if anOpenSpec pin bump moves it.
tasks.md, the binary refuses under Bun on macOS butlists the change on Linux. cospec relays whichever answer the binary gives
on that OS.
Round-2 review fixes (tasks 11.1–11.13)
validate <id> --type specon a spec that discovery skips (adot-directory, a linked capability) validates that file, where it used to
print an empty passing report.
generateswith the binary'sglob semantics (braces, ranges, extglobs, negation) through
core/glob.ts.parseSchemaConformanceJsonreturns null for a refused orincomplete validate document.
statustakes a custom schema's status from its own artifacts.validate --archivedrelays the binary's failure document. It no longersays "needs >=1.9.0".
openspec/changes/archive/leavesvalidateandapplyanswering, with an
archive_unreadablewarning.validate --jsonoutside a root prints the oneno_openspec_rootdocument.
list --specsrelays the binary's document, or its message andfix in text.
spec.mdis onemeta/unreadable-artifactERROR,and every other spec is reported as it is alone.
each fact are updated.
tasks.mdis answered as the binary answers it on each OS(CI was red on Linux before this fix).
exponential reference regex; a star-height guard flags it instead.
Round-3 review fixes (tasks 12.1–12.13)
--type change/--type spec); a refusal the binary answers with astatus[]document is theitem's
openspec/validateERROR, never an empty passing report;--strictfails a warning-only spec.
an unreadable one is
meta/unreadable-artifactnaming the file.validate,statusorapply(anunreadable
openspec/changes/,openspec/specs/or capability directory) isone
--jsondocument with the binary's per-command code and payload.validate <name>with noopenspec/directory resolves the name asunknown_item, as the binary does.validateChangeExistslooks itup.
statusrelays from the binary is spelled through theremedy allowlist, in text and under
--json.artifacts: []under--json,singly and in the sweep.
statusrefuses a change the binary refuses, including in text modefor a change cospec cannot read.
applyrelays the binary's failure document wheninstructions applyrefuses after the gate clears.
answer unchanged.
Round-4 review fixes (tasks 13.1–13.3)
statusasks the binary for a cospec-typed change whoseschema it cannot load (missing, unreadable, unparsable or invalid), singly
and in the
--allsweep, and relays its refusal as--jsonalready did.Before, text mode rendered cospec's own table at exit 0 where
--jsonandthe binary both refused at exit 1.
statusasks the binary for a cospec-typed change whose.openspec.yamlthe binary'sreadChangeMetadatarefuses (unreadable, notYAML, an unlisted schema, or failing
ChangeMetadataSchema), the same way.changeMetadataRefused's ownlistSchemascall (13.2's mechanism) canitself fail with an errno — the project
openspec/schemasunreadable or nota directory, distinct from the per-schema listing 13.1 already guards. That
escaped
binaryDecidesuncaught in both the--allsweep's upstream-fetchdecision and the
--changelookup's own decision, neither inside aper-change try/catch, crashing the whole command with a bare top-level
cospec: ENOTDIR …line instead of the per-change refusal--jsonalreadyanswered correctly.
changeMetadataRefusednow catches it like every otherread failure in that function. Found and fixed in post-round-4 re-review.
Merge stage
main(67f20c5d, through#61), 64 commits, zero conflicts;bun install --frozen-lockfileunchanged.mise run checkgreen on the rebased tree: unit 1858, contract 2521,integration 176, bench 343, release-test 14 — 0 fail.
mise run docs:buildgreen separately.
mise run cospec -- validate cli-surface-parity --strictclean.
mise run cospec -- archive cli-surface-parity, no--force*flag of anykind. The archive left two of its own newly-created capabilities
(
json-document-parity,nested-change-detection) with its generic"TBD - created by archiving"
## Purposeplaceholder, which failsvalidate --all --strict; both got a real Purpose paragraph in the samearchive commit.
git show --staton the archive commit (f53fdab3): 20 files, all underopenspec/— the 11-file rename intoopenspec/changes/archive/2026-10-05-cli-surface-parity/and 9 merged/createdspec files under
openspec/specs/.🤖 Generated with Claude Code