Commit Graph
51 Commits
Author SHA1 Message Date
1d35e90880 fix(windows): preserve a file's existing line endings on rewrite (#1958)
* fix(windows): preserve a file's existing line endings on rewrite

The parsers normalize CRLF to LF on read, but nothing restored it on
write. On a Windows checkout (core.autocrlf=true) that turned every
rewrite into a whole-file change: applying a delta that added one
requirement produced a diff of 21 insertions and 14 deletions, burying
the real change. Archiving the same spec now writes 7 insertions and 0
deletions.

- specs-apply: write an updated spec back with the convention the file
  already used; a spec that does not exist yet stays LF.
- file-system: same fix for updateFileWithMarkers, so installing shell
  completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings,
  which bash reports as "$'\r': command not found".
- pack-version-check: spawn npm through cross-spawn, since execFile
  cannot resolve npm.cmd on Windows.

Adds src/utils/line-endings.ts for the detect/restore pair, plus tests
pinning the CRLF round trip through the real write paths. Also adds
regression tests for path containment under Windows case variance:
path.win32.relative already folds case, and those tests pin both halves
of the contract so a future "case-insensitive" change cannot quietly
loosen the traversal guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(windows): keep removeMarkerBlock on the file's own newline

Addresses the two review points and one more instance of the same bug.

`removeMarkerBlock` collapses a run of blank lines, and rebuilt the
separator as a bare '\n' regardless of the file it came from. Removing a
managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF
behind - the mixed ending this PR exists to prevent. It now uses the
newline it already detects for the trailing ending.

Test fixes:

- `marker-updates.test.ts`: close `describe('line endings')` so
  `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`.
- `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin`
  and `resolveProjectArtifactPath` instead of a private copy of the
  containment logic, which passed whatever the production guard did. The
  guard had no coverage at all; a prefix-comparison regression now fails
  the sibling case.

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

* fix(windows): read the file's convention consistently, and only ENOENT as absent

Three follow-ups from CodeRabbit's pass on the superseding PR.

`writeUpdatedSpec` turned every read error into "no previous file", so an
existing but unreadable spec was treated as absent and rewritten as LF.
Only ENOENT means absent now; everything else propagates.

`removeMarkerBlock` chose CRLF whenever the content held one anywhere, so
a single stray CRLF in an otherwise-LF file pulled the whole rewrite to
CRLF. It now uses detectLineEnding, the same dominant-ending reading
matchLineEnding uses, so both write paths agree.

Added the alias-path case the containment suite was missing: a directory
link inside the root that resolves outside it. That exercises the
canonicalization half of the guard, which a lexical check cannot do - the
link's own path looks contained. Skipped where creating a directory link
needs a privilege the runner lacks.

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

---------

