mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-09-14 20:16:53 +08:00
@fission-ai/openspec@1.12.0
298 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db03c6c4b0 |
feat(validate): add findings-only bulk reports (#1713)
* feat(validate): propose findings report * docs(validate): clarify findings report contract * feat(validate): implement and harden bulk findings reports * test(validate): canonicalize store paths natively --------- Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
cdd06a0594 |
docs(specs): align four requirements with current behavior (#1707)
* docs(openspec): correct 4 requirements that the code has outgrown - ai-tool-paths/path-configuration-for-supported-tools#2: Changed the windsurf scenario's required skillsDir from `.windsurf` to `.devin`. - cli-update/slash-command-updates#6: Require $ARGUMENTS to be placed in the file body (not frontmatter) for OpenCode archive commands. - rules-injection/validate-artifact-ids-during-instruction-loading#6: Updated the expected warning text to use double quotes and to state it matches no artifact in any available schema, listing known artifact IDs. - specs-sync-skill/skill-output#3: Changed the expected no-changes message to 'Specs already in sync; no files changed.' to match the code. None of these reduce what a requirement demands. Scanned at |
||
|
|
2fa679f180 |
fix(schema): make schema init --default actually set the default (#1709)
* fix(schema): make schema init --default actually set the default --default wrote defaultSchema to openspec/config.yaml, but the config loader only reads schema, so new changes kept using spec-driven while the command reported success. Write the key that is read, and drop the dead one a previous run may have left behind. Fixes #1708 * fix(schema): preserve supported configs when setting default * docs(schema): specify default config file handling * docs(schema): protect default config migration * fix(schema): make default initialization atomic * fix(schema): hide init staging and backup dirs from discovery `schema init` stages into `.init-staging-<rand>` and moves an existing schema aside to `<name>.init-backup-<pid>-<ts>`, both inside the schemas dir. `schema fork` already did this and the resolver filters its temp names out of discovery; the init names were never added, so `listSchemas` and `listSchemasWithInfo` surfaced them as real schemas -- in shell completions, "available schemas" error lists, and change-metadata validation. The backup is the durable case: cleanup failure is deliberately tolerated with a warning, so a blocked cleanup (or a crash mid-transaction) leaves a permanent phantom schema behind. Generalize the fork-only filter to cover both commands' staging and backup names. Real schema names are kebab-case, so excluding these dot-bearing names can never hide a legitimate schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
126c5d6c59 |
fix(validate): report a Purpose left as the archive placeholder (#1671)
* docs(openspec): propose warn-on-purpose-placeholder
When a delta introduces a capability with no usable `## Purpose`, archive
writes `TBD - created by archiving change <name>. Update Purpose after
archive.` into the new main spec. Three places already tell authors to
replace it -- the `specs` instruction ("including a leftover `TBD`
placeholder"), the sync-specs summary step ("so it gets written now rather
than lingering"), and the cli-archive contract -- but nothing reports that
it is still there.
`--strict` cannot reach it. The check meant to catch a Purpose nobody wrote
is a 50-character floor and the placeholder is 91 characters, so the one
rule that exists to catch a thin Purpose is satisfied by the exact text
meaning nobody wrote one: a Purpose reading "Does stuff." fails --strict
today, while one saying nothing at all passes.
Proposes reporting it as a warning against the spec's Purpose -- silent by
default, failing under --strict, so a project already carrying placeholders
keeps validating until it opts into the stricter gate. Detection is narrow:
the generated sentence wherever it appears, and otherwise only a `TBD`
opening the Purpose, so prose raising an open question is left alone.
Planning artifacts only; no source changes.
Refs #369
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(validate): report a Purpose left as the archive placeholder
When a delta introduces a capability with no usable `## Purpose`, archive
writes a placeholder into the new main spec. Nothing read it afterwards, so
the capability kept a to-do in it while every command reported success.
`--strict` could not reach it. The check that exists to catch a Purpose
nobody wrote is a 50-character floor, and the placeholder clears it: a spec
whose Purpose read "Does stuff." failed --strict, while a spec whose Purpose
said nothing at all passed. #369 reported agents leaving the placeholder
behind and stayed open seven months; every remedy since has been an
instruction, which is the mechanism that report described as unreliable.
validate now reports it as a warning against the Purpose, naming the line
and saying to edit the main spec directly -- a delta's `## Purpose` is read
only when the capability is created, so it cannot replace an existing one.
Warning rather than error, because strict mode already means "warnings
fail": a project carrying placeholders keeps validating by default and only
--strict fails. Archive is untouched -- it validates rebuilt specs without
--strict, so a spec archive writes still passes the validation it would have
passed before, and the text archive writes is byte-identical.
The placeholder is recognised through the same constants the writer composes
it from, so the check cannot drift from the sentence it looks for -- the
failure mode of a second, hand-copied spelling being a check that matches
nothing and looks exactly like a check that found nothing. The one case that
cannot be a lookup is an agent-written placeholder, kept to a `TBD` opening
the Purpose: "the retry budget is TBD pending benchmarks" is authored prose
and is left alone.
Verified: 209 archive tests pass unchanged (the placeholder text is
asserted literally, so the output is provably identical); full suite 138
files / 3993 tests; 36/36 strict spec validations; build, lint and typecheck
clean. Against a project carrying four real placeholders, default mode still
exits 0 and --strict fails exactly those four.
Cross-platform CI is not yet confirmed -- it needs a pushed branch. Line
endings are covered by tests asserting a CRLF spec and an LF spec produce
identical findings, and the module does no path handling.
Refs #369
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(validate): make every placeholder guard load-bearing
A mutation pass over the seven guards -- revert one, see which tests die --
found two that no test held.
The prefix/suffix test did not exercise the guard it named. Its Purpose read
"Explains what happens when archiving change my-change runs twice", which
contains neither half of the generated sentence, so it passed whether or not
the suffix was required. Matching on the prefix alone killed nothing. The
Purpose now embeds the real prefix constant and asserts the suffix is absent,
so the case is the one the name claims; the mutation kills it.
The empty-Purpose early return was genuinely dead. Neither rule matches empty
text, so removing the branch changed no behaviour and failed no test. Rather
than keep a guard nothing can hold, the branch is gone and the comment says
why an empty Purpose still yields null. The tests asserting that behaviour
are unchanged and still pass.
Every guard now dies under mutation:
whole check removed from applySpecRules ......... 6 tests
brevity no longer suppressed (else -> if) ....... 1
word boundary dropped from the TBD marker ....... 1
generated placeholder matched on prefix alone ... 1
line-ending normalisation removed ............... 2
section-boundary guard removed from locator ..... 1
Full suite 138 files / 3993 tests, lint and typecheck clean.
Refs #1670
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(openspec): record the mutation pass in the task list
The mutation work changed the implementation -- a test rewritten and a dead
branch removed -- but no task covered it, so the plan claimed less work than
was done. Added as group 6, marked complete, with why it was not planned.
5.4 now says what blocks it. It needs a pushed branch for the cross-platform
matrix, and the note records that line endings are covered locally by tests
asserting a CRLF spec and an LF spec produce identical findings, so a reader
can tell the difference between unverified and unverifiable-from-here.
The specs, proposal and design are unchanged and were checked: the delta's
empty-Purpose clause constrains behaviour, not structure, and that behaviour
is the same -- the redundant branch went, the rule did not.
26 of 27 tasks complete; the change still validates --strict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(openspec): close 5.4 on a green cross-platform matrix
CI dispatched on the fork against this branch: lint & typecheck, and the test
suite on linux-bash, macos-bash and windows-pwsh -- all green. The Windows job
installed, built and ran the suite rather than short-circuiting, which is the
part 5.4 existed to check, since the placeholder locator counts lines in files
that may carry either ending.
Recorded as a workflow_dispatch run on the fork, not the upstream pull-request
run, because those are not the same gate and the note should not let a reader
assume otherwise. Nix Flake Validation and Validate Release Tracking skipped:
this branch touches neither the flake nor release tracking.
27 of 27 tasks complete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(validate): name the placeholder's line, not the prose above it
The warning tells you which line to fix, and named the wrong one when the
generated sentence did not open the Purpose:
3 ## Purpose
4 Handles widget retries. <- warning pointed here
5
6 TBD - created by archiving ... <- placeholder is here
The locator asked "what is the first non-blank line after ## Purpose?"
rather than "where is the placeholder?". Those are the same line in five of
the six shapes a placeholder can take -- a leading TBD marker is the first
non-blank line by definition, and archive writes the generated sentence as
the section's only content -- so the two questions only diverge when a human
types prose above a leftover placeholder.
Pointing at that prose is worse than pointing nowhere: the reader sees a
sentence that is plainly fine and concludes the check is broken. design.md
already said a wrong line number is worse than none, and the delta already
required naming the line the placeholder is on, so this is the
implementation meeting a contract that was already written, not a change of
contract.
The locator is now told which rule matched. A leading marker keeps the
first-non-blank behaviour, because that is where it sits; the generated
sentence is located by its own text. When both match the leading marker
wins, being the earlier of the two.
Found by CodeRabbit on #1671. The finding was real despite its own
"Addressed" marker, which only tracked the file changing in a later commit.
Two test gaps let it through. The case that covered this input asserted
only that something was reported, never which line -- so it now asserts the
line, and a table pins every position a placeholder can occupy, each case
first checking that the line it expects really carries the placeholder. The
mutation pass could not have caught it either: mutation proves a test dies
when a guard is broken, and cannot invent an assertion nobody wrote.
Reverting the branch fails exactly the three new expectations. Full suite
4000 tests / 138 files, lint and typecheck clean.
Refs #1670
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(validate): set the placeholder line unconditionally
ValidationIssue.line is optional and the project does not enable
exactOptionalPropertyTypes, so a plain assignment typechecks and
JSON output is unchanged - JSON.stringify drops undefined values.
findPurposePlaceholderIssue already returns the key unconditionally,
and the neighbouring push sites assign line plainly, so the
conditional spread was the odd one out.
* fix(validate): widen the placeholder check to TODO and read fences as quoted
#1670 left two questions open. Both are answered here, against how OpenSpec
already reads a spec.
A `TODO` opening the Purpose now reports as the same finding as a `TBD`.
Nothing OpenSpec writes produces one, but the marker an author leaves behind is
whichever word they reached for, and a Purpose reading `TODO: fill this in` is
as unwritten as one reading `TBD`. Only the opening position counts, as before,
so `TODOs are tracked in the linked issue` is still authored prose.
Fenced code inside a Purpose is now read as quoted material rather than as the
Purpose speaking, through the `buildCodeFenceMask` the requirement and structure
parsers already share. Without it a spec documenting the sentence archive writes
is reported as carrying it, which is the check failing the one document that
explains it - and a warning that fires on the docs teaches people to ignore the
warning. Fenced lines are skipped when locating the placeholder too, so a
`## Purpose` or `## Requirements` quoted in a fence can neither be mistaken for
the section header nor end the section early.
The message now names both what archive writes and a marker left in its place,
since one message covers both. Severity is unchanged: still a warning, so a
project carrying placeholders keeps validating and only --strict fails.
Every new guard is mutation-checked: dropping `TODO` kills 3 tests, unmasking
detection kills 2, unmasking the line locator kills 3, unmasking the header
search kills 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(validate): read the marker boundary in any script, not just ASCII
Review found `\b` reading `TODOé` and `TBD١` as a marker followed by
punctuation, because `\b` only knows ASCII word characters. A Purpose is prose
and prose is not always Latin script, so the rule that a longer word beginning
with those letters is not a marker has to hold in any script.
The lookahead rejects letters, digits, combining marks and `_`, and nothing
else, so `TODO:`, `TBD -` and `TODO(owner):` are still the marker they look
like. Held in both directions: loosening it back to `\b` kills 1 test,
tightening it to reject punctuation kills 4.
Also reworded a task line that opened with `#1670`, which markdownlint reads as
a heading missing its space.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(validate): locate the matched purpose placeholder
* docs(validate): remove trailing task whitespace
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Clay Good <hi@claygood.com>
|
||
|
|
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 |
||
|
|
c747ed1f34 |
feat(init): add language option (#1685)
* feat(init): add language option * fix(init): harden language configuration * fix(init): fail when language config cannot be written |
||
|
|
fc0fec1250 |
fix(feedback): keep full reports in issue bodies (#1653)
* fix(feedback): keep full reports in issue bodies * fix(feedback): preserve report formatting |
||
|
|
8364428661 |
fix(schemas): honor canonical root selection (#1616)
* docs(openspec): propose schemas root selection fix * fix(schemas): honor canonical root selection * test(schemas): assert complete JSON schema shape * docs(stores): drop view from the cwd-only, no --store list view already accepts --store <id> (registered in src/cli/index.ts), so listing it among the commands that act on the current directory only was incorrect. Remove it; templates and the deprecated noun forms remain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: regenerate skills and parity hashes after rebase onto main Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
804427b6ff |
fix(telemetry): suppress first-run notice in --json mode (#1609)
* fix(telemetry): suppress first-run notice in --json mode The first-run telemetry disclosure notice was written to stdout from the global preAction hook. On a user's first-ever command with --json this polluted stdout and could break JSON parsers. Read the executing command's --json flag (actionCommand.opts().json) and, when set, skip the notice and leave noticeSeen unset so the disclosure is deferred to the first later non-JSON run rather than lost. Spinner suppression, new-change --json output, and structured JSON errors already landed on main (#960, #1190); this closes the one remaining stdout writer in --json mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harden: detect --json from argv to cover all invocation forms The preAction guard read actionCommand.opts().json, which only sees a declared leaf option. That missed two supported --json forms that emit a single JSON document to stdout: - openspec store --json (permissive group reads --json from residual args; never declares the option, so opts().json is undefined) - openspec workset --json <sub> (--json on the parent group, consumed before the leaf; leaf opts().json is undefined) Both would still print the first-run telemetry notice ahead of their JSON. Detect --json from process.argv instead: it covers leaf, parent, and residual-arg forms uniformly. Suppressing is always safe (the disclosure defers to the next non-JSON run, never lost), so a broad argv check is the correct, conservative signal. Also add a direct assertion that noticeSeen stays unset after a silent run, and note the pre-existing raw-stdout commands (completion generate, config get/path, __complete) as out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: derive --json from parsed command state + regression test Replace the process.argv check with isJsonRun(command), an exported pure helper that reads Commander's parsed state: optsWithGlobals().json (leaf and parent-group forms) OR command.args (residual --json on permissive bare groups like store). This is tied to the actually-parsed command rather than raw args, and — unlike process.argv — is unit-testable in-process. Add test/core/cli-is-json-run.test.ts: a synthetic program reproducing all three registration patterns proves isJsonRun returns true for status --json, store --json, workset --json list, and workset list --json, and false otherwise. This locks in the store/workset coverage against future regressions (an e2e test can't: telemetry is disabled under CI, so the notice never fires there). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): qualify first-command notice scenario as non-JSON The generic 'First command execution' scenario asserted the notice displays on every first command, contradicting the JSON scenario that says it does not. Qualify it as 'without --json' so the required behavior is unambiguous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3d0701f871 |
fix(workflows): preserve nested spec paths (#1508)
* fix(workflows): preserve nested spec paths * fix(workflows): key conflicts by capability path * fix(workflows): preserve full paths in examples * fix(workflows): clarify nested path inputs * test(workflows): align parity hashes after rebase |
||
|
|
afea111cd4 |
fix(status): clarify planning completion (#1505)
* fix(status): clarify planning completion * test(status): cover skipped planning artifacts * fix(workflows): gate archive guidance on implementation * fix(status): clarify human completion message * fix(status): make completion guidance stage-neutral * test(status): align parity hashes after rebase |
||
|
|
ece8660d44 |
fix(validate): allow non-English requirements (#1502)
* fix(validate): allow non-English requirements * test(validate): cover non-English change deltas * test(validate): distinguish missing bodies from guidance |
||
|
|
521ee33e6e |
feat(archive): let a change retire a capability it empties (#1484)
* fix(archive): retire a capability when a change removes its last requirement
A delta whose REMOVED entries cover every requirement rebuilt the main spec
empty, and an empty spec fails validation ("Spec must have at least one
requirement"), so the archive aborted with no way forward. Pre-deleting the
main spec did not help: the delta was then treated as a create and landed on
the same empty spec.
Archive now treats an emptied capability as retired. It deletes the
capability's spec.md and any directory the deletion leaves empty, stopping
short of the specs root, and reports the removals in the totals. Nothing is
deleted unless this run actually removed a requirement, so a re-applied or
already-synced delta still leaves the file alone.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): decide retirement from the validator and contain the deletion
Adversarial review found the original rule unsound. It retired whenever no
canonical `### Requirement:` blocks were left, but the validator counts
requirements differently: MarkdownParser accepts any `###` heading under
`## Requirements`, while the delta block parser indexes only canonical headers
and sweeps the rest into the preamble, which survives into the rebuilt spec. A
strict-valid spec could therefore be deleted on an archive that previously
succeeded. Retirement is now decided by putting the rebuilt spec to the
validator and retiring only when its sole error is that it has no requirements,
which makes "this spec could not have been written anyway" true by construction.
Also fixed:
- The directory prune walked string prefixes, but path.resolve does not resolve
symlinks and readdir/rmdir both follow them, so a symlinked capability
directory let it delete directories outside the repository. Pruning is now
bounded by real paths and refuses to descend through a symlink.
- A spec that was already requirement-less and lost nothing this run is no
longer skipped past validation; it aborts exactly as it did before.
- Deletions are deferred until every spec write has succeeded, so a later
failure cannot leave a spec already deleted.
- Retirement is recorded in `warnings`, naming any other sections the deleted
file held, so JSON consumers and humans can both see what went.
- Totals carry every applied operation; a rename applied on the way to the
removal was being dropped.
- bulk-archive guidance, the sync/archive skill specs, and the docs that
described archive as never deleting a spec.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): close the retirement gaps a second review round found
Five adversarial reviews, mutation testing and CodeRabbit went at the reworked
retirement. The findings, all verified by repro before fixing:
- The archive-name collision check ran AFTER the spec merge, so archiving twice
in one day deleted the capability's spec and then failed, leaving the change
unarchived and the file gone. The destination depends only on the change name,
so it is now settled before any spec is written or deleted - which also closes
the same, older window for ordinary writes.
- `--no-validate` retired too, but the whole safety argument is the validator's
verdict, and that path produces none. It now writes the spec exactly as it did
before this feature existed, leaving no exception to the claim that nothing
previously working changes.
- The validator can be talked out of seeing a requirement: a stray
`### Requirements` under Purpose captures its section lookup, so a spec still
holding a real requirement reported "no requirements" and was deleted. Any
`###` heading left under `## Requirements` now vetoes retirement outright - a
reader is not fooled by the stray heading even when the parser is.
- A dangling symlink made `update.exists` false (`fs.access` follows links,
`unlink` does not), skipping the "removed something this run" guard: a run that
removed nothing deleted an entry and reported a removal. The no-target case is
now an explicit branch that never deletes, instead of an ENOENT probe.
- `findOtherSections` reported `## ` headings that were inside HTML comments and
listed duplicates; it now masks comments like every other structural scan here
and dedupes. The warning also names the `## Purpose`, which the deletion always
takes, and the resolved path when a symlink puts the file outside the repo.
- A failed `unlink` surfaced a bare errno; it now says what was being attempted
and what to do.
Tests grew from 19 to 33, killing every surviving mutant the review found:
deferral proven against a failing write (not just a failing validation), the
warnings payload, the already-gone path's output, multi-level pruning, the
`+ path.sep` boundary, a symlinked specs root, two retirements in one archive,
and `isRetirableSpec` unit-tested directly - including the two-error shape that
proves `every` rather than `some`.
Agent guidance, the three living specs and the docs now state the same
conditions the CLI applies, so a sync agent cannot delete a spec archive keeps.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): make the write-failure test platform-neutral and the path note meaningful
Windows CI and CodeRabbit each caught one:
- `chmod 0o555` is not a write barrier on Windows, so the test that proves
deletions are deferred until every write succeeds never failed a write there:
the archive completed, the spec was retired, and the assertion blew up. It now
puts a directory where the second spec's file belongs, which fails the write on
every platform. Verified it still kills the reordering mutant.
- The "resolved to" note compared a canonicalized path against a merely resolved
one, so any symlinked ancestor - the platform's own /var -> /private/var is
enough - decorated an ordinary retirement with a path that says nothing. It now
fires only when the spec really lived outside the specs tree, which is the fact
the nominal path hides. Both directions are pinned by tests.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): make the residual-heading veto position-independent
A third review round, scoped to the code the earlier rounds never saw.
The veto that is supposed to stop a retirement deleting hand-written content
only worked when that content sat ABOVE the first requirement. `parts.preamble`
is by definition the text before the first `### Requirement:` header; anything
after the last one belongs to that block's raw and is discarded with it, so the
rebuilt-body scan never saw it. Identical content, different position: one
aborted, the other was deleted silently. The veto now reads the original
Requirements section - preamble plus every block - so position does not matter.
Also:
- `realpath` follows a symlinked `spec.md` but `unlink` removes the link, so the
warning declared it had deleted a file outside the repo that was still there.
The note is now skipped when the target is itself a symlink.
- `findHeadings` masked HTML comments before code fences, so an unterminated
`<!--` inside a fenced example blanked the rest of the document and truncated
the very list of sections the deletion was reporting. Fence first, then
comments.
- Moving the collision check before the merge widened the window between it and
the move, where a claimed destination surfaced as a raw ENOTEMPTY and degraded
to `archive_error`. `moveDirectory` now reports that as `archive_target_exists`,
the same diagnostic the pre-flight check gives.
And a simplification the review asked for: the overlapping `retirable` /
`deletes` / `retired` booleans are now one `decideSpecOutcome()` returning
'write' | 'delete' | 'skip'. Behavior is identical - same clauses, same order -
but the fourth state that existed only as a comment is now a visible return.
Both guards were kept: the review constructed inputs where each is the sole
thing preventing a data-losing delete.
Two tests the review found wanting are gone or rewritten: one killed no unique
mutant, and one assertion straddled two editable message fragments and could
have gone vacuously true.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(archive): canonicalize both negative path assertions
CodeRabbit caught that `expect(warnings).not.toContain(shared)` passed
vacuously: on macOS the temp root lives under /var, whose realpath is
/private/var, so the warning would print a form the assertion never compared
against. The sibling assertion on `tempDir` had the same flaw.
Both now canonicalize first, and both were confirmed to fail against a mutant -
dropping the lstat guard, and forcing the resolved-path note on - which neither
did before.
Closes #1302
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): move a retired capability's spec into the archive instead of deleting it
Retiring a capability was the first case where archiving deleted a file
under `openspec/specs/`. Nothing in the repo had ever removed spec content
before, so the blast radius of a wrong verdict was a lost file with only
the reflog to recover it.
The spec now moves instead. It is staged into the change directory, which
the archive step renames onto the archive path moments later, so it comes
to rest at `<archive>/retired-specs/<capability>/spec.md` beside the
proposal and tasks that retired it. `git` records a rename, and bringing a
capability back is a `git mv` from the archive.
Staged into the change rather than written to the archive path after the
move, because the archive path must not exist yet and the ordering is
safer: if a later step fails, the spec sits in a change that is still
active and a rerun carries it through, versus stranding the live specs
tree without a spec it still needs.
A symlinked `spec.md` is copied by content and its link removed, rather
than moved: relocating the link itself would archive a relative path that
no longer resolves from where it landed. A spec already staged by an
earlier aborted run is never overwritten - it is the only copy once the
live one moves.
The retirement verdict, its guards, and the deferral until every write has
succeeded are all unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): clean up staging directories when a retirement move fails
The staging directories are created before the move, so any failure left an
empty `retired-specs/<capability>/` behind. That folder then rode into the
archive with the change, where it reads as a retirement that never happened -
a spec was supposedly retired here, and there is nothing to show for it.
The failure path now prunes back up to the change directory. Only empty
directories go, so a capability the same run already staged next to the
failing one is untouched, and the guard that refuses to overwrite a staged
spec still stops at a non-empty destination.
Both cases are covered by tests that fail without the prune: a dangling
symlink is the reproducible post-staging failure, since lstat sees a file and
the copy then follows the link and finds nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(archive): say "moved" where the retirement path still said "deleted"
Three leftovers from the deletion version: the `residualRequirementHeadings`
comment, `pruneEmptyDirs`'s `mainSpecsDir` parameter - now a boundary that is
the change directory on the cleanup path, not the specs root - and a sentence
in writing-specs.md that used "deleted" for the requirement and then again for
the file, two lines apart.
No behavior change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): roll back a staged copy when the live spec cannot be removed
Both non-atomic retirement routes - a symlinked main spec, and the
EXDEV/EPERM rename fallback - copy the spec into staging first and remove
the original second. A copy that landed before an `unlink` that failed left
the spec in TWO places, and the staged one then tripped the "already
staged" guard on every rerun. The error told the caller to rerun the
archive, and the rerun could never work.
Reproduced at the previous head with a symlinked `spec.md` in a read-only
capability directory: `copyFile` succeeded, `unlink` returned EACCES, and
both copies remained.
The failure path now deletes the destination this attempt created, so the
capability is left exactly as the attempt found it and the rerun works. The
rollback is gated on a flag set only after the destination is proven free,
so a spec staged by an EARLIER run is never the thing removed - the
overwrite guard still fires ahead of it and rolls nothing back. A partially
written copy is cleaned by the same call.
The message no longer promises more than it delivers: it reports that the
spec is still in place, or names the leftover copy when the rollback itself
failed.
Regression tests cover both routes and assert the rerun succeeds, not just
that the copy is gone. Both fail without the rollback. The cross-device
route injects EXDEV, which cannot be provoked inside one temp directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(archive): run the rename-fallback rollback case on Windows too
The two post-copy rollback cases shared one `skipIf(win32)`, inherited from
the symlink case, which needs privileges Windows does not grant by default.
The rename-fallback case uses regular files and spies only, and the sibling
errno it stands in for - EPERM - is the Windows case, so skipping it there
left that route untested on the platform that produces it.
Skipping is now per-case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): claim the retirement destination atomically
`fs.access` followed by a write is not an ownership claim. Two concurrent
retirements both saw the destination free and both set `destIsOurs`; one
moved the spec into staging, and the other - equally convinced the file was
its own - rolled it back out. The source and the staged copy both ended up
gone. Reproduced at the previous head in 36 of 40 iterations.
The claim and the content now arrive in one syscall: `copyFile` with
`COPYFILE_EXCL` fails with EEXIST rather than overwriting, so exactly one
caller can ever own the path. That is also the check that refuses to
clobber a spec an earlier aborted run staged, now decided atomically rather
than by a separate look beforehand.
The losing caller fails two ways, and both used to destroy the winner's
file. EEXIST is the obvious one. ENOENT is not: `copyFile` opens the source
first, so a loser that arrives after the winner removed the source fails
before creating anything - and treating that as "a partial copy of mine"
unlinked the winner's file. Neither errno now claims ownership. Fixing only
EEXIST left 4 of 40 iterations still losing both copies.
Copying rather than renaming is what makes the claim possible: `rename`
overwrites silently on every platform, so it cannot tell "I created this"
from "I destroyed someone else's". It also crosses filesystems, which
retires the EXDEV/EPERM fallback, and reads a symlink's content rather than
moving the link - so the two routes collapse into one shape.
Regression asserts the invariant over 25 rounds: exactly one caller
retires, the spec survives once and intact, and the source is gone. It
fails against the old access-then-write shape.
Not crash-safe, which is a weaker promise and now documented: a process
killed between the copy and the unlink leaves the spec in both places, and
the next run refuses rather than guessing which to keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): take retirement ownership from an exclusive create, not an errno
Claiming the destination with `copyFile(..., COPYFILE_EXCL)` closed the
concurrent race but kept reading ownership out of a failure code, and that
cannot be made correct however the errnos are partitioned. An errno says
what went wrong, not what was created: a source-side EACCES is
indistinguishable from a partial copy of our own, so the cleanup deleted a
recovery copy an earlier run had staged - the last remaining copy of a spec
whose live file could not even be read.
Reproduced at the previous head with an unreadable `spec.md` and a
pre-existing `retired-specs/legacy/spec.md`: the staged file was destroyed.
Ownership now comes from `open(dest, 'wx')`. O_CREAT|O_EXCL returns a
handle exactly when it created the file, so the question is answered by the
syscall instead of inferred afterwards, and every failure path leaves the
flag false. EEXIST remains the refusal that protects an earlier run's copy,
now decided by the same operation. Content is written through the claimed
handle, as bytes, and the handle is closed before any rollback so Windows
can unlink it.
The regression uses real mode bits, skipped on Windows and under root: the
defect was a source-side errno being read as proof about the destination,
and stubbing a JS-level read cannot reproduce it, because the copy it has
to fool never went through one. Verified it fails against the errno-
inference version.
All three findings on this path now hold together: the pre-existing copy
survives, 0 of 120 racing iterations lose a spec, and a post-copy unlink
failure still rolls back and reruns cleanly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): keep the staged copy when the source is already gone
The rollback exists for a copy that landed while the source survived - the
two-places state that blocks every rerun. It must not fire once the source
is gone: at that point the staged copy holds the only remaining content, and
the end state the retirement was reaching for is already reached.
An external delete landing between the read and the unlink produced exactly
that, and the rollback destroyed the spec outright - `retired: false`, no
live file, no staged copy, content gone.
`unlink` returning ENOENT is now a success rather than a failure to roll
back. Every other errno still throws: the source is still sitting there, and
leaving the staged copy beside it is the state that blocks a rerun.
Found reviewing the finished path rather than reported - the same class as
the three review findings before it, all of them the rollback reaching a
copy it should not have. Regression verified against the unconditional
unlink.
Also corrects a doc line that still credited the copy with claiming the
destination; the claim is the exclusive create.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(archive): gate retirement on a declared marker, drop retired-specs/
Reworks #1302 to follow the design that already exists instead of adding one.
The move-into-the-archive approach introduced two things OpenSpec did not
have: capability retirement as a lifecycle state, and `retired-specs/` as an
on-disk convention no schema declares - which a future unarchive command
would have to know about. Its whole justification was preserving content that
two existing mechanisms already preserve: the archived change carries the
delta naming every REMOVED requirement with its Reason and Migration, and git
carries the file. The approach even conceded the point by advertising `git mv`
as the recovery path.
The issue itself proposed neither. It asked for a delete, or an explicit
retirement marker. This does both: archive deletes the emptied spec, and only
when the change declares `retire_capabilities: true` in its `.openspec.yaml`.
`skip_specs` is the precedent. The marker reader is the same function,
parameterised by key, so the two can never drift apart on what counts as
honorable metadata - a marker in unparseable YAML, or one whose schema does
not load, is not a marker in either case. An explicit `false` is not an
unhonorable marker, it is simply undeclared.
Without the marker nothing changes: the unwritable spec aborts the archive
exactly as before, except the abort now names the marker as the way out - and
says nothing about it when retiring would not have made the spec writable
anyway, so it never sends an author after the wrong fix. Applying REMOVED
already deletes requirement content from a main spec, so deleting the spec
once nothing is left is that same operation carried to its end.
Every guard survives: the validator's verdict, the residual-heading veto,
something-removed-this-run, and never under --no-validate. What goes is the
exclusive claim, the rollback, the staging directories, and the four
data-loss windows they created across four review rounds. Net 307 lines
smaller than the move.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate parity hashes over the merged sync-specs template
#1482 and this branch both edit the sync-specs template, so the merged
template needs its own hash - neither side's committed value describes it.
* docs(archive): correct claims the redesign left false, and bump to minor
Review findings, all verified before fixing:
- `pruneEmptyDirs`'s doc claimed "two callers, two boundaries", naming the
change directory as the second. That was the staging walk from the move
design; there is one caller. The boundary stays a parameter, and the comment
now says why.
- Three comments still described the retirement as moving the file somewhere.
It deletes it.
- The sync skill told agents the retirement condition includes "no other
`###` headings or prose" and then claimed "openspec archive draws exactly
these lines". It does not draw the prose line: a main spec with loose prose
under `## Requirements` retires and is deleted, and the prose is not named
in the warning, which reports `## ` sections only. Verified against the
built CLI. The condition now states what the CLI enforces, and the template
tells the agent to read that prose back to the user, since the CLI cannot
see it for the agent.
- `docs/concepts.md`'s `.openspec.yaml` field list omitted the new marker -
the one place a user goes to learn what that file may hold.
- `docs/cli.md`'s `--no-validate` row did not mention that it disables
retirement, though the row two lines down documents retirement.
- Bumped patch -> minor. `skip_specs`, the marker this one mirrors, shipped as
a minor change in 1.7.0 (#1399); this adds a metadata field and an archive
outcome on the same footing.
No behavior change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): refuse to retire a spec with a second Requirements section
Four review agents ran against this branch. Two data-loss findings, both
reproduced before fixing.
1. A spec with a SECOND `## Requirements` section was deleted even though it
passed `validate --strict` with zero issues, and the report named only
`Purpose`.
`extractRequirementsSection` binds to the FIRST `## Requirements`, so
everything after it rides through the merge untouched: the residual-heading
veto never sees it, `findOtherSections` filters it out by title, and the
validator's own section lookup stops there too - which is why a second
section holding a `SHALL` with a scenario reads as valid and then died with
the file. The earlier round made that veto position-independent WITHIN the
section; this is the same evasion one level up.
Retirement is now refused outright for such a spec, so the archive aborts as
it did before #1302. The abort's marker hint takes the same conjunct, so it
never advises a marker that would not have helped.
2. The recovery line promised `git checkout HEAD -- <path>` unconditionally,
and the path was wrong twice over. Verified failures: an UNTRACKED spec -
the ordinary case, since an earlier `openspec archive` creates the main spec
and nobody has committed it yet - is deleted and the printed command errors,
so the file is gone for good; under a store-selected root the nominal
`openspec/specs/...` path does not exist in the caller's repo; and a
symlinked capability directory puts the file somewhere else entirely.
The line now names the path the file actually lived at, and is phrased as
the condition it really is rather than a promise archive cannot keep.
Regressions for both, plus the three fail-closed branches on the deletion
authorisation path that no test observed: a marker in unparseable YAML, and a
failing unlink. Each verified against a mutation - removing the veto, restoring
the unconditional promise, swallowing the unlink error, and honouring a marker
in broken YAML each fail their test.
Also pins the sync skill's retirement guidance by content rather than by golden
hash, since a hash proves only that it matches its source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(archive): note that retiring a capability strands an in-flight MODIFIED
A capability's main spec is the base #1482's scenario-loss check compares a
MODIFIED block against. Retire the capability and that check goes silent by
design (a missing main spec is the sister-change-in-flight case), so a change
that modifies the retired capability keeps validating clean and then refuses to
archive with "target spec does not exist". Nothing is lost - there are no
scenarios left to drop - but nothing connects the refusal back to the
retirement either, so the changeset says it up front.
Found by testing this PR against the three that merged into main today.
* fix(archive): veto retirement on any heading past the merged section
A sixth data-loss defect, from a second round of review agents. Reproduced
before fixing: a `validate --strict`-clean spec was deleted with a live SHALL
requirement in it, and the report named only "Purpose".
The cause is a mask disagreement. `extractRequirementsSection` - the function
that decides where the Requirements section ENDS - masks fenced blocks only.
`findHeadings`, which both retirement vetoes were built on, masks HTML comments
as well. So a multi-line comment holding a `## ` line terminates the section for
the merge while being invisible to the scan that had to notice it: everything
below became a tail no guard could see. The round-five guard counted `##
Requirements` headings, which the same trick skins straight past.
The veto is now asked of the tail itself - does anything `###`-shaped sit past
the boundary the merge actually chose - read with the fence-only mask, so it
answers the question whatever produced that boundary. That subsumes the
multiple-Requirements-sections case it replaces and every comment variant.
Also from this round:
- The recovery command is derived from the path that was unlinked, not rebuilt
from the capability id. On a case-insensitive filesystem the id and the real
directory differ in case, git is case-sensitive, and the printed command was
one git rejects.
- An absolute recovery path now says which checkout to run it in - for a
selected store, the file is not under the directory archive was run from.
- A declared marker refused by the tail veto says why, instead of dropping the
author who did what the docs asked back into the bare #1302 abort.
- Corrected "draws exactly these four lines" in the sync skill, a claim added
two commits ago that was false when written: the CLI checks two more.
Both regressions are mutation-verified. Reverting the veto to the narrow
multi-section count fails the comment-boundary test.
One reported finding was NOT actioned, because its premise does not hold: a
residual `###` heading INSIDE the section still counts as a requirement to the
validator, so that spec is valid and simply gets written - there is no silent
dead end there to explain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(archive): say the marker needs the schema key beside it
`.openspec.yaml` requires `schema:`, so a file holding only
`retire_capabilities: true` is not honorable metadata and the marker does
nothing. The docs and the abort hint both described adding one line, which sends
anyone creating that file from scratch into a dead end. The message did explain
itself once you were there ("schema: Invalid input: expected string, received
undefined"), but it should not need to.
Pre-existing shared behavior - `skip_specs` has the same requirement - so this
is wording, not a behavior change.
* chore: merge main (#1483) and keep both archive test suites
#1483 landed while this branch was in review. Three conflicts:
- `archive.ts`: one import line, both sides' imports kept.
- `skill-templates-parity.test.ts`: hash constants, resolved by key-union and
then regenerated from the merged source, which is the only authority once two
branches have edited the same template.
- `archive.test.ts`: the trap this repo documents. Both branches appended a
DIFFERENT describe block at the same place - `capability retirement (#1302)`
here, `non-interactive prompts (#1479)` on main - so taking either side would
have dropped 16 or 133 tests with a green suite. Both are kept.
The conflict boundary also cut the retirement describe's last two closing
braces, which `tsc --noEmit` accepted and only esbuild caught as "Unexpected
end of file". Restored by brace-balance against both parents.
Verified after: every one of main's 91 archive titles and 19 parity titles is
present, #1483's describe still holds its 16 tests, and its own non-interactive
repro still behaves as it does on main.
* fix(archive): only print a recovery command that would actually run
Both blockers from the last review.
The recovery line offered `git checkout HEAD -- <path>` for every retirement,
including ones where the file never lived under the directory archive was run
from: a selected store, or a symlinked capability directory. Git rejects an
absolute path from a different worktree however it is quoted, and an unquoted
path containing a space splits when pasted - a real store path reproduced both.
Those cases now say where the file was and leave recovery to the reader, rather
than handing them a command that cannot work. The ordinary case still gets the
command, quoted when the path needs it, via the portable quoting #1483 already
established for change names.
And `openspec/specs/specs-sync-skill/spec.md` still authorised deletion from the
four original conditions, with no mention of the tail-heading veto the CLI
gained - so the living spec permitted something the code refuses. It now carries
that condition, and a parity test pins it in the generated guidance so the two
cannot drift apart again.
Both fixes are mutation-verified: restoring the unconditional command fails the
escaped-path regression, and rewording the veto out of the template fails the
guidance test.
* fix(archive): retire only what the merge can account for
Replaces the tail-heading veto with a rule that does not read Markdown at all.
Six review rounds each found a different way to dress content so a heading scan
would miss it: a second `## Requirements` section, a `##` inside an HTML comment
ending the section early, a three-space indent, a setext underline. Every fix
was another regex approximating a parser, and every round found the next skin.
`extractRequirementsSection` has already split the file into the parts this
merge understands. So instead of asking "does anything here look like a
requirement" - a question a regex and a renderer answer differently - the guard
now asks where content ended up: anything non-blank between the `## Requirements`
header and the first requirement, or after the section ends, is content the merge
carried through without understanding, and a retirement that would delete the
file is refused. There is no second opinion to disagree with the first, because
there is no second parse.
The in-block heading guard stays, and its comment now says why: a `###` heading
that is not a requirement header is absorbed into the block above it, so it
never reaches the preamble or the tail. Folding that into the rule above needs a
parser that ends a block at any `###` heading, which belongs in the parser.
This narrows the feature: a spec carrying an authored section beyond Purpose can
no longer be retired automatically. That is deliberate. The abort names the
lines that stood in the way, and deleting a file whose contents this merge
cannot enumerate is exactly the case a person should decide.
Depends on #1490 for indented requirement headers, which are swallowed by the
block parser before any of this runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): account for the whole spec, not two slices of it
Defect eight, same class as the seven before it. The guard asked where content
landed, which was the right question, but it only read two of the five slices
`extractRequirementsSection` produces: the preamble and the tail. Content simply
moved somewhere nobody looked.
Reproduced: a hand-written migration runbook and a table written below a
requirement's scenarios live inside that requirement's `raw` - the block runs to
the next header the parser RECOGNISES - so removing the requirement deleted them,
and the report said "Its section(s) went with it: Purpose". Not silence: a false
statement the reader can act on. The same hole covered anything written above
the `## Requirements` section. And because the abort hint is gated on the same
checks, an unmarked run RECOMMENDED adding the marker that destroys it.
The audit now covers the whole file. Expected: the title, the `## Purpose`
section, the `## Requirements` header, and inside each block a requirement's own
parts - its header, its statement, its scenarios' bullets. Every other non-blank
line is reported and refuses the retirement. That folds in the `###`-heading
guard, which was a patch on this same leak using the technique the rewrite was
meant to abandon.
One reported shape is deliberately not a case: prose between `## Purpose` and
`## Requirements` IS the Purpose body, since the section runs to the next `##`,
and the warning already names Purpose as going with the file. The test says so.
Both regressions fail against the two-slice version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(specs): keep content absorbed into a removed requirement
A requirement block's `raw` runs to the next header the parser RECOGNISES, so a
heading it does not - one indented by the 0-3 spaces CommonMark allows, or a
plain `### Notes` - is absorbed into the requirement above it. Removing that
requirement deleted the absorbed content with it. Silently: nothing counted it,
so nothing warned, and the spec left behind still validated.
Reproducible on main with no marker and no capability retirement involved.
Anything from the first `#`/`##`/`###` heading after a removed block's own
header is now kept in place. `####` is excluded deliberately - a requirement's
`#### Scenario:` headings are its own and go with it.
This replaces an earlier attempt on this branch that widened every heading
pattern in both parsers to accept indentation. That was wrong twice over. It
reclassified content, so a spec that was valid became invalid - commented-out
and indented examples started parsing as real requirements, taking `list` from
1 requirement to 3. And it did not even fix the bug: moving the line out of the
block only meant the reconstruction dropped it at a different step, since
`rebuilt` is assembled from `before + header + kept blocks + after` and anything
skipped is simply gone.
So nothing is reclassified now. An indented heading is still not a requirement,
exactly as before; it just survives its neighbour's removal, which is all this
ever needed to do. The repo's own corpus produces byte-identical `list`,
`validate --specs --strict` and `validate --changes --strict` output.
Four regressions, each mutation-verified: removing the salvage fails the three
absorbed-content cases, and counting `####` as a boundary fails the scenario
case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(specs): keep notes absorbed into a modified or removed requirement
A slow audit of the previous commit found the fix covered one of three paths.
A requirement block absorbs anything below it that the parser does not read as
a new header - a note indented by the 0-3 spaces CommonMark allows, say - so
that content rides inside the block. The previous commit salvaged it when the
requirement was REMOVED and missed MODIFIED entirely: that path rebuilds the
block from the delta, which never carried the note, so it was dropped exactly as
before. Verified against the real CLI: main loses it on both paths.
RENAMED was the opposite trap. It rewrites the original block's header line in
place, so the note is already there - but it also deletes the original key from
the block map, which made the requirement look REMOVED to the salvage and
produced a duplicate. Tracking which operation applied is therefore not reliable
at this point in the merge, so the salvage now asks the assembled result
instead: re-insert a note only when nothing else in the rebuilt section already
carries it. That is correct for all three paths by construction.
Salvaged content also keeps its position now, next to the requirement it was
written beside, rather than being appended at the end of the section.
Six regressions, three of them mutation-verified against this logic: never
re-inserting fails four, always re-inserting duplicates on rename, and appending
at the end loses the position.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(specs): decide salvage by identity, not by matching text
Another audit pass, another defect in my own fix.
Deciding whether a note survived by searching the rebuilt section for its text
is wrong when two requirements carry the same note: the first copy is found,
and the second is dropped. Reproduced - two removed requirements each followed
by an identical `### Notes`, one note destroyed.
Survival is a question about the block, not about text. An untouched block is
the same object the parser produced and still carries its note; a replaced one
is a different object and does not. The RENAMED path previously blurred that by
copying the whole raw, so it now carries only the requirement's own lines and
the salvage puts the note back like every other path. With every replacement
uniformly lacking the tail, `replacement !== block` decides it exactly, and no
text is compared at all.
Four properties, each mutation-verified: matching text instead of identity
loses the duplicate note, always re-inserting doubles an untouched block's note,
letting RENAMED keep the tail doubles it on rename, and counting `####` as a
boundary severs a requirement from its scenarios.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(specs): warn when a note absorbed into a requirement will be deleted
An adversarial review found the previous approach was worse than the bug.
Salvaging the "foreign tail" out of a requirement block relied on a positional
rule: everything after the first heading-shaped line is not the requirement's.
That is not true. A `# comment` inside a scenario bullet, or a markdown example,
matches the same shape - and on MODIFIED the old text was then spliced back in
after the new, so the spec asserted both. The validator called the result valid,
and re-applying the same delta grew the file every time. Reproduced end to end.
It also turned a working archive into a hard abort: preserving an unindented
`### Notes` made the rebuilt spec fail validation as a scenario-less
requirement, so changes that archived cleanly on main stopped archiving, with an
error that never mentioned the note.
Measured before choosing: 3 of 742 requirement blocks in this repo contain a
heading-shaped line, and the repro shows those are false positives. Trading a
rare silent deletion for silent corruption on the most common operation is a bad
trade.
So the merge is left exactly as it was - byte-identical output, verified against
main - and the loss is reported instead. That fixes the part of the bug that
actually hurt: it was silent. A wrong warning costs a line of output; acting on
a wrong answer rewrites the spec.
Eight tests. Dropping the warning fails three; ignoring the fence mask fails
one - the fence case the previous version left unpinned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): scope a scenario's bullets, and stop refusing ordinary prose
Defect nine, plus the over-refusal it exposed.
Every bullet counted as a scenario's own, anywhere in the block. So an
operational note bulleted below the last scenario - "IMPORTANT: escrow keys
live in the legacy vault" - was deleted with the file, on a spec that passes
`validate --strict`, and the report named only "Purpose". A scenario's bullets
run unbroken beneath its header; a blank line after them ends the run, and
bullets past that point are the author's own note.
Measuring the guard against this repo's 36 specs then showed the opposite
failure was already there: 7 of them could never be retired, almost entirely
because every fenced line inside a requirement was treated as foreign. A code
example inside a scenario is that requirement's own content - a
`### Requirement:` inside a fence is not a heading to any reader - so fenced
lines are now accounted for, as are numbered lists and a statement that opens
with inline code.
One ambiguity is left deliberately unresolved: a scenario whose bullets are
split by a blank line reads exactly like a note bulleted below it, and no
line-based rule separates them. Those specs are REFUSED, never deleted. The
abort quotes the lines, and the author moves them or removes the file by hand.
Refusing costs a message; the alternative costs the file.
Two regressions: the bulleted note must refuse, and a requirement using a
numbered list, a fenced example and an inline-code statement must still retire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): a section is not only an ATX heading
Defect nine, from a deep adversarial pass, and it is the same species as the
eight before it: the guard decided what a section IS by one syntax while a
reader recognises three.
Once `## Purpose` was seen, every later line in the pre-requirements slice was
accepted as its body until the next ATX `##`. But a setext underline turns the
line above it into a heading, and raw HTML says so outright - a reader sees a
sibling of `## Purpose`, not more of it. So a whole authored section could sit
between Purpose and Requirements, pass `validate --specs --strict`, and be
deleted with the file while the report said only "Purpose". On main the same
archive aborts and loses nothing.
Reproduced with a `Data Migration Notes` section underlined with dashes: the
capability retired, the notes gone, unnamed. Now refused, with the lines quoted.
Two path defects from the same review, one fix: the reported path was rebuilt
from the capability id, so on a case-insensitive filesystem it differed in case
from the file actually unlinked and git rejected the printed command; and a
capability directory symlinked to a sibling deleted one spec while naming
another. `retireSpec` now always returns the path it unlinked, and archive
reports that. Whether to print a command at all is decided against the REAL
repo root, so a symlink that stays inside the repo still gets a working command
and only a path that genuinely leaves it falls back to prose.
Also pins `!skipValidation` in isolation. The existing --no-validate test passed
for the wrong reason - its fixture was blocked by the content guard - so the
conjunct itself was unpinned.
Four regressions, all mutation-verified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(archive): close remaining capability retirement gaps
* fix(archive): close final transaction safety gaps
* fix(archive): close retirement race windows
* fix(archive): preserve retirement authorization
* fix(archive): verify complete fallback copies
* fix(archive): preserve transactional safety
Reject structurally ambiguous or symlinked inputs before mutation, serialize archive claims safely, and preserve permissions during verified fallback moves.
Keep retired specs as inode-preserving backups until the archive commits, restore them on rollback, and retain any backup changed concurrently instead of deleting user data.
* fix(archive): preserve replaced claims on Windows
Add a per-claim nonce and verify stable claim contents before unlinking because Windows file IDs may not distinguish a replacement lock entry.
* test(archive): respect Windows deferred deletion
Skip the POSIX unlink-and-recreate claim simulation on Windows, where deletion of an open file remains pending until the original handle closes.
* test(archive): align symlink fixtures with path boundaries
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
84ebc57cb3 |
fix(validate): report scenarios a MODIFIED requirement would drop (#1482)
* fix(validate): report scenarios a MODIFIED requirement would drop `openspec validate <change>` accepted a MODIFIED requirement that omits a scenario the main spec still has, even with --strict. Archive refuses to apply that block (a MODIFIED replaces the whole requirement, so the omitted scenario would be lost), so the change could pass validation, be implemented and reviewed, and fail only days later at archive time (#1477). Validate now runs the same non-mutating check against the main specs and reports each omitted scenario, naming the delta file. The comparison itself moved to the parser module so archive and validate share one implementation and cannot drift. The check is silent when the main spec file or the requirement header is absent — a MODIFIED written against a sister change still in flight is a separate condition archive gates — so validate can only report what archive already refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(validate): tighten the scenario-loss check after review Keep the moved scenario parser module-private, derive change validate's main specs root from the changes root it already resolved, replace the rename re-keying with a lookup fallback, and say at archive's call site why it does not opt in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): follow rename chains when checking for dropped scenarios A delta that renames A to B and then B to C leaves C holding A's block at archive time. Walk the rename map instead of looking it up once, so the chained case reports the same loss archive refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): close the gaps five adversarial reviews found Code: - An unreadable main spec was swallowed, so a change archive aborts on validated clean. Only ENOENT/ENOTDIR mean "no main spec" now; anything else is reported. - A MODIFIED naming a header the same delta renames away no longer names scenarios from the block it would not land on. That contradiction is already reported on its own, and the scenario list pointed at the wrong requirement. Guidance: the sync-specs skill told agents a MODIFIED block may carry only the changed scenario, and its format reference showed one. Both validate and archive reject that shape, so the template, the generated skill, and the golden hashes are updated to match the schema's own rule. Tests: the CLI wiring had no coverage at all — removing the argument that turns the check on broke nothing. Adds end-to-end coverage of every entry point and exit code, plus the non-strict default, a fenced scenario in the delta, an unreadable main spec, the rename-away case, and a rename cycle. Loose assertions now pin the scenario-loss issue itself. Docs: a troubleshooting entry for the new message, and the changeset says that a stale change will newly fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): never turn a transient read error into a verdict The unreadable-main-spec report added in the last commit fired for any errno that was not ENOENT/ENOTDIR, which includes resource errors like EMFILE that say nothing about the file. `validate --all` reads six changes at once, so a busy process could have failed a change that is fine. Reported now only for the codes that mean the file itself is unusable and will be just as unusable when archive reads it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(troubleshooting): label the example fence (MD040) Every other fence in the file names its language. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1aa0f2abfc |
feat(init): add shared agents skills target (#1303)
Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
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
|
||
|
|
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> |
||
|
|
fc886af7f9 |
fix(templates): auto-select the only active change instead of always prompting (#1468)
* fix(templates): auto-select the only active change instead of always prompting
The continue, update, verify, sync, and archive workflows told agents
'Do NOT guess or auto-select a change. Always let the user choose', which
contradicted their own Input line ('check if it can be inferred from
conversation context') and stalled every invocation on a question with a
single possible answer when only one change was active. Align them with
the selection pattern /opsx:apply has used since #513: use the provided
name, infer from context, auto-select a sole active change, prompt only
when ambiguous, and always announce the selection with how to override.
Bulk archive keeps its always-prompt behavior.
Closes #679
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(openspec): add the announce clause to the update workflow's selection contract
CodeRabbit noted the add-update-workflow delta spec and design sketch
adopted auto-selection without the 'Using change: <name>' announcement
the other selection contracts require. Add the same announce-and-override
clause so the update skill's contract matches the template it describes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d32d49f066 | chore(openspec): archive schema init force validation change (#1467) | ||
|
|
f917b8be5e |
fix(status): order artifacts by the schema, not the alphabet (#1465)
* fix(status): order artifacts by the schema, not the alphabet
Artifacts that become ready at the same time were sorted alphabetically,
so spec-driven's `specs` and `design` - both requiring only `proposal` -
came back as design first. `openspec status` listed design above specs
and `nextSteps` pointed at design, sending agents to write design.md
before any spec existed. That contradicts the schema's own description
(proposal -> specs -> design -> tasks), the design instruction ("reference
the specs for requirements"), the workflow docs, and the schema `openspec
schema init` scaffolds (where design requires specs).
Break ties by the order the schema declares its artifacts instead. The
dependency edges are untouched, so nothing newly blocks and no artifact
becomes mandatory - only the order of equally-ready artifacts changes, and
it now follows the sequence the schema author wrote, for custom schemas
too.
Closes #692
Closes #695
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(status): re-sort the whole ready queue, not just new arrivals
CodeRabbit caught it: sorting only the newly ready artifacts left an
already-queued artifact ahead of one declared earlier. For [root, child,
laterRoot] where child requires root, the build order came out root ->
laterRoot -> child even though child is declared first and both are ready
after root. Sort the full queue after each push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(instructions): order unlocks like status, and document the guarantee
Adversarial review found `unlocks` was left alphabetical while build order,
ready lists and blocked lists moved to declaration order, so `openspec
instructions proposal` said "enables: design, specs" while `openspec status`
listed specs first - the one field whose job is naming what comes next
disagreed with everything else. getAllArtifacts() already yields declaration
order, so the stray sort is simply dropped.
Also make compareByDeclarationOrder a method rather than an arrow-valued
field: the field added an own enumerable function property that made
ArtifactGraph fail structuredClone.
Docs and specs updated for the new guarantee:
- openspec/specs/{artifact-graph,cli-artifact-workflow,instruction-loader}
- docs/agent-contract.md: status --json and instructions --json ordering
- docs/opsx.md: the status sample's missingDeps was missing design
- changeset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(commands): correct the continue transcript's blocked and unlocked lines
The sample said tasks was blocked by specs alone and that creating specs
made tasks available; tasks needs design too. Same class of inaccuracy as
the status samples this branch already corrected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: state the ordering guarantee as dependency-order-then-declaration
CodeRabbit was right that "artifacts appear in the order the schema
declares them" over-claims: dependency order still wins, and declaration
order only breaks ties. Proved with a schema that declares tasks, specs,
proposal - status renders proposal, specs, tasks, not the declared order.
Corrected in the cli-artifact-workflow spec, agent-contract.md, cli.md and
the changeset.
Also restores "status": "blocked" in the opsx.md status sample (split across
two lines so the ASCII box still aligns) and uses "recommends writing next"
in the artifact-graph spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
eac2973819 |
feat(instructions): add runtime context and operation guidance (#1062)
* docs(openspec): define runtime guidance for apply and archive - add typed apply and archive operation guidance - extend runtime instruction inputs for apply and archive - preserve existing archive execution and spec sync behavior * docs(openspec): refine apply and archive guidance design - carry artifact rules into archive-driven spec sync - reuse one config snapshot per instruction command - clarify that operation guidance is advisory - classify bulk archive skill as a new capability * docs(openspec): clarify artifact rule handling for archive and sync - define owning artifact resolution for mixed schemas - apply artifact rules in archive and standalone sync flows - align archive and bulk guidance conflict semantics - clarify existing apply pause-on-blocker behavior * docs(openspec): tighten archive and spec sync contracts - scope delta discovery and artifact rules to the specs artifact - fail closed on invalid archive and specs instruction responses - clarify no-write and no-move behavior for single and bulk archive * feat(workflow): extend config injection to apply and archive - expose project context and operation guidance in apply/archive instructions - apply context and guidance across apply, archive, bulk archive, and spec sync - preserve workflow state, artifact-rule boundaries, and fail-closed behavior - update generated skills, documentation, tests, and parity hashes * fix(skills): make the archive-inputs lookup fail open `openspec instructions archive` is introduced by this PR, so no released CLI has it. The archive and bulk-archive skills required a zero exit status from that lookup and told the agent to stop when it failed. `skills/` is installed standalone via `npx skills add Fission-AI/OpenSpec` and drives whatever CLI the user already has, so between merging this and publishing the next release every skills.sh consumer would have had archiving blocked outright — verified against @fission-ai/openspec@1.6.0, which exits 1 on that command. The lookup only supplies optional prompt inputs, so it now degrades: on a non-zero exit or invalid JSON the workflow continues with no context and no operation guidance. The `openspec instructions specs` lookup is an existing command and stays fail-closed, since a missing rule set there would silently change what gets written to main specs. Parity assertions updated to encode fail-open for archive inputs and fail-closed for specs rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: showms <showms@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5348da930c |
fix(schema): validate artifacts before forced init (#1446)
* fix(schema): validate artifacts before forced init * test(schema): assert successful forced init exit status --------- Co-authored-by: showms <showms@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
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> |
||
|
|
6a4f0d7f33 |
fix(archive): keep the delta spec's Purpose in a new main spec (#1431)
* fix(archive): keep the delta spec's Purpose in a new main spec Archiving a change that creates a brand-new capability always overwrote the delta's authored `## Purpose` with the TBD placeholder, so the Purpose had to be re-typed by hand after every archive. buildSpecSkeleton now takes the delta's Purpose when there is one. The placeholder still appears when the delta has no Purpose or an empty one, and an existing main spec's Purpose is never touched. The spec-driven schema now tells agents to open a new capability's delta with a `## Purpose` (and not to add one to a delta for an existing capability), so the default workflow stops producing placeholders. Closes #1413 Closes #369 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(archive): create the temp dir with fs.mkdtemp Matches the mkdtemp pattern the rest of the suite already uses and clears the CodeQL insecure-temp-file alerts on this file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(archive): pin fenced-Purpose behavior and align the spec wording Review flagged that the spec scenario read as "only non-fenced content counts", which the code does not do. Masking fenced lines out of the Purpose body would truncate a legitimate Purpose that includes an example block, so the code is right and the wording was wrong. - Reword the cli-archive scenarios: the fence check is on the `## Purpose` header, and the section body is copied verbatim. - Add regressions: fenced code inside a real Purpose survives, a Purpose header that only appears inside a fence falls back to TBD, and an empty Purpose section falls back to TBD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(archive): never let a carried Purpose abort the archive Self-review found a regression introduced by the carry-over: a delta whose `## Purpose` body contains a `### Requirement:` header put that header outside `## Requirements` in the new main spec, so the structure guard rejected it and archive exited 1. The same delta archived fine before this branch. Fall back to the placeholder and warn when the carried Purpose would make the new spec structurally invalid, so archive completes as it did before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(archive): make the Purpose carry-over safe and consistent Three adversarial reviews of the carry-over found the guard added in |
||
|
|
1dc670deea |
fix(templates): stop propose from skipping the specs artifact (#1412)
Squashed for rebase; see PR #1412 for the full commit history. |
||
|
|
0da5f98e14 |
fix(templates): show the main spec format in the sync-specs skill (#1402)
The sync-specs skill's only markdown example was the delta format, so agents (Junie in #1120) copied delta files into openspec/specs/ as-is, leaving ## MODIFIED Requirements headers that the spec parser rejects — openspec view reported 0 requirements. Add a Main Spec Format Reference, point step 4d at it, and add a guardrail against wholesale delta copies. Fixes #1120 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9b5d2cdd0c |
fix(templates): stop instructing a second date prefix on dated archive names (#1388)
* fix(templates): stop instructing a second date prefix on dated archive names The archive-change and bulk-archive-change workflow templates told agents to unconditionally build the archive target as YYYY-MM-DD-<name>, so a change already named with the common YYYY-MM-DD- convention came out double-dated — the template-side twin of the CLI bug fixed in #1316, which a CLI fix cannot reach because the behavior is baked into instruction text. The generate-target-name step and the bulk guardrail now mirror the CLI rule: use the change name as-is when it already starts with a YYYY-MM-DD- prefix, otherwise prepend the current date. The literal mv commands move to <target-name> so an agent copying them verbatim cannot stack dates, and the onboarding walkthrough's archived-path example carries the same caveat. Regenerated skills/ and updated the pinned parity hashes; a new parity test guards the caveat and rejects the raw stacked mv target. * fix(templates): report the derived archive name in success summaries The success and failure summaries still printed archive/YYYY-MM-DD-<name>, so an agent copying them would report a stacked date for a change whose name already carries a YYYY-MM-DD- prefix. Point those examples at <target-name> instead, and widen the regression guard from the mv target to any date used as a path segment, which leaves the rule statements that must keep explaining the derivation untouched. The opsx-archive-skill spec still specified the unconditional current-date rule the previous commit removed from the template, so bring it in line with the wording cli-archive already carries. * fix(specs): name the derived target in the archive scenario The successful-archive scenario still spelled the destination as archive/YYYY-MM-DD-<name>/, the same literal form this PR removed from the templates, so it contradicted the keep-as-is rule the behavior requirements now carry. |
||
|
|
c439a4ee48 |
fix(parser): stop delta section dividers from becoming phantom requirements (#1411)
* fix(archive): stop reporting phantom proposal warnings from delta specs `openspec validate --strict` reported a change as valid while `openspec archive` printed "Proposal warnings in proposal.md" for the same change, blaming requirements that do not exist. Archive validates the proposal with `validateChange`, which parses the change together with its delta specs. Requirement-level issues from those deltas were printed in the proposal block even though they are not proposal issues. Two problems followed: - The change parser records every requirement under both `requirement` and `requirements`, so each defect was printed twice, then a third time by the delta report. - A heading inside a delta section that is not a `### Requirement:` heading was parsed as a requirement, producing a scenario warning against a requirement that does not exist. The delta reader already handles this correctly and reports it as an informational note. Proposal warnings now report proposal-level issues only. Delta spec issues keep being reported once, by the delta report, with the capability file path and requirement name. Exit codes are unchanged: this block was already non-blocking, and blocking delta validation is untouched. Refs #498 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(archive): correct proposal-warning claims and pin bracket-path rules Review follow-ups, no behavior change: - The delta report prints only the issue message, never `issue.path`, so it does not name the capability file. Drop that claim from the spec scenario and the code comment; two capabilities with the same defect print two identical lines. - Only the missing-scenario class was reported three times. Say that precisely instead of generalizing to every delta error. - Widen the spec scenario: the filter applies to every archive, not only to changes carrying a stray heading. - Add a test pinning that applyChangeRules bracket paths (`deltas[<n>].description`) survive the dot-anchored filter, so a future path normalization cannot silently widen it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parser): ignore delta headers that are not "### Requirement:" Fixes the cause of #498 rather than one of its symptoms. A header inside a delta section that is not a `### Requirement:` header — a divider such as `### Documentation Requirements` — was read as a requirement with no scenario. That invented a delta that does not exist: `openspec archive` warned about a missing scenario, and `openspec show <change> --json` and `openspec change list` counted it. ChangeParser now filters those headers before reading requirements, matching REQUIREMENT_HEADER_REGEX, which the delta reader already uses. The override lives in ChangeParser, so main spec parsing — view, list, spec --json, spec validation — is untouched. The archive filter stays: it covers the half the parser cannot. The change parser records every requirement under both `requirement` and `requirements`, so each delta defect was printed twice, and REMOVED requirements are names-only by design yet were reported as missing a scenario on every correct removal. Also from review: - Soften the spec scenario; delta spec validation does not always run (the hasDeltaSpecs gate is case-sensitive), so it cannot be promised as the reporter. - Assert VALIDATION_MESSAGES constants instead of message literals. - Add parser-level and REMOVED-only regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
60f720c43a |
fix(feedback): submit feedback when the repo has no feedback label (#1396)
`openspec feedback` passed `--label feedback` unconditionally, but the repository does not define that label. gh resolves label names before creating the issue, so it failed with "could not add label: labels not found: feedback" on every invocation and the command exited non-zero, discarding the feedback the user had just composed. Retry once without the label when — and only when — gh's stderr reports that it could not add the label, and tell the user the label was not applied. Every other failure keeps its existing behavior: print gh's error and exit with gh's exit code, with no retry. Only stderr is matched, because the error message also embeds the command line, which carries the user's own feedback text. The cli-feedback spec gains a scenario for the unlabeled path, and its gh-failure scenario is narrowed to exclude it. The fallback scenarios are unchanged. Refs #1091 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b474f81cb4 |
fix(templates): don't archive a change before its spec sync finishes (#1394)
* fix(templates): wait for the spec sync before archiving a change The generated openspec-archive-change skill dispatched the spec sync to a subagent via the Task tool and then moved changeRoot in the very next step, with nothing requiring it to wait. Where subagents run asynchronously, the archive relocates the delta specs out from under the running sync, so the change is archived while openspec/specs/ is never updated — and the success summary still reports "Specs: ✓ Synced". Step 4 now requires waiting for the dispatched sync to return, verifying the synced requirements are present in the main spec, and stopping without archiving if either check fails. Adds a matching guardrail bullet and a parity assertion so the gate cannot silently disappear again. Fixes #1393 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(templates): run the spec sync inline and verify it before archiving Addresses review on #1394. The first pass asked the agent to "wait" for a dispatched subagent, but subagents run in the background by default and the wait is not reliably expressible in prose — the race survived. It also gated the archive on the synced requirements being *present*, which a correct REMOVED-only or RENAMED-only sync does not satisfy, turning a successful sync into a hard block. The sync now runs inline via the Skill tool, with a synchronous-subagent fallback for harnesses that need one. Verification follows delta semantics: ADDED/MODIFIED present, REMOVED gone, RENAMED under the new name, checked across every capability the sync touched. Also resolves the opsx command variant's contradiction with its own guardrail, stops the summary reporting a checkmark that step 4 never verified, and updates openspec/specs/opsx-archive-skill/spec.md, which still said the skill proceeds with the archive regardless of the sync choice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(specs): encode delta verification semantics in the archive skill spec The scenario said the agent verifies each capability "matches its delta", which is ambiguous about what a match means — and a REMOVED-only sync correctly leaves requirements absent. Spell out the predicate the template implements, and separate an explicit "Archive without syncing" choice from a requested sync that failed or could not be verified: only the former may skip verification, the latter must stop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(templates): close verification holes and drop Claude-only tool names Second review pass on #1394. The gate was weaker than it looked. "MODIFIED requirements present" is vacuous — a MODIFIED requirement exists in the main spec before the sync runs, so a no-op sync passed the check for the most common delta shape, which is the exact symptom #1393 reports. "RENAMED under their new name" passed a sync that copied rather than renamed, leaving both names behind. And scoping the re-check to "every capability it touched" derived the verification set from the artifact being verified, so a silently skipped capability escaped it. Verification is now bound to the delta specs in artifactPaths.specs, covers the changes each MODIFIED delta names, and requires RENAMED requirements to be gone from the old name. Separately, the previous pass named the Claude Code "Skill tool" and run_in_background in a template that is also the slash-command source for ~28 other tools, where skills are removed entirely for commands-only delivery. Both variants now use the runtime-neutral phrasing bulk-archive-change already uses. Also: route the prompt options explicitly instead of defaulting unknown answers to archive, tell the user a stopped archive is recoverable, and mark the summary line as a conditional rather than literal text to copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(templates): verify the sync by re-running step 4's own comparison The verification predicate restated delta semantics in its own words, which could drift from what openspec-sync-specs actually does. Anchor it instead to the comparison step 4 already performs before prompting: a successful sync leaves nothing to apply, so every capability must read as already synced. The explicit ADDED/MODIFIED/REMOVED/RENAMED bullets stay as the definition of what "nothing left to apply" means. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d2082d1f91 |
docs: align cli-update OpenCode spec with commands/ and opsx-* paths (#1170)
Update OpenCode cli-update spec to .opencode/commands/opsx-*.md and document legacy path cleanup via init. Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
9b70481df7 |
fix(archive): keep an existing date prefix instead of stacking a new one (#1316)
Archiving unconditionally prepended today's date to the change name, so a change already named with the common YYYY-MM-DD- convention came out double-dated (2026-07-07-2026-07-04-voice-copilot-v1) — and when archived on a later day, the folder sorted under a day on which the change did not happen. Detect a full YYYY-MM-DD- prefix and archive the change under its own name. Names without one (including partial dates like 2026-07-feature) keep the current behavior. This also makes the naming idempotent. Nothing in src/ parses the date back out of archive folder names — the prefix only drives human chronological sorting — so keeping the original date is the minimal, non-breaking choice. The cli-archive spec wording is updated to match. Fixes #1309 |
||
|
|
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> |
||
|
|
79f1dac668 |
feat(codex): make Codex skills-only and retire managed custom prompts (#1283)
* feat: make Codex skills-only * test: cover codex legacy prompt paths * fix: address review feedback for Codex skills-only migration * fix(codex): revalidate managed global prompt paths before cleanup * resolve conflicts with upstream/main * fix(codex): refine legacy prompt migration --------- Co-authored-by: showms <showms@users.noreply.github.com> |
||
|
|
4a0f15d3b2 |
feat: add Hermes Agent support (#1292)
* feat: add Hermes Agent support * feat(init): surface Hermes external_dirs setup note during init and update Hermes only loads skills from ~/.hermes/skills unless the project .hermes/skills directory is added to skills.external_dirs in ~/.hermes/config.yaml, so init could report success for skills Hermes ignores. Add a setupNote field to AIToolOption, print it after init and update (including the up-to-date path), and cover the adapterless init path, the adapter registry, and both update paths with tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e60ff53644 |
update Kimi CLI to Kimi Code (#1208)
* update Kimi CLI to Kimi Code * feat(migration): migrate OpenSpec skills from legacy .kimi to .kimi-code Renaming the Kimi skillsDir stranded OpenSpec-managed skills under .kimi/skills: update and cleanup only inspect current AI_TOOLS paths, so old installs would never be detected or refreshed again. Add a legacy skillsDir migration (run by init and update before tool detection) that moves openspec-* skill directories to .kimi-code/skills, preserves user files, and removes the legacy directories only when empty. Keep .kimi as a detection path and cover the migration with focused init and update tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): update cli-init Kimi scenario to .kimi-code Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8886e3ae22 |
feat: add Oh My Pi (OMP) tool support (#1276)
* feat: add Oh My Pi (OMP) tool support Add ToolCommandAdapter for Oh My Pi terminal AI coding agent. - New adapter: src/core/command-generation/adapters/oh-my-pi.ts - Commands: .omp/commands/opsx-<id>.md with description frontmatter - Hyphen transform: /opsx: -> /opsx- (filename = command name) - Argument injection: **Provided arguments**: $@ after **Input**: heading - escapeYamlValue applied to description field - Register in CommandAdapterRegistry and adapters/index.ts - Add oh-my-pi to AI_TOOLS with skillsDir: '.omp' - Add to hyphen command transformer whitelist in init.ts and update.ts - Full test coverage (10 cases) in adapters.test.ts - Update docs/supported-tools.md with directory reference and tool ID Closes #713 * fix: address CodeRabbit nitpicks - Move ohMyPiAdapter import before opencodeAdapter (alphabetical order) - Break long SHALL sentence and remove redundant 'follows after' in spec * docs: polish Oh My Pi support * docs: address Oh My Pi review nits --------- Co-authored-by: TabishB <tabishbidiwale@gmail.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com> |
||
|
|
3f0ca3f6ce |
feat: add Trae command adapter (#1090)
* feat(tools): add Trae command adapter - Added Trae command adapter for generating `.trae/commands/opsx-<id>.md` files - Complete unit tests (9 test cases) and integration tests - Updated documentation and .gitignore - Fixed YAML escaping for carriage returns (\r) Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: handle empty string in YAML escaping - Add explicit check for empty string in escapeYamlValue - Return quoted empty string '""' instead of unquoted empty scalar - Update test to verify empty string is properly quoted Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: address PR review feedback for Trae adapter - Update docs/commands.md Trae entry to reflect generated opsx-* commands - Export traeAdapter from adapters/index.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: align Trae command adapter docs --------- Co-authored-by: jjxyxsjr <jjxyxsjr@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com> Co-authored-by: TabishB <tabishbidiwale@gmail.com> |
||
|
|
a5bfedafc8 |
feat(skills): auto-approve the openspec CLI in generated skills and commands (#1300)
* feat(skills): auto-approve the openspec CLI in generated skills Emit `allowed-tools: Bash(openspec:*)` in every generated SKILL.md so agents that honor the Agent Skills standard run `openspec` commands without prompting on each call. Scope is limited to the CLI; per the standard `allowed-tools` pre-approves rather than restricts, so every other tool a skill uses stays available under the user's normal permission settings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(commands): auto-approve the openspec CLI in Claude slash commands Extend the allowed-tools pre-approval to the second surface: Claude Code /opsx:* slash commands share the skill frontmatter contract, so the Claude command adapter now emits `allowed-tools: Bash(openspec:*)` too. The value is single-sourced in `src/core/shared/allowed-tools.ts` (a leaf module both surfaces import). Other command adapters are unchanged — no other tool's slash-command format defines a per-command pre-approval field; on the skills side every tool already gets the standard field via generateSkillContent and non-implementing tools ignore the unknown key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9a0dfb5cd1 |
refactor: unify requirement reader and surface #498 (#1281)
* docs(openspec): propose spec parser reading fidelity (fixes #361, #498, #312) The requirement-parsing layer silently misreads valid Markdown: - #361: requirement-body extraction returns only the first non-blank line, so a SHALL/MUST that wraps onto line 2 fails `validate --strict`. - #498: `validate` (delta-block parser) and `archive` (full-spec parser) recognize requirements by different rules, so a stray `###` header passes validate but becomes a phantom requirement that blocks archive. - #312 (residual): the requirement-body loop breaks on any `#` line without consulting the code-fence mask, truncating bodies that contain fenced code with `#` comments. Proposal: one shared, multi-line, fence-aware requirement-body extractor used by both the validator and the markdown parser; recognize only `### Requirement:`-prefixed level-3 headers; guarantee validate/archive parity. Adds regression + parity tests. #559 investigated and deferred (ambiguous root cause — see design.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(openspec): bulletproof parser-fidelity proposal with empirical evidence Hardened the proposal after reproducing every claim against main with the bundled CLI and correcting two inaccuracies: - #498 reframed: archive does NOT hard-fail. validate passes; archive emits NON-BLOCKING phantom "Proposal warnings in proposal.md" because validateChange/parseRequirements counts every level-3 header as a requirement, while the delta-block parser (validate) and specs-apply (rebuild) only recognize canonical `### Requirement:`. It is a consistency bug, not data loss. Verified the rebuilt spec is clean. - #312 reframed: the original repro is already fixed by codeFenceLineMask (requirement count verified correct). The residual is a regression hazard: the body loop is fence-unaware, harmless only while first-line-only, so the multi-line fix must be fence-aware from the start. Also: unify recognition on the canonical REQUIREMENT_HEADER_REGEX (/^###\s*Requirement:\s*(.+)$/i, case-insensitive); surfaced a third latent inconsistency (Zod substring includes('SHALL') vs delta word-boundary \b(SHALL|MUST)\b) and added a single-predicate requirement; verified zero non-Requirement level-3 headers in repo specs (CI-safe); added edge-case scenarios (multi-line spec+delta paths, fenced scenario-looking lines, REMOVED/RENAMED unaffected, display vs detection); replaced broken relative links with plain paths. Proposal passes `openspec validate --strict`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(openspec): deepen parser-fidelity proposal — add #418, upgrade #312, tier the risk Second adversarial bulletproofing pass (reproduced everything against main): - Add #418 (metadata-before-description): live on the spec path (req.text = "**ID**: ...") but ALREADY fixed on the delta path. The asymmetry is direct evidence for unifying the two extractors. - Upgrade #312 from "regression hazard" to LIVE bug: a fenced code block before the prose line makes req.text = "```bash" on both paths today (distinct from the already-fixed section-count manifestation). - Tier the fixes by risk after auditing the existing test contract (markdown-parser.test.ts, 15 tests green on main): Tier 1 (false-negative fixes #361/#418/#312): only widens what is read; updates one test (:331, which asserts the first-line bug). Fence tests (:106/:139) preserved because skip-and-join keeps SHALL-first bodies. Tier 2 (recognition tightening #498): canonical ### Requirement: only; a deliberate behavior change that updates bare-header tests (:258/:310) and needs a migration note. Flagged for maintainer decision, with a conservative opt-in-lint alternative documented. - Surface the four-column extractor divergence table (capture / metadata / recognition / predicate) and an explicit "Behavior changes and test impact" section with exact test line refs. Proposal passes `openspec validate --strict`. Does not claim #1156 (PR #1280). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(openspec): third pass — reject recognition tightening, add fenced-scenario bug, #498→safe INFO Third deep pass found the prior Tier 2 (recognition tightening to `### Requirement:`) was the WRONG fix and over-scoped: - Bare `### <statement>` headers are a SUPPORTED, tested requirement format: test/core/validation.test.ts asserts a bare-header spec is valid, and bare headers appear across json-converter/archive/spec tests and tmp-init fixtures. Tightening would break a large test surface and silently drop requirements from real specs. REJECTED, with evidence documented. - Replace the #498 fix with a SAFE INFO note in validate <change> that surfaces non-`### Requirement:` headers in delta sections. INFO never fails validation (strict: valid = no errors && no warnings), so nothing newly fails. - New bug found and folded in: countScenarios is fence-unaware, so a `#### Scenario:` inside a fenced block is counted as real — a malformed delta passes validate <change> while validate <spec> correctly fails. Same fence family. - Proved the archive WRITE path is independent of the reader: specs-apply rebuilds from raw `### Requirement:` blocks (extractRequirementsSection + RequirementBlock.raw), never parseSpec/req.text → Part A cannot change archived content. Net effect: recognition is unchanged, so the proposal now updates exactly ONE existing test (:331, the first-line assertion) instead of breaking bare-header tests. Consolidated to a single cli-validate delta (dropped cli-archive and openspec-conventions deltas). Dropped the no-space-header hypothesis (no divergence). Passes `openspec validate --strict`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parser): unify the requirement reader, fence/metadata/multi-line aware (#361, #418, #312); surface #498 The requirement reader was implemented twice — MarkdownParser.parseRequirements (validate <spec>/archive) and Validator.extractRequirementText/countScenarios (validate <change>) — and the two had drifted. Both now delegate to one shared, fence-/metadata-/multi-line-aware extraction in parsers/requirement-text.ts so they cannot diverge again. Part A — unify the reader: - Capture the full requirement body up to the first non-fenced `#### Scenario:`, skipping blank, `**metadata**:`, and fenced-code lines; run SHALL/MUST detection over the whole body. Fixes a wrapped keyword being dropped (#361), metadata before the description failing validate <spec> (#418), and a fenced block before the prose line becoming the requirement text (#312). - Count only non-fenced `#### ` headers, so a `#### Scenario:` inside a fenced example no longer counts as a real scenario in validate <change> (parity with validate <spec>). - One whole-word `\b(SHALL|MUST)\b` predicate (containsShallOrMust) shared by the validator and base.schema, replacing the substring/word-boundary split. - Extract buildCodeFenceMask into the shared module; MarkdownParser and ChangeParser import it (single fence implementation). Part B — surface #498 safely: - validate <change> emits an INFO note when an ADDED/MODIFIED Requirements section contains a non-`### Requirement:` level-3 header (one the delta reader silently skips). INFO never changes the valid result, including under --strict, so nothing newly fails. Recognition is unchanged: bare `### <statement>` headers remain a supported requirement format. Write path is unaffected: specs-apply rebuilds from raw `### Requirement:` blocks, never req.text, so archived content cannot change. Displayed text in JSON output and delta descriptions now reflects the full body. Tests: markdown-parser.test.ts:331 updated to expect the full body; regression tests added for #361/#418/#312, the fenced scenario, the #498 INFO note, a single-line guard, and CRLF. Changeset added (patch). tasks.md completed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parser): add cross-reader predicate + metadata-only guards (design edge cases) Exhaustive verification of the unified reader surfaced two design "edge cases for tests" not yet covered by committed unit tests: - Cross-reader predicate agreement: a SHALL substring inside a word ("MARSHALL") is rejected identically by validate <change> and validate <spec> — proving the one shared whole-word predicate, and guarding against a regression to the old substring check. - Metadata-only body still fails validation (no requirement text) on the delta path. Behavior unchanged; tests only. Full end-to-end parity across all four spec requirements confirmed against the real Validator; no spurious INFO note fires on any existing repo change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parser): address review — metadata-only bodies, header-bounded extraction, reader-derived INFO - Skip **metadata**: lines only when other body text remains; a body written entirely as metadata (e.g. `**Constraint**: The system MUST ...`) is kept as the requirement text instead of being emptied (was a regression vs main). - Move the empty-body rule into the shared reader: both paths fall back to the header title, so the same block cannot pass one path and fail the other. - End body extraction at any non-fenced markdown header, restoring old-reader parity: a stray `### Background` divider's notes no longer satisfy the SHALL/MUST check. - Replace the standalone fence-aware INFO scanner with skipped-header collection inside parseDeltaSpec, so the note reflects exactly what the reader skipped (same section boundaries, no whole-file fence mask). - Special-case the nameless `### Requirement:` INFO message; document that the any-#### scenario match is deliberate spec-path parity; un-export REQUIREMENT_HEADER_REGEX; move the import up top. - Soften the changeset claim and list the known remaining divergences in design.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(openspec): record the no-space ###Requirement: divergence as a known leftover Jun's edge (reproduced): the delta/write reader's REQUIREMENT_HEADER_REGEX accepts `###Requirement:` with no space, but MarkdownParser.parseSections requires whitespace (per GFM) — so a no-space requirement validates as a change with zero INFO, syncs as-is, then fails validate <spec>. Pre-existing on main and out of scope here (tightening the shared regex would change write-path recognition); documented under known remaining divergences with the follow-up options, folded together with the bullet from the merge resolution. Corrects c63913b's 'no divergence' note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: TabishB <tabishbidiwale@gmail.com> |
||
|
|
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 (
|
||
|
|
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> |
||
|
|
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
|
||
|
|
0a01146c18 |
[codex] Fix workspace.yaml collision detection (#1165)
* Fix workspace.yaml collision detection * Store workspace view state under metadata * Keep top-level update out of workspace updates * Remove unused workspace root selector * Allow repo updates below workspace roots * Propagate repo state probe errors * Generalize workspace yaml collision coverage |
||
|
|
0c5f0c6c48 |
Improve context-store setup and cleanup UX (#1137)
* Improve context-store setup and cleanup UX * Address CodeRabbit context-store feedback * Canonicalize cleanup registry test assertion |
||
|
|
21c1805d80 |
[codex] Polish beta context workspace flow (#1136)
* Polish beta context workspace flow * Allow context-only initiative workspace open * Add workspace beta compatibility review item |
||
|
|
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 |
||
|
|
8498042fe8 |
[codex] Add workspace change planning workflow (#1089)
* Propose workspace change planning * Implement workspace setup skills phase * Implement workspace skill updates * Handle config profile workspace apply * Implement workspace change creation phase * Enrich planning context for workspace changes * Update workflow skills for planning context * Add workspace planning verification coverage * Fix workspace update review issues * Fix workspace skill drift comparison * Clean up workspace change planning artifacts * Archive workspace change planning * Fix archived workspace planning spec purpose * Address workspace planning review comments |