Repository navigation
fix(cli): match the binary on untyped list state, unreadable artifacts - #62
Merged
Merged
Conversation
replygirl
added a commit
that referenced
this pull request
Oct 5, 2026
PR #62's ci-bun run passed (ubuntu-latest, 32m4s) -- this repo's CI has no macOS runner, so rows 3.2 and 4.3 are corrected to say so rather than claim a second runner that doesn't exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scaffold the fix-typed cospec change for the four UNFIXED items from list-status-untyped-leftovers-verify.md: list.ts's untyped-schema state misclassification, validate.ts's --archived --json version-floor guard, upstream-spellings.test.ts row 3.7's ordering flake, and a differential pinning test for status's already-correct mode-000-artifact handling (contradicting the verify report's item 2, per end-to-end evidence in design.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
computeRow derived `state` from hasAnyArtifact, which only recognizes cospec's own fixed artifact filenames, never gated on isCospecType — so a custom/forked schema whose artifacts live under other filenames always read "no artifacts yet" even with a written artifact on disk (the same misclassification task 11.5 fixed for status's state/next). A schema cospec doesn't type now additionally checks its own declared schema's generates pattern against the change directory (hasDeclaredArtifact, mirroring core/change.ts's hasSchemaOutput), so it reads `building` once a file that pattern names exists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The --archived version-floor guard wrote to stderr unconditionally, never branching on flags.json like the no-root guard four lines above it — so `validate --archived --json` against an OpenSpec binary below ARCHIVED_SINCE printed no parseable document at all. Factored the refusal into archivedUnsupportedRefusal(version), a pure function unit- testable without a fake binary, and wired it through rootSelectionDocument exactly as every other early-exit refusal in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Row 3.7 asserted byte-identical stdout/stderr between a cospec and an openspec instructions call, each over its own independently-created remedyNamedRoot() copy. The pinned binary's own getAvailableChanges is unsorted and cospec relays it verbatim, so the assertion only holds when both calls observe the same on-disk directory-entry order, which two separate cpSync copies don't guarantee on every filesystem (overlayfs in particular). Call remedyNamedRoot() once and pass the same directory to both calls instead of sorting either side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
status's existing hasUnreadableEntry/binaryDecides routing (task 11.12) already walks every file under a change directory, not just tasks.md, so it already matches the pinned binary's mode-000-artifact behavior on both macOS and Linux (bun realpath differs per OS; cospec's own routing widens to ask the binary either way). Contract row 18.2, landed with list.ts's fix, differentially pins this down; this records the corresponding tasks/verification evidence, contradicting the stage's verify report, which reasoned from a primitive existsSync/accessSync probe rather than the end-to-end command (see design.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vers Full local gate green: 2523/2523 contract tests (0 fail), 1954/1954 unit, 193/193 integration, 343/343 bench, 14/14 e2e release — lint, format, typecheck, generate:check, vendor:openspec:check, cospec-validate-all, agents:check and openspec:schema:validate all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI (ubuntu-latest) failed both new rows, neither locally reproducible: - 18.1's text assertion anchored r-doc's row with a greedy \s+$, relying on it being the last line of the table. list's default order is recency (mtime), which is filesystem-timing-dependent; CI's order put r-doc first instead of last, and the greedy pattern had silently been matching across the newline into the next row. Now finds r-doc's own line and matches it directly, order-independent. - 18.2 predicted up.exitCode from one realpathRefuses() check shared across --change and --all. CI's runner showed --all refusing where --change did not for the same mode-000 proposal.md -- a divergence a local oven/bun:1.3.14 Docker probe could not reproduce, so its exact mechanism is unconfirmed. Rewrote to never predict an exit code: each invocation reads its own measured answer's shape to pick its branch, exactly as the proven 15.11/15.12 tasks.md rows do. design.md records both findings and the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #62's ci-bun run passed (ubuntu-latest, 32m4s) -- this repo's CI has no macOS runner, so rows 3.2 and 4.3 are corrected to say so rather than claim a second runner that doesn't exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
list.ts read only a change's own .openspec.yaml, so a change with none (taking its type from config.yaml's schema:, as status and hasSchemaOutput already do) fell back to an empty schema name and was always reported type '(none)', state 'in-progress', never archive-ready — disagreeing with `cospec status` on the same change. Share one resolver (defaultProjectSchema) across hasSchemaOutput, status's gradedChange and list's computeRow so the three never drift again. hasDeclaredArtifact also swallowed every loadSchema failure alike, so a declared schema that exists but fails to parse/validate silently reported the row as empty with no diagnostic, unlike `cospec status` on the same change. It now distinguishes "no such schema" (no signal, matching upstream) from "schema exists but won't load" (a schema_unreadable warning on the row). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No test drove the command's own flags.json branch for --archived's version-floor refusal: the unit test called the pure helper directly, and the contract/integration rows only ever run against the pinned binary (always >= ARCHIVED_SINCE), so reverting the command's refusal write back to an unconditional stderr line would still pass the full suite. wrappedOpenspecVersion memoizes its result once per process, so a real command-level test needs the version source injectable: run now takes an optional deps.wrappedOpenspecVersion, used only by tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ledger rows 4.1-4.3, design.md and the 18.2 test comment attributed --all's own exit 1 on this fixture to the mode-000 proposal.md lock and cited realpathRefuses, a helper commit a59e9b9 already dropped from this row. Reproduced directly against the pinned binary (macOS and an oven/bun:1.3.14 container as non-root), locked and unlocked: --all exits 1 regardless, because its sweep also walks the fixture's own namespace folder (mobile), which the binary reports as its own change_error ("is not a change") independent of any lock; --change alpha never sweeps mobile at all, and on Linux reads straight past the lock (its realpath needs no read permission there, unlike macOS/Bun's). The row's own exit-code equality assertion was already correct either way, since it never predicted a code; only the narrative explanation was wrong. Added an assertion pinning the mobile change_error down so this can't regress silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ledger row 2.2's recorded observation was only that the test file wouldn't compile against unmodified validate.ts, not a demonstration that the test could catch the bug. Row 2.3 records the actual red/green now available: reverting the command's refusal write to its pre-fix unconditional stderr line fails the new command-level test added in a34862b4, confirmed by hand (reverted, ran red, restored, ran green). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The config.yaml-schema fallback added for a genuinely bare change also caught a namespace folder (no .openspec.yaml of its own, only a nested child one level down): its row got typed by the project's default schema (e.g. 'feat') instead of staying '(none)', even though its state already correctly reports it as 'not-a-change'. cospec status --all avoids this the same way — it discards a namespace folder's gradedChange-resolved schema entirely, reporting a failure entry with no type field. Caught by the existing contract key-oracle row (1.1), which still had mobile expecting type '(none)'; added a faster unit regression alongside it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
replygirl
force-pushed
the
worktree-list-status-untyped-leftovers
branch
from
October 5, 2026 11:21
33fa09e to
9c765a1
Compare
Tick tasks.md's final row now that the archive commit follows this one, and refresh verification row 5.1 with the fresh mise run check counts from a re-run at the merge stage (rebased onto main, four review findings fixed: config.yaml schema fallback, namespace-folder guard, the command-level --archived --json test, 18.2/ledger correction) — 1974 unit tests (was 1954), same contract/integration/ bench/e2e counts, all green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Archived via cospec archive — validated, no hard-gate refusal. Specs: skipped (fix schema has no spec-sync deltas). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
replygirl
marked this pull request as ready for review
October 5, 2026 11:48
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
Four items from
list-status-untyped-leftoversobligations(
.claude/handoff/reports/list-status-untyped-leftovers-verify.md), verifiedUNFIXED-or-inconclusive against
main:list.tsuntyped-schema misclassification (verify item 1, CONFIRMEDUNFIXED):
computeRow'sempty/stateonly checked cospec's own fixedartifact filenames, never a schema cospec doesn't type's own
generatespattern. A change on a custom/forked schema with an artifact under another
filename always read "no artifacts yet". Fixed by
hasDeclaredArtifact,mirroring
core/change.ts'shasSchemaOutput.validate --archived --jsonbelow the version floor (verify item 3,CONFIRMED UNFIXED): the version-floor guard wrote stderr text
unconditionally, never branching on
flags.jsonlike the no-root guardimmediately above it — so
--jsongot no document at all on an oldbinary. Factored into a pure, unit-testable
archivedUnsupportedRefusaland wired through
rootSelectionDocument.upstream-spellings.test.tsrow 3.7 overlayfs flake (verify item 4,INCONCLUSIVE): two independent
remedyNamedRoot()copies assertedbyte-identical, which only holds when both sides observe the same on-disk
entry order. The pinned binary's own
getAvailableChangesis unsorted andcospec relays it verbatim (confirmed by reading the dist and
instructions.ts), so the fix is sharing one root between the two calls,not sorting either side.
status's mode-000-artifact handling (verify item 2, CONFIRMEDUNFIXED by the verify report) — reopened here with further evidence:
the verify report reasoned from a primitive
existsSync/accessSyncprobe, not an end-to-end run.
hasUnreadableEntry(task 11.12) alreadywalks every file under a change directory, not just
tasks.md, sostatus's existing routing already matches the pinned binary on bothmacOS (both refuse, exit 1) and Linux (
oven/bun:1.3.14, non-root UID1000: both read past it, exit 0) — verified directly against the real
binary on both OSes. No product change; a new differential contract row
(
18.2) pins the behavior down instead. Seedesign.md"Context" for thefull evidence trail and why it supersedes the verify report.
Review follow-ups (fixed before merge)
A review round confirmed four further findings, all fixed on this branch
before merge:
a change with no
.openspec.yamlof its own (taking its type fromconfig.yaml'sschema:, asstatusalready does) still reportedtype
(none)/in-progress/never-archive-ready inlist.computeRownowshares
defaultProjectSchemawithstatus'sgradedChangeandhasSchemaOutput, and a namespace folder is explicitly excluded from thatfallback (it stays
(none)/not-a-change, matchingstatus --all).validate --archived --json's new stdout branch: theunit tests only drove the pure
archivedUnsupportedRefusalhelper, andevery contract/integration row runs the real pinned binary (always above
the version floor), so the command's own
flags.jsonbranch was neverexercised — reverting it to an unconditional stderr write would still pass
the full suite.
runnow takes an injectabledeps.wrappedOpenspecVersion(test-only), and a new command-level test drives both the
--jsonand textbranches end to end against a stubbed old version.
--allexit 1 and cited a helper (
realpathRefuses) already dropped from thatrow: reproduced directly against the pinned binary on both OSes —
--allexits 1 regardless of the mode-000 lock because its sweep also walks the
fixture's own namespace folder (
mobile), which the binary reports as itsown
change_errorindependent of any lock. Narrative corrected indesign.md/verification.md; the 18.2 row gained an assertion pinning themobilechange_errordown.hasDeclaredArtifactsilently swallowed everyloadSchemafailure: adeclared schema that exists but fails to parse/validate was counted as "no
artifacts" with no diagnostic. It now distinguishes "no such schema" (no
signal, matching upstream) from "schema exists but won't load" (a
schema_unreadablewarning on the row).Design, tasks and verification ledger (now archived):
openspec/changes/archive/2026-10-05-list-status-untyped-leftovers/.Two of the new contract rows (
18.1,18.2) failed on their first CI runfor reasons that didn't reproduce locally (non-deterministic
listrecencyorder;
--changevs--alldisagreeing on refusal for the same mode-000file on that runner) — both rewritten to stop predicting an environment-
dependent outcome and instead assert order-independently / against each
invocation's own measured answer, matching the proven 15.11/15.12 pattern.
design.md's Risks section records both findings.Test plan
apps/cli/test/unit/commands/commands.test.ts— newlistrows for anuntyped schema's own declared artifact and the config.yaml-fallback/
namespace-folder cases (red before, green after)
apps/cli/test/unit/commands/validate.test.ts— newarchivedUnsupportedRefusalunit tests plus a command-level testdriving the
flags.jsonbranch end to endapps/cli/test/contract/cli-surface.test.ts— new18.1(list stateparity) and
18.2(mode-000 differential pin, now with amobilechange_errorassertion) rowsapps/cli/test/contract/upstream-spellings.test.tsrow 3.7 — sharedroot, run 20x locally with no divergence
mise run docs:buildsucceeds after thecommands.mdeditmise run checkgreen at the merge stage, rebased ontomain(1974unit / 193 integration / 2523 contract / 343 bench / 14 e2e, 0 fail)
mise run cospec -- validate list-status-untyped-leftovers --strictpassed (0 errors, 0 warnings)
ci-bunonubuntu-latest— this repo's CI has no macOSrunner)
mise run cospec -- archive list-status-untyped-leftovers— no flag,no refusal; move verified on disk
🤖 Generated with Claude Code