Co-authored-by: Travis James <travis@tribehealthsolutions.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 14:30:24 +00:00
Clay GoodandClaude Opus 5 11a9691524 fix(tasks): count unrecognised checkbox markers as not done (#1773)
* fix(tasks): count unrecognised checkbox markers as not done

A checkbox marker the task parser did not recognise was dropped from
progress entirely: it counted toward neither the numerator nor the
denominator. A tasks.md whose remaining work was written `- [~] ...`
therefore reported `✓ Complete` in `openspec list`/`status`, and
`openspec archive` raised no incomplete-task warning for it. Marking
open items with such a marker *shrank* the denominator instead of
leaving them counted as not-done.

Widen the marker to any single non-`]` character and keep `x`/`X` as
the only done state, so an unrecognised marker reads as not-done - the
fail-safe direction the pattern's own docblock argues for. OpenSpec
adopts no new marker semantics; it just stops losing the line.

Closes #1761

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

* fix(tasks): close the remaining silent-loss holes in checkbox parsing

Hardening pass on the #1761 fix.

Parser: an empty `[]` and a padded `[ x]` were dropped exactly the way
`[~]` was - checkbox-like lines counting toward neither the numerator
nor the denominator, so archive stopped warning about them. The marker
is now "at most one non-whitespace token", which covers all three.
Multi-character brackets stay unmatched on purpose: widening to
`[^\]]*` would match `- [Some doc](./doc.md)`, whose `]` is followed by
`(` rather than a space, and turn every Markdown link list into phantom
unfinished work. The docblock states that boundary and a test pins it.

Guidance: archive, bulk-archive and verify told agents to count
`- [ ]` vs `- [x]` by hand, which reproduced the same bug one layer up -
an agent following it saw no incomplete tasks in a `[~]` file even with
the CLI fixed. All six bodies (skill + command per workflow) now state
the rule the parser implements; skills/ mirror and parity hashes
regenerated.

Coverage now spans every consumer of the shared counter: `openspec
list`, archive's gate, the apply task list, validate's task-numbering
check and the parser itself, including the reported 42-done/17-deferred
ratio. Reverting only src/utils/task-progress.ts fails 7 of them.

Verified empirically: the widened pattern newly matches 0 lines across
all 1090 .md/.ts files in the repo, templates and docs included.

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

* fix(tasks): state the padded-tick rule in workflow guidance too

CodeRabbit review, verified and valid: the new guidance named only
exact `[x]`/`[X]` as complete, while the parser also counts `[ x]`,
`[x ]` and `[ x ]`. An agent hand-counting by that wording would have
reported a padded tick as unfinished work and disagreed with `openspec
list` - the same guidance/CLI split this PR set out to close.

All six bodies now phrase it as the parser implements it: complete
means the box holds only `x`/`X`, spacing inside the brackets ignored;
every other marker, including an empty box, is incomplete. skills/
mirror and parity hashes regenerated.

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

* fix(tasks): keep one-character link bullets out of the task count

alfred-openspec on #1773: the widened marker class let a one-character
Markdown link label parse as a checkbox, so `- [A](https://example.com)` and
`- [1](./one)` read as unfinished tasks and made progress and archive report
phantom work. The multi-character guard did not cover them: a one-character
label is one token.

The closing bracket may now not be followed by `(` or `[`, the only two
characters that continue Markdown link syntax. Nothing that used to count is
lost: a checkbox is followed by its description or by end of line, and
`- [x]done` still parses. Regressions cover both link forms, the reference
form, and a link inside a real task description.

Also updates the canonical contract, which said tasks not written `- [ ]` are
not tracked while this change deliberately tracks `[~]`, `[-]`, `[]` and a
padded `x`. Fixed in schemas/spec-driven/schema.yaml and in the docs-lab page
that quotes it, so the quote stays faithful.

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

* docs: say that a done checkbox is case-insensitive

CodeRabbit on #1773: the parser lowercases the marker before comparing it with
`x`, so `- [X]` is done, but the contract I added said only `- [x]` counts.
Corrected in both copies, which are the same text.

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

* fix(tasks): keep empty boxes followed by link syntax in the task count

The one-character link guard also dropped `- [ ](...)` and `- [ ][...]`,
which the strict pre-#1761 pattern counted as unfinished tasks. A line the
parser drops is one archive stops warning about, so a whitespace-only box
now bypasses the guard. The canonical tasks instruction also names `[-]`
and the padded `x` rule, in schema.yaml and docs-lab in lockstep.

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

* docs(changeset): drop em dashes and state the padded-x done rule

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:37:25 +00:00
Clay GoodandClaude Opus 5 626269ed73 fix(templates): stop generated skills naming workflows the profile omits (#1775)
* fix(templates): stop generated skills naming workflows the profile omits

The `core` profile installs six of the twelve workflows, but the update
and apply templates named `/opsx:continue` (6 times) and `/opsx:new`
(twice) regardless. On a default install those became
`/openspec-continue-change` and `/openspec-new-change` — skills that were
never written — so `update-change` refused to create a missing artifact
and handed off to a dead end. The only guard was a sentence asking the
model to check availability at runtime, 70 lines above the two places it
hits the wall.

`command-references.ts` decides how a reference is spelled; nothing
decided whether it should be emitted at all. Add that: templates author
both wordings with `optionalWorkflow()`, and `getSkillTemplates()` /
`getCommandTemplates()` — the one place every generation path already
funnels the resolved workflow set through — pick a branch before the
reference transformers run. A profile that omits a workflow now gets a
concrete `openspec status` / `openspec instructions` fallback instead of
a reference to a skill that does not exist.

Closes #1734

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

* chore(changeset): describe profile-aware workflow references

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

* test(init): assert the core profile names no uninstalled workflow

The end-to-end init test pinned the runtime availability hedging that
#1734 is about, and asserted `/opsx:continue` appears in the default
profile's generated update workflow — the bug itself. Assert the fixed
behavior instead: neither `/opsx:continue` nor `/opsx:new` appears, and
the CLI fallback is stated outright, for both the update and apply
surfaces.

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

* fix(templates): resolve every cross-workflow reference, not just core's

The first commit fixed the two templates the default profile broke on.
Every other cross-workflow handoff had the same shape, and arbitrary
subsets are reachable: a `custom` profile is whatever the user picked,
and `openspec update` re-derives a workflow set from what it finds on
disk (legacy tool overrides, inferred Codex workflows) without passing
it through getProfileWorkflows.

So resolve all of them:

- `apply` -> archive; `continue` -> apply, archive; `ff` -> apply;
  `new` -> continue; `propose` -> apply; `update` -> apply, archive;
  `archive` and `bulk-archive` -> sync.
- `onboard`'s two command-reference tables are built from the installed
  set rather than printed in full with an "only if installed" caveat, and
  its explore, resume and next-step prompts are resolved the same way.

Two supporting changes:

- `onlyWithWorkflow()` plus a whole-line rule in the resolver: a
  conditional that owns its line takes the line with it when it resolves
  to empty, so a dropped table row cannot leave a blank line that ends
  the table in markdown.
- `generateSkillContent()` and `generateCommand()` now throw on an
  unresolved marker. A generation path that skips the choke point fails
  loudly instead of writing `[[opsx:...]]` into a user's SKILL.md.

The propose and ff surfaces keep their deliberate wording difference
(#258): the command surface never invites "ask me to implement", so its
missing-`apply` fallback names the CLI rather than a conversation.

The guard test now runs the property over every subset that could expose
a reference — each workflow alone, everything but one, the empty set, and
the two shipped profiles — for skills and commands, in both spellings.
Twenty-plus of those cases fail against the previous commit.

Only `openspec-onboard` changes in the skills/ mirror: with every
workflow installed, all other templates render byte for byte as before.

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

* chore(changeset): cover the full cross-workflow reference fix

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

* fix(templates): validate conditionals before choosing a branch

Resolution discarded the unselected branch and only then checked for
residual markers, so a truncated block inside the *missing* branch was
accepted for a profile that installs the workflow and rejected for one
that does not. Profile-dependent authoring errors are exactly what this
module exists to remove.

Validate the authored text up front instead: every marker must be one of
the three recognized forms, and they must appear as a flat sequence of
if / else / end. A malformed block now throws identically for every
profile. The post-resolution check stays as a backstop.

Caught by CodeRabbit on #1775.

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

* docs: state the profile-aware handoff rule in the skills reference

alfred-openspec on #1775: docs-lab/reference/skills.md described several
handoffs as unconditional while this change deliberately emits a CLI or
conversational fallback when the profile omits the target.

Stated once, above the entries, rather than as a caveat on each of the eleven
affected Response and Creates rows: the page's recipe is one fact per row, and
repeating the same conditional eleven times would bury the contracts it exists
to state. The rows keep naming the skill that owns the next step, which is the
fact a reader looks up; the rule above them says what happens when that skill
is not installed.

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

* fix(workflows): fold #1735 into the profile-aware references

#1735 fixes the same issue (#1734) by removing the optional handoffs outright.
This PR resolves them at generation time instead, which is strictly better for
the template layer: an install that has `continue` still gets told about it.
So the mechanism here wins and #1735's content is folded in, rather than the
two competing for the same lines.

What #1735 had that this did not:

- src/commands/workflow/instructions.ts. The CLI's own runtime strings named
  the openspec-continue-change skill. Those are chosen at run time, so
  optionalWorkflow() cannot reach them; taken from #1735 verbatim.
- The blocked-state fallback. It was a one-line pointer; it now carries #1735's
  full CLI recovery (select the next `ready` artifact, not `skipped` or
  `blocked`, read its rules with `openspec instructions`, keep the selected
  `--store` on both commands) plus the tracking-file repair path and the
  `missingArtifacts` field it branches on. The installed branch still names
  `/opsx:continue`, so neither audience loses.

#1735's update-change.ts rewrite is not carried over: this PR already covers
all six of those sites conditionally, which is the better answer.

Both of #1735's test suites come across, and they are worth more here than
there. test/core/templates/profile-handoffs.test.ts asserts that no generated
file names an uninstalled workflow across every tool and all three delivery
modes, which is the property this PR's mechanism exists to provide, and it
passes against it. test/commands/profile-handoffs.test.ts covers the runtime
CLI strings. The two guards are complementary: that one is broad on tools and
deliveries, this PR's own profile-workflow-references.test.ts is broad on
workflow subsets.

#1735's command-references.test.ts assertions could not be carried as written,
since they assume the reference is gone unconditionally. Replaced with a case
that resolves the template against a set without `continue` and asserts the
fallback carries the whole recovery. Verified it fails when the fallback is
shortened.

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

* docs: name the core-profile fallback on the two rows that hit it

The rule above the entries covers every profile, but apply-change and
update-change are Core skills whose rows name openspec-continue-change, which
the core profile never installs. On the default install those rows now say what
the generated skill points to instead: openspec status and openspec instructions.

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

* docs(changeset): drop em dashes from the release note

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:17:38 +00:00
8fc65b7f70 fix(tasks): count checkboxes under every list marker (#1862)
* fix(tasks): count checkboxes under every list marker

The task counter behind list, status, view, instructions apply,
validate --archived and archive's incomplete-task check read only `-`
and `*` bullets. A GFM task is a list item, and CommonMark also allows
`+` and ordered `1.` / `1)` markers, so unchecked ordered or plus tasks
were dropped from every count: the change read as complete and archive
skipped its incomplete-task warning.

Accept every CommonMark list marker in TASK_LINE_PATTERN, keeping its
existing tolerances (indentation, CRLF, a missing space after the
marker). Every consumer goes through parseTaskLines, so this one change
fixes them all, and task-numbering validation now sees these tasks too.

* docs(tasks): list every counted checkbox marker in schema.yaml reference

The apply.tracks section listed only - and * checkbox forms. Task lines under + and ordered markers now count, so show them and name the marker rule. Also use American spelling in the changeset.

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 16:21:23 +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
Александр МелентьевandClay Good 44a39eb24b feat(core): add codeassistant support (#1171)
* feat(core): add codeassistant support

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: change format file and add test

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: add tests

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* fix: escaped description yaml values

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: add test

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: change adapter

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: handle \r in description

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore: use common escapeYamlValue helper

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>

* chore(release): track sourcecraft support

---------

Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru>
Co-authored-by: Clay Good <hi@claygood.com>
2026-09-02 19:40:20 +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
Clay Good 18688c8b27 fix(archive): never dead-end a capability retirement (#1699)
* fix(archive): never dead-end a capability retirement

A change whose delta removes the last requirement a capability has
rebuilds the main spec empty, which can never validate. Archive already
knows retiring is the fix and names the `retire_capabilities: true`
marker that authorises deleting the spec - but only when the marker is
the single thing missing.

If the spec also holds a line the merge cannot account for (a `## Notes`
section, a comment under a requirement - both ordinary), that hint was
suppressed, and the hint that names such lines only spoke to authors who
had already set the marker. Neither fired, so the archive aborted on
"Spec must have at least one requirement" with no guidance at all: the
exact dead end the marker exists to close.

Archive now names the blocking content in that case. It deliberately
does not name the marker there - adding it would not have let this run
through, and the marker is only ever named when it really is the one
thing missing. Once the content is resolved, the rerun names the marker.

Closes #1696

* fix(archive): harden the blocked-retirement abort

Three follow-ups to the same message.

The blocking lines are authored spec content printed verbatim to a
terminal, so they now get the treatment `describeChangeName` already
gives a change directory name: control characters replaced, since a raw
CR could forge a line of its own and an ESC could redraw the screen.
Each line is bounded too - one very long line would push the way out of
the abort off the reader's screen - and the cut counts code points so it
can never leave half a surrogate pair. Both the declared and undeclared
branches share the helper, so the marker-declared abort that shipped
with #1484 is hardened with it.

The wording no longer claims retiring is "the way through". It is not,
in the one case this fires on that has a live requirement hiding in a
second `## Requirements` section: merging the sections fixes that spec
without deleting anything.

`openspec/specs/cli-archive/spec.md` records the behavior change - the
blocking lines are named whether or not the marker was declared, and the
marker is still named only when adding it would let the archive through.

* refactor(archive): drop a helper the revised wording made single-use

The marker sentence is said in one place again, so it goes back inline
rather than through a function that now has one caller. Also corrects
the comment above `emptiedByThisRun`: retiring is not the only fix in
every case it covers, which is exactly why the message stopped saying so.

* docs(openspec): record the change as a delta, not a direct spec edit

Both conventions exist in this repo's history, but the two most recent
behavior fixes (#1609, #1616) carry an `openspec/changes/` delta rather
than editing the main spec in place, which is also the workflow this
project asks of everyone else.

The delta reproduces the whole Capability Retirement requirement, so
archiving it drops no scenario. Verified by archiving into a scratch
copy of `openspec/`: the merged main spec differs from today's by
exactly the three added bullets.

* fix(archive): report an unhonorable marker alongside the blocking content

An author who set `retire_capabilities: yes-please` believes they have
authorised the deletion. Clearing the blocking content first, only to
then learn the marker was never read, is two aborts for one mistake.

The abort still never invites the marker to be added while content
blocks the retirement - it only reports the one already there. The spec
delta records that distinction, which the old bullet ("say nothing about
the marker") did not draw.

* style(archive): use one sentence for an unhonorable marker in both aborts

* fix(metadata): strip control characters from an unhonorable marker reason

Every reason a boolean change-metadata marker gives quotes something the
author wrote - a schema name, a parser message carrying one, a
filesystem error carrying a path - and two commands print it straight to
a terminal. A schema name carrying a raw ESC, with the marker set, put
that ESC on screen through `openspec archive`; `openspec validate`
prints the same reason.

Fixed at the source in `readBooleanMarker` rather than at either call
site, so no consumer has to remember. The reason still quotes the name
recognisably; only control characters are replaced.

Reported by CodeRabbit on #1699. Pre-existing on main, and this PR would
have added a second place it reaches the terminal.

* test(archive): fix a comment left behind by the reworded abort
2026-08-19 20:19:34 +00:00
Clay GoodandClaude Opus 4.8 9ae75c86ef fix(archive): don't write ANSI escape codes to a redirected (non-TTY) stdout (#1603)
* fix(archive): stop non-TTY confirm prompts from writing ANSI escapes to stdout

`openspec archive` asks up to three yes/no questions through @inquirer's
`confirm`, which renders by writing ANSI cursor-movement escape sequences —
and emits them even when stdout is not a TTY. When archive runs with its
output captured to a file or pipe (an agent's background task, CI), those
escapes are noise, and in some non-TTY hosts the render loop never settles
and repeats `ESC[NNG` moves until the disk fills (reporter hit 19.8 GB).

Add `confirmPrompt` in interactive.ts: a real terminal (stdin AND stdout
TTY) still gets @inquirer's rich prompt; every other case reads one plain
line via node:readline with `terminal:false`, emitting no escapes. Parsing
mirrors @inquirer/confirm exactly (prefix match on y/yes and n/no, else the
default), and an unreadable stdin rejects with an ExitPromptError-shaped
error so the existing #1479 "rerun with --yes" guidance is unchanged.
archive's confirmOrBlock now calls confirmPrompt.

Closes #1526

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

* test(interactive): cover Windows CRLF and drained-stdin paths; doc note

Adds two regression tests surfaced by adversarial review of the #1526 fix:
- Windows CRLF piped input (`y\r\n`) parses as a clean yes with no ANSI —
  the reporter's platform, previously untested (all inputs used `\n`).
- A second prompt after stdin was already drained blocks with an
  ExitPromptError instead of hanging, exercising the readableEnded guard.

Also documents in troubleshooting.md that a redirected/agent archive run
that pipes an answer no longer writes terminal escape codes into the capture.

Refs #1526

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

* fix(interactive): align non-interactive classification and handle readline errors

Addresses two review findings on the #1526 confirm-prompt fix:

- confirmPrompt drops to the plain reader whenever either stream is not a
  TTY, but isNonInteractivePromptError only checked stdin. A stdin-TTY /
  stdout-redirected run that hit EOF leaked the raw ExitPromptError instead
  of the #1479 "rerun with --yes" guidance. Classification now also counts a
  redirected stdout, matching how the prompt mode is chosen. (isInteractive,
  used broadly elsewhere, is left untouched.)

- readYesNo never listened for the readline/input 'error' event, so a stdin
  error would hang the promise (and go unhandled). It now settles with the
  underlying fault, guarded so the promise resolves or rejects exactly once.

Tests: TTY-stdin/redirected-stdout EOF is classified non-interactive; an
erroring input stream rejects instead of hanging; the archive usable-terminal
test now models a full terminal (both streams TTY).

Refs #1526

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

* fix(archive): gate the change picker on a TTY and tidy the reader

Follow-ups from a second review round:

- selectChange (the no-argument change picker) called @inquirer's `select`
  unconditionally. `select` writes ANSI escapes to stdout even when redirected
  — the same #1526 mechanism the confirm prompts were fixed for — so
  `openspec archive > log.txt` with no change name still spewed cursor moves
  into the capture before blocking. Refuse before rendering when either stream
  is not a TTY, with the same "pass a change name / --yes" guidance the caught
  ExitPromptError already gives. A new test asserts the picker is never
  reached in a non-terminal run.

- readYesNo now removes its input-stream 'error' listener on every settle path
  (it lives on the long-lived process.stdin) and closes the readline interface
  on error too, so nothing accumulates across archive's sequential prompts.

- troubleshooting.md now notes the picker also stays clean.

Refs #1526

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

* chore(changeset): add patch changeset for the archive non-TTY fix (#1526)

User-facing patch note for the archive ANSI/disk-fill fix. Also drops an
unnecessary optional-chain on the non-nullable readline handle in readYesNo
(the listener is only attached after the interface exists).

Refs #1526
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:53:41 +00:00
Clay GoodandClaude Opus 4.8 83be9d113e feat(validate): add --archived to lint task completion of archived changes (#1604)
* feat(validate): add --archived to lint task completion of archived changes

`openspec validate --archived` scans every change under changes/archive/
and fails (exit 1) if any has unchecked tasks in tasks.md. This catches
changes archived with unfinished work — which the normal validate flow
never sees, since it only looks at active changes — and is meant for a
pre-commit or CI hook.

It is a standalone, opt-in scope: it returns before any existing bulk
path, so no current `validate` invocation changes behavior, and it does
not re-validate already-applied spec deltas. Reuses getTaskProgressForChange
(the same counter status/list/archive use) so task counting never forks,
and reads root.archiveDir so it is store-aware.

Closes #205

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

* fix(validate): fail loudly on archive read errors and unreadable task files

Address adversarial + CodeRabbit review of `validate --archived`:

- listArchivedChangeIds now returns [] only for ENOENT (missing archive
  dir) and rethrows permission/I/O/ENOTDIR errors, so a real archive-read
  failure exits 1 instead of silently reading as "no archived changes".
- Add getTaskProgressDetailForChange, which reports task files that exist
  but cannot be read; --archived turns those into an ERROR (naming the
  file) rather than silently counting them as zero tasks. The shared
  getTaskProgressForChange now wraps it and drops the detail, so
  status/list/archive totals are byte-identical.
- Start the spinner after listing so a thrown listing error never leaves
  a spinner running.

Adds regression tests (archive path is a file; archived tasks.md is
unreadable) and unit tests for the new detail variant. Docs: align the
--archived table verb and add a troubleshooting one-liner.

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

* test(validate): address CodeRabbit nits on archived-task tests

- Build the unreadable-fixture path from separate path.join components
  instead of a hard-coded Unix-separator string.
- Assert the reported unreadable path (canonicalized with
  realpathSync.native), not just the count, so a wrong path can't pass.

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

* refactor(validate): address round-3 review of --archived

From three fresh adversarial reviews (scale/perf, flag/output-shape,
filesystem/security):

- perf: memoize schema→glob resolution across archived changes via a
  run-scoped SchemaGlobCache, so the same schema.yaml isn't re-parsed
  once per change (the archive is append-only and can hold thousands).
  Threaded as an optional arg; existing callers are unchanged. Loop stays
  sequential by design (per-change work is synchronous) — now documented.
- output shape: issue `path` now follows validate's convention —
  'tasks.md' for incomplete tasks, and the POSIX root-relative file path
  for an unreadable file (one issue per file) instead of the bare 'tasks'.
- plain output: print `change/<id>` (matching the JSON `type` and bulk
  validation) instead of `archived/<id>`.
- docs: correct the "Never throws" docstrings (glob resolution can throw
  on a malformed/unsafe schema; the caller guards it) and note the
  load-bearing projectRoot override for the archive path depth.

Store-mode resolution confirmed correct by review. Tests updated + a memo
regression test added.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:53:06 +00:00
Clay GoodandClaude Opus 4.8 13e213e00f feat(tools): add Atlassian Rovo Dev CLI as a first-class tool (#1516)
* feat(tools): add Atlassian Rovo Dev CLI as a first-class tool

Rovo Dev CLI loads project Agent Skills from `.rovodev/skills/<name>/SKILL.md`
(Atlassian docs), the same SKILL.md format OpenSpec generates. It was usable
only via the generic "Shared .agents skills" fallback; this makes it a named,
selectable target in `openspec init`.

Rovo has no slash-command surface, so it is registered as an adapterless
skills-only tool (like CodeArts/ForgeCode/Hermes) — no command adapter.

Closes #212

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

* fix(tools): reference Rovo skills by natural language, not dead slash commands

Rovo Dev CLI has no slash-command surface — it matches skills
automatically or by prompt, and `/skills` only manages them. The
generated skills and the getting-started hint still advertised
`/openspec-*` slash commands (18 references across the skill bodies plus
the "Start your first change" hint), so every one was a dead command.

Adds a natural-language skill-reference path for no-slash tools:
`/opsx:<id>` now renders as "the openspec-<skill> skill" for rovodev, in
both skill bodies and the init hint. Other tools are unchanged.

- src/utils/command-references.ts: NATURAL_LANGUAGE_SKILL_TOOLS +
  usesNaturalLanguageSkillReferences(); getSkillReferenceTransformer
  returns the prose transformer for rovodev.
- src/core/init.ts: phrase the skills-only hint as an instruction for
  no-slash tools ("ask Rovo Dev CLI to use the openspec-propose skill…").
- docs/supported-tools.md: correct the Rovo row (was "use skill-based
  /openspec-* invocations").
- tests: assert generated Rovo skills contain no /openspec-* or /opsx
  slash tokens, the hint advertises no dead command, and the transformer
  emits prose.

Addresses alfred-openspec review on #1516.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 17:11:21 +00:00
FasterPHPandMarcus Don 622c509a13 fix(telemetry): honor telemetry.enabled in global config (#1513)
* fix(telemetry): honor telemetry.enabled in global config

Honor the documented global config opt-out while preserving environment and
CI overrides. Keep runtime-managed telemetry identity fields intact and apply
the same privacy setting to update checks.

AI: agentic

* docs(telemetry): address automated review feedback

Document the full opt-out behavior in the changeset and describe the new
test helper so automated documentation coverage meets the project threshold.

AI: agentic

---------

Co-authored-by: Marcus Don <marcus.don@team.blue>
2026-08-05 15:23:41 +00:00
Clay GoodandClaude Opus 4.8 06b310bf57 fix(templates): restore intentional apply skill/command separation (#1514)
* fix(templates): restore intentional apply skill/command separation

Revert the deduplication from #1153. Skills and commands are different
ways to invoke the apply workflow: commands reference /opsx:*, while
skills reference other skills by name and avoid /opsx: (a skill may be
installed without the commands). Teams choose skills-only, commands-only,
or both through profiles, so generating both is intentional, not drift.

#1153 collapsed getApplyChangeSkillTemplate() and getOpsxApplyCommandTemplate()
into one shared body and added a test asserting they are byte-identical,
erasing four deliberate differences (change-name example, contextFiles
note, blocked-state pointer, and completion hint). This restores the two
separate templates and removes the identical-body assertion.

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

* fix(templates): keep apply skill invocations transformable per target

Address alfred's review on #1514. A plain revert of #1153 restored the
skill template's bare `openspec-continue-change` prose and dropped the
archive/input invocations. The generator only rewrites canonical
`/opsx:<id>` tokens, so bare prose is dead text for skills-only targets:
skills.sh, Codex, and Kimi lost valid continue/apply/archive invocations.

Keep the skill and command templates split (no shared constant, no
identical-body assertion — the design separation #1153 erased stays
reverted), but author the skill's three invocation references as
transformable `/opsx:*` tokens. The generator now emits the correct
per-target skill invocation: `/openspec-continue-change` (default),
`$openspec-continue-change` (Codex), `/skill:openspec-continue-change`
(Kimi) — i.e. "invoked as skills," spelled for each tool.

Regenerated the static SKILL.md and parity hashes, and added
default/Codex/Kimi generation regressions that pin the apply skill's
per-target invocations so this break can't recur silently.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 12:59:33 +00:00
Clay Good 59bfb27a76 fix(codex): install skills in canonical agents directory (#1511)
* fix(codex): install skills in canonical agents directory

* fix(codex): preserve shared agents compatibility

* fix(codex): harden shared skill migration

* fix(codex): preserve customized legacy skills

* fix(codex): reject malformed generated versions
2026-08-05 01:43:49 +00:00
Clay Good 9cd845fc45 fix(security): keep paths on a short leash (#1499)
* fix(security): keep paths on a short leash

* fix(security): tighten linked path handling

* test(security): prove schema escape rejection

* fix(security): close remaining trust boundary gaps

* fix(security): close review-found read windows

* fix(security): preserve safe linked workflows

* fix(schema): preserve fork failure details
2026-08-04 18:28:03 +00:00
Clay GoodandClaude Opus 5 2b3d368539 fix(archive): tell the caller which flag to pass when archive can't ask its questions (#1483)
* fix(archive): name the flag when a prompt has no terminal to answer it

An AI agent runs the CLI with stdin closed, so every confirmation
`openspec archive` asks rejects with @inquirer's "User force closed the
prompt with 0 null" - true, and useless: it names neither the question
nor the flag that answers it, so agents abort and guess (#1479).

Each confirmation now reports the same guidance JSON mode has always
given for that decision point, with a pasteable command. The change
picker got the opposite treatment: it swallowed the same failure,
printed "No change selected. Aborting." and exited 0, reporting success
for a run that archived nothing. It now exits 1 asking for a change
name, matching `openspec show` and `openspec validate`.

The detection is reactive - a prompt that already failed, at a stdin
that is not a terminal - so piped answers, --yes, --json and Ctrl-C at a
real terminal are untouched.

Closes #1479

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): carry the caller's flags into the suggested rerun, and honor every non-interactive signal

Adversarial review of the first commit found four defects in it:

- The suggested rerun dropped the flags the caller had passed. For
  `archive x --skip-specs` it suggested a bare `--yes` rerun, and
  following it merged deltas into the main specs - the exact thing
  --skip-specs was passed to prevent.
- The change name went into that command unquoted, so a change named
  `my change` produced an unrunnable paste and one named `a;touch x`
  produced a paste that runs a second command.
- The predicate keyed on stdin.isTTY alone, so a CI runner that
  allocates a pty still got the raw @inquirer failure - #1479 unfixed
  under the very signals `isInteractive()` already treats as
  authoritative.
- A genuine Ctrl-C reaches a process whose stdin is a pipe, and that
  was reported as "this terminal is not interactive", telling a user
  who deliberately quit to rerun with --yes.

The signal is now `!isInteractive()` with SIGINT excluded, so the
terminal proves capability and the signal proves intent. Messages say
what happened ("no answer could be read from stdin") rather than
asserting a property of the terminal, which was false under MinTTY.

Mutation testing found four more gaps in the tests: an unconditional
`throw blocked()`, a stripped `withStoreFlag`, and either half of the
predicate's `||` all left the suite green. Each now has a test, along
with the flag carry-forward, the quoting, the pty-CI case, and the two
prompts that had no end-to-end coverage. docs/cli.md documents the
behavior without a terminal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(archive): close two gaps CodeRabbit found in the new guards

The template guard required a space after `openspec archive`, so a
regression to a bare `openspec archive` line - which blocks agents
exactly as #1479 describes - would have passed it. Verified by
mutation: the widened pattern fails on that edit.

Expected filesystem paths in the new e2e assertions are built from
path segments, per the repo's testing guideline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): make the suggested rerun runnable for dashed names, stores, and Windows shells

A second adversarial pass, scoped to the previous two commits, found
three defects in the fix itself:

- A change named `--force` was emitted bare, and commander reads it as
  an option however it is quoted, so the suggested command failed with
  `unknown option`. Such changes do archive, so the case is reachable:
  the name now goes behind a `--`, with the store flag kept in front of
  it where it is still read as an option.
- The change-name-required path was the one blocked site left
  hard-coded, so `archive --skip-specs` with nothing to answer the
  picker suggested a rerun without `--skip-specs` - the same merge the
  previous commit set out to prevent.
- Quoting was POSIX-only: cmd.exe does not treat `'` as quoting at all,
  and PowerShell escapes an embedded quote by doubling it, so the
  emitted command was wrong on Windows. Names now use double quotes,
  which bash, zsh, PowerShell and cmd.exe all read the same way, and a
  name containing something with no portable spelling (a quote,
  backslash, `$`, backtick, newline) names the placeholder rather than
  emitting a command that could expand.

Two tests were pinning less than they claimed. The real-terminal
cancellation test had become a duplicate of the piped one, since the
SIGINT check short-circuits before the terminal is consulted; it now
covers the terminal leg with a non-SIGINT failure, which is the leg
nothing else guarded. The template guard iterated two identical strings
and could not see an indented invocation; it now sweeps every rendered
skill and command template, and both mutations were confirmed to fail
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(changeset): name the quoting form the fix actually emits

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): stop quoting change names cmd.exe would expand anyway

`%USERNAME%` is a legal change directory name, and cmd.exe expands it
inside double quotes, so the suggested rerun `openspec archive
"%USERNAME%" --yes` targets a different change than the one that was
blocked. `!` has the same problem under cmd.exe's delayed expansion and
bash's interactive history expansion.

Both characters now fall back to the `<change-name>` placeholder, the
same path a `$`/backtick name already took: a rerun the reader has to
fill in beats one that silently archives something else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): stop a change directory from forging its own Fix line

Four adversarial reviews of this branch turned up one real defect and two
guards that were not actually pinned.

The human-mode message for an unanswerable incomplete-task confirmation
interpolated the change name raw, and archive resolves a change by stat-ing
its directory, so the name is attacker-influenceable. A newline in it added a
second, forged `Fix:` line - and because `quoteChangeName` degrades the real
fix to `<change-name>` for exactly those names, the forged line was the only
pasteable command on screen. Control characters are now collapsed.

Also pinned two mutations that passed the whole suite green: dropping
`withStoreFlag` from only the dash-leading branch of `rerunCommand`, and
dropping the `validate === false` leg of the `--no-validate` test - the one
leg Commander actually produces.

The --yes parity guard only saw invocations that opened a line, so a `$ `
prompt, a list marker or `openspec --store x archive` slipped past it. It now
matches those and names the onboarding floor instead of trusting `total > 0`.

Docs and spec catch up: a troubleshooting entry under the message people
actually search for, and cli-archive scenarios for the unanswerable-prompt
paths, including that Ctrl-C stays a cancellation.

* test(archive): tokenise the --yes guard instead of pattern-matching it

Accepting a global flag between `openspec` and `archive` needed nested
quantifiers, and CodeQL was right to call that a ReDoS shape (js/redos, high)
even in a test over our own templates. Splitting the line into tokens decides
the same question in linear time - a 20k-flag line now costs ~2ms - and reads
more plainly than the pattern did.

Same classifications as before, plus it correctly ignores `openspec list
archive`, where `archive` is an argument rather than the subcommand.

* test(archive): skip the forged-Fix-line case on Windows

Windows rejects control characters in a filename, so the change directory the
test needs cannot be created there - which is also why the hole it covers is
POSIX-only. Matches the existing `it.skipIf(process.platform === 'win32')`
idiom in the suite.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:25:13 +00:00
Clay GoodandClaude Opus 5 427abf40ac fix(tasks): count indented sub-tasks in task progress (#1486)
Both checkbox parsers anchored the bullet at column 0, so an indented
sub-task was invisible to `openspec list`/`view` progress, to the apply
task list, and to archive's incomplete-task check. A change whose
sub-tasks were unfinished reported "✓ Complete" and archived with no
warning.

One shared `parseTaskLines()` now backs both surfaces and allows leading
whitespace. It matches every line the two patterns it replaces matched,
and more - including a tab or non-breaking space inside the brackets,
which the old counting pattern accepted - so task counts can rise but
never fall: no change starts reporting less work than before, and
archive's gate can only get stricter.

Checkboxes still count wherever they sit, including inside a code fence.
Skipping fenced ones was implemented and dropped: every rule for deciding
which fence is real has an input where a stray or unbalanced ``` swallows
genuine tasks, which is the silent failure this fix exists to remove.

Verified differentially against a build of main over hand-built fixtures
and the repo's own 120 tasks.md files: 0 files count fewer tasks, 0 lose
an incomplete-task warning.

Closes #1485

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:41:57 +00:00
Clay GoodandClaude Opus 5 17af60c66e fix(archive): make the scenario-drift check fence-aware, plus release-audit follow-ups (#1475)
* fix(archive): make the scenario-drift check fence-aware

parseScenarioBlocks matched #### Scenario: headers on raw lines while the
validator's countScenarios masks fenced code blocks (#1151). The drift
check (#1391) inherited the raw scan, so a fenced scenario example in the
current spec aborted an archive that validate had passed, and a fenced
name in the MODIFIED block counted as keeping a scenario the block had
actually dropped. Build the shared code-fence mask and skip masked lines
in both the header scan and the block-end scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): tear down the redirected request when the budget expires

The overall request budget was armed inside the first send() and its
callback closed over that hop's request. After a redirect the timer
destroyed the already-dead first request, so a redirect target that
trickled bytes kept resetting its idle timeout and held the socket open
until the body-size cap. Track the in-flight request and have the budget
timer destroy whichever one is open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(release): add changesets for user-facing changes missing from the 1.7.0 notes

18 feat/fix commits merged since v1.6.0 without a changeset, so the
pending Version Packages PR would have released them silently: five tool
integrations (ZCode, Hermes, CodeArts, Kimi Code rename, Codex
skills-only), skills.sh distribution, symlinked schema dirs, nested spec
discovery, drift multiplicity, checkbox markers, Windows welcome input,
npx avoidance, doctor store drift, local dates, missing-core-workflows
warning, store-aware main specs, open-questions guidance, and spec
content guidance. Plus changesets for this branch's two fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): escape TOML-active characters in Gemini command files

The gemini adapter interpolated the description into a TOML basic string
and the body into a multiline basic string with no escaping. Every
current template value happens to be safe; the first description with a
double quote or backslash would silently produce invalid TOML for all
Gemini command files. Escape both contexts (#1447 fixed the same class
for the YAML adapters but scoped itself to YAML).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): harden install detection and redirect handling

Three follow-ups from the release audit:
- A path segment literally named volta (a user or project directory)
  classified the install as volta-managed and swallowed the upgrade
  offer. The undotted spelling now requires volta's own tools/image
  layout, matching how pnpm and yarn already demand corroboration.
- The Windows npm-ownership fallback checked that the npm prefix exists,
  which is true of any X\node_modules\pkg tree, hand-copied ones
  included. Corroborate with the openspec.cmd shim npm actually writes.
- A https registry redirecting to plain http was followed; a MITM on
  that reply controls the newer-version answer. Refuse the downgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(cli): export zcodeAdapter from the barrel and sync a completion description

zcode was registered but missing from the adapters barrel (its test
imported the module directly), and the completion registry still carried
the pre-#1062 description for the instructions command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parser): strip a UTF-8 BOM before parsing specs and deltas

A BOM-prefixed delta spec (Windows editors, PowerShell Out-File) failed
validate and archive with 'No delta sections found' because the first
line never matched '## ADDED Requirements'. Strip the BOM in both
normalizers, the same way tool detection already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): reject over-long change names with a validation message

A 300-character change name surfaced two raw ENAMETOOLONG errno dumps
from stat and mkdir. Bound the name at 200 characters in
validateChangeName so the failure is a normal validation error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(archive): finish the early-sync no-op rules for MODIFIED and RENAMED

Two asymmetries left over from the #1376/#1386/#1437 no-op work:

- MODIFIED counted every delta as applied even when the block was
  byte-equal to the main spec, so a fully early-synced change rewrote
  the file (normalization churn), printed '~ N modified', and reported
  specsUpdated: true where its ADDED/REMOVED/RENAMED twins print 'Specs
  already in sync; no files changed.' Count only real replacements.
- RENAMED's already-synced skip (source gone, target present) had no
  near-miss guard: a case/whitespace variant of the source still in the
  spec means a typo'd header, and REMOVED already hard-aborts on that
  signal. Apply the same guard, excluding the target itself so a
  case-only rename still no-ops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(validate): stop reporting an unreadable specs dir as 'no deltas'

The delta-validation loop swallowed every error as 'if no specs dir,
treat as no deltas', so an EACCES capability folder produced the
misleading 'Change must have at least one delta' while archive let the
same error propagate. Tolerate only ENOENT and ENOTDIR (a stray specs
file); anything else stays loud, matching discoverSpecFiles' documented
fail-loud contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): say when commands-only delivery leaves a tool with nothing

Under delivery: commands, update removed the skills of adapterless
skills-only tools (Hermes, Kimi Code, Vibe, CodeArts, ForgeCode) without
a word — leaving zero OpenSpec artifacts while the tool's detection dir
kept re-suggesting an init that would also generate nothing. Print the
same per-tool configuration correction init already prints, pointing at
'openspec config set delivery both'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): honor $ZSH and $ZSH_CUSTOM for Oh My Zsh installs

The installer used a set $ZSH only as an is-installed signal and then
wrote to ~/.oh-my-zsh regardless, so a custom OMZ location got a
freshly created ~/.oh-my-zsh tree that no shell ever loads — and
isInstalled/uninstall looked in the same wrong place. Route every path
through the $ZSH/$ZSH_CUSTOM-aware helpers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(init): make the static welcome screen wait for the Enter it asks for

The static branch printed 'Press Enter to select tools...' and returned
immediately, so the Enter landed in the tool picker and submitted the
pre-selected set sight-unseen. #1462 routed reduced-motion,
OPENSPEC_NO_ANIMATION, --no-animation, NO_COLOR, and narrow-terminal
users onto this path. Wait in a TTY; drop the prompt line when there is
no TTY to wait on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(feedback): keep the manual fallback on every gh failure

Only missing-gh and unauthenticated flows showed the formatted feedback
and pre-filled submission URL; issues-disabled, network, or rate-limit
failures printed gh's stderr and discarded the path to submit what the
user had already typed. Route those through the same manual fallback,
preserving gh's exit code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(update): model the npm shim in the Windows prefix fixture

The ownership corroboration now checks for the openspec.cmd shim npm
writes beside node_modules; the Homebrew-prefix fixture built the layout
without it, so the test failed on windows-pwsh. Write the shim in the
fixture and pin the inverse: the same shape with nothing npm wrote (a
hand-copied portable tree) is not an npm install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(update): require volta's full tools/image layout for the undotted spelling

The corroboration used has('tools', 'image'), which is some() — volta
AND (tools OR image) — so /srv/volta/tools/apps/... still classified as
a Volta install and swallowed the upgrade offer. Require both segments,
matching the real %LOCALAPPDATA%\Volta\tools\image layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): escape control characters in Gemini multiline prompts

escapeTomlMultilineBasicString handled backslashes and quote-triples but
not the C0 controls that are as invalid in a multiline basic string as
in a single-line one. Reuse TOML_CONTROL_CHARS, applied last so the
escapes it introduces are not re-doubled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): finish the $ZSH_CUSTOM support and isolate it in tests

The fpath verification advice still grepped the literal
custom/completions, which a relocated $ZSH_CUSTOM need never contain —
grep the actual directory instead. The installer tests cleared only
$ZSH, so on a machine exporting $ZSH_CUSTOM they would have written
into (and deleted from) the developer's real OMZ custom dir — the same
leakage class #1400 fixed for $ZSH. Clear/restore both, and pin the
custom-location paths with two new tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(release): correct the hermes and zcode changeset wording

Hermes is skills-only (no command adapter), and zcode's namespaced
commands register /opsx:<id>, not /opsx-* — the release notes must not
reintroduce the invocation-spelling confusion #1471 removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(feedback): pin the manual fallback on a non-label gh failure

The new reportGhFailure output (formatted feedback + pre-filled URL) had
no coverage; the network-failure test now asserts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(completion): match fpath entries as literal strings in the OMZ guidance

The verification advice interpolated the completions dir into
grep "<dir>" where regex metacharacters make the check unreliable and
quotes could break the displayed command. Print one fpath entry per
line and match with grep -F on a shell-quoted literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): never emit a bare carriage return in Gemini TOML prompts

A lone CR is illegal in a multiline basic string — Python 3.13 tomllib
rejects the file — and the control-char pass deliberately skipped it on
the assumption it only appears as CRLF. Normalize CRLF to LF and escape
any remaining CR as \r. The escaping guarantee is now parser-backed:
smol-toml (new devDependency) round-trips every hostile body in the
regression matrix (lone CR, CRLF, CR before a quote run, NUL/VT/FF,
trailing backslash, four- and five-quote runs), and the same outputs
were verified against Python tomllib.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build(nix): update the pnpm deps hash for the smol-toml devDependency

The lockfile changed, so the fixed-output derivation hash moved; value
taken from the CI mismatch report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:57:41 +00:00
1637856c42 feat(adapters): follow the Windsurf rename to Devin Desktop (#1167)
* proposal: add devin desktop support

* feat(adapters): add devin desktop command adapter

- Create new Devin Desktop adapter for .devin/workflows/opsx-<id>.md
- Register adapter in CommandAdapterRegistry
- Export adapter from adapters index
- Update docs/supported-tools.md with Devin Desktop entry
- Add 'devin' to available tool IDs list

Devin Desktop uses the same Cascade workflow system as Windsurf,
making it a natural migration path for existing users.

* fix(config): add devin desktop to AI_TOOLS

Add Devin Desktop entry to AI_TOOLS configuration so that:
- getToolsWithSkillsDir() includes 'devin' as a valid tool ID
- getWorkspaceSkillToolIds() returns 'devin' in the list
- parseWorkspaceSkillToolsValue() accepts 'devin' as valid input
- openspec init --tools devin works correctly

This fixes validation failures where 'devin' was documented in
docs/supported-tools.md but not recognized by validation functions
that derive valid IDs from AI_TOOLS.

* fix(devin-adapter): escape implicit YAML scalars in frontmatter

Update escapeYamlValue to detect and quote implicit YAML scalars that
would be coerced by parsers:
- Booleans: true, false, yes, no, on, off
- Null variants: null, ~
- Numbers: integers, floats, exponentials, hex (0x), octal (0o)
- Edge cases: standalone dash (-) and dot (.)

This ensures values like 'true', '123', 'null' remain strings in YAML
frontmatter instead of being interpreted as booleans, numbers, or nulls.

Preserves existing escaping logic for special characters and newlines.

* test(devin-adapter): add comprehensive tests for Devin Desktop adapter

Add test coverage for the Devin Desktop adapter including:
- Command reference transformation from colon to hyphen syntax
- YAML frontmatter escaping for special characters and implicit scalars
- File path generation for workflows
- Integration with available tools detection
- Init and update command workflows

* Add cross-platform testcase.

* fix(devin): refresh deltas against canonical specs and point skills at skills

Addresses the two release blockers on this PR.

Archive: the change's MODIFIED blocks were written against an older
canonical `cli-init`, so `openspec archive add-devin-desktop-support`
aborted rather than merging. The deltas are regenerated from the current
canonical specs (cli-init `Skill Generation` + `Slash Command
Generation`, cli-update `Slash Command Updates`, and a new
`ai-tool-paths` delta for the `.devin` skillsDir), each restating every
existing scenario so archive is purely additive.

Invocation syntax: only Devin Desktop reads `.devin/workflows/`, so a
`/opsx-*` workflow reference is dead text on Devin Local, which supports
skills only. Devin now takes the skill-reference transformer, so skill
bodies and the getting-started hint say `/openspec-*`. Workflow bodies
keep hyphen references, applied by devinAdapter itself.

The adapter also drops its private copy of escapeYamlValue /
formatTagsArray in favor of the shared helpers main centralized in
#1447, which quote unconditionally and escape control characters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): correct commands-only hint, fill doc gaps, cover both surfaces

Follow-up from adversarial review of the previous commit.

The devin special case in getTransformerForTool was unconditional, so
under commands-only delivery — where `.devin/skills/` is deleted — the
getting-started hint named `/openspec-propose`, a skill that is not on
disk. Devin now takes the skill transformer only when skills are
generated, and the hyphen form otherwise. The cli-init delta records the
fallback, and a unit test pins all three delivery modes.

Docs: `devin` was missing from the `--tools` list in docs/cli.md (which
mirrors the list supported-tools.md already had) and from the
command-syntax tables in docs/commands.md and docs/how-commands-work.md.
The supported-tools row gains a footnote citing Cognition's docs for the
`.windsurf/` -> `.devin/` move and the Devin Local workflow gap.

Tests: init and update now assert both surfaces — workflows carry
`/opsx-*`, skills carry `/openspec-*`, neither carries `/opsx:` — and
update checks the seeded skill was actually refreshed. Adds the negative
detection case. Drops three devin-only YAML assertions that duplicated,
less rigorously, the registry-derived escaping matrix that now enrolls
devin automatically.

Also reverts an unrelated zcode export and lingma reorder that a merge
resolution had pulled into adapters/index.ts. zcodeAdapter is registered
but missing from that barrel on main; that is a pre-existing gap and
belongs in its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): name the right command in the profile migration notice

The profile-migration notice printed by both `init` and `update` hardcoded
`/opsx:propose` for every adapter-backed tool. Devin registers no such
command on any surface — its workflows answer to `/opsx-propose` and its
skills to `/openspec-propose` — so an upgrading Devin user was told to run
something that does not exist:

  Migrated: custom profile with 6 workflows
  New in this version: /opsx:propose.

The reference now goes through getTransformerForTool, the same call
init.ts already makes for the getting-started hint. Devin prints
`/openspec-propose`; opencode and the other filename-invoked tools are
corrected to `/opsx-propose` as a side effect; claude is unchanged.

Also corrects two inherited false claims in the cli-update delta — Devin
workflows carry no OpenSpec markers, and update writes every profile
workflow rather than only refreshing files that already exist, which the
PR's own test demonstrates. Qualifies the supported-tools footnote for
commands-only delivery, and strips trailing whitespace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): keep the cli-update delta in step with the canonical spec

The delta restates the whole 'Slash Command Updates' requirement, and its
copy of the OpenCode scenario predated #1471 — archiving it would have
quietly reverted the spec to calling the hyphen rewrite an OpenCode special
case, the hand-maintained framing #1471 removed. Archive on a scratch copy
is now purely additive.

Also point tasks.md at the generator rather than the deleted
transformToHyphenCommands, and enroll devin in the pure-formatter tripwire —
it is the one adapter whose private body transform was just removed, so it
is the likeliest to have it re-added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(adapters): follow the Windsurf rename to Devin Desktop, with migration

Windsurf was rebranded to Devin Desktop on 2026-06-02 and its config
directory moved: `.devin/` is the preferred read+write location, `.windsurf/`
a legacy read-only fallback. Devin Local does not read `.windsurf/` at all,
so an existing Windsurf user's OpenSpec files are invisible to it.

Carrying `devin` as a second tool id alongside `windsurf` would list one
product twice and leave upgraders with two parallel installs — `openspec
update` even told them to create the second one ("Detected new tool: Devin
Desktop"). This follows the rename instead, as the repo already did for
Kimi CLI -> Kimi Code:

- `windsurf` is retired as a tool id; `devin` takes its place, with
  `detectionPaths: ['.devin', '.windsurf']` so pre-rebrand projects are
  still recognized. The Windsurf adapter is replaced, not duplicated.
- `TOOL_ID_ALIASES` keeps `--tools windsurf` resolving, so existing setup
  scripts and CI keep working; they now configure `.devin/`.
- OpenSpec-managed skills (`openspec-*`) and command files (`opsx-*`) under
  `.windsurf/` move to `.devin/`. The kimi migration handled skills only;
  command files now move too, deriving the legacy path from the adapter's
  own getFilePath rather than hard-coding a layout.
- The move is offered, not taken: nothing on disk distinguishes a user who
  took the rebrand from one still on a pre-rebrand Windsurf build that reads
  only `.windsurf/`. `openspec update` explains the rename and asks; --force
  and non-interactive runs migrate; declining leaves every file untouched and
  says what that costs. Files the user wrote are never moved.

Also gives Devin its own row in the authoritative invocation table — the
catch-all row claimed `/opsx-<id>` for both agents, which is wrong for Devin
Local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): stop the migration from deleting anything it does not own

An adversarial pass found two ways the move destroyed files.

Symlinked roots wiped the install. `ln -s .devin .windsurf` is a realistic
way to straddle the rebrand, and it makes source and destination the same
file — so the "destination exists, drop the legacy copy" branch deleted the
only copy. Twelve generated files, gone, and not regenerated: the wipe
happens before tool detection, so update then reported no configured tools.
Both roots are now realpath'd and a self-move is skipped.

User content inside an OpenSpec-managed path was deleted. The same branch
rm -rf'd the whole legacy skill directory, taking a hand-written
reference.md beside SKILL.md with it, and deleted a legacy command file even
when the user had edited it. Now only SKILL.md is removed from a skill
directory, and a command file is removed only when byte-identical to the
one that survives — an edit is left where it is.

Also: declining the move stranded the user. `update` then printed "No
configured tools found. Run openspec init", which is wrong — the project is
configured, just in the directory OpenSpec no longer writes. It now says so
and how to resume. A closed stdin during the prompt aborted the whole
update; it is treated as a decline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(devin): add a changeset for the Windsurf rename and migration

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): move only SKILL.md, never the skill directory around it

alfred caught a data-loss path the earlier fix missed. When the destination
did not yet exist, migration renamed the whole legacy skill directory into
`.devin/` — carrying any file the user kept beside `SKILL.md` with it. That
destination is a directory OpenSpec owns and removes on its own: under
commands-only delivery, or for a workflow outside the active profile. So the
move handed the user's file to a later rm and it vanished.

Reproduced on `d94af8b`: with `delivery: commands`, a `reference.md` beside a
legacy `SKILL.md` was gone after `openspec update`.

Only `SKILL.md` crosses now, in both branches; anything else stays under the
legacy root, and the legacy directory is still removed when the move leaves
it empty. Regression tests cover the commands-only and deselected-workflow
cases and both fail against the previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): treat an edited skill the way an edited command is already treated

A final adversarial pass found the two paths disagreeing. When both roots
held the same file with different content, the command path compared bytes
and kept the user's version; the skill path deleted it with no comparison —
so one `openspec update` destroyed an edited SKILL.md while preserving an
edited opsx-*.md in the same project.

Both now share one `classifyManagedFile` rule: move when the destination is
empty, drop the legacy copy only when byte-identical, otherwise leave it.
Anything left behind is reported, so a user who customized a file knows two
copies exist rather than discovering it later.

Note on the other finding from that pass: OpenSpec regenerating or pruning
the files it owns is long-standing behavior, not something this PR
introduces. Verified against main — an edited SKILL.md under a deselected
workflow, and an edited selected skill and command, are all destroyed by
`openspec update` on 9a937cb too. No regression, so left alone here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(devin): report divergent legacy files even when nothing is movable

collectLegacyToolMigrations only returned a result when something moved, so
a project where EVERY legacy file differs from its counterpart produced no
output at all — two divergent copies and not a word about them. That is the
one case where the report matters most, since it is entirely made of files
the migration deliberately refused to touch.

Kept-only results are retained now. Callers gate on hasMovableContent(), so
a kept-only result reports what was left without offering to move nothing
and without claiming a migration that did not happen.

Also reworded the notice. A legacy file can differ because the user edited
it or simply because an older OpenSpec generated it, so it no longer asserts
an edit — it states that nothing was overwritten and leaves the user to
compare the two copies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(devin): stop matching the unrelated profile-migration line

The kept-only regression asserted no line matched /Migrated\s*:/, which also
matches OpenSpec's profile migration message, "Migrated: custom profile with
N workflows". That line only prints when the global config has no profile
yet — true on a fresh CI runner, false on a developer machine that has run
OpenSpec before — so the test passed locally and failed on all three CI
platforms.

Now matched on the directory arrow, ".windsurf → .devin", which is specific
to a migration report and unaffected by config state.

Reproduced both ways with an empty XDG_CONFIG_HOME: the old assertion fails
there, the new one passes, and the full suite is green under CI's
XDG_CONFIG_HOME + VITEST_MAX_WORKERS=4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 21:02:23 +00:00
Clay GoodandClaude Opus 5 9a937cb9b3 fix(adapters): reference slash commands by the names each tool registers (#1471)
* fix(adapters): reference slash commands by the names each tool registers

Generated command bodies, skills and the post-setup hints all advertised
/opsx:<id>, but only 7 of 28 adapter-backed tools register that name. The
other 21 write .../opsx-<id>.md, where the filename is the command, so
their users were told to type a command their palette never had. Codex,
which registers no slash commands at all, was told to type them too.

The invocation style is now derived from the command file each adapter
writes rather than a hand-maintained tool list, so every tool-specific
surface - command bodies, SKILL.md cross-references, and the init,
update and migration hints - names the command that tool answers to.

Closes #1307
Closes #727
Closes #1379
Closes #1110
Refs #1129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: name the per-tool invocation exceptions in the table itself

Review follow-up: the "every other adapter-backed tool" row swept Amazon Q,
Cline and Kilo Code into the plain /opsx-<id> form. Each is now its own row
with the wrapper it actually uses, and the command-references tests pass the
now-required invocation style explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: make every invocation reference match what OpenSpec generates

Review follow-up across the docs, the living specs and two hardcoded
strings:

- supported-tools: the How To Invoke section no longer splits the "How It
  Works" profile paragraph from its heading, keys rows on the file shape
  rather than a `.md` extension the Gemini/Continue/Copilot/Kiro adapters
  do not use, and drops the Cline/Kilo Code/Amazon Q rows. Kilo Code's
  docs say the current format drops the `.md` suffix, and the Cline and
  Amazon Q forms could not be confirmed - a wrong exception row is worse
  than none, so the caveat now describes the shape without asserting a
  spelling OpenSpec does not generate.
- commands, how-commands-work: the two partial nine-row tables that drifted
  into #727/#1307 now key on the same file shape and defer to the
  authoritative table; both note that skill rows carry skill names, which
  are not command ids.
- faq, troubleshooting, installation, README: stop telling skills-only
  users they have no slash command, stop offering "/opsx autocompletes" as
  a health check on tools where it never will, and name Hermes with the
  other adapterless tools.
- specs: cli-init no longer claims every tool gets `commands/opsx/`,
  cli-update no longer frames the hyphen rewrite as OpenCode-specific, and
  command-generation describes the classifier the code implements.
- the legacy-cleanup summary and the pre-selection welcome banner no longer
  print `/opsx:*` at users whose tool never registers it.
- adds the missing changeset; it supersedes the Codex sentence in the
  pending adapterless-skill-references note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: cover the update and migration paths a mutation run found unguarded

Mutation testing showed five ways to delete parts of this change without
failing a single test. All five now fail:

- `openspec update` had no flat-tool coverage at all, so the headline
  upgrade path - an existing Cursor project still carrying `/opsx:`
  references - was asserted nowhere. Two tests now cover it: one heals a
  project seeded with stale references, one runs claude+qwen together and
  pins each to its own form.
- the legacy-upgrade getting-started menu is covered for a newly
  configured Cursor project, so passing the wrong invocation style there
  is caught.
- migration.ts had no flat-tool case: reverting it to a hard-coded
  `/opsx:propose` passed the whole suite. A qwen-only migration and a
  claude+qwen disagreement now pin the message.
- the unknown-command-id guard in `transformToHyphenCommands` was new
  behaviour with no test; removing it was invisible.

Also tightened assertions the same run showed were weak: the
`resolveCommandInvocationStyle` loop compared the implementation against
itself, the per-id consistency check asserted only that a style was
uniform rather than which one, and the init test's `/opsx-` assertion was
satisfied by frontmatter rather than a body reference. The adapter tests
that moved to `generateCommand` are renamed after their real subject, and
a new case pins the contract those five adapters now rely on: they stay
pure formatters and do not rewrite the body themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: assert the rewritten form, not just the absence of the old one

Review follow-up: the refreshed-skill checks were negative-only, so a
regression that dropped every command reference rather than rewriting it
would have passed. Each now pins the invocation its tool registers, the
stale fixture asserts it really seeded a colon reference into the skill,
and the claude+qwen case pins Claude's namespaced skill alongside Qwen's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(adapters): spell Amazon Q's prompts with @, not as slash commands

The invocation model derived the whole command name from the file an
adapter writes, which covers `/opsx:<id>` versus `/opsx-<id>` but not the
wrapper around it. Amazon Q loads `.amazonq/prompts/opsx-<id>.md` into its
prompt library, invoked as `@opsx-propose`; it registers no slash command,
so its command bodies, skills, and the "Getting started" hint all named
something the tool never answers to.

The name still comes from the file path. The prefix is now adapter
metadata (`invocationPrefix`, defaulting to `/`), so it cannot be guessed
wrong and a new adapter has to declare it deliberately — invocation.test.ts
fails if one appears undeclared.

Also fixes three copy issues:
- The FAQ told users to run `openspec update` when command files are
  missing; update only refreshes files for already-configured tools, so a
  tool that was never initialized needs `openspec init`.
- The installation prompt omitted Kimi Code's `/skill:openspec-propose`.
- The welcome screen promised "opsx slash commands" before tool selection,
  which is wrong for skills-only tools that correctly get no command files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(init): stop naming slash commands where none are registered

Two spots still promised a slash command to users who get none:

- The welcome screen's quick start shows canonical names (/opsx:propose),
  but renders one prompt before tools are picked — an Amazon Q user types
  @opsx-propose and a Codex user $openspec-propose. It now says the
  spelling varies by tool, so the canonical form stops reading as the
  literal thing to type. "Getting started" still prints the real form.
- The post-setup restart line said "slash commands to take effect"
  whenever commands were generated. Amazon Q's generated files are prompt
  library entries, not slash commands, so it now says "the new commands".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: name Amazon Q's @ form where the other exceptions are listed

The README's one-line exception list and the troubleshooting checklist
both enumerated the per-tool spellings and skipped Amazon Q. The
troubleshooting entry was actively misleading: it explains that /opsx
never autocompletes for tools without command files, and Amazon Q is not
one of those — it has command files, they just land in the prompt library.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(migration): cover the legacy-upgrade hint for amazon-q

The migration hint resolves its propose reference through the same
transformer as init and update, but no case exercised a non-slash prefix
there. The second test is the one that matters: @opsx-propose and
/opsx-propose are both "flat", so a style-only model would treat Amazon Q
and Qwen as agreeing and advertise one form to both. Reverting the prefix
to a constant '/' fails 5 tests, so neither assertion is a tautology.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:09:36 +00:00
Clay GoodandClaude Fable 5 19d41714c8 fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups (#1437)
* fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups

Follow-ups from the post-v1.6.0 full-branch audit:

- archive: a REMOVED delta whose requirement is already gone from the main
  spec (early-sync pattern) now warns and continues instead of aborting,
  matching the ADDED (#1376) and RENAMED (#1386) escapes; spec-update totals
  now count applied removals only
- archive: the has-delta-specs gate matches section headers
  case-insensitively like the parser, so lowercase headers get the same
  delta validation errors validate reports
- discovery: a symlinked specs/<cap>/spec.md is resolved instead of being
  invisible (hasAnyFileUnder and the artifact graph already counted it);
  dangling links are skipped
- show: a plain `openspec show <change>` no longer warns about the
  never-passed `scenarios` flag (commander defaults --no-scenarios to true)
- parsers: buildCodeFenceMask now has a single implementation in
  code-fence.ts; requirement-text.ts re-exports it
- templates: apply/update/onboard no longer dead-end core-profile users on
  /opsx:continue and /opsx:new - they name the CLI fallback (openspec
  status/instructions) for profiles that do not install those workflows
- qwen/bob: command bodies and skills reference commands by the hyphen
  names their files actually answer to (/opsx-<id>), matching
  opencode/pi/oh-my-pi
- specs-apply: remove the dead applySpecs export (no callers, bypassed
  store-aware roots)

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

* fix(archive): reject RENAMED+REMOVED conflicts, surface JSON warnings, skip no-op writes

Adversarial-review round for #1437:

- a delta that both RENAMEs and REMOVEs the same requirement is rejected
  explicitly by both validate and archive - the warn-and-continue REMOVED
  path would otherwise have masked the contradiction that previously
  failed incidentally at apply time
- buildUpdatedSpec collects its warnings and archive --json carries them
  in a new optional `warnings` array, so agent flows see the same
  skipped-REMOVED signal humans get on stdout
- archive skips rewriting a spec whose operations were all already
  synced, instead of churning normalization differences into the file
  (and no longer materializes an empty skeleton for a REMOVED-only new
  spec)
- init's getting-started hint uses each tool's real invocation form
  (/opsx-propose for qwen/bob/opencode/pi/oh-my-pi)
- onboard's pause guidance names the CLI fallback when /opsx:continue is
  not installed (CodeRabbit)
- openspec-conventions spec updated to state the idempotent archive
  semantics; changeset added

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

* fix(archive): abort on near-miss REMOVED typos, honest specsUpdated for no-op archives

Round-2 adversarial review for #1437:

- a REMOVED header that differs only in case or interior whitespace from
  an existing requirement is a typo, not an early sync - it stays a hard
  abort naming the near-miss, instead of degrading to warn-and-continue
- specsUpdated is true only when a spec file was actually written; a
  fully-already-synced change prints "Specs already in sync; no files
  changed." and reports specsUpdated: false in JSON (CodeRabbit)
- agent-contract documents the archive warnings field and specsUpdated
  semantics; changeset wording fixed (CodeRabbit)

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

* fix(archive): compare the RENAMED+REMOVED conflict case- and whitespace-insensitively

Addresses alfred's review on #1437: `RENAMED FROM: Old Name` plus
`REMOVED: old name` slipped past the exact-match cross-section guard,
so validate passed, archive renamed the requirement, reported the
removal as already synced, and archived the change.

Both the validator and the apply-side guard now compare the two
spellings with the shared foldRequirementName (lowercase, collapsed
whitespace), and the error names the variant spelling when it differs.
Focused regressions cover both paths; requirement matching everywhere
else stays case-sensitive.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:53:31 +00:00
Clay GoodandClaude Opus 4.8 6a5171e186 fix(validate): allow numeric-prefixed change names (#1435)
* fix(validate): allow numeric-prefixed change names

`validateChangeName` required a leading letter, so `openspec new change
100-add-feature` or `00001-add-auth` failed with "Change name must start
with a letter". This contradicted the rest of OpenSpec: the shared kebab-id
grammar in src/core/id.ts (store ids, workset names, change metadata ids)
already allows a leading digit, and archive explicitly supports
`YYYY-MM-DD-` prefixed change names as a convention (#1309).

Reuse the canonical `isKebabId` grammar for change names so numeric prefixes
work, keeping the tailored error messages for the other failure cases. Fully
backward-compatible: every previously valid name still validates
(`[a-z]` ⊂ `[a-z0-9]`), and consecutive/leading/trailing hyphens, uppercase,
spaces, underscores and other characters are still rejected.

Closes #850
Closes #1169

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs, changeset, tests for numeric-prefixed change names

Address review of the numeric-prefix change:
- add the required changeset (patch)
- fix docs/cli.md which still said names "cannot start with a number"
  and advised prefixing ticket IDs with a word (website copy regenerates
  from this file at build time)
- pin the all-numeric case (`100`) so accepting it is a conscious decision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: name the tiered-prefix test for what it covers

CodeRabbit noted the 101-01-fix-auth fixture contains letters, so the old
title 'all digits and hyphens' was inaccurate. Rename it to describe the
tiered numeric-prefix case (#850); the dedicated all-numeric case is the
separate '100' test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:04:36 +00:00
Clay GoodandClaude Opus 4.8 26f009d940 fix(change): resolve changes by directory instead of requiring proposal.md (#1433)
* fix(show): resolve changes by directory instead of requiring proposal.md

`openspec show <change>` and shell completion resolved a change only when
`openspec/changes/<name>/proposal.md` existed. Every sibling command --
`list`, `status`, `instructions`, `validate` -- resolves a change by its
directory (`getAvailableChanges`). The two rules disagree the moment a
change is created: `openspec new change <name>` scaffolds only
`.openspec.yaml`, so `list` showed the change while `show` reported
`Unknown item`. A custom schema that defines no proposal artifact was
never resolvable at all (#1161).

Resolve by directory in `getActiveChangeIds`/`getArchivedChangeIds`, and
report a change that exists without a proposal accurately -- pointing at
`openspec status --change <name>` -- rather than as missing.

The deprecated `openspec change list` keeps its own proposal-backed scan;
its JSON output parses proposal.md per change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(show): offer proposal-less changes in the no-name selector too

`ChangeCommand.show` resolved a named proposal-less change but still built
its no-name selector (and the non-interactive "Available IDs" hint) from
the proposal-gated scan, so a scaffolded change could not be picked.

Use directory-based discovery there as well. `list` keeps the local
proposal-backed scan: its --json output parses proposal.md per change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(change): unify list/show discovery and report a missing proposal honestly

Adversarial review of the first two commits surfaced four defects.

`change list` still used a private proposal-gated scan, so the deprecated
alias reported a different set than `openspec list` -- the command its own
deprecation warning tells you to use -- while the `show` selector beside it
offered the wider set. Move it to `getActiveChangeIds` and drop the now
unused helper and its ARCHIVE_DIR constant.

Widening that list exposed three follow-on bugs, all fixed here:

- Task counts were computed inside the proposal try block, so a change with
  tasks but no proposal.md reported 0/0. Task progress is independent of the
  proposal; resolve it first.
- `--long` printed "(unable to read)" and `--json` "Unknown" for a change
  that is simply not written yet. Distinguish a missing proposal from an
  unreadable one by testing existence, not by sniffing error codes.
- `show` reported a stray file under changes/, or a traversing name such as
  `../..`, as a change awaiting its proposal, pointing the user at a
  `status --change` call that cannot work. Require a directory that is a
  direct child of changes/.

Also drops a duplicated proposal.md read in the --long path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(change): contain change lookup and stop guessing at unreadable proposals

Second review round.

`isDefinitelyMissing` replaces the plain existence check: fs.access can fail
with EACCES or an I/O error, and treating that as "no proposal.md yet" hid a
real read failure behind an ordinary-looking state. Only ENOENT counts as
absent; anything else falls through to the existing unreadable handling.

`show` now rejects a name that is not a direct child of changes/ before
touching the filesystem. This closes a pre-existing traversal on main:
`openspec change show ../..` resolved openspec/changes/../../proposal.md and
printed a file from outside the changes directory. Both new tests fail
without the guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:32:10 +00:00
Clay GoodandClaude Opus 4.8 a874d1d671 chore(security): create test temp dirs with mkdtemp and override two pinned CVEs (#1432)
* test: create temp dirs with fs.mkdtemp

Every one of these suites built its temp directory by hand:

    testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
    await fs.mkdir(testDir, { recursive: true });

That check-then-create is what CodeQL's js/insecure-temporary-file flags,
and it accounted for 452 of the repo's 467 dismissed code-scanning alerts.
Dismissing them one by one is a treadmill: every new suite that copies the
idiom mints fresh alerts, and a real finding is easy to lose in that volume.

fs.mkdtemp creates the directory atomically at mode 0700 with a random
suffix, so there is no window to pre-empt and no name to guess. The suites
that already used it are the evidence this silences the rule: 33 of the 34
files calling mkdtemp carry zero alerts. The lone exception, archive.test.ts,
was scanned one commit before its own mkdtemp fix landed and is left to that
change rather than conflicting with it.

The dirs named on Date.now() alone were genuinely predictable; the randomUUID
ones were not, but they trained the same copy-paste. Both are gone now.

Behavior is unchanged: mkdtemp creates the root the old mkdir created, and
no assertion depended on the root being absent. Verified with the full suite
(2196 tests, 111 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(deps): override postcss and sharp in the website

Two advisories on the docs site have no Dependabot PR and never will:
Next pins postcss at 8.4.31 as a direct dependency and sharp at ^0.34.5 as
an optional one, so Dependabot cannot raise either without a Next release
that does it first. 16.2.11 does not — it still pins both. Left alone these
sit open indefinitely.

  postcss  8.4.31 -> 8.5.22   GHSA-qx2v-qp2m-jg93  (XSS via unescaped </style>)
  sharp    0.34.5 -> 0.35.3   GHSA-f88m-g3jw-g9cj  (4 libvips CVEs)

A version-ranged selector (`postcss@<8.5.10`) was the first instinct, since it
lapses on its own once Next moves past it. It is the wrong tool: it pins the
override to one advisory's floor, and silently stops applying when the next
advisory raises that floor. GHSA-6g55-p6wh-862q landed while this branch was
open and moved postcss's patched floor to 8.5.12 — under the ranged selector a
dependency pinning 8.5.11 would have resolved to 8.5.11 and stayed vulnerable.
A plain floor cannot under-match, so that is what this uses.

The floor also covers the new advisory: 8.5.22 is past 8.5.12. postcss dedupes
to the single copy the site already had for Tailwind.

These are the last two open Dependabot alerts on the repo. `pnpm audit` on
website/ goes from 2 advisories to "No known vulnerabilities found". The site
builds clean, OG image generation included, and the package set grows by
exactly two platform-gated wasm32 binaries that never install on CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): audit the docs site too

Both `pnpm audit` steps run at the repo root. The docs site keeps its own
lockfile and is not a workspace member, so neither step could see it — the
root audit passed all the way through while postcss and sharp sat open in
website/. That blind spot is why they needed a manual override to find.

Blocking rule copied from the published-dependency audit: advisory on pull
requests, blocking on the weekly schedule and on pushes to main. An
always-advisory step would only relocate the blind spot — the sweep would stay
green with a live advisory and someone would have to read the log of a passing
run to notice.

`!cancelled()` because the two audits above can fail hard. Without it a root
advisory would skip this step entirely, in exactly the situation where the
site's own state matters most.

Verified against the pre-fix lockfile — the step reports the two advisories it
would have caught:

  2 vulnerabilities found
  Severity: 1 moderate | 1 high

and reports "No known vulnerabilities found" against the fixed one. Confirmed
it reads website/pnpm-lock.yaml and not the root: with a vulnerable website
lockfile and a clean root, it exits 1; a missing website lockfile is a hard
ERR_PNPM_AUDIT_NO_LOCKFILE rather than a false clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:17:26 +00:00
Clay Good 27b22ab4cb feat(validate): accept zero-delta changes that declare skip_specs (#1399)
Squashed for rebase; see PR #1399 for the full commit history.
2026-07-22 15:55:59 +00:00
Clay GoodandClaude Fable 5 a84ae70e8c fix(init): use skill references for tools without a command adapter (#1404)
* fix(init): use skill references for tools without a command adapter

Adapterless tools (kimi, vibe, hermes, forgecode, codeartsagent, agents)
skip command generation even under the default 'both' delivery, but their
generated SKILL.md files still told agents to run /opsx:* commands that
were never created, and the init summary suggested /opsx:propose. Route
the existing skill-reference transform by command-surface capability so
these tools get /openspec-* references, and point the getting-started
hint at the skill when no selected tool got commands.

Fixes #1155

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

* fix(init): address adversarial review findings for adapterless skill references

- transform the committed skills.sh distribution too: pass
  transformToSkillReferences in generate-skillssh.mjs and the parity
  test, regenerate skills/ (that channel installs SKILL.md files only,
  so /opsx:* commands never exist there)
- key the getting-started hint purely on whether any selected tool got
  commands, so the delivery=commands + adapterless corner can no longer
  print /opsx:propose
- make the one-time profile-migration message capability-aware for
  projects whose detected tools have no command adapter
- import CommandSurfaceCapability type-only instead of duplicating the
  union inline (a value import would close a module cycle)
- cover the update path: the kimi migration test now asserts refreshed
  skills contain no /opsx references

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

* fix(init): honor Kimi Code's documented /skill: invocation syntax

Per review: the blanket /openspec-* rewrite contradicted Kimi's
documented invocation contract (/skill:openspec-*, see
docs/supported-tools.md). Skill-reference transforms are now selected
per tool via getSkillReferenceTransformer, with Kimi mapped to
/skill:<name> and every other tool keeping the documented /<name>
form; the getting-started hint and migration message use the same
per-tool syntax. End-to-end Kimi assertions cover generated skill
content, the refreshed update path, and the hint.

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

* fix(init): gate the getting-started hint on a generated surface

Per review: with delivery=commands and only adapterless tools selected,
init generated neither skills nor commands yet still advertised an
invocation. Print a configuration correction instead, with the exact
'openspec config set delivery both' remedy, covered by an end-to-end
commands-only adapterless test. Also from the adversarial review round:
mixed selections that disagree on invocation syntax (kimi + vibe) now
fall back to the default /openspec-* form in the shared hint and
migration message instead of picking the first tool's syntax; add the
missing changeset; correct the codex doc comment.

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

* fix(init): suppress the restart hint when no surface was generated

From the third adversarial review round: the 'Restart your IDE for
slash commands' line printed directly after the message saying nothing
was generated. Gate it on an actually generated surface and pin that in
the commands-only adapterless test. Also: use randomUUID() for init
test temp dirs (matches update.test.ts, removes a theoretical Date.now
collision), and clarify the changeset wording about the skills.sh
channel's default reference form.

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

* fix(init): print one usable getting-started hint per invocation syntax

Per review: the mixed-syntax fallback advertised /openspec-propose,
which Mistral Vibe accepts but Kimi Code does not. Group successful
tools by their transformed reference and print one labeled hint line
per distinct form, so every advertised instruction is usable by the
tool it names; the mixed-tool test asserts exactly that. The migration
message compares transformed outputs instead of function identities
(also per review) and stays syntax-neutral ('the openspec-propose
skill') when detected tools disagree.

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

* fix(init): keep codex hints syntax-neutral (skills-invocable, no slash surface)

Codex has no slash-command surface: docs direct users to
.codex/skills/openspec-*. The getting-started hint and the one-time
migration message now name the skill ('the openspec-propose skill')
instead of advertising a /openspec-* form Codex does not accept, and the
restart line only claims slash commands when commands were generated.

Hint lines are also limited to tools that actually got skills: under
delivery=commands, codex+kimi previously advertised /skill:openspec-propose
for Kimi while .kimi-code was never created.

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

* fix(init): advertise a usable instruction for every configured tool

Adversarial-review round fixes:

- Mixed adapter-backed + skill-only selections (claude+kimi, claude+codex)
  printed a single unlabeled /opsx:propose hint that the skill-only tool
  cannot use; hints are now derived per tool from its generated surface
  and labeled when the selection disagrees.
- The delivery=commands configuration correction keyed on the global
  aggregate, so a tool that got zero artifacts lost its correction as
  soon as any other tool generated something; it is now per-tool.
- The migration message advertised /opsx:propose under an explicit
  'delivery: skills' config where commands will never exist; the command
  form is now gated on the effective delivery.
- Migration-message coverage extended (kimi, codex+kimi, delivery=skills,
  commands-installed); profile-describe init tests use randomUUID temp
  dirs like the first describe block.

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

* fix(update): derive migration and legacy-upgrade references per tool surface

The one-time migration message collapsed mixed command + skill-only
selections to /opsx:propose (Claude commands + a Kimi skill told the
Kimi user to run a command it cannot invoke); the reference is now
computed per detected tool and falls back to the syntax-neutral form on
disagreement. The legacy-upgrade getting-started menu had the same
capability blindness with hard-coded /opsx:new/continue/apply — a legacy
Codex upgrade advertised commands Codex lost in #1283; menu lines are
now derived the same way (byte-identical for command-tool upgrades).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:53:49 +00:00
Jun b7c85c741c fix: use skill references in SKILL.md for skills-only delivery (#1194)
* fix(skills): use skill references in skills-only delivery mode

When delivery is configured as 'skills', generated SKILL.md files
contained hardcoded /opsx:* command references pointing to commands
that were never generated, breaking cross-skill workflows.

Add transformToSkillReferences() with the explicit command-to-skill
mapping (kept in sync with WORKFLOW_TO_SKILL_DIR) and wire it in
init and update wherever skill content is generated, following the
approach outlined in #881.

Closes #881
Closes #879

Generated with Claude (Cowork) using claude-fable-5; verified with the
full vitest suite (1683 tests passing).

* fix(skills): wire skill references in workspace skill generation

Review follow-up for the skills-only delivery fix: workspace skill
setup (src/core/workspace/skills.ts) generates SKILL.md via the same
generateSkillContent path but was not wired with
transformToSkillReferences, leaving dangling /opsx:* references when
delivery is 'skills'. Wire both call sites, add a regression test
(verified to fail against the unwired code), strengthen the update
skills-only test with content assertions, and correct the
COMMAND_TO_SKILL_REFERENCE comment (WORKFLOW_TO_SKILL_DIR exists in
both profile-sync-drift.ts and init.ts).

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint and targeted vitest suites (144 tests passing).

* refactor(skills): extract transformer selection into getTransformerForTool

Address CodeRabbit review: the tool/delivery transformer selection was
duplicated at five call sites across init.ts, update.ts, and
workspace/skills.ts. Extract it into a documented helper in
command-references.ts with unit tests locking the selection matrix
(opencode/pi precedence, skills-only delivery, default).

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint, tsc, and targeted vitest suites (147 tests passing).

* fix(skills): prioritize skill references over hyphen commands in skills-only delivery

Address review: getTransformerForTool returned transformToHyphenCommands
for opencode/pi before checking delivery, so skills-only delivery still
emitted /opsx-* references to commands that were never generated. Check
delivery === 'skills' first so skill references win for every tool, and
keep the hyphen transform for opencode/pi only when commands are
generated. Add unit coverage for the opencode/pi selection matrix and an
opencode skills-only init integration test asserting no /opsx: or /opsx-
references remain.

Generated with Claude (Cowork) using claude-fable-5; verified with
eslint, tsc, and targeted vitest suites (148 tests passing).

* docs(skills): add docstrings to init/update generation entry points

* test(skills): reject stale hyphenated references in skills-only assertions

* fix(skills): add missing update entry to command-to-skill reference map

COMMAND_TO_SKILL_REFERENCE was missing the update workflow, so a
/opsx:update reference in any skill template would survive skills-only
transformation untouched. Align the map with the canonical 12-entry
WORKFLOW_TO_SKILL_DIR and assert the generated openspec-update-change
skill carries no raw command references.
2026-07-18 20:54:44 +00:00
9acddcda07 fix: use local dates for CLI date-only values (#1361)
Co-authored-by: showms <showms@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-07-18 13:29:29 +00:00
Clay GoodandClaude Opus 4.8 3fdd2f2f7b fix(specs): discover nested spec paths recursively across parse, apply, and archive (#1355)
* fix(specs): discover nested spec paths recursively across parse, apply, and archive

Delta discovery previously read only specs/<name>/spec.md one directory
level below a change's specs root, so nested layouts like
specs/<area>/<capability>/spec.md were silently skipped: show reported
deltaCount 0, and archive/apply completed without merging the delta into
the main specs directory. Main-spec discovery had the same one-level
assumption, so nested capabilities were also invisible to list, show,
view, and validate.

Introduce a shared recursive discoverSpecFiles() helper and use it in the
change parser, findSpecUpdates (apply/sync/archive), archive's delta
detection, and main-spec discovery (item-discovery, list, spec list,
view). Capability ids are the directory path relative to the specs root,
forward-slash separated on every platform, and apply/archive preserve the
relative path when writing the target spec. Symlinks are not followed and
dot-directories are skipped.

Fixes #1353

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

* fix(specs): surface non-ENOENT errors in spec discovery

discoverSpecFiles swallowed every readdir error, so an unreadable
(EACCES/EIO) capability directory silently vanished from validate/show/
archive/apply — recreating the data-loss class #1353 is closing, now on
the merge path. Suppress only the expected missing-root ENOENT and rethrow
everything else. Adds regression tests for non-ENOENT (ENOTDIR + guarded
EACCES) and the documented symlink-not-followed behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(specs): sort discovered specs by code point, not locale

localeCompare follows the process's ICU locale, so ordering could vary by
OS/CI. Code-point comparison guarantees the deterministic output the
docstring promises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:41:39 +00:00
Clay GoodandClaude Fable 5 f58b445692 fix(completion): install the right completions for fish users (#1364)
* fix(completion): detect the interactive shell from the parent process

`openspec completion install` read only $SHELL, the login shell, so users
whose interactive shell differs (e.g. fish users on distros where the login
shell is bash) got bash completions installed by default (#1197).

Detection now consults the parent process via `ps` before falling back to
$SHELL. It only trusts a parent that maps to a supported shell, so npx/npm
and other non-shell parents still fall back cleanly; Windows is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(completion): match shell basename exactly and pin platform in parent-process tests

Two fixes from CI and review on #1364:

- matchSupportedShell now matches the executable basename exactly
  (stripping a login-shell leading dash) instead of substring matching,
  so parents like fish-lsp or bash-language-server no longer get
  mistaken for the shell (CodeRabbit review).
- The parent-process tests pin process.platform to linux so they
  exercise the ps path on Windows CI, where detection otherwise
  short-circuits and the tests failed.

Adds regression tests for -zsh login shells and fish-lsp fallback.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:20:35 +00:00
Clay GoodandClaude Fable 5 a70daccf0e feat(skills): propose /opsx:update planning-artifact update skill (#1278)
* docs(openspec): propose add-update-workflow — graph-driven /opsx:update + cohesive audit

Dogfooded OpenSpec proposal for the missing first-class "update" action:
a /opsx:update workflow that propagates an edit to one artifact across its
downstream dependents (targeted mode) or audits a whole change for stale/
incoherent artifacts (audit mode) — driven by the schema's artifact graph,
never hardcoded filenames, editing planning artifacts only (never code).

- artifact-graph: expose reverse-dependency queries (getDependents/getDownstream)
  + a requires-edge mtime staleness signal (the engine already builds the
  dependents map at graph.ts:98 and discards it).
- cli-artifact-workflow: surface requires/dependents/stale on `openspec status
  --json` and add a `--impact <artifact>` downstream-revisit-order selector.
- opsx-update-skill: the user-facing /opsx:update command (targeted + audit).

Supersedes the proposal-only stub add-artifact-regeneration-support. Addresses
the cluster #1188/#705/#673/#247 (closes), #694/#684/#618 (answers), and is
graph-driven to avoid the #777/#666 hardcoded-artifact-pattern bug class.
Validates clean under `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): make add-update-workflow deterministic & grounded (Tabish review)

Reframe per the steer "more deterministic and grounded in reality":

- Deterministic spine: the CLI computes the impact set (which downstream
  artifacts to revisit, in build order, with paths) as a pure function of
  schema edges + filesystem. The agent only rewrites prose. Grounded in real
  APIs already present: getUnlockedArtifacts (direct dependents), getBuildOrder
  (order), resolveArtifactOutputs (paths); reverse map built at graph.ts:82-87.
- Replace fragile mtime staleness with a newline-normalized SHA-256 content
  digest (reproducible cross-platform). Drift = upstream digest vs recorded
  baseline; no baseline => "unknown", never a false positive. mtime and pure-git
  rejected with rationale; digest ledger is a separable, optional layer.
- Explicit determinism boundary decision (CLI decides files/order/drift; agent
  rewrites). Skill MUST source the file list/order from `openspec status
  --impact`, never compute it.
- Corrected all code citations to verified lines (graph.ts:82-87,
  instruction-loader.ts:366/429, status.ts); noted #1277's coverage helpers are
  not in this branch's base (coordinate, don't reuse).
- Specs updated: artifact-graph Content Digest requirement; cli status digest +
  deterministic impact ordering; skill determinism + baseline-aware audit.
  tasks add digest/determinism/cross-platform tests + optional ledger section.

Still validates clean under `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): harden add-update-workflow determinism; drop direct name refs

- Digest ledger tracks DIRECT upstream digests; document that transitive drift
  emerges hop-by-hop as downstream is reconciled (no transitive bookkeeping).
- Ground audit's no-baseline structural facts on signals available in this
  branch (missing/empty output, blocked/incomplete); capability-coverage is an
  add-on only when #1277's validateChangeCapabilityCoverage is present.
- Add the "update revises only existing downstream; defer not-yet-created ones
  to /opsx:continue" rule across proposal/design/specs/tasks; impact entries now
  carry existence/status.
- Note artifact-level (not file-level) granularity and that getDownstream
  terminates by the schema's acyclic guarantee.
- Remove direct personal references from the docs.

Validates clean under `openspec validate add-update-workflow --strict`; 10 deltas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): full issue/PR/discussion coverage + command-family design

After a comprehensive sweep of open issues, PRs, and discussions, grounded the
proposal in the complete adjacent landscape and answered the open design
questions the cluster raises:

- #783 (Cross-artifact quality review before apply) is now a primary Closes:
  it IS audit mode. Answer its open "new skill vs. extend validate" question via
  the determinism split — deterministic checks (drift/completeness/coverage) are
  CLI/validate-shaped; the semantic cross-artifact review is the skill. Added a
  skill spec scenario for the #783 patterns (scope contradiction, spec gap,
  duplication).
- Discussion #1206 ("refine proposal now?") + prior-art PR #372: official answer
  is /opsx:update.
- New design Decision 8 (command family): delineate /opsx:update from
  /opsx:clarify (#702, within-artifact), /opsx:review (#1251, plan-vs-code), and
  verify; /opsx:update consolidates update+regen+refine into one action,
  addressing skill-sprawl (#1263, #783).
- Reuse, don't reinvent: audit's empty/incomplete check reuses #1098's
  artifactOutputComplete (same outputs.ts the digest helper lives in); capability
  coverage reuses #1277's validateChangeCapabilityCoverage.
- New open questions: surface deterministic coherence in `validate` for a CI gate
  (#783-B, #829); naming reconciliation with #783's /opsx:refine.
- Confirmed add-update-command* branches are the `openspec update` tool-file
  refresh (not artifact update) — no collision.

Validates clean under --strict; 10 deltas; all relative links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): resolve open questions to committed decisions; drift in scope

Per review steer, every open question is now a committed happy-path decision so
build-out has no dangling forks, and the deterministic drift baseline is pulled
into scope (it is what makes audit-mode drift deterministic vs. agent-guessed):

- Digest ledger IN SCOPE (design Decision 3): per-artifact DIRECT upstream
  digests in ChangeMetadataSchema, written by a deterministic `openspec status
  --record`; pre-existing changes (no baseline) degrade to drift `unknown` +
  structural checks. Generating-flow auto-recording stays optional (graceful).
- cli-artifact-workflow spec: folded drift into the digest requirement (record
  baseline / drift vs baseline / unknown-without-baseline) — stays at 10 deltas.
- opsx-update-skill spec: skill records baseline via `--record` after each
  confirmed edit, so audits clear once reconciled.
- Replaced "## Open Questions" with "## Decisions resolved": ledger in scope;
  targeted entry baseline-aware; apply stays standalone (points to update on
  drift); cross-change (#247), continue/ff de-hardcoding (#777), and validate
  CI-gate (#783-B/#829) are named follow-ups, not deferrals of the core feature;
  /opsx:update kept as the umbrella name.
- Migration Plan + Capabilities + Impact + tasks updated; status JSON gains
  `drift`, CLI gains `--record`. Re-synced with upstream main (0 behind).

Validates clean under --strict; 10 deltas; all links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): harden add-update-workflow — close cross-OS, read-only, edge gaps

Stress-tested every claim against live source and fixed the soft spots:

- Cross-OS digest determinism (real bug): resolveArtifactOutputs (outputs.ts:34)
  sorts ABSOLUTE paths via .sort(), which differs by OS — so a multi-file glob
  artifact (specs/**/*.md) would hash differently on Windows vs POSIX. Digest now
  specified to order files by change-relative forward-slash path and hash
  relpath+content. Added spec scenarios (cross-platform glob stability; rename
  changes digest) and a cross-OS test task.
- Read-only status invariant: moved baseline recording OFF `openspec status`
  (a read command silently mutating the drift reference is a footgun) to a
  dedicated `openspec reconcile` write verb. Updated spec, skill, design, impact,
  capabilities, tasks; reconciled the "no new verb" claims.
- Edge case: missing upstream at record time is stored as an explicit `absent`
  marker so later creating it registers as drift (spec scenario added).
- Edge case: coherent change yields no edits (clean-path scenario).
- Grounding fixes: continue-change hardcoded block is duplicated (skill 103-112 +
  command 225-234) — both must be fixed in the #777 follow-up; verified no
  content-hash util exists.
- Fixed two stale claims the layered edits left: the Impact digest bullet
  (concatenation→relative-path) and the naming-boundary line.

Validates clean under --strict; 10 deltas (4+3+3), 44 scenarios; all links
resolve; re-synced with upstream main (0 behind); issue/PR/discussion sweep
re-run, no new items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): pin data contracts + digest forward-compat; delineate #880

Grounded the surface so an implementer builds it without guessing, and added
proportionate forward-compatibility:

- New design "Data contracts" section with exact shapes: extended ArtifactStatus
  (requires/dependents/digest/drift/driftFrom — additive to the real interface at
  instruction-loader.ts:120), the --impact response, and the `.openspec.yaml`
  baselines ledger. All additive; nothing existing changes type.
- Digest scheme tag (`sha256-relpath-v1:`) + forward-compat: drift compares only
  same-scheme digests; an unrecognized/older scheme reports `unknown` rather than
  silently mis-comparing — re-reconcile restores it. Added a cli spec scenario
  and tasks for it.
- Grounded the ledger write: there is no central change-metadata writer today
  (change-metadata/index.ts only re-exports schema), so reconcile does a safe
  read-modify-write of .openspec.yaml mirroring the store's
  parse/serialize/writeStoreMetadataState pattern (foundation.ts).
- Coverage: re-swept; folded #880 (/opsx:validate code-vs-living-specs) into the
  plan-vs-code delineation alongside #1251/#1073. Main unchanged (546224e); all
  citations still valid.

Validates clean under --strict; 10 deltas; links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): simplify add-update-workflow to a thin /opsx:update skill

Rework per @TabishB review (PR #1278): the proposal over-built. Drop the
deterministic-spine machinery and lean on the existing status command.

- Cut the reverse-dependency graph API (getDependents/getDownstream),
  SHA-256 content digests, the .openspec.yaml baseline ledger, the
  `openspec reconcile` write op, the drift report, and `status --impact`.
  Removes the artifact-graph and cli-artifact-workflow spec deltas.
- Reframe propagation as bidirectional coherence (editing design can
  require revising proposal), not downstream-only.
- Center the feature on one thin skill over the existing
  `openspec status` / `openspec list`; design now sketches the actual
  minimal skill instruction body ("written by hand").
- v1 adds no new CLI/graph/schema code: just update-change.ts + wiring.

Validates clean: `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(update-workflow): pin the status path contract to existingOutputPaths

Address @alfred-openspec's review: the skill's write target was described
loosely as "resolved paths." Make it precise across proposal/design/spec/tasks:

- `openspec status --json` already returns everything the skill needs, in the
  top-level `artifactPaths` map — `resolvedOutputPath` and `existingOutputPaths`
  per artifact. No new CLI field is required.
- The skill edits `existingOutputPaths` (the concrete, glob-expanded files) and
  never writes to `resolvedOutputPath`, which for a glob artifact like
  `specs/**/*.md` remains the glob pattern rather than a real file.
- Add spec scenarios for editing a glob artifact's concrete files and for
  deferring a brand-new file under a glob artifact to `/opsx:continue`.
- Tighten the cross-platform scenario and add a template test (3.4) asserting
  the write target is `existingOutputPaths`, not a glob `resolvedOutputPath`.

Validates clean under `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(update-workflow): address review — default profile, next-step guidance, change-scoped naming

- Register /opsx:update in the default core profile, not expanded-only
  (maintainer call on the PR)
- Add next-step guidance: after updating, recommend /opsx:continue,
  /opsx:apply (esp. when the change was already implemented), or
  /opsx:archive — guidance only, never acted on
- Pin naming scope: skill openspec-update-change, change proposals only;
  generalizing update to other graph types is an explicit non-goal

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

* feat(skills): implement the /opsx:update skill (openspec-update-change)

Implements the approved add-update-workflow change: one thin skill over
the existing status/list commands, in the default core profile.

- new update-change.ts template (skill + command), registered across
  init, profiles, skill-generation, tool-detection, profile-sync-drift
- update joins CORE_WORKFLOWS and ALL_WORKFLOWS
- docs: opsx.md command row + usage note, commands.md reference section,
  supported-tools.md skill list
- retire the superseded add-artifact-regeneration-support stub
- template tests pin the guardrails (schema-driven ids, planning-only,
  existingOutputPaths write contract, next-step guidance); parity hashes
  regenerated; profile/init/update/config tests cover the new core set
- tasks.md checked off; validate --strict passes

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:13:36 +00:00
Clay GoodandClaude Opus 4.8 a3253051ea fix(resolution): converge validate, view, and archive onto canonical resolution (#1182, #1202, #1156) (#1280)
* docs(openspec): propose resolution/validation parity bug bundle (#1182, #1202, #1156)

Planning artifacts only (proposal/design/spec deltas/tasks) for a focused
bug-fix bundle. Three read/validate paths silently diverge from the canonical
logic a sibling command already gets right:

- #1182 validate ignores workspace planning homes that status/instructions resolve
- #1202 view counts only changes/<name>/tasks.md, ignoring the schema tasks glob
- #1156 the SHALL/MUST body-keyword hint fires for deltas but not main specs

Fix converges each divergent path onto the canonical one; parity is asserted by
test. No new surface, no behavior change to the already-correct paths. Validates
--strict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): bulletproof the parity bundle after adversarial source review

Hardened all three bugs after tracing each path to source with parallel
verification agents. Material corrections:

- #1182: reframed from "workspace planning home resolution" (planning homes are
  repo-only; the feature is now 'stores', and validate already accepts --store)
  to the real, reproducible-at-HEAD mechanism: validate's proposal.md membership
  gate (getActiveChangeIds) vs status/instructions' directory-existence rule
  (validateChangeExists). Pulled nested specs/<area>/<cap> delta discovery and
  bulk --all into scope; noted show.ts sibling.
- #1202: widened from view-only to the shared helper's real blast radius — also
  the archive incomplete-task gate (silently archives unfinished glob-tasks
  changes: data safety) and a 2nd hardcoded copy in change.ts. Pinned apply.tracks
  as the source, change-dir scope containment, and the no-schema fallback. Added
  cli-archive delta for the gate.
- #1156: the main-spec parser discards the requirement header before Zod runs, so
  the hint can't be "lifted" — fix needs header recovery (reuse requirement-blocks)
  + Zod de-dup, and the main-spec message can't be byte-identical to the delta's
  (no ADDED prefix). Pinned the actionable sentence + single-emission + regression
  scenarios across all main-spec surfaces.

4 deltas (cli-validate x2, cli-view, cli-archive). Validates --strict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): deep-harden the parity bundle with empirical reproduction

Round 2 of bulletproofing: 3 parallel agents reproduced every bug against the
built (pre-fix) CLI and traced fix sites. This pass corrected two substantive
errors in my own prior spec and closed several gaps.

#1202 (two corrections to the prior draft):
- apply.tracks is a FILENAME that selects the tracked artifact, NOT a glob; the
  glob is that artifact's `generates`. status resolves via
  resolveArtifactOutputs(changeDir, artifact.generates). Fixed all wording.
- "view/archive counts equal status" is FALSE: status checks file EXISTENCE, not
  checkboxes (proven: status calls a 3/5 change isComplete:true). Deleted the two
  count-parity scenarios; reframed as resolution-mechanism parity (same files).
- Added schema-resolution-failure fallback (resolveSchema throws; helper must
  catch or view/list/archive crash). Added projectRoot param + 6-site wiring.
- Empirically PROVEN data-safety bug: archive moved a 3/5 unfinished change into
  changes/archive/.

#1182:
- Found a THIRD getActiveChangeIds site (interactive selector, validate.ts:97).
- Proven: --all with a lone proposal-less change exits 0 silently. Added
  exit-code scenarios. Trimmed over-scope: getSpecIds spec-side is NOT a bug;
  no store-specific scenario needed; noun-form scoped out.

#1156:
- Refine-relaxation regression resolved: deltas don't use the Zod refine
  (validate imperatively), so REMOVE it (not relax) once applySpecRules owns both
  header-only and no-keyword cases. Added RENAMED (out-of-scope), lowercase, and
  the new no-body-line-valid-today scenarios; pinned exact message + prefix.

Still 4 deltas; validates --strict; empirical evidence section added to design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: converge validate/view/archive onto canonical resolution (#1182, #1202, #1156)

Implements the resolution/validation parity bug bundle planned in
openspec/changes/fix-validate-view-resolution-parity. Each fix points a
divergent read/validate path at the canonical implementation a sibling
command already gets right, with parity tests guarding against re-forking.

#1182 — validate resolves changes like status. validate now resolves a
change by directory existence (shared getAvailableChanges) instead of
requiring proposal.md, at all three sites (targeted, bulk, interactive
selector). A scaffolded/still-authoring change is validated rather than
reported Unknown item; a resolved-but-invalid change exits non-zero.
show.ts and the deprecated noun-form change validate are scoped out.

#1182b — validateChangeDeltaSpecs recurses the nested multi-area layout
(specs/<area>/<capability>/spec.md) via a new findDeltaSpecFiles walker,
so a resolved multi-area change validates its deltas instead of reporting
"No delta sections found".

#1202 — getTaskProgressForChange resolves task progress through the
tracked-tasks artifact's generates glob (the same resolveArtifactOutputs
status uses), aggregating checkboxes across every matched tasks.md scoped
to the change dir, with a never-throw fallback to a single top-level
tasks.md. Updates all four callers (view/list/archive x2) for the new
projectRoot arg and folds the second copy in change.ts onto the helper.
Fixes view's Draft misclassification and the archive incomplete-task
gate that let an unfinished glob-tasks change archive (data safety).

#1156 — the SHALL/MUST body-keyword hint applies to main specs.
applySpecRules recovers the requirement header via extractRequirementsSection
and emits the targeted hint (header-only) or generic message (no keyword),
exactly once; the Zod refine is removed (deltas never used it). The
actionable sentence is byte-identical to the change-delta path.

Adds parity/regression tests (Decision 7): validate<->status resolution
incl. exit code, view/archive resolve the same files as status, and the
main-spec<->delta actionable-sentence parity. Full suite green (1791
passed; only the pre-existing, environment-specific zsh-installer
failures remain). Change validates --strict; all 36 repo specs pass
--specs --strict with no new false positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 08:00:44 +00:00
Tabish Bidiwale 41ceebe2d8 fix(ci+installers): harden permission checks and guard completion/profile writes (#1247)
* fix: harden permission checks in root CI

* fix: close permission guard review gaps

* test: address coderabbit installer comments
2026-06-24 06:18:54 +00:00
Tabish Bidiwale a0decbe3fa feat(stores)!: replace workspaces and initiatives with stores (#1190)
* Implement context store root parity

* Clarify simplified model roadmap

* Add roadmap progress checklists

* Number roadmap work items

* Add --store root selection for normal commands

Implements the store-root-selection slice (1.2, with 2.1 pulled forward):

- Add a shared OpenSpec-root resolver (src/core/root-selection.ts) behind
  new change, status, instructions, list, show, validate, and archive.
  --store <id> resolves a registered context store to an ordinary OpenSpec
  root; identity and root-health failures point to context-store doctor.
- Leftover workspace view state never wins root resolution for these
  commands, and a no-root directory with registered stores errors with a
  store-selection hint instead of scaffolding an implicit root.
- Selected-store runs print "Using OpenSpec root: <id> (<abs path>)" to
  stderr and JSON successes carry an additive shared root block.
- --store-path is rejected deliberately with context-store register
  guidance, including on show despite allowUnknownOption.
- new change is root selection only: initiative-link creation is removed,
  --initiative and --areas reject before any writes, --goal stays ordinary
  metadata. openspec set change is removed along with initiative-link.ts.
- archive gains --json: non-interactive, machine-readable diagnostics for
  blocked paths, and no prose or blank lines on stdout.
- list gains minimal --specs --json support so specs listing participates
  in the root reporting contract.
- context-store setup/register next steps show --store usage.

* Fix stream-purity and message bugs found in review

- archive --json: silence the REMOVED-deltas-on-new-spec warning from
  buildUpdatedSpec so the JSON payload stays pure.
- Resolver: wrap registry reads so a corrupt registry surfaces as a
  RootSelectionError; JSON mode now emits a machine-readable diagnostic
  instead of a blank stdout line.
- archive --store (human): per-spec update lines use the absolute store
  path, matching the cross-root absolute-paths contract.
- Noun-form spec show keeps its forward-slash relative not-found message
  on all platforms; root-aware show reports the absolute path.
- Tests: archive --json purity for REMOVED-delta and spec-update-failure
  paths, corrupt-registry JSON diagnostics, and running inside the
  standalone store repo without --store.

* Validate all rebuilt specs before writing any

The archive spec-update phase validated and wrote each rebuilt spec in a
single loop, so a later validation failure could leave earlier specs
already modified while reporting "No files were changed". Split it into
two passes: validate every rebuilt spec first, then write only after all
pass. Regression test covers a two-spec change where one rebuilt spec
fails validation and asserts no target spec was created or modified.

* Mark beta context-store and workspace docs as transition history

Rewrites the opening sections of the old initiative and workspace
reimplementation artifacts as transition evidence and beta history, and
adds the direction-git-native-work transition note. Readers are pointed
to openspec/work/simplify-context-and-workspace-model/ for the active
direction.

* Record store-root-selection slice artifacts and roadmap progress

Adds the slice 1.2 spec, plan, and decision-review evidence, and updates
the roadmap: 1.2 is implemented and tested on this branch, with review
follow-up and merge remaining.

* Record store-lifecycle-proof slice artifacts and roadmap progress

Spec and plan for slice 1.3 (prove the standalone repo lifecycle end to
end), with two review rounds folded in. Adds slice 1.4 to the roadmap,
parks archive browsability as L11, and records the single-branch
workflow for the whole roadmap.

* Prove the standalone store lifecycle end to end

Implements slice 1.3 (store-lifecycle-proof):
- Setup defaults to Git with a pathspec-limited initial commit of exactly
  the files it created, writes store.yaml before committing, anchors
  empty directories with .gitkeep, preflights commit identity via git var
  before creating anything, and requires an explicit --path (interactive
  setup prompts with a visible user path).
- Doctor reports read-only Git facts (commits, uncommitted changes,
  remote) and warns on commitless repos and clone-fragile directories.
- Register errors are terminal: one-checkout-per-id with the unregister
  escape, registration-aware id-mismatch fix text, and an empty-clone
  explanation on unhealthy roots.
- Selected-store hints carry --store, the root banner prints at
  resolution time so post-resolution failures keep it, new change names
  its next command, and status drops the workspace-era Planning home
  line.
- Adds the two-checkout journey e2e test (machine A lifecycle, machine B
  clone/register/continue) with fully isolated Git config and XDG state.

* Fix review findings in the store lifecycle slice

Two adversarial subagent reviews of the slice 1.3 implementation found
one spec violation and several correctness risks; all are fixed:

- Hints carry --store everywhere: validate/show non-interactive hints,
  archive blocked-path fix texts, and status JSON nextSteps now thread
  the selected store. Status JSON also drops the workspace-era
  planningHome field.
- Reruns of an already-registered store no longer git-init it (the CLI
  default is resolved against the registry via resolveSetupGitEnabled),
  keeping reruns strict no-ops.
- Failed initial commits unstage setup's files so a user repo is not
  left with a dirty index; once the commit lands, cleanup no longer
  deletes the committed files; fresh-dir cleanup is non-recursive again
  so it can never delete content setup did not create.
- Corrupt or fake .git dirs report Git facts as unknown instead of
  commitless, avoiding misleading empty-clone advice.
- The sharing next-step line only prints for actual repositories.
- Journey test: Windows-safe path assertions, telemetry opt-out, machine
  B now runs the full enumerated command set (instructions, validate),
  asserts register creates no commits, covers the banner-on-failure and
  store-carrying-hint contract, and doctor human output. Unit tests gain
  isolated git config, register error-text coverage for both mismatch
  branches, and a default-flags rerun no-op regression test.

Full suite: 93 files, 1729 tests, green.

* Keep validate and show hints inside the selected store

Follow-up review findings: the invalid-report next step pointed at the
deprecated cwd-based 'openspec change show <id>' and dropped --store; it
now names the supported top-level 'openspec show <id> --json
--deltas-only' with the actual change id and the store flag. The
nothing-to-show fallback hints and the ambiguous-item advice in validate
and show no longer suggest noun-form commands when a store is selected,
since those commands cannot reach a store root; store mode gets
--type-scoped top-level equivalents instead. No-store output is
unchanged.

* Derive setup's commit from the store shape and extract Git mechanics

Code-quality review follow-up:

- The initial commit was built from the rollback ledger, which is the
  wrong concept: for a converted (existing, non-Git) root it committed
  only the new anchors and identity file, leaving config and specs
  uncommitted and clones unhealthy. When setup initializes the repo
  itself, it now commits the full store shape (openspec/ plus
  .openspec-store/), while pre-existing repos keep the
  only-what-setup-created commit that protects user history and staged
  files. Old beta files outside the store shape are never swept in.
- Identity-file creation is now owned solely by setup; registration runs
  with writeMetadataIfMissing: false and verifies instead of writing,
  removing the split ownership that made the commit plan leaky.
- Git probing, init, identity preflight, and commit mechanics moved from
  operations.ts (1204 lines) into src/core/context-store/git.ts;
  operations.ts is back to 1077 lines and owns only the lifecycles.
- Git lifecycle tests split into test/commands/context-store-git.test.ts
  with shared fixtures in test/helpers/context-store-git.ts, including a
  new conversion test that proves a clone of a converted root is
  immediately healthy.

Spec and plan updated to lock the two commit modes. Full suite: 94
files, 1730 tests, green.

* Point the roadmap's next-item marker at slice 1.4

* Restructure the roadmap around root relationships

Fresh-eyes review outcome, settled in discussion: the layered
PM/architect-to-dev use case (high-level requirements in a standalone
store, implementation work in the app repo's own OpenSpec root) replaced
the rejected project-to-store binding idea with declared relationships
between roots and a fixed resolution precedence — explicit --store, then
nearest local root, then a declared default only when no local root
exists, then error with hint. References never change where commands
act.

- Slice 1.4 becomes one guidance pass (absorbs old 2.2; ~13 surfaces
  from research) gated on the context-store terminology decision
  promoted from L7.
- Phase 2 is fully absorbed: 2.1 shipped in 1.2, 2.2 into 1.4, 2.3 into
  4.1 (initiative selection is hardcoded into ~5,500 lines of opening
  machinery that 4.1 rebuilds; refactoring first is wasted motion).
- Phase 3 rewritten around relationships in both directions, references
  first: repo-references-stores, declared-store fallback, canonical
  remote in store identity, then store-level target declarations, local
  repo map, and relationship health reporting.
- Phase 4 reframed as context assembly; editor opening is one consumer,
  an agent session brief is another.
- New guardrails: references are repo-level config, never per-change
  lifecycle links; one change lives in one root.
- goal.md gains the layered reference experience.

* Lock the naming, Phase 3, and Phase 5 decisions

Decisions settled after parallel product-level and staff-engineer
analyses:

- Naming: the noun is 'store', defined as 'a standalone OpenSpec repo
  you've registered'. The context-store → store group rename plus the
  full machine-token rename (diagnostic codes, JSON keys, data dir) land
  first in slice 1.4; --store stays; committed store-repo formats are
  already aligned and stay. openspec repo/--repo rejected: the --repo
  prior means the code repo being operated on, colliding with target
  project repos.
- Phase 3: index-not-inline reference injection; references: and the
  fallback store: pointer both live in openspec/config.yaml (top-level
  marker rejected — .openspec.yaml is taken by change metadata); one
  typed id namespace with the kebab grammar locked for all id kinds;
  relationships are location, declaration, or citation — never managed
  per-artifact links, which is what initiative links were.
- Phase 5 criteria agreed: delete rather than hide, sequenced across
  1.4, a small command-group deletion slice, and 4.1; never auto-delete
  user data.

* Add the roadmap loop runbook

* Make the roadmap loop fully autonomous with layered reviews

No pause gates: unlocked decisions are made autonomously and recorded
as 'Decided autonomously (review me)' changelog lines; Phase 5
deletions proceed without confirmation. Review phases run as parallel
multi-agent Workflows plus the /code-review skill (high effort) and
codex CLI; /simplify runs serially after correctness fixes.

* Add the loop's parallelism policy

Serial across slices (single branch, shared junction files, mass
rename/deletion commits make cross-track rebases the riskiest
unattended operation); Workflow fan-outs within slices for mechanical
sweeps; read-only lookahead research for the next slice's code map.

* Switch the roadmap run driver from /loop to /goal

The docs position /loop as interval-based and /goal as the
condition-based counterpart: turns fire back-to-back until a verifiable
completion condition is met, with full main-loop tool and skill access
per turn and persistence across resume. That matches the queue's
semantics (next unit when the previous finishes, stop when done), so
loop.md becomes runbook.md, reframed around goal-driven turns with an
explicit per-turn status block for the goal evaluator and a declared
completion signal.

* Add the final acceptance capstone and standing quality bars

The goal condition previously checked activity (boxes ticked, suite
green); it now checks the product claim. Phase 6 / capstone 6.1: four
persona journeys including a cold-start agent dogfood, usability audits
(error catalog, vocabulary sweep, time-to-first-success), technical
audits (single-resolver invariant, dependency direction, dead code,
module sizes, agent-contract inventory, net LOC delta vs origin/main),
a whole-delta review gauntlet, and a committed release-readiness
report. Runbook gains standing per-slice quality bars: locked
vocabulary only, pasteable store-carrying errors, consistent agent
contracts, ~600-line module budget, no speculative abstractions, one
resolver.

* Bake the /goal invocation into the runbook header

* Write and review the store-rename-and-guidance slice spec

Two parallel adversarial reviews (subagent, codex CLI) converged on the
same flaw in the first draft: exempting the legacy groups from the token
rename contradicted the locked machine-token decision. The spec now
states one rule - total mechanical token rename, surgical prose rewrite,
behavior changes limited to the two riders - and folds the corrected
45-code token inventory, the missed guidance surfaces, and a
sweep-as-test acceptance criterion.

* Write and review the store-rename-and-guidance plan

Four green checkpoints: mechanical rename, the two riders, guidance
regeneration (three disjoint streams), and sweep/guards/dogfood. Both
parallel reviews (subagent, codex CLI) approved with fixes, all folded:
exact rider-1 deletion list with persisted path-bound views preserved,
Commander command:* error ownership, docs/concepts.md and beta-doc
runtime fixes, sweep roots excluding openspec/ history, old-data-dir
negative fixtures, pinned non-interactive dogfood init flags.

* Rename the context-store surface to store

Mechanical, total token rename per the slice spec: command group
(context-store -> store, subcommands unchanged), 45 diagnostic codes,
dotted context_store.* targets, JSON keys (context_store/context_stores
-> store/stores everywhere, legacy groups included), the machine-local
data dir (context-stores/ -> stores/), internal modules and symbols
(src/core/context-store -> src/core/store, ContextStore* -> Store*),
and every help/error/hint string. Committed store-repo formats are
untouched (.openspec-store/store.yaml, registry.yaml). The dead
getDefaultContextStoreRoot export is deleted; its negative path
assertion is kept inline. The --store flag description now carries the
locked definition, identical in Commander and completions metadata.

Full suite green (94 files, 1730 tests).

* Land the two store-rename riders

Rider 1: workspace open loses its legacy --store/--store-path initiative
selectors (the second live meaning of --store). The unreachable guard
branch and its workspace_open_store_without_initiative diagnostic are
deleted; --initiative keeps resolving through the cross-store scan, the
qualified <store>/<id> form, and the interactive picker; persisted
path-bound views still reopen and doctor (tests now write the view-state
fixture directly). Selector-advertising fix texts in initiative
resolution name only surviving forms.

Rider 2: the store group owns its unknown-subcommand path - the error
names the real subcommands (including ls) and points lifecycle-shaped
mistakes at the normal command with --store, same stderr text for human
and --json runs, exit 1. New tests cover the hint, the no-alias
negative, and the --help listing.

Full suite green (94 files, 1735 tests).

* Regenerate guidance around stores

Templates: every generated workflow skill (and its opsx command twin)
now carries a shared store-selection block - discover ids with
'openspec store list --json', carry --store <id> on every command,
hints keep the flag. The three out-of-guard workspace-planning prose
mentions reword to schema language; the five live workspace guards are
untouched. Parity hash tables updated deliberately and the test now
asserts the store teaching in all generated skills.

Docs accuracy pass: docs/cli.md store section renamed with the locked
vocabulary, removed workspace-open selector rows and example, stale
XDG-default setup text corrected; docs/concepts.md token renames;
workspaces-beta docs renamed plus correctness fixes (--path in setup
examples, current prompt-flow prose). Every documented invocation
smoke-ran against the built binary. The workspace and initiative group
one-liners are labeled legacy beta in Commander and completions.

Note: the .codex/skills/use-openspec guidance was also rewritten around
store discovery (beta reference deleted), but that directory is
git-ignored (the L8 ignored-local-skill), so those edits live on disk
only and cannot appear in this commit.

Full suite green (94 files, 1736 tests).

* Record the git-ignored .codex discovery in the slice artifacts

* Guard the rename with sweeps, format pins, and the dogfood proof

New tests: a vocabulary sweep over src/, test/, docs/, scripts/ (and
.codex/ when present) that fails on any reintroduction of the retired
tokens; committed-format pins (.openspec-store/store.yaml literals, the
stores/ data dir, pre-rename store registration); old-data-dir negative
fixtures (valid and corrupt old registries are ignored, never read or
migrated); a --store description exact-equality walk across every
lifecycle command; and a store:setup telemetry-path assertion.

Dogfood proof committed as dogfood-transcript.md: a fresh headless agent
session, one plain prompt naming the team store in words, discovered the
registered store via --help and store list and created the change with
--store - six tool calls, zero initiative/workspace invocations, local
root untouched.

Full suite green (95 files, 1742 tests).

* Fix the post-implementation review findings

Three parallel review mechanisms (spec-compliance agent: compliant with
findings; /code-review high: 10 verified findings; codex CLI: approve
with fixes) converged on two P2s and a set of cheap P3s, all fixed:

- The store group's unknown-subcommand hint no longer emits invalid
  suggestions: 'store new <id>' without 'change' falls back to the full
  form, flag-interleaved operands (which Commander cannot attribute)
  use the generic example, the lifecycle-redirect set derives from
  COMMAND_REGISTRY, and the subcommand list derives from the live
  Commander group instead of a hardcoded string.
- Store-selection guidance names the seven commands that accept --store
  instead of claiming every command does, and is removed from the
  feedback workflow (whose only command rejects the flag); presence
  coverage extended to all 11 opsx command templates; hash tables
  re-pinned.
- Pasteable hints: 'Run store unregister' fix texts now name
  'openspec store unregister <id>'; the empty-list setup hint carries
  the mandatory --path.
- STORE_OPTION_DESCRIPTION now imports the completions description
  instead of duplicating it; the path-bound view fixture persists
  through the production writeWorkspaceViewState; the pre-rename
  register test writes old-format bytes inline; the vocabulary sweep
  file carries no retired tokens and no longer self-exempts.

Full suite green (95 files, 1745 tests).

* Apply the simplify-pass cleanups

Test guards now iterate the production registries: store-selection
presence checks run over getSkillTemplates()/getCommandContents() (new
workflows are covered automatically) and assert full-constant
containment; the --store description walk pins the exact seven command
names and ties each to the guidance prose, so a stale taught surface
fails tests. The store group one-liner derives from the completions
registry entry; the command:* flag predicate is derived, not restated;
retired-token constants are hoisted once per file; a redundant
assertion and a dynamic import are gone.

Skipped deliberately: the sweep's hand-rolled walker (measured ~48ms,
works), a cross-file retired-token helper (two files only), and
pre-existing duplications on surfaces the next slices delete.

Full suite green (95 files, 1745 tests).

* Tick slice 1.4 in the roadmap and point at the deletion slice

* Write and review the delete-legacy-command-groups slice spec

Both parallel adversarial reviews rejected the first draft on verified
grounds and every finding is folded: the config command's
workspace-profile integration (which executes a dead command) is in
scope; binding.ts stays because the planning-home carve-out depends on
it through workspace/foundation.ts; a dead-export carve-out ledger owned
by 4.1 is specified; concepts.md loses its whole Coordination Workspaces
section; the surviving 'Use initiatives' constraint rewords to read-only
compatibility language. The locked 5.1 'opening machinery' wording is
narrowed (recorded as a reviewable autonomous decision): the state model
and workspace-planning mode die in 4.1; zero-consumer opening helpers
die with the command groups.

* Write and review the delete-legacy-command-groups plan

Five deletion waves with grep-before-delete discipline. Both parallel
plan reviews folded: the planning-home mode pin (nothing asserts
actionContext.mode today) and the docs pointer grep gate are new
explicit steps; docs/cli.md dead-command references outside the cited
ranges are mapped (agent-table rows, Stores summary cell, config
section); the config.ts map gained the interface and core-preset call
sites with full test ranges; the parity test's initiative carve-out
removal is a named fourth partial edit; the spec's byte-stable clause
now permits the new removal-coverage tests.

* Delete the workspace and initiative command groups

The legacy beta command groups stop existing, and everything only they
consumed goes with them: the command layer (workspace.ts, initiative.ts,
the 11-file workspace/ command dir), the orphaned core (workspace
registry/openers/open-surface/skills/link-input and the whole
collections tree), the completions entries, the config command's
workspace-profile integration (which executed a dead command), the
update command's workspace detection, the docs that documented nothing
else (cli.md sections, concepts.md Coordination Workspaces,
docs/workspaces-beta/), and the tests of all of it.

Kept deliberately: planning-home and its state model (foundation,
state-io, legacy-state, store binding types - 4.1 owns their end),
legacy initiative metadata display, the --initiative rejection, and
every byte of user data. The 'Use initiatives' constraint rewords to
read-only compatibility language.

Ground truth recorded: workspace-planning mode has been CLI-unreachable
since slice 1.2's resolver demotion (toPlanningHome hardcodes repo
kind); the spec scenario was corrected to pin the byte-stable repo-local
behavior plus the library contract.

New removal-coverage tests (7) pin unknown-command rejection, help
cleanliness, update fall-through, user-data byte-identity, legacy
display, and the library contract. deletion-ledger.md records the 41
removed diagnostic codes and the dead-export carve-outs owned by 4.1.

Full suite green (85 files, 1614 tests). Pointer grep gate clean.

* Fix the deletion-slice review findings

Three parallel review mechanisms (spec-compliance: compliant with
findings, no P1; /code-review high: surgery residue and test-robustness
items; codex CLI: three P3s) converged on a small list, all applied:
the dead hasRepoLocalOpenSpecProject helper and its orphaned import are
deleted; the maybeWarnConfigDrift pass-through wrapper is collapsed and
its stale awaits dropped; the byte-identity test asserts the update
spawn's exit code and snapshots directories (not just files) so empty
subdirectory deletions cannot pass; the frozen-legacy-bytes fixture is
documented as deliberate; the project-apply accept path regained
coverage (lost with the deleted workspace tests); a sweep test pins the
ledger's surviving-token claim so workspace/initiative token regrowth
fails fast; the ledger records the state-io dead-export carve-outs, the
EACCES error-fidelity collateral, and the L2 pointer for the accepted
spec library that still describes deleted behavior.

Full suite green (85 files, 1616 tests).

* Apply the deletion-slice simplify pass and tick the roadmap

Simplify: the redundant hand-written store.yaml fixtures are gone
(registerStore writes identical metadata), the update action lost its
vestigial path.resolve scaffolding, and the sweep's four token spellings
collapsed to one concatenation-built regex. Skipped deliberately:
cross-suite snapshot helper extraction, state-io trimming, and barrel
removal - none pay for themselves before 4.1 deletes that code.

Roadmap: Phase 5 first tranche recorded (-12,903 net lines, ledger,
~25 fewer modules per CLI invocation), the workspace-planning
CLI-unreachability ground truth logged as a reviewable decision, and
the pointer moved to 3.1.

Full suite green (85 files, 1616 tests).

* Write and review the store-references slice spec (3.1)

Two adversarial rounds folded. The subagent's P1s were both grounding
failures: parseSpec() throws on imperfect upstream specs, so the index
extracts summaries tolerantly; and apply instructions have a real human
surface, so the index lives in both surfaces and both modes. Codex
added the async command-boundary assembly (the sync generators receive
the index as input), the 50KB shared budget with order-preserving
truncation, and registry-corruption degradation. Five warning codes
degrade instructions instead of failing them; references parse raw and
validate in the assembler; the index is one level deep by rule.

* Write and review the store-references plan (3.1)

Two checkpoints (config + assembler core; instruction surfaces + docs).
Both plan reviews approved with fixes, all folded: pure renderers live
in core beside the assembler so the 50KB budget measures real output
(truncation stops before the cap, warning line exempt); the
inspectRegisteredStore extraction is pinned narrow - metadata/health
stages only, registry lookup stays in resolveStoreRoot and its seven
error codes stay byte-identical; config is read once at the command
boundary and suppresses the generator's internal read; the Purpose-line
scanner is self-contained; the test matrix gained symmetric --store,
boundary byte-identity, no-recursion, nothing-frozen, and not-inlined
assertions.

* Add the references config field and the index assembler core

openspec/config.yaml gains references: (raw strings kept, deduplicated,
order-preserving; grammar validation is the assembler's job so bad ids
surface as diagnostics). New src/core/references.ts assembles the
referenced-store index: one registry read per call, the narrow
inspectRegisteredStore extraction shared with resolveStoreRoot (whose
seven error codes stay byte-identical, pinned by the existing
root-selection tests), tolerant first-Purpose-line summaries, five
warning diagnostic codes, self-reference omission by id and path, and
the 50KB budget with order-preserving truncation measured by the pure
renderers that the command layer will print.

Full suite green (86 files, 1630 tests).

* Wire the referenced-store index into both instruction surfaces

The command layer reads the resolved root's config once (suppressing
the generator's internal read), assembles the index, and threads it
into generateInstructions and generateApplyInstructions. Artifact human
mode prints the <referenced_stores> XML block after project context;
apply human mode prints a '### Referenced Stores' markdown section.
JSON gains an additive references field, omitted when none are
declared. docs/cli.md gains the 'Referencing stores from a project'
subsection.

Seven new surface tests pin: both surfaces both modes, live (unfrozen)
summaries, field omission, symmetric --store declarations, the
one-level rule, non-instruction byte-identity with the store untouched,
and the full PM-to-dev layered flow including the verbatim fetch.

Full suite green (87 files, 1637 tests).

* Fix the 3.1 review findings

Three review mechanisms converged on six real issues, all fixed with
regression tests: extractFirstPurposeLine is fence-aware and accepts
CommonMark closing hashes; an index emptied by self-reference omission
now omits the JSON field (omitted-not-empty contract); truncation
renders its message as a Note line instead of an orphan fix; the budget
measures the real rendering in UTF-8 bytes (problem entries and
diagnostics included; only the truncation warning exempt) with a
binary-search prefix; registry-independent checks (invalid id,
self-reference) run before the corrupt-registry branch; the assembler
catches inspection throws and degrades them; the resolveStoreRoot
switch is explicit (return fromStoreError) with an exhaustiveness
guard; generateApplyInstructions takes an options bag instead of a
fifth positional; the dead config-read catch is gone; spec files read
concurrently.

Full suite green (87 files, 1641 tests).

* Apply the 3.1 simplify pass and tick the roadmap

Simplify: the two new test suites share test/helpers/openspec-fixtures
(createOpenSpecRoot/writeSpec); the dead canonicalize wrapper is gone
(canonicalizeExistingPath never throws); the 50KB cap is single-sourced
from project-config's exported MAX_CONTEXT_SIZE; the registry-unreadable
state collapsed into one nullable variable; spread and JSDoc nits.
Skipped with reasoning: renderer branch merge, binary-search
replacement (measured: cap self-bounds the cost), cross-suite snapshot
consolidation, and the remaining ~1ms duplicate config read (the
project's own perf note rejects that trade).

Roadmap: 3.1 boxes ticked, changelog round recorded, pointer moved to
3.2. Full suite green (88 files, 1641 tests).

* Write and review the declared-store-fallback slice spec (3.2)

Both adversarial reviews converged on the same P1: the spec claimed
declared roots behave exactly like --store roots while its own UX
example printed a relative path, and the scope named only two of the
seven source-keyed consumers. The fix is one store-selected predicate
(storeId set) adopted everywhere. Also folded: init refuses to bury a
pointer under a scaffold; malformed pointers error
(invalid_store_pointer) instead of silently flipping the write target;
one-hop pointer resolution; warning-silent resolver config reads;
directory-typed shape stats; the true-prefix declaredOrigin mechanism;
and the recorded amendment relocating the both-shapes warning from the
nonexistent project doctor to resolution stderr.

* Write and review the declared-store-fallback plan (3.2)

Both plan reviews approved with fixes, folded: the eighth
source==='store' check (show.ts printNonInteractiveHint) joins the
predicate inventory with a recorded spec amendment; the init guard
anchors immediately after validate() so legacy cleanup and the
global-config migration write cannot precede the refusal; the
declaration-origin prefix is a call-site rewrap (codes preserved, fix
unprefixed) covering the fromStoreError pass-throughs; the targeted
config read is a shared exported helper; the test matrix covers all
five prefixed taxonomy codes, the malformed-pointer no-write
assertion, deterministic byte-identity, and positive config-only
assertions.

* Add the declared-store fallback to root resolution

A config-only openspec/ directory with a store: pointer now resolves
the declared store: the nearest-root arm classifies the found dir with
two directory stats, reads the pointer via the new warning-silent
readStorePointer helper (malformed pointers error with
invalid_store_pointer - never a silent local write), and resolves
through the shared resolveStoreRoot pipeline with source 'declared'
and a declaration-origin rewrap (codes and fixes untouched). A real
root with a pointer warns once on stderr and stays nearest - fallback
never override. The new isStoreSelectedRoot predicate (storeId set)
replaces all eight source==='store' checks so declared roots get
identical cross-root behavior: banner, --store hints, absolute paths,
suppressed noun-form suggestions.

Nine new resolver tests cover the pointer, precedence, the both-shapes
warning, malformed pointers, all five prefixed taxonomy codes, one-hop
resolution, and .yml origins.

Full suite green (88 files, 1650 tests).

* Add the init pointer guard, externalized-planning e2e, and docs

openspec init now refuses to scaffold a config-only pointer directory,
anchored immediately after validate() so the refusal precedes legacy
cleanup, migration writes, and prompts - the test pins that nothing
changes on disk and that removing the store: line converts cleanly.
The e2e journey runs the full lifecycle (new change through archive)
in a pointer repo without --store anywhere: work lands in the store,
the pointer repo stays byte-identical, the banner and JSON root block
report declared, nextSteps hints carry --store, and the 3.1 references
composition surfaces the store's own upstream index. docs/cli.md gains
the 'Declaring a default store' subsection.

Full suite green (89 files, 1654 tests).

* Fix the 3.2 review findings

Three review mechanisms converged; all real findings fixed with
regression tests: empty or comments-only configs in config-only dirs
are plain roots again (the documented comment-out conversion path no
longer strands every command behind invalid_store_pointer; non-mapping
scalars carry no pointer); the malformed reason splits into
unparseable vs non-string with accurate messages and fixes; the init
guard now refuses malformed pointers too and walks ancestors so a
pointer-repo subdirectory cannot grow a nested root that silently
diverts work; resolver and init share one classifyOpenSpecDir (the
classification can never diverge); readProjectConfig and
readStorePointer share one .yaml/.yml probe; the fourth copy of the
snapshot test helper is consolidated into test/helpers/fs-snapshot.ts;
the resolver header documents invalid_store_pointer; the
absolute-path warning wording is recorded as a spec amendment.

Full suite green (89 files, 1656 tests).

* Apply the 3.2 simplify pass and tick the roadmap

Simplify: isStoreSelectedRoot is a type guard (three redundant
conjuncts gone); the malformed-pointer reason strings single-source
through storePointerProblem in project-config (init's copies were
unpinned and could drift); the init guard drops its ternary for the
walk that finds projectPath in extend mode anyway. Skipped with
reasoning: directoryExistsSync consolidation (four pre-existing private
copies, out of slice), the warnings-array altitude (3.6 owns the
structured surface), the classification's module home (revisit when
3.6 consumes it).

Roadmap: 3.2 boxes ticked, changelog round recorded (including the
detached-HEAD process note), pointer moved to 3.3.
Full suite green (89 files, 1656 tests).

* Write and review the store-canonical-remote slice spec (3.3)

Two adversarial reviews converged on the contract holes, all folded:
the setup-rerun origin-erasure P1 (probe in both flows so
storeBackendsMatch stays consistent and the 1.3 rerun no-op survives);
register's write contract stated precisely (never commits, never
modifies an existing store.yaml; conversion identity stays
remote-free); the one-way strict-schema compatibility recorded as a
standing constraint for 3.4; mixed references dedup semantics
(normalize, dedup by id, first remote wins); verbatim-pasteable clone
fixes via ~/openspec/<id>; setup --remote refuses to be silently
ignored; the doctor example redrawn from the real layout; the
no-network clause pinned testably.

* Write and review the store-canonical-remote plan (3.3)

Both plan reviews approved with fixes, folded: clone fixes render
absolute home paths (tilde never expands outside a shell; agent JSON
consumers execute argv directly) with the spec amended to match;
setup's origin probe reaches both backend-resolution sites so reruns
cannot re-introduce the erasure P1, and stays out of
resolveGitStoreBackendConfig's hot read paths; the sharing-guidance
plumbing is concrete (StoreMutationResult carries canonical/observed,
JSON drops them, printMutationHuman renders the preference chain); the
setup-JSON contradiction resolved for the unchanged StoreOutput shape;
getOriginUrl trims; the --remote-vs-existing refusal fires in
prepareStoreSetup before any prompt or write; fill-if-absent dedup
pinned; registry anchors and test filenames corrected; TEST-NET
fixtures via git remote add.

* Record canonical and observed store remotes (3.3 checkpoint 1)

store.yaml gains an optional remote (strict schema retained; pre-3.3
files parse; unknown keys and empty remotes still fail). setup --remote
writes it before the initial commit, fails on empty values before
creating anything, and refuses with the hand-edit fix when store.yaml
already exists - silent flag acceptance is the forbidden outcome. Both
setup backend-resolution sites and register probe the local git origin
(gitOriginUrl, config read only) into the machine-local registry entry,
so reruns stay no-ops that preserve the record and re-register
refreshes it; conversion-created identity stays {version, id}. Doctor
surfaces metadata.remote and git.origin_url, with one human Remote line
preferring canonical. Sharing guidance names the canonical remote, then
the observed origin, then keeps today's wording - threaded through
StoreMutationResult.remotes and dropped from JSON.

15 new tests; three additive pins updated (doctor git shape x2, the
completions flag registry friction pin).

Full suite green (90 files, 1671 tests).

* Carry clone sources in reference declarations (3.3 checkpoint 2)

references: entries now accept {id, remote} maps alongside plain ids,
normalized to ReferenceDeclaration[] (dedup by id keeps the first
position; the first remote seen fills a missing one, never overrides).
The unresolved-reference fix becomes a verbatim-pasteable
git clone <remote> <home>/openspec/<id> && openspec store register ...
- absolute home path because tilde never expands outside a shell and
agent JSON consumers execute argv directly. An invalid id still wins
over any declared remote. The e2e onboarding journey executes the
printed fix verbatim (scratch HOME, local-path remote, split on the
shell &&) and continues to a resolved index - including the clone-trap
lesson that the origin must track anchor files. docs/cli.md documents
--remote, the store.yaml field, and the reference-with-remote form.

Full suite green (90 files, 1674 tests).

* Fix the 3.3 review findings

Three review mechanisms converged; all real findings fixed with
regression tests: register (and both setup sites) no longer probe the
origin of a non-repo store folder nested inside another repository -
git -C walks up, so the enclosing repo's origin could be durably
recorded and printed as sharing guidance (the shared
resolveBackendWithObservedOrigin helper guards with an at-root check
and deduplicates the triplicated probe block); the clone fix quotes
the checkout path, separates the remote with --, and renders only
shell-inert remotes (a config-committed --upload-pack or
metacharacter-bearing remote falls back to the teammate wording -
agents execute these fixes verbatim); setupPreparedStore re-asserts
the hand-edit refusal so metadata materializing between prepare and
execute cannot silently swallow --remote; a same-checkout origin
backfill now reports already_registered: true while still refreshing
the entry (the 1.3 rerun-reporting contract); the references warnings
distinguish dropped entries from dropped remotes; the dead zod union
for references is gone (the manual parser is the documented single
source); foundation's duplicate empty-remote message names its layer.

New pins: setup-rerun remote preservation, origin-backfill reporting,
the nested-repo guard, and the shell-safety gate.

Full suite green (90 files, 1678 tests).

* Apply the 3.3 simplify pass and tick the roadmap

Simplify: the duplicated store_remote_requires_hand_edit throw is one
factory (the TOCTOU re-assert can no longer drift from the prepare
guard); commitStoreRegistration restructures around a normalized
sameCheckout predicate - three near-identical returns become one, and
a symlinked-path remote refresh no longer misreports as a fresh
registration. Skipped with reasoning: the test fixture consolidation
(near the option ceiling), the checkout-location prose/computed split
and the ext:: transport hardening (both recorded as capstone notes),
doctor divergence display (spec-locked quiet form).

Roadmap: 3.3 boxes ticked, changelog round recorded, pointer moved to
3.4. Full suite green (90 files, 1678 tests).

* Write and review the store-targets slice spec (3.4)

Both adversarial reviews approved with fixes, folded: the apply
surface's indirect metadata flow (assembly runs inside
generateApplyInstructions with store targets passed through the
options bag); empty narrowing treated as undeclared; status always in
the JSON shape so agents see degradation; remote inheritance under
narrowing; the change-level grammar cliff owned explicitly;
KebabIdentifierSchema as the named validator with a neutral shared
kebab predicate replacing store-flavored naming; declared-root
sessions and the inert pointer-dir wrong turn covered.

* Write and review the store-targets plan (3.4)

Both plan reviews approved with fixes, folded: the artifact human
rendering anchored to printInstructionsText (instruction-loader
renders nothing); the unknown-store and root-resolution pins added;
validateStoreId delegates to the neutral isKebabId so one kebab regex
remains; the label-factory call corrected; the apply options bag
carries the resolved config path for fix text; inline expected strings
replace snapshot wording; the e2e gains a second non-narrowed change.

* Add the targets declaration layer (3.4 checkpoint 1)

One shared declaration-list parser now backs both references: and the
new targets: config field (identical normalization, dedup, and split
warnings - the 3.1/3.3 references pins stay green untouched).
ChangeMetadataSchema gains targets as kebab-validated ordinary
metadata, and the kebab grammar finally has one source of truth: the
exported isKebabId in change-metadata/schema, which validateStoreId
now delegates to. The pure src/core/targets.ts assembles the effective
set (change narrowing replaces the store list with remote inheritance
by id join; empty narrowing means undeclared; target_invalid_id and
target_not_declared degradation) and renders the XML block and
markdown section with pinned provenance wording.

Full suite green (91 files, 1690 tests).

* Surface effective targets in instructions (3.4 checkpoint 2)

Both instruction surfaces in both modes now carry the effective target
set: the artifact path assembles in instructionsCommand (change
context and config both in hand) and threads through
GenerateInstructionsOptions; the apply path passes storeTargets and
the resolved config path through the options bag and assembles inside
generateApplyInstructions where the change metadata loads. JSON gets
{source, repos, status} omitted-when-none; human output renders the
target_repos XML block and the Target Repos markdown section after the
referenced-stores blocks. Six surface tests cover provenance on both
surfaces, narrowing with remote inheritance beside a non-narrowed
sibling change, vocabulary warnings in JSON and human at exit 0,
omitted-when-none, pointer sessions reading the resolved root (the
pointer dir's own targets are inert), the unknown-store pin for target
ids, and non-instruction byte-identity. docs/cli.md documents the
declaration and the targets-vs-affected_areas split.

Full suite green (92 files, 1696 tests).

* Fix the 3.4 review findings

Three review mechanisms converged on polish-level findings (no P1/P2),
all folded: change-level target duplicates dedup to a set (first
occurrence wins); the non-array config warning names repo ids for
targets instead of borrowing the references noun; both instruction
surfaces now share ONE wiring shape - the artifact path passes raw
storeTargets/storeConfigPath like apply and assembly happens inside
the generator where change metadata lives (the silently-degrading
asymmetry a second caller would have tripped on); the shared
declaration type is renamed DeclarationEntry (it backs repos and
stores alike) with the stale references-only comment gone; the dead
KEBAB_ID_REGEX export is private again; METADATA_FILENAME is exported
and reused instead of two string literals; the spec's severity-cliff
wording amended to the real blast radius (instructions/status read
metadata; show/validate/archive never did). Recorded for later: the
workspace kebab-regex copy dies with 4.1; the all-invalid-store-ids
empty-repos render is distinguishable by status and stays.

Full suite green (92 files, 1696 tests).

* Apply the 3.4 simplify pass and tick the roadmap

Simplify: the conditional spreads at both command boundaries collapse
to plain optional fields (internal options, not JSON output); the
loader falls back to the self-read config's targets so library callers
omitting the option agree with the CLI wiring; cosmetic blank-line and
spec-wrap leftovers fixed. Skipped with reasoning: a shared id.ts home
for the kebab grammar (3.5's natural move), the references barrel
export note and parseJson consolidation (capstone), import-statement
merges (trivia).

Roadmap: 3.4 boxes ticked, changelog round recorded, pointer moved to
3.5. Full suite green (92 files, 1696 tests).

* Write and review the repo-map slice spec (3.5)

Both adversarial reviews approved with fixes, folded. The P1: the four
registry state-rebuild sites would silently erase the new repos:
section on the next store write - preservation is a pinned scenario
naming the sites. Also folded: repo-check precedence over both
unknown-store branches with a non-looping zero-stores fix; path AND id
cross-section uniqueness with four claimant codes; invalid_repo_id
wording with the --id hint for default folder names; the kebab
predicate's neutral id.ts home; pinned JSON contracts; the honest
one-additional-read wiring; TargetRepoEntry; the recorded Unicode
arrow and corrupt-registry silence decisions.

* Write and review the repo-map plan (3.5)

Both plan reviews approved with fixes, folded: the cross-section check
lives inside assertNoRegisteredStoreConflict (four call sites incl.
three operations preflights - hooking only the write helper would let
setup scaffold files before failing, so an early-reject pin is
planned); getRepoPath reconciled as a dumb id lookup whose 3.5 caller
is repo unregister while the enrichment uses listRepoEntries on its
own read; six missing test mappings added (store list/doctor with both
sections, empty-list verbatim, repo_not_found, mixed-registry positive
resolution, directory-untouched unregister, both-surface enrichment);
two code-map anchors corrected.

* Add typed registry sections and the repo map core (3.5 checkpoint 1)

The machine-local registry gains an optional strict repos: section
beside stores:, carried through parse, serialize, and both store write
helpers (the preservation matrix is pinned - a schema-only change
would have silently erased every repo mapping on the next store
write). Cross-section uniqueness for ids AND paths lives inside
assertNoRegisteredStoreConflict (covering the three operations
preflights) and the new assertNoRegisteredRepoConflict, with the four
claimant codes plus in-section repo_id_conflict/repo_path_conflict.
registerRepo/unregisterRepo/listRepoEntries/getRepoPath form the core
API (rerun no-op, repo_not_found, corrupt-registry null). The kebab
grammar moves to its neutral src/core/id.ts home; change-metadata
re-exports, store foundation and targets consume it, and registry key
validation produces label-accurate wording.

Full suite green (93 files, 1705 tests).

* Add the repo command group, typed rejection, and path enrichment (3.5 checkpoint 2)

openspec repo register/unregister/list manage the machine-local repo
map with the pinned JSON contracts (folder-name default ids with the
--id fix when grammar fails; repo_path_missing/not_directory;
repo_not_found; rerun no-op; unregister never touches the checkout).
--store with a registered repo id now rejects with store_id_is_repo
before BOTH unknown-store branches - including zero-stores, whose fix
suggests a different id instead of looping into the cross-section
conflict - and propagates through the 3.2 pointer with the Declared-in
prefix. Effective-target entries gain a local path when the repo map
resolves them (TargetRepoEntry; arrow and combined renders; one
additional registry read in loadRootConfigContext; corrupt registry
yields bare entries silently). Completions registry, friction pins,
and docs updated; store setup with a repo-claimed id is pinned to
create nothing.

Full suite green (94 files, 1714 tests).

* Fix the 3.5 review findings

Three review mechanisms converged; all fixed with regression tests:
the library API enforces its own invariants (registerRepo validates
path-then-id with typed repo_path_missing/not_directory and
invalid_repo_id errors; unregisterRepo validates ids - a 4.1 caller
gets input errors, not serialize-time registry-corruption noise; the
command rewraps default-folder-name grammar failures with the --id
fix); no-op reruns never take the write lock or rewrite the registry
file (mtime/format churn pinned away); the stale getRepoPath pre-read
in unregister is gone (the locked removal is authoritative); the repo
map is read unconditionally so change-only targets enrich too; a
hand-edited registry with one id in both sections now fails clearly at
parse time instead of resolving ambiguously; store_id_is_repo embeds
its action in the message (human wrappers print message only - the
recorded family precedent); the register/unregister JSON shapes split
into total types; the docs Repo map heading no longer re-parents the
default-store subsection.

getRepoPath stays exported as recorded 4.1 groundwork (unit-tested,
no production caller yet - the 3.3 persisted-remote precedent).

Full suite green (94 files, 1718 tests).

* Apply the 3.5 simplify pass and tick the roadmap

Simplify: the third copy of the JSON/failure plumbing collapses into
commands/shared-output (one definition of the failure contract, used
by store and repo); the same-mapping predicate is hoisted in
registerRepo; the kebab grammar wording single-sources through
KEBAB_ID_DESCRIPTION; an unused test import and two docs nits fixed.
Skipped with reasoning: the registry-state builder quadruplication
(settled mirror territory), validator placement, the unconditional
registry read (measure-by-reasoning verdict: the only correct gate
needs data that arrives after the read on the apply path).

Roadmap: 3.5 boxes ticked, changelog round recorded, pointer moved to
3.6. Full suite green (94 files, 1718 tests).

* Write and review the relationship-health slice spec (3.6)

Both adversarial reviews approved with fixes (two P1s each,
converging), all folded: the exit-code rule now mirrors store
doctor's REAL contract (health findings exit 0; the draft cited a
nonexistent errors-exit-1 behavior); the JSON shape gains the lock's
separate store-metadata section and the 3.4-recorded inert-pointer
deferral lands as pointer_declarations_inert; a real
includeSpecs:false assembler mode replaces the strip-after hedge; the
assembler accepts a pre-read registry so one read feeds everything;
target_unmapped suppressed under unreadable registries;
grammar-invalid targets synthesize bare entries; the both-shapes
detection mechanism and stderr duplication recorded; the
STORE_SELECTION_GUIDANCE consequence scoped; missing scenarios added.

* Write and review the relationship-health plan (3.6)

Both plan reviews converged on three P1-grade holes, all folded: the
registry-injection option inverted the established null semantics (a
fresh machine with no registry file would have been marked unreadable
- the option is now registryEntries with [] = empty and null =
unreadable, mirroring the assembler's post-read variable);
resolveRootForCommand needs an additive allowImplicitRoot
pass-through (it forwards only store/storePath today); and the
invalid-target synthesis would have required parsing ids out of
message strings (the inspector receives raw declarations and uses
isKebabId). Plus: the inert-pointer re-walk named (the declared root
is the store; findRepoPlanningRootSync(cwd) finds the pointer dir);
the human-rendering contradiction resolved in favor of the spec
transcript; truncation-never and pass-through pins mapped; the dead
status key dropped from the failure payload.

* Add the health-mode assembler options and the relationship inspector (3.6 checkpoint 1)

assembleReferenceIndex gains includeSpecs:false (skipping the
spec-file reads AND the byte budget - health entries carry no
specs/fetch keys and the content-only truncation diagnostic can never
appear) and registryEntries injection with the [] -vs- null semantics
that mirror the assembler's own post-read variable (a naive raw-read
injection would mark every fresh machine unreadable). The pure
src/core/relationship-health.ts composes the doctor command's gathered
inputs into the lock's four separated categories, synthesizing
target_unmapped (suppressed under unreadable registries), structural
target_invalid_id entries from the raw declarations (never parsed from
messages), relationship_registry_unreadable, root_pointer_ignored,
pointer_declarations_inert, and the store_remote_divergence info note.

Full suite green (95 files, 1727 tests).

* Add openspec doctor (3.6 checkpoint 2)

The root-scoped relationship-health command: resolves like every
normal command (with the new additive allowImplicitRoot pass-through
on resolveRootForCommand and the null-shape failure payload), gathers
with ONE registry read feeding references, targets, and the unreadable
signal coherently, detects the both-shapes and inert-pointer wrong
turns (the latter via the cwd re-walk, working from subdirectories),
reads store facts for explicit and declared store-backed roots, and
renders the three-heading transcript voice with (none declared)
sections and Fix lines. Health findings of any severity exit 0; only
command failures exit 1. STORE_SELECTION_GUIDANCE gains doctor and the
skill-template parity hashes update deliberately; completions and the
--store description pins extended. Eight e2e tests cover the full
matrix incl. empty-vs-unreadable registries, divergence info, and the
read-only snapshot.

Full suite green (96 files, 1735 tests).

* Fix the 3.6 review findings

Three review mechanisms converged; all fixed with regression tests:
human-mode command failures now print the taxonomy Error/Fix lines
instead of a raw stack trace (the action gained the sibling-standard
try/catch); stale repo mappings surface as target_path_missing (the
lock's 'target checkout health' now actually stats mapped paths);
self-reference-emptied reference lists render '(declared references
all resolve to this root)' instead of the false '(none declared)'; a
malformed store: pointer on a real root surfaces as
root_pointer_invalid (the resolver is silent there); the synthesized
target_invalid_id fix carries the real config path; the inspector
reuses toRootOutput; instructions' registry read now feeds the
reference assembler through the 3.6 injection point (no more torn
snapshots between repoPaths and the index); the human renderer's
duplicated section loops collapse into shared helpers; the spec's
exit-1 list gains the recorded corrupt-store.yaml amendment (store
resolution rejects before doctor runs - a doctor-only resolution path
would break the one-resolver invariant).

Full suite green (96 files, 1739 tests).

* Apply the 3.6 simplify pass and tick the roadmap - Phase 3 complete

Simplify: readRegistrySnapshot extracts the torn-snapshot invariant
into one place (doctor and instructions both consume it); doctor's
catch routes through emitFailure, fixing a --json inconsistency where
post-resolution failures printed human lines without a JSON payload;
shared asStatus duck-types the diagnostic envelope so
RootSelectionError fixes survive; the inspector reuses
storePointerProblem (the fifth phrase copy dies); the existsSync sweep
stats only declared targets; the dead toRootOutput import removed.
Skipped with reasoning: the warning-factory extraction (the fourth
copy does not fit the shape), the config-path-fallback micro-helper.

Roadmap: 3.6 boxes ticked, Phase 3 marked complete on the branch,
changelog round recorded, pointer moved to 4.1.
Full suite green (96 files, 1739 tests).

* Trim the review profile for Phase 5 deletion slices

* Write and review the assemble-working-context slice spec (4.1)

Both adversarial reviews approved with fixes, converging on the
deletion-grounding P1s: binding.ts dies whole (5.1 kept it only for
workspace/foundation's import - with workspace/ gone it would be
exactly the hidden-not-deleted state the criteria reject) and the five
workflow-template workspace-planning guards 5.1 deeded here join the
deletion list with their parity churn named. Also folded: the
change-status-policy cascade enumerated; the shared doctor/context
data gather made mandatory with context recorded as silent on wrong
turns; the member-mapping table pinned; code-workspace write semantics
pinned; getRepoPath deleted rather than re-hidden; fetchRecipe
exported; the naming paragraph recorded.

* Write and review the assemble-working-context plan (4.1)

Both plan reviews approved with fixes, folded: the spec's
code_workspace_exists diagnostic collides with the vocabulary sweep's
workspace_* ban - amended to context_file_exists; the parity test's
workspace-planning guard assertion flips to absence; the policy
tranche names ChangeStatus.affectedAreas and the artifact-graph barrel
re-export; doctor-extraction weakened to behavior-identical; the
unresolved-members-stderr e2e mapped; the sweep guardrail reworded
honestly; stale hedges resolved. Both reviewers verified the deletion
order dependency-safe and every anchor accurate.

* Delete the workspace opening machinery (4.1 checkpoint 1)

The absorbed 2.3, executed leaves-first: the ten workspace-planning
template guards (parity test flipped to a no-residue assertion); the
change-status-policy cascade (summarizeAffectedAreas,
AffectedAreasSummary, affectedAreas plumbing, workspaceName, the
workspace-planning mode member, the workspace next-steps, the
artifact-graph barrel re-export); planning-home collapsed to repo-only
(PlanningHomeKind = 'repo'; the workspace state read and
workspace-planning default schema die); src/core/workspace/ whole
(897 lines) with its barrel line and tests; store/binding.ts whole
(~300 lines - 5.1 kept it only for workspace/foundation's import)
with its barrel line and binding tests; getRepoPath (its recorded
consumers evaporated). The library pins that froze the carve-outs die
with the behavior; the six legacy-groups CLI-surface pins stay green
untouched. The deletion ledger marks the carve-outs executed and the
workspace_skills vocabulary-allowlist entry is pruned. No
.openspec-workspace reads remain anywhere in src.

Net: 27 files, -2,196 lines / +40.

Full suite green (94 files, 1706 tests).

* Add openspec context, the assembled working set (4.1 checkpoint 2)

The working set a root's declarations describe, in one command: the
JSON agent brief (root + members with roles, absolute paths, fetch
recipes on available stores, and the existing fixes verbatim on
unavailable members), the human listing with the Not-available
section, and the --code-workspace editor view (available members only;
ref:/repo: folder prefixes; the pinned write matrix - typed
context_file_exists refusal, --force, no implicit mkdir, stderr
confirmation under --json; stale mapped paths excluded - reported, not
guessed). Assembly is presentation over the 3.6 composition through
the new shared command gather (doctor refactored onto it,
behavior-identical); fetchRecipe exported as the one recipe source.
STORE_SELECTION_GUIDANCE gains context with the parity hashes and
completions pins updated deliberately; docs add the section and the
project-context vs working-context disambiguation.

Full suite green (95 files, 1711 tests).

* Fix the 4.1 review findings

Three review mechanisms converged; all fixed with regression tests:
the --json + --code-workspace failure path now leaves exactly one JSON
document on stdout (the write runs before the brief is printed; both
failure modes pinned); context mirrors doctor's self-reference honesty
('Declared references all resolve to this root' instead of the false
'nothing declared'); the registry degradation is selected by
diagnostic code, never by array position (the fragile health.status[0]
coupling and the redundant boolean+diagnostic pair are gone); the
write summary names the skipped member ids instead of pointing JSON
users at a listing that is not there, with the count arithmetic in
plain form; the dead planningHome params on
buildNextSteps/buildActionContext inputs and their loader threading
are removed; the leftover binding imports in registry.test.ts and
three pieces of edit debris are swept; the ledger's Surviving-tokens
section is pruned; the doctor docs section cross-links context; and
the spec's working-set/builder unit-test bullet is fulfilled
(test/core/working-set.test.ts - the mapping table, ordering,
availability rule, by-code selection, and builder shape).

Skipped with reasoning: suppressing the resolver's both-shapes stderr
warning for context runs (codex P3) - that warning is 3.2 family
behavior for every command at resolution time; forking it per command
would fragment the one-resolver contract. Recorded for the capstone.

Full suite green (96 files, 1715 tests).

* Apply the 4.1 simplify pass and tick the roadmap - Phase 4 complete

Simplify: the stale-path stat sweep moves into shared-gather as
missingDeclaredRepoPaths (doctor and context both consume it; the
header comment now tells the truth); the dead Windows-path machinery
in planning-home dies with the stale workspace-kind test that was its
only exerciser (formatChangeLocation collapses to path.relative); the
garbled vocabulary-sweep comment is repaired; doctor's dead fs import
removed; the context_output_dir_missing code recorded as a plan
amendment instead of silent drift. Skipped with reasoning: the
printEntryDiagnostics extraction (net-zero lines, couples two
surfaces' voices); the three filter passes (readability beats a
one-pass accumulator at single-digit N); PlanningHomeSummary identity
(recorded for the capstone).

Roadmap: 4.1 boxes ticked, Phase 4 complete on the branch, pointer
moved to the Phase 5 remainder.
Full suite green (96 files, 1714 tests).

* Execute the Phase 5 remainder - 5.1 fully closed

Per the locked delete-don't-hide criteria, after 4.1 as queued
(decision record: slices/delete-legacy-command-groups/remainder.md):
schemas/workspace-planning/ deleted (openspec schemas still advertised
the dead workflow); the four workspace-* beta change folders deleted
(unimplemented relics - archiving would assert completion; git
preserves); L2 decided - the four wholly-workspace accepted specs
deleted (capability gone = spec gone) and the workspace requirements
excised from cli-config and cli-artifact-workflow (two requirements,
eight scenarios - bounded short of the docs rewrite the roadmap
forbids). Incidental mentions in five other specs recorded for the
capstone vocabulary audit. All 36 remaining accepted specs validate;
full suite green untouched (96 files, 1714 tests).

* Capstone: all four persona journeys pass (6.1)

Journeys 2 and 3 land as standing e2e in
test/cli-e2e/capstone-journeys.test.ts - the layered PM-to-dev flow
(an app-repo agent discovers the reference from config via openspec
context, cites the upstream spec by following the fetch recipe
verbatim, and writes its design change in the app repo's own root
while the store stays read-only) and externalized planning (a code
repo with only a store: pointer runs new-change through archive with
zero --store flags and never grows planning state). Journey 1 is the
standing store-lifecycle e2e. Journey 4 ran as a live cold-start
headless dogfood: a fresh codex session given only a vague prompt and
--help output assembled the full intended topology - store setup,
targets declaration, pointer config, repo mapping, and
doctor/context/validate self-verification. Results recorded in
capstone/journeys.md.

Full suite green (97 files, 1716 tests).

* Capstone: usability audits done (6.1)

Error-catalog walk: 55 wrong turns exercised live across 13 families
(human + JSON) against the actionable/store-carrying/correct-exit/
honest bar - 46 pass. The resolution-layer taxonomy held
(differentiated no-root hints, single-document JSON failures,
shell-parseable clone fixes, bidirectional namespace collisions). Nine
failures recorded and queued for the capstone fix round: 1 P1 (raw
YAML stack trace on unparseable real-root configs), 4 P2 (pathless
corrupt-registry fix that dead-ends through store doctor, instructions
dropping its Fix line, validate summaries without drill-down,
implicit scaffolding creating doctor-unhealthy roots), 4 P3.
Vocabulary sweep incl. docs/cli.md: clean except the legacy
ChangeStatus.initiative JSON passthrough (queued; the schema keeps
parsing user data). Time-to-first-success measured live: 2 commands,
2 concepts, each step printing the next command.

* Fix the capstone usability-audit findings

All nine error-catalog failures plus the vocabulary finding, with the
test pins updated deliberately:

P1 - unparseable real-root configs no longer dump a YAMLParseError
stack trace: readProjectConfig warns with one line naming the file and
the first error line only (pinned: single line, no node_modules).
P2 - the corrupt-registry fix names the actual registry file path; the
CLI's shared error wrapper (17 catch sites) now prints the diagnostic
fix line it used to drop, so instructions and every sibling carry the
pasteable next step; validate failure summaries print a drill-down
command carrying --store (derived from the resolved root); implicit
scaffolding (new change in a bare dir, non-interactive init) now
creates the complete healthy shape - specs/, changes/archive/, and a
minimal config.yaml - so doctor calls the result ok instead of
unhealthy.
P3 - the malformed-pointer warning on real roots names the file; the
declared-pointer unknown-store fix is reshaped for the actual mistake
(register the store or edit the named config - the user never passed
--store); the store-register-at-code-repo fix offers repo register;
archive not-found lists available changes like its status sibling.
Vocabulary - the legacy ChangeStatus.initiative passthrough is gone
from every surface (status JSON/human, instructions XML, apply text);
the metadata schema still PARSES stored links (user-data tolerance,
pinned by the flipped legacy tests: tolerated, not re-emitted).

Full suite green (97 files, 1716 tests).

* Capstone: technical audits done (6.1)

Single-resolver invariant HOLDS: one precedence implementation, nine
command entry points through it, doctor/init extra walks verified as
post-resolution diagnostics and scaffold guards; one latent
unreachable fallback queued for deletion. Dependency direction HOLDS:
zero core->commands/cli imports. Dead-code sweep over the 213-file
delta: no P2s, five P3s queued, four notes recorded (incl. the ext::
transport status: zero occurrences, the shell-safe gate and
team-committed trust boundary hold). Module sizes bounded (largest
1,160 lines). docs/agent-contract.md committed - every JSON shape,
the diagnostic envelope, failure payloads, the exit-code contract, and
the full diagnostic-code catalog verified against emitting code, with
14 consistency findings; the gauntlet-grade one (several --json
failure paths emit no JSON document) is queued for the gauntlet fix
round. Net LOC vs origin/main: src -4,478, test -325 - net-negative
as the roadmap expected.

* Capstone: whole-delta gauntlet run - findings ledger (6.1)

Four mechanisms over origin/main...HEAD: /code-review at max effort
(all 12 verified candidates CONFIRMED, most live-reproduced), a
32-agent adversarial Workflow (six lenses, refute-style verification:
25 confirmed + 7 completeness gaps), a codex whole-delta review
(FIX-FIRST), and the audits' queued items. Consolidated: 2 P1 (the
~/openspec layout turning $HOME into a phantom nearest root that
captures every lifecycle command under the home tree; status/
instructions --json errors emitting no JSON document), 13 P2 (the
JSON-failure-contract family, the --store-path seam, doctor's
up-walking origin probe, the stale registry lock, config-only
half-scaffolds, prompt-injection via verbatim hostile strings, five
more accepted specs requiring deleted behavior, stale planningHome
guidance in generated skills, a syntactically-broken zsh completion
script, store-remove delete-before-commit, the setup TOCTOU pair, the
orphaned-.git empty-clone path, the metadata rollback race), and a
triaged P3 set split into queued-cheap vs recorded-for-report. The
gauntlet box ticks only when every P1/P2 is fixed and re-verified.

* Fix every gauntlet P1/P2 plus the cheap P3 set (6.1)

P1: the nearest-root walk now skips openspec/ directories that are
neither planning-shaped nor configured - the recommended ~/openspec
store layout no longer turns $HOME into a phantom root that captures
every command under the home tree (regression test: the
registered-store hint fires instead). status/instructions/list/show/
validate --json failures all emit exactly one JSON status document
(JSON-aware shared failure helper; the stray blank stdout lines are
gone; store <unknown subcommand> --json emits a typed document; list
carries its null-shape).

P2: doctor/context gain the --store-path rejection seam; doctor's
origin probe is guarded by isGitRepositoryAtRoot (no more enclosing-
repo origins or spurious divergence notes); the registry lock steals
orphans older than 30s, names the lock path in the busy fix, and
reports permission problems as what they are; change scaffolding
completes the root shape for config-only roots and records the project
default schema, never a one-change --schema override; hostile-content
renders are sanitized at the index/render boundary (spec ids,
summaries at index time, remotes in targets and divergence messages -
control characters can no longer forge instruction lines); the five
remaining workspace-requiring accepted specs got the bounded excision
(all 36 validate); status JSON carries planningHome again (the
generated skills' published archive contract - restored rather than
rewriting eleven template references); the zsh completion generator
uses the correct quote idiom (generated script now passes zsh -n);
store remove commits the registry removal BEFORE deleting files (a
failed deletion degrades to a store_files_left_on_disk warning, never
a phantom registration); setup re-asserts directory facts at execute
(store_setup_path_changed) killing the stale-kind recursive-rm TOCTOU;
the half-made .git cleanup no longer hides behind the created-paths
ledger (no more commitless-store reruns); the metadata rollback
re-reads the registry and never deletes metadata a committed
registration depends on.

P3 (cheap set): CommonMark-correct fence tracking in purpose
extraction; the stale-target sweep requires a DIRECTORY; pretty JSON
for empty list; the declared-pointer repo-id fix names the config
file; absolute change location when the root is not the cwd; docs
fixes (affected_areas legacy wording, --remote in the setup table,
vibe in --tools, the real list output example); agent-contract.md
updated to match (planningHome restored, the failure-contract claim
now true).

Test pins updated deliberately: the store --json hint, the remove
ordering contract, the zsh escaper, the truncation corpus (summaries
now cap at index time, so the budget trips on count).

Full suite green (97 files, 1717 tests).

* Capstone complete: gauntlet passed, release-readiness report committed (6.1)

All 15 gauntlet P1/P2 fixes re-verified live (the JSON-contract codes
on show/validate/status/instructions/store, the --store-path seam on
doctor, the stale-lock steal, the config-only scaffold completion, the
phantom-root regression). The gauntlet ledger marks every finding
fixed. The release-readiness report lands with the five-minute
new-user story (2 commands, 2 concepts, proven cold by a headless
agent), the full audit results, the 18-entry autonomous-decision
ledger, and known gaps mapped to Later Ideas - no open P1/P2 findings
anywhere. Every queue item's roadmap boxes are ticked except Merged to
main, which this run deliberately does not perform.

Full suite green (97 files, 1717 tests); 36 accepted specs validate.

* Fix the phantom-root regression test's environment dependence

The G1 test omitted globalDataDir and never registered a store, so it
passed locally only by accident (it saw this machine's REAL registry)
and failed on the clean CI runner, where the empty registry correctly
fell through to the implicit root. The test now registers a store in
its isolated registry and passes globalDataDir, making the
no_root_with_registered_stores expectation deterministic everywhere.

Full suite green (97 files, 1717 tests).

* Record the user-directed workset correction (post-capstone review)

* Record 4.2 personal worksets with FR1; supersede the change-anchored direction

* Record 4.2 FR2: tool opening with the two-style extensible opener pattern

* Flesh out 4.2 personal worksets as a full roadmap item with its goal run

* Renumber personal worksets to Phase 7 item 7.1

* Add the 7.1 capstone dogfood and branch-push steps to the goal run

* Add the 7.1 personal-worksets research checkpoint

Evidence base for the spec: the f858c19^ opener archaeology (two-style
launch split, PATH/PATHEXT scan, cross-spawn handoff mechanics, the
not-to-inherit ledger), current-tree idioms (registry lock/atomic-write,
the pure .code-workspace builder, the @inquirer house rules, JSON
contracts), and live CLI verification of code/cursor/claude/codex flag
spellings and hazards.

* Windows-compatibility pass per test/AGENTS.md

A two-sided audit of the whole delta (production code and tests)
against the cross-platform rules, with every finding fixed:

Production: extractFirstPurposeLine splits on \r?\n (CRLF checkouts -
the Git-for-Windows default - previously got empty summaries for every
referenced spec); the clone-recipe fix quotes for the rendering
platform (single quotes are literal characters in cmd/PowerShell -
win32 now gets double quotes); the registry path-comparison fallback
resolves nonexistent paths instead of raw string-comparing them; the
manual-deletion fix drops its POSIX-only rm -rf; repo register expands
~ via the same expandUserPath every store command uses.

Tests: the onboarding e2e no longer depends on HOME (USERPROFILE set
alongside), declares its local remote in shell-safe forward-slash
form, pins the platform-correct quote style, and executes the fix via
argv arrays instead of split(' ') re-tokenization (paths with spaces);
the store-references normalizer matches the JSON-escaped needle
(serialized Windows paths double their backslashes); the
metadata-path assertion uses path.join; snapshot keys are
POSIX-normalized in the shared helper and both local copies.

Audited clean: registry/conflict path identity (canonicalized both
sides via realpathSync.native), cross-drive path.relative guards,
getGlobalDataDir's win32 branches, git invocations (argv arrays),
the lock and atomic-write semantics, XDG isolation, fetch-recipe
splits (no paths), and the deliberate-POSIX display literals.

CI: the OS test matrix (linux/macos/windows) previously ran ONLY on
push to main - it now also runs on workflow_dispatch so branches can
get a real Windows verification before merge.

Full suite green locally (97 files, 1717 tests).

* Make the clone-fix unit pin platform-aware

The references.test.ts pin asserted the POSIX single-quote form; the
implementation now deliberately renders double quotes on win32 - the
one remaining windows-pwsh matrix failure. The doctor/context pins are
quote-agnostic (stringContaining on the unquoted prefix) and the
onboarding e2e was already platform-aware.

* Write the 7.1 personal-worksets spec; fold the dual spec review

Subagent: approve-with-fixes; codex: reject (converging). The P1 -
attach-dirs argv now carries one attach pair per member, primary
included, per the locked FR2 wording. Folded: no-tool open path,
stale-saved-tool rule, signal exit contract, the hand-edit parse
contract, pinned JSON envelopes (incl. the open --json typed
rejection), derived-file locking with ENOENT-tolerant remove, the
teammate scenario, the win32 availability matrix, and opener-config
touchpoints. Research+spec roadmap box ticked; changelog entries added.

* Write the 7.1 personal-worksets plan; fold the dual plan review

Subagent: approve-with-fixes; codex: reject (converging). The shared P1:
open now regenerates the .code-workspace under the lock BEFORE tool
resolution, so every fallback names an existing current file. Also
folded: real busy-error factory sites with new byte-shape pins (the
suite never covered the lock mechanics), withWorksetsLock, cross-spawn
import shape, the --member collector, injectable-spawn units for
SIGINT/launch-failed, in-process cancellation coverage with enumerated
capstone carve-outs, the win32 stat-seam fixture strategy, the recorded
TOCTOU, anchor drift fixes, and the spec d12 amendment dropping the
dead workset_create_cancelled code.

* 7.1 CP1: worksets core, opener table, shared file-state mechanics

src/core/file-state.ts extracts writeFileAtomically and the lock-acquire
loop from store foundation (errors stay caller-owned via the injected
factory; store shapes pinned byte-identical by new tests - the suite
never covered the lock mechanics before). src/core/worksets.ts: the
saved-views file under <dataDir>/worksets/ on the registry idiom (strict
zod + version 1, hand-edit parse contract, withWorksetsLock
read-without-write, pure rebuilds, the .code-workspace builder).
src/core/openers.ts: the locked built-in table, per-field config merge
over built-ins, the PATH/PATHEXT availability scan with an injectable
stat seam, and the pure two-style launch-command builder (one attach
pair per member, never a positional). GlobalConfig gains the openers
key. 42 new unit tests; full suite green (99 files, 1759 tests).

* 7.1 CP2: the workset command group, registration, docs, and tests

src/commands/workset.ts: create (guided 3-step wizard / non-interactive
--member collector with name=path labels), list, open (regenerate the
.code-workspace under the lock before any tool resolution so every
fallback names a current file; cross-spawn handoff with honest
exit-code and 128+signal propagation; the Open manually: block on every
cannot-drive failure; hidden --json rejected as one typed JSON
document), remove (plan-then-confirm, --yes, ENOENT-tolerant derived
cleanup under the lock), and the command:* unknown-subcommand handler.
isPromptCancellationError extracted to shared-output (third copy).
CLI + completions registration, the docs/cli.md section and table rows,
the resurrected path-env helper, the fake-tool recorder, 34 command
tests (incl. in-process launch mechanics and interactive-cancellation
coverage via mocked prompts), and the two e2e journeys (no-footprint +
teammate isolation). Full suite green (101 files, 1795 tests).

* Tick 7.1 implementation and tests boxes; record the implementation round

* 7.1 review round: fix all converged P2s from the three review mechanisms

Spec-compliance (compliant-with-fixes), /code-review seven-angle
fan-out, and codex (approve-with-fixes) converged with no P1s.
Behavioral: structural open-fallback rule (surviving members, every
post-regeneration failure except cancellation), the primary-
reassignment note, honest zero-tools message, pasteable launch-failed
alternative, post-save Ctrl-C declines instead of cancelling, parent
signal guard during launch (the 128+n contract was unreachable for tty
SIGINT), sync spawn throws wrapped, tool.cmd PATHEXT double-append
removed, bare workset --json keeps the one-document contract,
deadline-bounded lock stat failures, remove cleanup after the durable
write, early flag-member validation, lazy cross-spawn (~6ms per CLI
invocation). Structure: command layer split (workset / prompts /
input); shared homes for formatZodIssues, folderStyleNameProblem,
KEBAB_ID_FIX, pathIs*; cancellation lifted into emitFailure with
store collapsed onto it. Tests: +6 cases, controlled PATH for the
in-process interactive suite, win32 path-env key fix. Spec amended to
the shipped contracts. Full suite green (101 files, 1799 tests).

* 7.1 simplify pass: collapse the parallel mechanisms the reviews queued

makeLockErrorFactory in file-state (both lock-error factories were
data-twins; store shapes stay byte-pinned), optsWithGlobals over the
hand-rolled group-option merge, the prompt preview ladder flattened
with one assertKnownTool spelling, asErrorMessage hoisted to
shared-output, formatMemberRows deduping three renderers, per-branch
opener resolution in open (dead branch + redundant re-scan gone),
serialize emits validated entries directly, toWorkset dedup, remove
--yes skips the duplicate pre-read, KEBAB_ID_FIX adopted, dead exports
trimmed. Skips recorded (store-group fallback convergence queued for
the next store touch). Full suite green (101 files, 1799 tests).

* 7.1 capstone dogfood passes; transcript committed, box ticked

Scripted walk (both launch styles, exact argv incl. the no-prompt
rule, strand-test fallback, missing-member skip, safe remove,
byte-untouched members), the interactive wizard from a real pty, live
cancellation, and the cold-start headless agent reaching an opened
workset from --help alone. No product findings. Full suite green
(101 files, 1799 tests).

* Close 7.1: pushed-branch box ticked, glance and pointer finalized

* Fix the Linux-only CI failure in the workset launch-failure test

The fixture was a shebang-less text file: macOS posix_spawn rejects it
with ENOEXEC (the spawn error the test wants), but glibc execvp
retries ENOEXEC via /bin/sh, so on Linux the child runs and exits 127
instead of erroring. A shebang pointing at a missing interpreter fails
ENOENT on every POSIX libc with no shell fallback; a garbage
claude.exe covers the win32 matrix leg the same way. Verified in a
node:20 Linux container (old fixture reproduces exit 127; full workset
file passes with the fix) and on macOS (full suite, 1799 tests).

* Add the stores beta user guide

A problem-first guide for the new surface (stores, references,
targets, repo map, doctor, context, worksets) under docs/stores-beta/,
mirroring the old workspaces-beta layout. Built around two team
stories — one team sharing a planning repo, and requirements crossing
team lines — with every command output captured from a live walk of
the current build in isolated scratch state. Carries the beta notice
(shapes may change), the verified resolution-precedence table, known
limitations including the one-checkout-per-store-id rule and the
commands that stay cwd-based, and the real on-disk state locations.
Linked from the README, getting-started, and the cli.md stores
section, which gains the same beta note.

* Carry the beta note on the worksets section of the CLI reference

The note at the top of the Stores section names worksets but is
invisible to a reader deep-linking straight to Personal worksets.

* Fix store --json missing-subcommand output

* Disable CLI-agent workset openers by default

* Remove targets and repo map commands

* Update simplify-context docs after removing targets

* Harden store-root test isolation

* Remove generated review HTML artifacts

* Refresh PR cleanup evidence
2026-06-23 16:53:23 +00:00
Tabish Bidiwale fd92ccca74 [codex] Add context stores and initiative views (#1127)
* Document initiative-led workspace direction

* Add context stores and initiative change links

* Let workspaces open initiative views

* Add workspace root bundle artifacts

* Support legacy workspace roots in planning resolution

* Remove accidental workspace root bundle artifacts

* Bundle workspace reimplementation docs into roadmap

* Preserve workspace context store bindings

* Address review feedback for context store initiatives

* test: canonicalize context store path assertions

* Refine context store and workspace core boundaries

* Avoid initiative diagnostic regex backtracking
2026-05-27 08:06:02 +00:00
Tabish Bidiwale 93f7b797cf fix: prefer native realpath for canonical paths (#972) 2026-04-14 07:43:43 +00:00
pig 62d4391268 fix: improve Windows compatibility in tests (#646)
- Replace Unix 'cat' command with fs.readFile in spec.test.ts

- Replace 'mkdir -p' and 'bash' commands with fs.mkdir/writeFile in validate.enriched-output.test.ts

- Skip symlink test on Windows (requires admin privileges) in file-system.test.ts
2026-02-01 22:57:47 -08:00
Rodrigo Passos 697738bc9b fix(opencode): transform command references from colon to hyphen format (#626)
* Add OpenCode files to gitignore

* docs(changes): add opencode-command-references change artifacts

* fix(opencode): transform command references from colon to hyphen format
2026-01-30 13:51:47 -08:00
Tabish Bidiwale 12a7224dc6 chore: remove TDD schema and all references (#586)
* chore: remove TDD schema and all references

TDD was an internal test example that should not be in user-facing docs.
This removes:
- The schemas/tdd directory and all its templates
- All TDD references from documentation
- TDD examples from skill templates and source code comments

* test: update tests to remove TDD schema references

All tests that referenced the removed TDD schema have been updated
to use spec-driven or custom-schema names instead.

* chore: remove accidentally committed files
2026-01-26 02:31:48 -08:00
Tabish Bidiwale 39bebefcc4 feat(cli): merge init and experimental commands (#565)
* feat(core): add legacy cleanup detection functions for init migration

Implement src/core/legacy-cleanup.ts with detection and cleanup functions
for all legacy OpenSpec artifact types:

Detection functions:
- detectLegacyConfigFiles() - checks for config files with OpenSpec markers
  (CLAUDE.md, CLINE.md, CODEBUDDY.md, COSTRICT.md, QODER.md, IFLOW.md,
  AGENTS.md, QWEN.md)
- detectLegacySlashCommands() - checks for old /openspec:* command
  directories and files across all 21 tool integrations
- detectLegacyStructureFiles() - checks for openspec/AGENTS.md and
  openspec/project.md (project.md preserved for migration hint)
- detectLegacyArtifacts() - orchestrates all detection

Utility functions:
- hasOpenSpecMarkers() - checks if content has OpenSpec markers
- isOnlyOpenSpecContent() - checks if file is 100% OpenSpec content
- removeMarkerBlock() - surgically removes marker blocks from mixed content

Cleanup functions:
- cleanupLegacyArtifacts() - orchestrates removal with proper edge cases:
  - Deletes files that are 100% OpenSpec content
  - Removes marker blocks from files with mixed content
  - Deletes legacy slash command directories and files
  - Preserves openspec/project.md (shows migration hint only)

Formatting functions:
- formatDetectionSummary() - formats what was detected before cleanup
- formatCleanupSummary() - formats what was cleaned up after

This is task 1.1 for the merge-init-experimental change.

* feat(utils): add removeMarkerBlock() for surgically removing marker blocks

- Add removeMarkerBlock() function to file-system.ts that properly handles
  inline marker mentions by using findMarkerIndex/isMarkerOnOwnLine
- Refactor legacy-cleanup.ts to use the shared utility
- Export removeMarkerBlock from utils/index.ts for reusability
- Add comprehensive tests for inline marker mention edge cases
- Add tests for shell-style markers and various whitespace scenarios

The new implementation correctly ignores markers mentioned inline within
text and only removes actual marker blocks that are on their own lines.

* feat(core): add formatProjectMdMigrationHint() for migration messaging

- Add standalone formatProjectMdMigrationHint() function for reusable
  migration hint output directing users to migrate project.md content
  to config.yaml's "context:" field
- Update formatDetectionSummary() to include the migration hint when
  project.md is detected (not just in cleanup summary)
- Refactor formatCleanupSummary() to use the new function for
  consistency
- Add unit tests for the new function and updated behavior

* test(init): rewrite init tests for experimental workflow approach

Rewrites the init command tests to verify the new experimental workflow
implementation. The new tests cover:

- OpenSpec directory structure creation (specs, changes, archive)
- config.yaml generation with default schema
- 9 Agent Skills creation for various tools (Claude, Cursor, Windsurf, etc.)
- 9 slash commands generation using tool-specific adapters
- Multi-tool support (--tools all, --tools none, specific tools)
- Extend mode (re-running init)
- Tool-specific adapters (Gemini TOML, Continue .prompt, etc.)
- Error handling for invalid tools and permissions

Removes old tests for legacy config file generation (AGENTS.md, CLAUDE.md,
project.md, etc.) as the new init command uses Agent Skills instead.

* test(update): rewrite tests for skills/commands refresh behavior

Update the update command tests to match the new implementation that
refreshes skills and opsx commands instead of config files.

Changes:
- Remove old ToolRegistry import (deleted module)
- Rewrite tests to verify skill file updates
- Rewrite tests to verify opsx command generation
- Add tests for multi-tool support (Claude, Cursor, Qwen, Windsurf)
- Add tests for error handling and tool detection
- Fix test assertions to match actual skill template names

The update command now:
- Detects configured tools by checking skill directories
- Updates SKILL.md files with latest skill templates
- Generates opsx commands using tool-specific adapters

* docs(readme): update documentation for new init behavior

- Replace tool list with simplified supported tools section (skills-based)
- Update init instructions to document --tools flag, --force, and legacy cleanup
- Replace project.md with config.yaml documentation
- Update workflow examples to use /opsx:* commands instead of /openspec:*
- Add command reference table for slash commands
- Update Team Adoption and Updating sections for new workflow
- Replace Experimental Features with Workflow Customization section

* refactor(cli): remove legacy configurators and merge experimental into workflow

- Delete src/core/configurators/ directory (ToolRegistry, all config generators)
- Delete legacy templates (agents-template, claude-template, project-template, etc.)
- Move experimental commands to src/commands/workflow/ with cleaner structure
- Remove experimental setup.ts and index.ts (functionality merged into init)
- Update CLI to register workflow commands directly instead of through experimental
- Update openspec update command to refresh skills/commands instead of config files
- Update tests for new command structure

* refactor: extract shared modules and move AGENTS.md to root

- Move AGENTS.md from openspec/ to project root
- Add shared module with tool-detection and skill-generation utilities
- Update legacy-cleanup with improved cleanup logic
- Enhance update.ts with additional functionality
- Add comprehensive tests for shared modules

* fix(ui): update welcome screen tagline

Change from experimental reference to reflect the merged workflow.

* fix: improve Windows cross-platform compatibility

- Handle both forward and backward slashes in path parsing
- Normalize paths before regex matching for legacy artifact detection
- Use regex split for both path separators in tool directory extraction
- Handle CRLF line endings when cleaning up multiple blank lines
- Add retry logic for test file cleanup to handle Windows file locking

* fix(init): use dynamic counts for skills and commands in success message

Replace hard-coded "9 skills and 9 commands" with dynamic values from
getSkillTemplates().length and getCommandContents().length to prevent
the message from diverging from reality when skills/commands change.

* fix: various small improvements across init, cleanup, and file handling

- Remove shell prompt characters from README bash examples (MD014)
- Show actual config filename (config.yaml vs config.yml) in init output
- Include hasProjectMd in hasLegacyArtifacts to show migration hint
- Add existence check before AGENTS.md deletion to avoid spurious errors
- Preserve leading whitespace and original newline style in file operations
- Use dynamic tool list from CommandAdapterRegistry in tests
2026-01-23 19:51:31 -08:00
Tabish Bidiwale 20714c1c28 feat(config): add project-level configuration via openspec/config.yaml (#499)
* feat(config): add project-level configuration via openspec/config.yaml

Adds openspec/config.yaml support for project-level customization without
forking schemas. Teams can now:

- Set a default schema (used when --schema flag not provided)
- Inject project context into all artifact instructions
- Add per-artifact rules (e.g., proposal rules, specs rules)

Key changes:
- New `src/core/project-config.ts` with Zod schema and resilient parsing
- New `src/core/config-prompts.ts` for interactive config creation
- Updated schema resolution order: CLI → change metadata → config → default
- Updated instruction generation to inject <context> and <rules> XML sections
- Integrated config creation prompts into `artifact-experimental-setup`

Schema resolution precedence:
1. --schema CLI flag (explicit override)
2. .openspec.yaml in change directory (change-specific)
3. openspec/config.yaml schema field (project default)
4. "spec-driven" (hardcoded fallback)

* test(config): add e2e tests and performance benchmarks for project config

- Add project config integration tests in artifact-workflow.test.ts:
  - Test new change uses schema from config
  - Test CLI schema overrides config schema
  - Test context and rules injection in instructions
  - Test backwards compatibility without config file
  - Test immediate reflection of config changes

- Add performance benchmark tests in project-config.test.ts:
  - Typical config (1KB): <20ms target
  - Large config (50KB): <50ms target
  - Repeated reads consistency check
  - Missing config fast-path test

- Add project configuration documentation in experimental-workflow.md:
  - Config fields reference (schema, context, rules)
  - Schema precedence explanation
  - Artifact IDs by schema
  - Troubleshooting guide

- Mark all tasks complete in tasks.md

* refactor(config): remove benchmark tests, document decision in source

Remove performance benchmark tests from project-config.test.ts and
document the results directly in the source code instead. Benchmarks
showed config reads are fast enough (~0.5ms typical) that caching
is unnecessary.

* fix(config): resolve three issues in project config feature

- Remove nested <template> tags: generateInstructions() no longer wraps
  template content since printInstructionsText() already handles XML
  structure
- Fix schema message accuracy: newChangeCommand() now uses the resolved
  schema returned from createChange() instead of hardcoded fallback
- Add TTY check: artifact-experimental-setup skips interactive prompts
  in non-TTY environments (CI, automation) to prevent hangs
2026-01-19 16:39:43 -08:00
Tabish Bidiwale c4a54a8d54 fix: skip Windows-specific permission tests that rely on chmod() (#464)
fs.chmod() on directories doesn't restrict write access on Windows since
Windows uses ACLs that Node.js doesn't control. Additionally, admin users
and CI runners can bypass read-only attributes. Skip these tests on Windows
and use platform-specific invalid paths in cross-platform tests.

Fixes #401 (bash/pwsh completion commit breaking Windows e2e tests).
2026-01-09 16:06:15 -08:00
38d2356836 feature/bash_fish_power_shells_completions (#401)
* added CLI completions support for: bash, fish and powershell

* Add bash/fish/powershell completions

* Archive extend-shell-completions

* Archive extend-shell-completions

* Fix canWriteFile control flow and add tests

* Fix bash completion fallback and security escaping

  - Add _init_completion fallback for systems without bash-completion
  - Fix command injection escaping in Fish/PowerShell generators
  - Add Bash command name escaping for security
  - Add comprehensive security tests for all generators
  - Fix test placement issues in bash/powershell test files

* refactor: extract completion templates and standardize naming

Extract static template literals from generators into separate template files.
Standardize naming to {SHELL}_STATIC_HELPERS and {SHELL}_DYNAMIC_HELPERS.

- Create bash/fish/powershell/zsh template files
- Rename constants: BASH_HELPERS → BASH_DYNAMIC_HELPERS,
  FISH_HELPER_FUNCTIONS → FISH_STATIC_HELPERS,
  POWERSHELL_HELPERS → POWERSHELL_DYNAMIC_HELPERS
- Update generator imports
- Remove ~99 lines of boilerplate from generators

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

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

* docs: update spec to reflect multi-shell support

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

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

* fix: add all shells to zsh completion suggestions

* feat: add --yes flag to completion uninstall

* fix: remove bash-completion dependency from fallback

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

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

* fix: use printf instead of echo for Fish tab output

Fish's echo doesn't interpret escape sequences, so \t outputs
literally instead of as a tab character. Use printf for proper
tab-separated completion output.

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

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

* fix: make UX messages shell-aware

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

* fix: add Homebrew paths for bash-completion detection

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

* fix: support both PowerShell Core and Windows PS 5.1

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

* fix: preserve colon handling in bash completion

Add -n : option to _init_completion to prevent colons from being
treated as word separators. This is important for spec/change IDs
that may contain colons.

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

* fix: update completion tests to match implementation changes

Updated bash-generator test to expect `-n :` flag in _init_completion call.
Updated powershell-installer tests to match refactored implementation that
supports both PowerShell Core and Windows PowerShell 5.1.

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

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-01-09 15:49:50 -08:00
Tabish Bidiwale 1786684af6 feat: add per-change schema metadata (.openspec.yaml) (#443)
This feature enables workflow schema auto-detection for changes:

- Add ChangeMetadataSchema Zod schema to types.ts
- Create change-metadata.ts with writeChangeMetadata(), readChangeMetadata()
- Update createChange() to accept optional schema param and write metadata
- Modify loadChangeContext() to auto-detect schema from .openspec.yaml
- Add --schema option to openspec new change command
- Update status/instructions commands to auto-detect schema from metadata

Schema resolution order:
1. Explicit --schema flag (if provided)
2. Schema from .openspec.yaml in change directory
3. Default 'spec-driven'
2026-01-05 17:13:52 -08:00
Tabish Bidiwale 4f4af5708d feat: add change creation utilities (#408)
* proposal: add change manager - extract + new functionality

Slice 2 of the artifact tracker POC. Creates ChangeManager module that:

**Extracts existing functionality:**
- `listChanges()` from ListCommand + ChangeCommand.getActiveChanges()
- `changeExists()` from inline fs.access() checks
- `getChangePath()` from inline path.join() calls
- `isInitialized()` from ListCommand directory check

**Adds new functionality:**
- `createChange(name, description?)` - create change directory + README
- `validateName(name)` - enforce kebab-case naming

**Refactors CLI commands to be thin wrappers:**
- ListCommand delegates to ChangeManager
- ChangeCommand delegates to ChangeManager

Also updates docs/artifact_poc.md to reflect XDG decisions from Slice 1.

* proposal: simplify to utility functions only

Remove extraction/refactor scope. Just add:
- createChange(projectRoot, name, description?)
- validateChangeName(name)

Simple utility functions in src/utils/change-utils.ts.
No class, no abstraction layer.

* docs: update artifact_poc.md for simplified Slice 2

- Rename ChangeManager to change-utils (simple utility functions)
- Remove extracted methods (listChanges, getChangePath, etc.)
- Keep only new functionality: createChange(), validateChangeName()
- Update component diagram and summary table

* docs: clarify existing vs new functionality in artifact_poc.md

Audit artifact_poc.md against existing codebase to mark what already
exists vs what's genuinely new:

- Slice 4 CLI table: Added Status column (NEW/EXISTS)
- Added "Existing CLI commands" section listing what's not in scope
- Updated Implementation Order Slice 4 with explicit new vs existing
- Summary table: Updated status + added "What already exists" section

Key finding: The document was already well-simplified. Only truly new
functionality (createChange, validateChangeName, InstructionLoader,
artifact graph CLI commands) is proposed.

* rename: change-manager -> change-creation capability

The capability name "change-manager" implied a manager class abstraction,
but the simplified proposal uses only utility functions. Renamed to
"change-creation" to accurately reflect what the capability provides.

* feat: implement change creation utilities

Add createChange() and validateChangeName() functions for programmatic
change directory creation with kebab-case validation.

- createChange(projectRoot, name) creates openspec/changes/<name>/
- validateChangeName() enforces kebab-case naming conventions
- Comprehensive test coverage (21 tests)

* chore: archive add-change-manager change

Move change to archive and create change-creation spec with
requirements for createChange() and validateChangeName().

* docs: update change-creation spec purpose
2025-12-26 22:14:56 +11:00
Tabish Bidiwale 9ac6330430 fix(cli): respect --no-interactive flag in validate command (#395)
* fix(cli): respect --no-interactive flag in validate command

The validate command's spinner was starting regardless of the
--no-interactive flag, causing hangs in pre-commit hooks.

Changes:
- Pass noInteractive option to runBulkValidation
- Handle Commander.js --no-* flag syntax (sets interactive=false)
- Only start ora spinner when in interactive mode
- Add CI environment variable check to isInteractive() for industry
  standard compliance

* test: add unit tests for interactive utilities and CLI flag

- Export resolveNoInteractive() helper for reuse
- Add InteractiveOptions type export for testing
- Refactor validate.ts to use resolveNoInteractive()
- Add 17 unit tests for isInteractive() and resolveNoInteractive()
- Add CLI integration test for --no-interactive flag

This prevents future regressions where Commander.js --no-* flag
parsing is not properly handled.
2025-12-23 12:42:25 +11:00
1adf3cea88 feature/oh-my-zsh-completions (#289)
* shell completions for zsh

* after code review changes

* expose only postinstall.js script

* Replace _openspec "$@" with compdef in zsh-generator.ts to prevent execution during load

* Update test/commands/completion.test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* - Fix dispatcher to use $words instead of $line for subcommand routing
  - Add __complete endpoint with tab-separated output for safe parsing
  - Replace brittle awk parsing with __complete in completion helpers
  - Add uninstall confirmation prompt with --yes flag to skip
  - Prefer $ZSH env var for Oh My Zsh detection before dir check
  - Add fpath verification guidance for OMZ installations
  - Update cli-completion spec to document generate subcommand

* improve shell detection and installation handling

  - Return structured result from detectShell() with shell and detected name
  - Detect already-installed completions and skip reinstall
  - Add update detection with automatic backup of previous version
  - Add debug logging to silent catch blocks for diagnostics
  - Quote fpath directories to handle paths with spaces
  - Verify Oh My Zsh fpath configuration and add to .zshrc if needed
  - Show helpful error for detected but unsupported shells
  - Update all tests for new detection API

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2025-12-12 21:30:40 +11:00
Tabish Bidiwale 9c9e57daa1 Ensure slash command paths resolve on Windows platforms (#144)
* Ensure slash command paths work on Windows

* Add Linux home path coverage for joinPath
2025-10-11 00:27:16 +11:00
Tabish Bidiwale f8eca37796 feat(init): slim root agent instructions (#98)
* feat(init): slim root agent instructions

* Fix marker updates to ignore inline mentions

* styling updates

* update instructions

* fix tests
2025-09-30 17:03:56 +10:00
Tabish Bidiwale 20b2fee749 feat(init): support multi-select extend flow 2025-09-17 11:36:56 +10:00