Commit Graph
24 Commits
Author SHA1 Message Date
Tabish Bidiwale 2500d6da97 fix(view): keep archived changes off the dashboard (#2031)
* fix(view): keep archived changes off the dashboard

openspec view is a one-screen dashboard for a person reading a terminal.
#399 added every archived change to it, so projects with hundreds of
archived changes pushed active work off the screen (#2030). The dashboard
shows current work again; `openspec list --archived` still shows history.

To catch this class of mistake earlier, the cli-view spec now states who
the command serves and that it shows current work only, view.ts says the
same where the code lives, and CONTRIBUTING asks how a human view grows
as a project ages before anything is added to it.

* docs(view): describe archive exclusion without promising a screen height

* docs(view): keep internal rationale out of the user reference

The CLI reference describes what view prints, so it goes back to its
pre-#399 text. The why lives in the cli-view spec Purpose, the code
comment points there, and the CONTRIBUTING rule no longer names a PR.

* revert: drop bug-specific guardrails

The CONTRIBUTING section, the cli-view spec requirement, and the view.ts
comment each restated this one bug instead of guarding the general
mistake. The regression test stays as the guardrail.
2026-10-02 17:34:46 +00:00
ded99e27de feat(cli): show archived changes in list and view (#399)
* feat: show archived changes in list and view commands

Add --archived and --all flags to the list command to display archived changes.
The view dashboard now includes an "Archived Changes" section with a count in
the summary.

- list --archived: shows only archived changes
- list --all: shows both active and archived changes
- view: displays archived changes section in dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(list): reject malformed archive parents on Windows

* fix(list): preserve browsing of archived symbolic links

* fix(list,view): keep archived entries off nested findings and render past a file archive

An archived change that shares a name with an active namespace folder was
reported as nested. view now treats an archive path that is a file as no
archived changes, as it did before, instead of failing mid-dashboard.
The archive symlink test now uses a portable link and checks the link itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 20:23:52 +00:00
Clay Good 56528ea454 feat(cli): report version and update metadata (#2001)
* docs(openspec): propose version reporting command

* docs(openspec): clarify update guidance availability

* feat(cli): report version and update metadata

* fix(cli): harden version install detection
2026-09-29 18:26:04 +00:00
c21d897261 feat(view): display workflow status in active changes (#807)
* feat(view): display workflow status in active changes

Active changes now show workflow artifact status below each entry,
indicating schema name and completion state of each artifact (done✓,
ready→, blocked). Powered by loadChangeContext and formatChangeStatus.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore(release): track dashboard workflow status

* fix(view): neutralize controls in workflow output

* docs(view): move workflow status docs to docs-lab

* docs(view): preserve store in status guidance

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:57 +00:00
47thandClay Good 9557b43aaf fix(init): install integrations with external stores (#1969)
* fix(init): install integrations with external stores

* Canonicalize pointer root and skip config writes in integrations-only mode

* test(init): cover external-store integration choices

* docs(init): document store-only integration setup

* fix(init): preserve pointer-repo planning files

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-29 18:25:36 +00:00
Ryan de Melo d28fb49c1c fix(show): include requirement and scenario names in JSON (#1972)
show --json described each requirement as its SHALL sentence and each
scenario as its bullets. The parser read both headers and dropped them,
so a JSON reader could not name a requirement the way archive matches it.

Requirements and scenarios now carry a name, normalized by the same
helpers archive and the MODIFIED scenario loss check use. The field is
additive and optional in the schema.

Closes #1971
2026-09-29 18:25:33 +00:00
Clay Good e7a9512d7c fix(apply): include task source locations (#1994)
* fix(apply): include task source locations

* fix(apply): verify task locations before updates

* docs(apply): document task source locations
2026-09-29 18:25:18 +00:00
072de6bc39 fix(verify): do not report unverified dimensions as passing (#1732)
* fix(verify): do not report unverified dimensions as passing

Step 5 gates task and spec coverage on `contextFiles.tasks` and
`contextFiles.specs`. `contextFiles` is an artifact-id map and artifact
ids come from the active schema, so on a schema that defines neither, both
branches are no-ops: nothing is checked, no issues are raised, and step 8
concludes "All checks passed. Ready for archive."

The Graceful Degradation guardrail already asks the agent to note skipped
checks, but nothing stopped the all-clear verdict. Mark an unchecked
dimension `Not verified` in the scorecard and require the final assessment
to name it.

* fix(verify): map skipped checks to report outcomes

* fix(verify): retain no-task and skipped-check context

* fix(verify): harden evidence gaps and final assessments

* fix(verify): preserve optional workflows and task artifact fallback

* fix(apply): resolve tracked task globs by schema path

* fix(verify): preserve unavailable task evidence

* fix(verify): distinguish untracked tasks from missing evidence

* docs(apply): document tracked globs and JSON evidence

* test(parity): regenerate hashes after merging #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): restore the #1837 regression tests dropped in the merge

The earlier conflict resolution took our whole side of the parity file,
which discarded the two threshold tests main gained in #1940. Take main's
file verbatim and regenerate the hashes instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1795 and #1926

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(parity): regenerate hashes after merging #1731

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 17:38:11 +00:00
Clay GoodandClaude Opus 5 9827762d2d fix(skills): stop workflows from adopting a project that never ran init (#1787)
* fix(skills): stop workflows from adopting a project that never ran init

Generated skills and commands are installed once per machine and offered in
every repository the agent opens, including ones with no OpenSpec at all.
Nothing stopped the workflow there: root resolution falls back to an implicit
root at the current directory, so `openspec new change` quietly creates
`openspec/` in whatever repo the agent happened to be standing in (#1645).

Two changes, both in the generated instructions:

- Every workflow now carries a shared project check. Before the first step
  that writes, the agent reads `root.source` from `openspec status --json`;
  `implicit` (or a `No OpenSpec root found` error) means the project is not
  set up, and the agent stops and asks the user whether to run `openspec
  init`, target a store, or drop OpenSpec for that request. It may not
  initialize the project on its own or let a command create the root as a
  side effect.
- Every deployed skill description now names OpenSpec. Hosts pick skills by
  description, and "Enter explore mode - a thinking partner..." reads as a
  generic offer in a repository that has never heard of OpenSpec.

Closes #1645

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): note the uninitialized-project guard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): let onboarding run init once the user asks for it

The guard read as an absolute ban on `openspec init`, which contradicts the
option it offers one sentence earlier and the onboard workflow's job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(troubleshooting): explain an OpenSpec workflow starting in an unset-up project

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): check the root with a command that never fabricates one

`openspec status --json` demands --change once a project has changes, so the
guard's own check could fail in exactly the projects it should wave through.
`openspec list --json` answers in one shape everywhere: a root object when the
project is set up, `root: null` both when nothing is set up and when only
stores are registered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(skills): pin the guard against every write, not the first fence

CodeRabbit's point: checking only the first ```bash fence would miss a write
outside a fence. Assert instead that nothing preceding the guard runs a
command or writes, and that the guard sits directly under the store-selection
guidance - both fail when the guard is moved down a workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(new-change): say when the command had to create the root itself

The generated workflows now check for a root before writing, but the guard is
instructions - an agent that ignores it, or a human running the CLI directly,
still turned an unset-up directory into an OpenSpec project without a word.
Creating the root stays zero-config; it is no longer silent.

Human output only: --json is unchanged, and `root.source` already carried the
same fact for programmatic callers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): tell agents the root check's non-zero exit is the answer

`openspec list --json` exits 1 when there is no root. An agent that reads that
as a broken CLI is one step from hand-creating `openspec/` instead, which is
the failure the guard exists to prevent.

Also drops a vacuous assertion: the notice test now checks that the note names
the directory it created and that the change really landed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(skills): read the guard back and untangle its two 'in that case' clauses

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): carry the store flag into the root check; pin the notice path exactly

CodeRabbit, both valid:
- With a store selected the store IS the root, so the check has to run as
  `openspec list --json --store <id>`. The store-selection paragraph above
  already says to append the flag to every command it lists, but leaving it
  implicit here invited a check against the wrong directory.
- The notice assertion matched any `openspec/` suffix. It now pins the exact
  rendered path, and a new case runs the command from a subdirectory to show
  the note names the directory actually adopted (and that the repo above it
  is left alone).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the no-root contract to the canonical docs-lab pages

alfred-openspec on #1787: docs-lab/README.md makes docs-lab/ canonical and the
old docs/ tree legacy, and the canonical pages were stale in the two places the
review named.

- docs-lab/reference/cli.md, 'openspec new': documents the implicit-root notice
  after the 'Next:' line, with the exact output the CLI prints, that it goes to
  stdout and never appears with --json, and that JSON carries the same fact as
  root.source: implicit. Verified against a real run in an empty directory with
  an isolated HOME.
- docs-lab/reference/skills.md: states the shared response and stop behavior
  once, above the index table, since it now holds for every skill: confirm the
  resolved root before the first write, stop when there is none, offer init, a
  store, or dropping OpenSpec, wait for the answer, never create openspec/ on
  its own.

Drops the legacy docs/troubleshooting.md addition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): make the no-root answer depend on how the workflow was reached

alfred-openspec's product call on #1787. One answer could not serve both
arrivals: #1645 asks the workflow to get out of the way ('it can go through the
normal general propose not the openspec'), while a user who typed the skill's
name is owed an answer about OpenSpec.

The guard now branches after the same `openspec list --json` check:

- Auto-selected: the model picked this workflow without the user naming
  OpenSpec, naming the skill, or running its command. Drop OpenSpec and answer
  the request normally, with no setup question and no mention of OpenSpec.
- Explicit OpenSpec request: stop before writing and ask whether to run
  `openspec init`, target a store, or continue without OpenSpec, then wait.

Neither branch may create the root as a side effect, stated once for both.

One text serves both surfaces rather than a command-only variant, because
apply-change and onboard render a single body into the skill and the command
alike; a command-only constant would mean threading a surface flag through
bodies that deliberately have none (#1515). The bullets scope themselves
instead, and a slash command is an explicit invocation, so only the ask branch
can apply there. A test pins that branch reaching every generated opsx command.

Four regressions: the auto-selected branch (asserting it does not mention
`openspec init` or `--store`), the explicit branch, the explicit branch's
presence in every command file, and the shared no-side-effect rule.

docs-lab/reference/skills.md said every no-root invocation asks. It now carries
the same two branches as scan anchors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): keep the root guard off store-only projects and fix its docs

A store-only project whose `store:` line names a store this machine has
not registered reports `"root": null` from `openspec list --json`, so the
guard read a real OpenSpec project as uninitialized. The guard now checks
for the `Declared in` status message first and shows the store error
instead. A stale global defaultStore reports the same codes in unrelated
repositories, which is why the message prefix, not the code, decides.

Propose's context step from #1657 offered `openspec init` on
`no_openspec_root` regardless of how the workflow was reached. It now
defers to the project check, so an auto-selected propose skill stays
silent.

The changeset names both no-root branches, and the `new change --json`
docs separate the initialized example from the verified `implicit` one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(skills): keep the root guard off projects with a malformed store line

A config-only project whose `store:` line is malformed reports
`"root": null` with an `Invalid store declaration in` message, not
`Declared in`, so the project check read it as never initialized and
would drop OpenSpec or offer `openspec init` there. The guard now names
both prefixes, and a root-selection test pins that every declaration
failure starts with one of them while a stale global defaultStore
starts with neither.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(skills): check guard ordering in the skill body, not its frontmatter

A skill's YAML frontmatter is metadata a host reads to choose the skill,
not instructions the agent runs, so a description that quotes a command
name must not trip the ordering check. Scope the scan to the text after
the closing frontmatter delimiter; a command injected into the body ahead
of the guard still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:05:28 +00:00
605d9e7a2b fix(config): leave an unparseable global config untouched (#1876)
* fix(config): leave an unparseable global config untouched

A typo in config.json made getGlobalConfig() fall back to defaults, which telemetry read as consent: any command, even a read-only list, minted a new anonymous id and wrote it over the whole file, dropping a telemetry.enabled false opt-out and every other setting. config set, unset and profile likewise saved the defaults over it.

saveGlobalConfig() and telemetry's writeConfig() now refuse to overwrite a file they cannot parse, telemetry and the update check treat such a file as opted out, and config set, unset and profile exit with an error pointing to config edit. config reset --all can still replace the file, and the existing warning is unchanged.

* fix(config): treat a non-object global config as unreadable

Valid JSON that is not an object (null, an array, a string) also makes
getGlobalConfig() fall back to defaults, silently, so `config set` still
saved those defaults over the user's file. isGlobalConfigUnreadable() now
reports such a file as unreadable, which keeps telemetry off and routes
every save through the same refusal as a parse failure. This matches how
completion-tip already treats a non-object config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(config): document the refusal to rewrite an unparseable config

docs-lab/reference/cli.md said `config unset` always exits 0. With an
unparseable global config, `config set`, `config unset` and
`config profile` now exit 1 and leave the file unchanged; say so, show
the message and the two fixes, and note telemetry and the update check
stay off until it is fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): warn about an unparseable global config once per command

Telemetry, the update check and the command each read the global config,
and now that none of them rewrites the broken file, the "Invalid JSON"
warning printed two or three times per command. Warn once per path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(migration): skip profile migration for a config that is not a JSON object

A global config holding [] reached saveGlobalConfig, which now refuses
it, so init and update failed. null already crashed on a property read.
migrateIfNeeded now skips such a file, as it does for a parse failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(telemetry): refuse to write over a non-object global config

The telemetry writer had its own notion of an unreadable config: only a
JSON parse failure counted. Valid JSON that is not an object slipped
through, so updateTelemetryConfig() merged into it and replaced the file
-- an array, a number or a boolean became a bare telemetry object, a
string spread into numeric character keys, and null threw a TypeError
instead of the actionable refusal every other writer reports.

Funnel both notions through one predicate: isConfigRootObject() in
core/global-config.ts now backs isGlobalConfigUnreadable() and the
telemetry reader, so every shape the global guard rejects is classified
invalid on read and refused on write. Both writers report the same
one-line message via unreadableGlobalConfigMessage().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): read a non-object global config as plain defaults

getGlobalConfig() spread the parsed root into its result before the
unreadable predicate was consulted, so the shape of the root leaked to
every caller: a config of "abc" returned defaults plus the numeric
character keys 0, 1 and 2. Check isConfigRootObject() right after
parsing and answer with plain defaults, as for a file that did not parse
at all.

Reported by CodeRabbit as an outside-the-diff finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(config): stop `config list` crashing on a null config root

`config list` re-reads the raw file to mark each value explicit or
default, and assigned JSON.parse() straight to rawConfig. A root of
`null` then crashed the command with a TypeError stack trace, the one
failure mode this PR is meant to remove, and it did so on a read-only
command. Normalize a non-object root to {} through the shared
isConfigRootObject() predicate so the listing shows plain defaults.

Reported by CodeRabbit as an outside-the-diff finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(config): show the telemetry notice before the first-run write check

Since #1835, nothing is tracked until the notice has been shown, so the
first-run test must show it before tracking the command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:24:40 +00:00
7090e16d74 fix(schema): validate the apply block against declared artifacts (#1868)
* fix(schema): validate the apply block against declared artifacts

parseSchema checked each artifact's requires but never the apply block, so schema validate passed a schema whose apply.requires named an artifact that does not exist or whose apply.tracks named a file no artifact generates. At run time apply skipped the unknown id, turning the apply gate off, or blocked forever on a tracked file nothing produces.

Reject both in parseSchema, naming the bad value and what the schema declares. tracks is compared to generates by exact string, the same comparison the tracked-tasks lookups use, so any schema that parses is one they can resolve.

* fix(schema): warn instead of failing the load on an unmatched apply.tracks

apply.tracks is a path that apply reads as written, not an artifact id.
A schema that tracks a hand-written TODO.md, or one file under a glob
generates such as tasks/main.md, loads and applies correctly on main.
Rejecting it in parseSchema made every command on that schema fail.

Keep the unknown apply.requires id as a load error, the same as an
unknown artifact requires. Report an apply.tracks path that matches no
artifact's generates as a warning from `openspec schema validate`,
which still exits 0.

Move the docs note from legacy docs/customization.md into docs-lab
(schema-yaml.md validation section, cli.md schema validate). The
schema-yaml.md table had said unknown apply.requires IDs go unreported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(schema): describe apply.tracks as a generates mismatch, not as ungenerated

The apply.tracks check compares `tracks` to each artifact's `generates` with
exact string equality, but its warning said the tracked file "is not generated
by any artifact". That is false for the case this PR deliberately supports:
`tracks: tasks/main.md` under `generates: tasks/*.md`, where the glob really
does generate the file and only the strings differ.

The diagnostic now names the real condition (the `tracks` value does not
exactly match any `generates` value, so OpenSpec cannot tell which artifact's
progress it tracks) and keeps both remedies. Both docs-lab pages, the changeset
and the JSDoc that repeated the claim are corrected the same way, and a new
CLI test pins that the glob case is described as a mismatch and never as
ungenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(schema): pin apply.tracks matching across Windows path separators

The tracked-tasks lookup compares apply.tracks and generates as plain
strings, so a backslash on one side and a forward slash on the other must
warn, and the same backslash spelling on both sides must not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:11:23 +00:00
2ef6fbde3d fix(config): run an EDITOR that carries arguments (#1878)
* fix(config): run an EDITOR that carries arguments

config edit passed the whole EDITOR/VISUAL value to spawn as the program
name with shell: false, so common settings such as `code --wait` failed
with ENOENT, and the uncaught rejection printed a raw Node stack trace.

Run the value the way git does: through sh -c '<editor> "$@"' with the
config path as a positional argument, and through cmd.exe on Windows so
.cmd shims resolve. A value that is itself the absolute path of an
existing file still runs directly, so unquoted paths with spaces keep
working. A failed start, a non-zero exit or a signal is now reported as
a one-line error and the command exits 1.

* fix(config): split EDITOR into argv instead of running a shell

Run the editor without a shell. The EDITOR or VISUAL value is split into a
program and arguments (double quotes everywhere; single quotes and backslash
escapes on POSIX; literal backslashes on Windows) and the config path is
appended as its own argument, spawned via cross-spawn with shell: false so
Windows .cmd shims such as code.cmd still resolve. Shell metacharacters in
the value are now inert.

Show the install hint only when the program is missing (ENOENT), not for
EACCES or EPERM. Move the EDITOR documentation from legacy docs/cli.md to
docs-lab/reference/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:38 +00:00
9f8dec5dd9 fix(store): refuse to remove a store that contains another store (#1880)
* fix(store): refuse to remove a store that contains another store

store remove deleted the target folder recursively after checking only
the target's own metadata. Another registered store living inside that
folder, such as a shared store vendored as a git submodule (a layout
store register accepts), was deleted with it, uncommitted work
included, and its registry entry was left pointing at a missing path.

Refuse with store_remove_contains_registered_store when another
registration's canonical root is inside the folder. The check runs in
the beforeCommit hook, under the registry lock that commits the
removal, which now receives the registrations that will remain.

* docs(store): move nested-store remove refusal to docs-lab

Document store_remove_contains_registered_store on the canonical docs-lab CLI reference instead of legacy docs/cli.md. docs/agent-contract.md keeps the error-code entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:36 +00:00
5d221456e5 fix(store): let setup --no-init-git run inside a git repository (#1884)
* fix(store): let setup --no-init-git run inside a git repository

The nested-repo guard refused a setup path inside another Git
repository even with --no-init-git, which creates no repository, so a
store at ~/openspec/<id> was impossible for anyone whose home directory
is a dotfiles repo. The CLI never passed the existing bypass either:
prepareSetupInput ignored its options.

Skip the guard when initGit is false, and forward --init-git and
--no-init-git into the prepare step. The default setup and an explicit
--init-git still refuse. Two store.test.ts cases passed --no-init-git
while asserting the refusal; they now run the default setup the guard
exists for.

* docs(store): move setup nested-repo note to docs-lab

The setup --no-init-git explanation belongs on the canonical docs-lab CLI reference, not legacy docs/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(store): compare canonical paths in setup --no-init-git test

Canonicalize payload.store.root and the registry local_path with fs.realpathSync.native before comparing, per review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:31 +00:00
Clay GoodandClaude Opus 5 3b8e5b6616 fix(nix): install shell completions with the flake package (#1785)
* fix(nix): install shell completions with the flake package

The flake exposed `openspec completion generate SHELL` but installed no
completion scripts, so a Nix install had no completions at the standard
locations. Generate the Bash, Fish, and Zsh scripts during postInstall and
place them with installShellCompletion.

Closes #1740

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: note that cross-compiled Nix builds omit completions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: run the Nix job when the completion generator changes

The Nix build now runs `openspec completion generate`, so a change under
src/core/completions can break packaging without touching flake.nix.

Also drops the cross-compilation caveat from the Nix install docs: every
package this flake exposes is native (`legacyPackages.<system>` has
buildPlatform == hostPlatform), so `canExecute` is always true and the
completions are never omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the Nix completions note to the canonical pages

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the fact is recorded in the two docs-lab pages that own it and docs/cli.md and
docs/installation.md are back to their state on main.

- docs-lab/start/installation.md, Nix: the package ships the scripts at the
  standard locations, so completion install is not needed.
- docs-lab/reference/cli.md, openspec completion: the same exception, stated
  where a reader looking up the command will hit it, linking to the Nix
  section rather than repeating the paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:11 +00:00
Clay GoodandClaude Opus 5 8b99c07bd0 fix(status): name the command that resumes a change (#1786)
* fix(status): name the command that resumes a change

`openspec status` printed the artifact checklist and stopped. The command
that moves the change forward was computed already and shipped in the JSON
`nextSteps` sentence, but the text surface never rendered it, so anyone
resuming a change had to know the next command by heart.

Extract `resolveNextStep` so the command and the published sentence come
from one place, and print it as a `Next:` line — matching the idiom
`openspec new change` already uses to hand off to `openspec status`.

The completion case matters most: "All planning artifacts complete!" reads
as "you are done" even while implementation tasks remain, and it is now
followed by the `openspec instructions apply` command that resumes the work.

Closes #906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(status): keep the loadStatus comment on loadStatus

The store-flag note landed between the "single definition" comment and the
declaration it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): cover the next-step line, and record it in the spec

The behavior change shipped without the OpenSpec change this repo requires
of user-facing work, and without coverage for the paths a reviewer would
reasonably ask about.

Adds `add-status-next-step`, whose delta modifies `Next Artifact Discovery`
in `cli-artifact-workflow` - the requirement that already says status is how
you learn what comes next. It now also says status names the command.

Coverage added:
- Unit tests for `resolveNextStep`, pinning the published `nextSteps`
  sentences verbatim. Confirmed byte-identical to main's output, so the
  split into command + sentence provably did not reword the contract.
- Custom-schema case: the line is built from the resolved artifact id, so a
  project with neither proposal/specs/design/tasks still gets a usable
  command.
- Skipped artifacts are never named - they satisfy dependents but must not
  be created.
- `--json` stays parseable and carries no `Next:` line.
- `--all` gives every change its own line, and a failed entry none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): assert the next-step line closes the output

The spec delta says the text output ends with the `Next:` line, but the
assertions used toContain, which a later line would still satisfy. Compare
the last non-empty line instead, across all four ready/complete cases and
the skipped one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): read the closing line CRLF-safely

Split on /\r?\n/ so a CRLF stream cannot leave a carriage return attached
to the line under comparison, and route the parity check through the same
helper instead of its own scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: move the status Next: line to the canonical CLI page

docs-lab/README.md makes docs-lab/ canonical and the old docs/ tree legacy, so
the entry now lives under 'openspec status' in docs-lab/reference/cli.md and
docs/cli.md is back to its state on main.

Both documented outputs were captured from real runs rather than written by
hand: the blocked case in a change with proposal and specs, and the complete
case with all four artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(status): drop em dashes from the changeset and proposal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:08 +00:00
Clay GoodandClaude Opus 5 09a999bbb2 fix(changes): report a change nested in a namespace folder (#1849)
* fix(changes): report a change nested in a namespace folder

Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so
laying changes out the same way looks reasonable - but a change is only ever a
directory directly under `changes/`. `changes/mobile/refresh-token/` left the
real change invisible to every command while `mobile`, the folder around it,
was reported as an ordinary task-less change. Nothing said so, and
`openspec archive mobile` moved the unfinished change into the archive under
the namespace's name with none of its deltas applied.

A directory under `changes/` is now recognised as a namespace folder when it
carries no change-root artifact of its own and wraps a directory that does.
The probe is deliberately conservative: a miss behaves exactly as before, and
an ordinary change - including a scaffolded one with no artifacts yet, and one
whose only content is a root delta spec - is never reported as a folder.

- `openspec list` marks it `not a change` and explains the flat-only layout
  instead of printing `No tasks`; `--json` gains an additive `warnings` entry.
- `openspec show` and `openspec status --change` say the same rather than
  reporting a change that has no proposal yet.
- `openspec validate` reports the nesting instead of "must have at least one
  delta", with next steps that name the fix rather than delta authoring.
- `openspec archive` refuses it outright - burying an active change is data
  loss, not something to warn about.

Closes #1846.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(agent-contract): document the list --json nesting warning

Agents read 4.1 as the shape contract, so the additive `warnings` array and
per-entry `nested` field have to appear there, along with the instruction not
to treat a flagged entry as a change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(changes): harden the nested-change probe and cover status --all

Adversarial review of the first commit found three holes.

A custom schema may generate every root artifact into a subdirectory
(`generates: rfc/proposal.md`). Created by hand, such a change carries no
`.openspec.yaml`, so the marker probe read it as a namespace folder wrapping a
change called `rfc` - a legitimate change that validated clean before, now
refused by validate, status, instructions and archive, with no flag to get
past it.

The same probe missed the case it was written for whenever the nested change
started from its delta specs rather than a proposal: `mobile/refresh-token/`
holding only `specs/auth/spec.md` still archived as `mobile`, deltas dropped.
A nested change is always hand-made - `openspec new change` rejects a name with
a separator - so "mkdir the tree, write the deltas first" is a common way to
arrive here.

Both come from asking one question. A directory is now recognised as a change
by a root artifact OR a populated `specs/`, and a directory holding any file of
its own is never a namespace folder. The `specs/**/*.md` shape is fixed by the
delta format rather than by the schema, which is what makes it safe to lean on.

`change show archive` also reached the probe - it has no reserved-name guard -
and offered to rename every dated archive entry into an active change. The
reserved name is rejected in the probe itself, so no caller can repeat it.

`status --all` read each directory straight through `loadStatus`, bypassing the
lookup guard, and printed a whole artifact plan for work that is not there. It
now carries the same per-change diagnostic a malformed change does, and the
sweep continues past it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(changeset): state the nesting-detection bound

CodeRabbit asked for the three-level limit in the release note. Stated as a
closing sentence rather than a qualifier on the headline, so the note still
leads with what changed for users.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(changes): trust the resolved schema before calling a folder a namespace

A hand-made change under a custom schema that generates into a subdirectory
(rfc/proposal.md), with no .openspec.yaml and no delta specs yet, was read as a
namespace folder and refused by status, show, validate, instructions and
archive. The probe now also counts a file at any path the resolved schema
generates, the same check status uses to mark an artifact done.

Move the flat-change-folder docs from legacy docs/ to docs-lab reference/cli.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:06 +00:00
Clay GoodandClaude Opus 5 09984b8242 fix(validate): warn when tracked tasks have no checkboxes (#1774)
* fix(validate): warn when tracked tasks have no checkboxes

Progress counts checkboxes and nothing else, so a tasks.md written as
plain bullets or a numbered list is worse than an empty one: `openspec
list` and `openspec status` report "No tasks", and `openspec archive`
has no incomplete task to warn about. The file reads as finished to the
tool and unfinished to a human.

`openspec validate` now warns when every task file the change's schema
tracks contains list items but not one checkbox, pointing at the first
offending line. Reported per change, not per file, so a checklist
alongside a prose file stays silent, and only files an artifact actually
declares are linted - a bare tasks.md no schema tracks is left alone.

Closes #354

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): honor fence delimiter length and width

CommonMark closes a fence only on the same character, a run at least as
long as the opener's, and no info string. Comparing the first character
alone let an inner ``` end an outer ```` block, exposing the bullets of
a nested code sample as a task list. The delimiter pattern also loses
its end anchor: `.` does not match `\r`, so an anchored info-string
group matched nothing in a CRLF file and blinded the scan to fences.

Adds the nested-fence, annotated-closer, tilde/backtick, longer-closer
and CRLF cases, plus an e2e change whose nested task files are all
bullets, asserting both reported paths stay POSIX-separated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): scan rendered content and complete evidence only

Hardening pass over the checkbox warning.

The scan for the offending line now skips YAML front matter and HTML
comment blocks alongside fenced code. A list under `tags:` is metadata
about the file rather than the work it tracks, and a commented-out list
is not work either; each exclusion can only silence a warning, never
drop a real task, which is the opposite trade from the task parser. An
unterminated `---` opener rewinds to the top, because that is a thematic
break and everything below it is still content. Only a comment opening
its own line hides that line, so the template's `## 1. <!-- Task Group
Name -->` heading cannot swallow the checklist beneath it.

A tracked file that exists but cannot be read now withdraws the warning
entirely: "no file here holds a checkbox" is a claim about the whole
tracked set, and the checkboxes may be in exactly the file that would
not open. `validate --archived` stays the surface that reports an
unreadable task file loudly (#205).

The message leads with the consequence rather than an accusation, since
a file may legitimately carry a bulleted note and no tasks yet.

New coverage: every packaged tasks template is asserted checkbox-shaped
(the guard fails if a template loses its boxes), a schema tracking tasks
by artifact id with no `apply` block, the deprecated `change validate`
text output, and an unreadable tracked file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): match CommonMark on fence indent and front matter

Two block-scanning defects, both of which hid list items.

A fence indented four spaces is an indented code block, not an opener.
Accepting it left the scan inside a block that never began, so every
list below it went unseen. Fence recognition now stops at three spaces.

`----` is a thematic break, not a YAML front-matter delimiter. Matching
three-or-more dashes let one open a block that swallowed the list under
it until the next `---`. Front matter is now exactly three dashes.

Two test defects alongside them. The deprecated-command test claimed to
assert the reported line, but the text renderer prints no line for any
issue; it now asserts the level and path prefix that surface actually
emits, with the line left to the JSON assertion that already covers it.
The unreadable-file fixture would have passed for the wrong reason had
the mode not taken, since the checkbox it hides would have silenced the
warning by itself; the read failure is now asserted first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): name task files relative to a canonical change dir

Windows CI caught the report naming a task file
`../../../../../../../../runneradmin/AppData/.../tasks.md` instead of
`tasks.md`. `resolveArtifactOutputs` hands back real paths while
`changeDir` carries whatever spelling the caller resolved, and a short
8.3 alias against its expanded form is a difference in spelling, not in
location, so the relative path escaped the change. A symlinked project
directory reproduces it off Windows.

Canonicalizing both sides recovers the relationship. A path that still
escapes falls back to the file name, so no report can leak an absolute
filesystem path. Numbering issues are named through the same helper and
gain the same fix.

The deprecated-command test now asserts the `[WARNING] tasks.md:` prefix
that exposed this, and the Windows job is its regression guard: the
mismatch cannot be staged on POSIX, where the spawned CLI's cwd is
already physical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): keep indented code out of the uncheckboxed-task scan

alfred-openspec on #1774: the scan said it reads rendered content, but
LIST_ITEM began with \s* and so reported top-level four-space-indented code
such as '    - example output' as an uncheckboxed task list. Under --strict
that false positive failed validation on a correct file.

A list-shaped line is now taken only below four visual columns of indent, the
same cut the fence logic already applies, with tabs counting as four. Genuine
nested lists are untouched: this scan reports the first list item it finds and
a nested item always sits under a shallower parent, so the parent is reported
exactly as before. A list-shaped line four columns deep with nothing shallower
above it is not nested under anything, which is what makes it code.

Regressions cover space-indented, tab-indented and numbered code samples, and
pin both the nested-list case (parent still reported) and three-space indent
(not code). Verified the guard bites: removing the column test fails them.

Also moves the documentation to its canonical home. docs-lab/README.md says the
old docs/ tree is legacy and must stay untouched, so the docs/concepts.md line
is dropped and the warning is documented under 'openspec validate' in
docs-lab/reference/cli.md, beside the archive merge findings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(validate): skip BOM-prefixed front matter and cap ordered markers at nine digits

A prose-only task file that opened with a UTF-8 BOM before its front
matter was warned about, because the opener never matched and the
tags list was scanned. A number longer than nine digits followed by a
period also matched as a list item, which CommonMark does not allow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changeset): drop the em dash

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(tasks): use a multi-character marker for the unrecognised-checkbox case

A single-character marker such as `[~]` becomes a task once #1773 lands,
which would flip this expectation. `[ab]` is not a task under either
parser, so the test keeps asserting that checkbox-looking list items that
count as no task still warn, whichever PR merges first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:47:04 +00:00
Marzx13andClay Good db03c6c4b0 feat(validate): add findings-only bulk reports (#1713)
* feat(validate): propose findings report

* docs(validate): clarify findings report contract

* feat(validate): implement and harden bulk findings reports

* test(validate): canonicalize store paths natively

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 21:07:46 +00:00
Ryan de MeloandClay Good a4fcdbece6 feat(validate): report the deltas archive would refuse (#1710)
* feat(validate): report the deltas archive would refuse

validate checked a change's deltas against themselves and, for MODIFIED
blocks, against the main spec's scenarios. It never checked whether the
main spec can supply the target a delta acts on, so a MODIFIED naming a
requirement that is not there, a RENAMED whose source is gone, or an
ADDED whose name already exists all validated clean and failed at
archive instead - typically weeks later, after the implementing PR had
shipped and the authoring session was gone.

Run the merge archive runs and report what it refuses. buildUpdatedSpec
returns the rebuilt content without writing it, so the preflight is the
same function on the same inputs with the result discarded, and cannot
disagree with the code that does the writing. That matters here: several
of those preconditions deliberately read a missing target as
already-synced rather than as a failure, and a second copy of the rules
would be free to drift.

Reported as INFO so no verdict changes in any mode. A MODIFIED whose
target is missing is also what a change modifying a sibling's unarchived
requirement looks like, and validate stays valid for that case today;
telling the two apart needs the opt-in marker #1112 asks for. What is
missing until then is the information, not the verdict.

Refs #1112

* fix(validate): skip preflight for deltas whose errors come after the loop

missingHeaderSpecs and emptySectionSpecs are collected inside the
per-spec loop but only become issues after it, so a suppression set
built from the issues raised so far could not see them. A headerless or
empty-section delta has nothing for the merge to apply, so the preflight
reported that as a blocker of its own, on top of the error that names
the actual mistake.

* fix(validate): harden archive preflight diagnostics

* fix(validate): preserve reports when archive preflight cannot start

---------

Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 20:59:09 +00:00
2fa679f180 fix(schema): make schema init --default actually set the default (#1709)
* fix(schema): make schema init --default actually set the default

--default wrote defaultSchema to openspec/config.yaml, but the config
loader only reads schema, so new changes kept using spec-driven while
the command reported success. Write the key that is read, and drop the
dead one a previous run may have left behind.

Fixes #1708

* fix(schema): preserve supported configs when setting default

* docs(schema): specify default config file handling

* docs(schema): protect default config migration

* fix(schema): make default initialization atomic

* fix(schema): hide init staging and backup dirs from discovery

`schema init` stages into `.init-staging-<rand>` and moves an existing
schema aside to `<name>.init-backup-<pid>-<ts>`, both inside the schemas
dir. `schema fork` already did this and the resolver filters its temp
names out of discovery; the init names were never added, so `listSchemas`
and `listSchemasWithInfo` surfaced them as real schemas -- in shell
completions, "available schemas" error lists, and change-metadata
validation.

The backup is the durable case: cleanup failure is deliberately tolerated
with a warning, so a blocked cleanup (or a crash mid-transaction) leaves a
permanent phantom schema behind.

Generalize the fork-only filter to cover both commands' staging and backup
names. Real schema names are kebab-case, so excluding these dot-bearing
names can never hide a legitimate schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 20:42:30 +00:00
dd7cea3ffe feat(show): diff delta requirements against the main specs (#980)
* Proposed feature: openspec show --diff to see changed requirements more clearly

* Implementation of the --diff feature, which led to some spec changes during implementation.

* Update the proposal to be clearer. Thanks coderabbit

* Fix a bug identified by coderabbit with excessive trimming, and add a testcase for it.

* fix(show): harden --diff for review feedback

Keeps `openspec show <change>` without `--diff` a raw proposal
passthrough, reports when a change has no delta specs instead of
returning silently, preserves the Reason/Migration body of a REMOVED
requirement, and resolves main specs through the command's root so
`--store <id>` diffs against that store.

Text mode and JSON mode now render from one shared collection pass, the
CLI tests drive argv arrays from a mkdtemp project instead of
interpolated shell strings, `--diff` is registered for shell
completions, and the stray package-lock.json is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* build(deps): declare the diff dependency and refresh the flake hash

Adds the `diff` runtime dependency that requirement-diff.ts imports,
updates pnpm-lock.yaml, and regenerates the flake's pnpmDeps hash so
`nix build` matches the new lockfile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(show): diff nested capabilities too

collectSpecDiffs enumerated only top-level directories under the
change's specs/, so a nested capability (specs/<area>/<id>/spec.md) was
skipped: text mode printed nothing for it and its MODIFIED deltas came
back from --json with no diff. It now uses the same discoverSpecFiles()
helper ChangeParser uses, so the capability ids match the `spec` field
of the JSON deltas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(show): report header mismatches instead of hiding them

Two cases where --diff quietly showed something misleading:

A MODIFIED requirement whose capability has no main spec was rendered as
all-additions, which reads like a new capability. It is an authoring
error archive will reject, so it now prints the raw text with a warning
naming the missing spec.

A header that differs from the main spec only in case or interior
spacing found no match at all under exact lookup, or matched under a
lowercase-only comparison that let a real mismatch through silently.
Lookup is now exact first, then the shared foldRequirementName fallback,
and a folded match prints the diff the author meant alongside a warning
that archive matches names exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(show): name the main spec as such in collectSpecDiffs

Comment and locals still called the main spec the "base" spec, and the
no-main-spec comment described the old all-additions behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps-dev): bump the development-dependencies group with 2 updates

Bumps the development-dependencies group with 2 updates: [smol-toml](https://github.com/squirrelchat/smol-toml) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `smol-toml` from 1.7.1 to 1.8.0
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.7.1...v1.8.0)

Updates `typescript-eslint` from 8.66.0 to 8.67.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(nix): invalidate pnpm dependency hash

* fix(nix): update pnpm dependency hash

* fix(show): harden requirement diff output

* build(nix): pin combined dependency hash

* test(show): assert proposal precedes diffs

* docs(show): clarify JSON diff diagnostics

* fix(show): retain unified diff hunk headers

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 20:18:03 +00:00
a7353aea9a feat(status): add --all for batch status of every active change (#1301)
* feat(status): add --all for batch status of every active change

`openspec status --all --json` reports every active change in one
process instead of one CLI spawn (~500ms module-load) per change,
mirroring the existing `validate --all`. Emits a single
`{ changes: [ChangeStatus, ...], root }` envelope sorted by change
name; a change that fails to load contributes a per-change error entry
instead of failing the sweep. `--all` and `--change` are mutually
exclusive, honoring the --json null-shape on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): harden --all per adversarial review findings

- Validate --schema before the no-changes early return so a bogus
  schema fails consistently whether or not any change exists.
- Text mode now exits 1 when any change fails to load (mirrors
  validate --all); JSON mode still exits 0 with per-change diagnostics.
- Add tests for the --all --schema interaction (unknown schema
  null-shape, override propagation, broken-metadata precedence) and
  text-mode failure rendering.
- Changeset heading to "### New Features" per repo convention; add
  status --all to the agent quick-reference table in docs/cli.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): thread batch null-shape through root resolution, align sort with validate

Code-review findings on --all:
- Pass failurePayload: { changes: [] } to resolveRootForCommand so a
  root-selection failure under --all --json still emits the documented
  batch null-shape (siblings like list/doctor/context already do this).
- Sort with localeCompare to match validate --all's ordering for
  mixed-case change names.
- Extract a shared loadStatus helper so the batch and single-change
  payloads cannot drift apart.
- Changeset no longer claims exact validate --all parity (JSON exit
  semantics deliberately differ).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(status): give the --all failure null-shape a single owner

Simplify pass on the --all diff: hoist the { changes: [] } batch
null-shape into an exported BATCH_STATUS_FAILURE_PAYLOAD constant so
the root-resolution and CLI-wrapper failure paths cannot drift, replace
the conditional spread with the plain ternary the sibling call site
already uses, drop a redundant array copy before sort, and narrow the
text-mode failure counter to the boolean it actually is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): point the missing-target error at --all, correct the docs

`openspec status` with neither --change nor --all listed the available
changes and named only --change, so the batch path was discoverable
only from --help. The error now offers both.

Also corrects two stale claims in the status section of docs/cli.md
that the new row sits next to: the command never prompts for a change
(it errors), and bare `openspec status` is not an interactive check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(status): assert --all carries store context like the single-change path

The batch sweep resolves the root once and threads the store id into
every entry. Nothing pinned that: a regression would have shown up only
as a wrong path inside an agent's JSON. Assert the sweep's envelope root
and per-change payload match `status --change` in a registered store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(status): fail incomplete batch reports

* docs(status): clarify empty and batch output

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
2026-08-26 19:25:39 +00:00
Tabish Bidiwale f1b521dffa docs: rebuild docs site from docs-lab (#1649)
* docs: rebuild docs site from docs-lab

Replace the docs site's source tree with docs-lab, a page-by-page rebuild
of the OpenSpec docs (40 pages: Start / Guides / Customize / Multi-repo /
Reference / Help).

- Point website/docs.sync.config.mjs at ../docs-lab and restructure the
  sidebar into nested groups; sync script gains nested meta.json emission,
  leading-quote descriptions, idempotent writes, and diagram asset copying
- Remove the marketing landing page; / now redirects to /docs
  (meta-refresh page + Cloudflare _redirects)
- Add remark plugins (faq, file-steps, gfm-alert) and the FileSteps
  component backing the new page formats
- Add install.md at the repo root, curled by docs-lab/start/installation.md
  as an agent-executable install prompt
- Add the docs authoring skills (.agents/skills/{write,draft,verify}-
  openspec-docs); docs-lab/README.md links into write-openspec-docs

The old docs/ tree is now unused by the site and left for a follow-up.

Claude-Session: https://claude.ai/code/session_01BMMLYNJQPKXx1QHpnDn4ho

* docs: hold back unwritten pages, add worksets, drop diagram drafts

- website: comment out Overview, Guides, Architecture, Help, Legacy in
  docs.sync.config.mjs until those pages are written; temporary
  /docs -> /docs/installation redirect (Cloudflare _redirects + static
  export meta-refresh fallback in page.tsx)
- docs-lab: new multi-repo/worksets.md page, published under Multi-repo
- docs-lab: content revisions across start/, customize/, reference/,
  help/, multi-repo/; add review notes (Notes.md)
- remove docs-lab/diagrams option-* drafts and their website copies
- write-openspec-docs skill: add spoken-flow sentence rule

* docs: address review on PR #1649

- sync-docs: read the existing output directly instead of exists-then-read
  (CodeQL TOCTOU alert)
- hold back the headings-only Environment variables and Stores reference
  pages until written; links to them fall back to their GitHub source
- sources.md: cutover keeps docs/ in place and points at public/_redirects
- setup.md: label the workflow tree as the default set plus two optional ones

* docs: two review nits (spoken-flow rule, XDG_DATA_HOME note)
2026-08-21 20:45:19 +00:00