mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-15 05:30:21 +08:00
@fission-ai/openspec@1.13.2
51
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1d35e90880 |
fix(windows): preserve a file's existing line endings on rewrite (#1958)
* fix(windows): preserve a file's existing line endings on rewrite The parsers normalize CRLF to LF on read, but nothing restored it on write. On a Windows checkout (core.autocrlf=true) that turned every rewrite into a whole-file change: applying a delta that added one requirement produced a diff of 21 insertions and 14 deletions, burying the real change. Archiving the same spec now writes 7 insertions and 0 deletions. - specs-apply: write an updated spec back with the convention the file already used; a spec that does not exist yet stays LF. - file-system: same fix for updateFileWithMarkers, so installing shell completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings, which bash reports as "$'\r': command not found". - pack-version-check: spawn npm through cross-spawn, since execFile cannot resolve npm.cmd on Windows. Adds src/utils/line-endings.ts for the detect/restore pair, plus tests pinning the CRLF round trip through the real write paths. Also adds regression tests for path containment under Windows case variance: path.win32.relative already folds case, and those tests pin both halves of the contract so a future "case-insensitive" change cannot quietly loosen the traversal guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(windows): keep removeMarkerBlock on the file's own newline Addresses the two review points and one more instance of the same bug. `removeMarkerBlock` collapses a run of blank lines, and rebuilt the separator as a bare '\n' regardless of the file it came from. Removing a managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF behind - the mixed ending this PR exists to prevent. It now uses the newline it already detects for the trailing ending. Test fixes: - `marker-updates.test.ts`: close `describe('line endings')` so `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`. - `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin` and `resolveProjectArtifactPath` instead of a private copy of the containment logic, which passed whatever the production guard did. The guard had no coverage at all; a prefix-comparison regression now fails the sibling case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): read the file's convention consistently, and only ENOENT as absent Three follow-ups from CodeRabbit's pass on the superseding PR. `writeUpdatedSpec` turned every read error into "no previous file", so an existing but unreadable spec was treated as absent and rewritten as LF. Only ENOENT means absent now; everything else propagates. `removeMarkerBlock` chose CRLF whenever the content held one anywhere, so a single stray CRLF in an otherwise-LF file pulled the whole rewrite to CRLF. It now uses detectLineEnding, the same dominant-ending reading matchLineEnding uses, so both write paths agree. Added the alias-path case the containment suite was missing: a directory link inside the root that resolves outside it. That exercises the canonicalization half of the guard, which a lexical check cannot do - the link's own path looks contained. Skipped where creating a directory link needs a privilege the runner lacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Travis James <travis@tribehealthsolutions.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
11a9691524 |
fix(tasks): count unrecognised checkbox markers as not done (#1773)
* fix(tasks): count unrecognised checkbox markers as not done A checkbox marker the task parser did not recognise was dropped from progress entirely: it counted toward neither the numerator nor the denominator. A tasks.md whose remaining work was written `- [~] ...` therefore reported `✓ Complete` in `openspec list`/`status`, and `openspec archive` raised no incomplete-task warning for it. Marking open items with such a marker *shrank* the denominator instead of leaving them counted as not-done. Widen the marker to any single non-`]` character and keep `x`/`X` as the only done state, so an unrecognised marker reads as not-done - the fail-safe direction the pattern's own docblock argues for. OpenSpec adopts no new marker semantics; it just stops losing the line. Closes #1761 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): close the remaining silent-loss holes in checkbox parsing Hardening pass on the #1761 fix. Parser: an empty `[]` and a padded `[ x]` were dropped exactly the way `[~]` was - checkbox-like lines counting toward neither the numerator nor the denominator, so archive stopped warning about them. The marker is now "at most one non-whitespace token", which covers all three. Multi-character brackets stay unmatched on purpose: widening to `[^\]]*` would match `- [Some doc](./doc.md)`, whose `]` is followed by `(` rather than a space, and turn every Markdown link list into phantom unfinished work. The docblock states that boundary and a test pins it. Guidance: archive, bulk-archive and verify told agents to count `- [ ]` vs `- [x]` by hand, which reproduced the same bug one layer up - an agent following it saw no incomplete tasks in a `[~]` file even with the CLI fixed. All six bodies (skill + command per workflow) now state the rule the parser implements; skills/ mirror and parity hashes regenerated. Coverage now spans every consumer of the shared counter: `openspec list`, archive's gate, the apply task list, validate's task-numbering check and the parser itself, including the reported 42-done/17-deferred ratio. Reverting only src/utils/task-progress.ts fails 7 of them. Verified empirically: the widened pattern newly matches 0 lines across all 1090 .md/.ts files in the repo, templates and docs included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): state the padded-tick rule in workflow guidance too CodeRabbit review, verified and valid: the new guidance named only exact `[x]`/`[X]` as complete, while the parser also counts `[ x]`, `[x ]` and `[ x ]`. An agent hand-counting by that wording would have reported a padded tick as unfinished work and disagreed with `openspec list` - the same guidance/CLI split this PR set out to close. All six bodies now phrase it as the parser implements it: complete means the box holds only `x`/`X`, spacing inside the brackets ignored; every other marker, including an empty box, is incomplete. skills/ mirror and parity hashes regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): keep one-character link bullets out of the task count alfred-openspec on #1773: the widened marker class let a one-character Markdown link label parse as a checkbox, so `- [A](https://example.com)` and `- [1](./one)` read as unfinished tasks and made progress and archive report phantom work. The multi-character guard did not cover them: a one-character label is one token. The closing bracket may now not be followed by `(` or `[`, the only two characters that continue Markdown link syntax. Nothing that used to count is lost: a checkbox is followed by its description or by end of line, and `- [x]done` still parses. Regressions cover both link forms, the reference form, and a link inside a real task description. Also updates the canonical contract, which said tasks not written `- [ ]` are not tracked while this change deliberately tracks `[~]`, `[-]`, `[]` and a padded `x`. Fixed in schemas/spec-driven/schema.yaml and in the docs-lab page that quotes it, so the quote stays faithful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say that a done checkbox is case-insensitive CodeRabbit on #1773: the parser lowercases the marker before comparing it with `x`, so `- [X]` is done, but the contract I added said only `- [x]` counts. Corrected in both copies, which are the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tasks): keep empty boxes followed by link syntax in the task count The one-character link guard also dropped `- [ ](...)` and `- [ ][...]`, which the strict pre-#1761 pattern counted as unfinished tasks. A line the parser drops is one archive stops warning about, so a whitespace-only box now bypasses the guard. The canonical tasks instruction also names `[-]` and the padded `x` rule, in schema.yaml and docs-lab in lockstep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes and state the padded-x done rule Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
626269ed73 |
fix(templates): stop generated skills naming workflows the profile omits (#1775)
* fix(templates): stop generated skills naming workflows the profile omits The `core` profile installs six of the twelve workflows, but the update and apply templates named `/opsx:continue` (6 times) and `/opsx:new` (twice) regardless. On a default install those became `/openspec-continue-change` and `/openspec-new-change` — skills that were never written — so `update-change` refused to create a missing artifact and handed off to a dead end. The only guard was a sentence asking the model to check availability at runtime, 70 lines above the two places it hits the wall. `command-references.ts` decides how a reference is spelled; nothing decided whether it should be emitted at all. Add that: templates author both wordings with `optionalWorkflow()`, and `getSkillTemplates()` / `getCommandTemplates()` — the one place every generation path already funnels the resolved workflow set through — pick a branch before the reference transformers run. A profile that omits a workflow now gets a concrete `openspec status` / `openspec instructions` fallback instead of a reference to a skill that does not exist. Closes #1734 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): describe profile-aware workflow references Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(init): assert the core profile names no uninstalled workflow The end-to-end init test pinned the runtime availability hedging that #1734 is about, and asserted `/opsx:continue` appears in the default profile's generated update workflow — the bug itself. Assert the fixed behavior instead: neither `/opsx:continue` nor `/opsx:new` appears, and the CLI fallback is stated outright, for both the update and apply surfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(templates): resolve every cross-workflow reference, not just core's The first commit fixed the two templates the default profile broke on. Every other cross-workflow handoff had the same shape, and arbitrary subsets are reachable: a `custom` profile is whatever the user picked, and `openspec update` re-derives a workflow set from what it finds on disk (legacy tool overrides, inferred Codex workflows) without passing it through getProfileWorkflows. So resolve all of them: - `apply` -> archive; `continue` -> apply, archive; `ff` -> apply; `new` -> continue; `propose` -> apply; `update` -> apply, archive; `archive` and `bulk-archive` -> sync. - `onboard`'s two command-reference tables are built from the installed set rather than printed in full with an "only if installed" caveat, and its explore, resume and next-step prompts are resolved the same way. Two supporting changes: - `onlyWithWorkflow()` plus a whole-line rule in the resolver: a conditional that owns its line takes the line with it when it resolves to empty, so a dropped table row cannot leave a blank line that ends the table in markdown. - `generateSkillContent()` and `generateCommand()` now throw on an unresolved marker. A generation path that skips the choke point fails loudly instead of writing `[[opsx:...]]` into a user's SKILL.md. The propose and ff surfaces keep their deliberate wording difference (#258): the command surface never invites "ask me to implement", so its missing-`apply` fallback names the CLI rather than a conversation. The guard test now runs the property over every subset that could expose a reference — each workflow alone, everything but one, the empty set, and the two shipped profiles — for skills and commands, in both spellings. Twenty-plus of those cases fail against the previous commit. Only `openspec-onboard` changes in the skills/ mirror: with every workflow installed, all other templates render byte for byte as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): cover the full cross-workflow reference fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(templates): validate conditionals before choosing a branch Resolution discarded the unselected branch and only then checked for residual markers, so a truncated block inside the *missing* branch was accepted for a profile that installs the workflow and rejected for one that does not. Profile-dependent authoring errors are exactly what this module exists to remove. Validate the authored text up front instead: every marker must be one of the three recognized forms, and they must appear as a flat sequence of if / else / end. A malformed block now throws identically for every profile. The post-resolution check stays as a backstop. Caught by CodeRabbit on #1775. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: state the profile-aware handoff rule in the skills reference alfred-openspec on #1775: docs-lab/reference/skills.md described several handoffs as unconditional while this change deliberately emits a CLI or conversational fallback when the profile omits the target. Stated once, above the entries, rather than as a caveat on each of the eleven affected Response and Creates rows: the page's recipe is one fact per row, and repeating the same conditional eleven times would bury the contracts it exists to state. The rows keep naming the skill that owns the next step, which is the fact a reader looks up; the rule above them says what happens when that skill is not installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(workflows): fold #1735 into the profile-aware references #1735 fixes the same issue (#1734) by removing the optional handoffs outright. This PR resolves them at generation time instead, which is strictly better for the template layer: an install that has `continue` still gets told about it. So the mechanism here wins and #1735's content is folded in, rather than the two competing for the same lines. What #1735 had that this did not: - src/commands/workflow/instructions.ts. The CLI's own runtime strings named the openspec-continue-change skill. Those are chosen at run time, so optionalWorkflow() cannot reach them; taken from #1735 verbatim. - The blocked-state fallback. It was a one-line pointer; it now carries #1735's full CLI recovery (select the next `ready` artifact, not `skipped` or `blocked`, read its rules with `openspec instructions`, keep the selected `--store` on both commands) plus the tracking-file repair path and the `missingArtifacts` field it branches on. The installed branch still names `/opsx:continue`, so neither audience loses. #1735's update-change.ts rewrite is not carried over: this PR already covers all six of those sites conditionally, which is the better answer. Both of #1735's test suites come across, and they are worth more here than there. test/core/templates/profile-handoffs.test.ts asserts that no generated file names an uninstalled workflow across every tool and all three delivery modes, which is the property this PR's mechanism exists to provide, and it passes against it. test/commands/profile-handoffs.test.ts covers the runtime CLI strings. The two guards are complementary: that one is broad on tools and deliveries, this PR's own profile-workflow-references.test.ts is broad on workflow subsets. #1735's command-references.test.ts assertions could not be carried as written, since they assume the reference is gone unconditionally. Replaced with a case that resolves the template against a set without `continue` and asserts the fallback carries the whole recovery. Verified it fails when the fallback is shortened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: name the core-profile fallback on the two rows that hit it The rule above the entries covers every profile, but apply-change and update-change are Core skills whose rows name openspec-continue-change, which the core profile never installs. On the default install those rows now say what the generated skill points to instead: openspec status and openspec instructions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changeset): drop em dashes from the release note Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8fc65b7f70 |
fix(tasks): count checkboxes under every list marker (#1862)
* fix(tasks): count checkboxes under every list marker The task counter behind list, status, view, instructions apply, validate --archived and archive's incomplete-task check read only `-` and `*` bullets. A GFM task is a list item, and CommonMark also allows `+` and ordered `1.` / `1)` markers, so unchecked ordered or plus tasks were dropped from every count: the change read as complete and archive skipped its incomplete-task warning. Accept every CommonMark list marker in TASK_LINE_PATTERN, keeping its existing tolerances (indentation, CRLF, a missing space after the marker). Every consumer goes through parseTaskLines, so this one change fixes them all, and task-numbering validation now sees these tasks too. * docs(tasks): list every counted checkbox marker in schema.yaml reference The apply.tracks section listed only - and * checkbox forms. Task lines under + and ordered markers now count, so show them and name the marker rule. Also use American spelling in the changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09a999bbb2 |
fix(changes): report a change nested in a namespace folder (#1849)
* fix(changes): report a change nested in a namespace folder Specs can be nested by domain (`specs/mobile/tutorial-videos/spec.md`), so laying changes out the same way looks reasonable - but a change is only ever a directory directly under `changes/`. `changes/mobile/refresh-token/` left the real change invisible to every command while `mobile`, the folder around it, was reported as an ordinary task-less change. Nothing said so, and `openspec archive mobile` moved the unfinished change into the archive under the namespace's name with none of its deltas applied. A directory under `changes/` is now recognised as a namespace folder when it carries no change-root artifact of its own and wraps a directory that does. The probe is deliberately conservative: a miss behaves exactly as before, and an ordinary change - including a scaffolded one with no artifacts yet, and one whose only content is a root delta spec - is never reported as a folder. - `openspec list` marks it `not a change` and explains the flat-only layout instead of printing `No tasks`; `--json` gains an additive `warnings` entry. - `openspec show` and `openspec status --change` say the same rather than reporting a change that has no proposal yet. - `openspec validate` reports the nesting instead of "must have at least one delta", with next steps that name the fix rather than delta authoring. - `openspec archive` refuses it outright - burying an active change is data loss, not something to warn about. Closes #1846. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(agent-contract): document the list --json nesting warning Agents read 4.1 as the shape contract, so the additive `warnings` array and per-entry `nested` field have to appear there, along with the instruction not to treat a flagged entry as a change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(changes): harden the nested-change probe and cover status --all Adversarial review of the first commit found three holes. A custom schema may generate every root artifact into a subdirectory (`generates: rfc/proposal.md`). Created by hand, such a change carries no `.openspec.yaml`, so the marker probe read it as a namespace folder wrapping a change called `rfc` - a legitimate change that validated clean before, now refused by validate, status, instructions and archive, with no flag to get past it. The same probe missed the case it was written for whenever the nested change started from its delta specs rather than a proposal: `mobile/refresh-token/` holding only `specs/auth/spec.md` still archived as `mobile`, deltas dropped. A nested change is always hand-made - `openspec new change` rejects a name with a separator - so "mkdir the tree, write the deltas first" is a common way to arrive here. Both come from asking one question. A directory is now recognised as a change by a root artifact OR a populated `specs/`, and a directory holding any file of its own is never a namespace folder. The `specs/**/*.md` shape is fixed by the delta format rather than by the schema, which is what makes it safe to lean on. `change show archive` also reached the probe - it has no reserved-name guard - and offered to rename every dated archive entry into an active change. The reserved name is rejected in the probe itself, so no caller can repeat it. `status --all` read each directory straight through `loadStatus`, bypassing the lookup guard, and printed a whole artifact plan for work that is not there. It now carries the same per-change diagnostic a malformed change does, and the sweep continues past it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): state the nesting-detection bound CodeRabbit asked for the three-level limit in the release note. Stated as a closing sentence rather than a qualifier on the headline, so the note still leads with what changed for users. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(changes): trust the resolved schema before calling a folder a namespace A hand-made change under a custom schema that generates into a subdirectory (rfc/proposal.md), with no .openspec.yaml and no delta specs yet, was read as a namespace folder and refused by status, show, validate, instructions and archive. The probe now also counts a file at any path the resolved schema generates, the same check status uses to mark an artifact done. Move the flat-change-folder docs from legacy docs/ to docs-lab reference/cli.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
44a39eb24b |
feat(core): add codeassistant support (#1171)
* feat(core): add codeassistant support Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: change format file and add test Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: add tests Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * fix: escaped description yaml values Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: add test Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: change adapter Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: handle \r in description Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore: use common escapeYamlValue helper Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> * chore(release): track sourcecraft support --------- Signed-off-by: Александр Мелентьев <aleksandr4842@ya.ru> Co-authored-by: Clay Good <hi@claygood.com> |
||
|
|
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> |
||
|
|
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 |
||
|
|
9ae75c86ef |
fix(archive): don't write ANSI escape codes to a redirected (non-TTY) stdout (#1603)
* fix(archive): stop non-TTY confirm prompts from writing ANSI escapes to stdout `openspec archive` asks up to three yes/no questions through @inquirer's `confirm`, which renders by writing ANSI cursor-movement escape sequences — and emits them even when stdout is not a TTY. When archive runs with its output captured to a file or pipe (an agent's background task, CI), those escapes are noise, and in some non-TTY hosts the render loop never settles and repeats `ESC[NNG` moves until the disk fills (reporter hit 19.8 GB). Add `confirmPrompt` in interactive.ts: a real terminal (stdin AND stdout TTY) still gets @inquirer's rich prompt; every other case reads one plain line via node:readline with `terminal:false`, emitting no escapes. Parsing mirrors @inquirer/confirm exactly (prefix match on y/yes and n/no, else the default), and an unreadable stdin rejects with an ExitPromptError-shaped error so the existing #1479 "rerun with --yes" guidance is unchanged. archive's confirmOrBlock now calls confirmPrompt. Closes #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(interactive): cover Windows CRLF and drained-stdin paths; doc note Adds two regression tests surfaced by adversarial review of the #1526 fix: - Windows CRLF piped input (`y\r\n`) parses as a clean yes with no ANSI — the reporter's platform, previously untested (all inputs used `\n`). - A second prompt after stdin was already drained blocks with an ExitPromptError instead of hanging, exercising the readableEnded guard. Also documents in troubleshooting.md that a redirected/agent archive run that pipes an answer no longer writes terminal escape codes into the capture. Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(interactive): align non-interactive classification and handle readline errors Addresses two review findings on the #1526 confirm-prompt fix: - confirmPrompt drops to the plain reader whenever either stream is not a TTY, but isNonInteractivePromptError only checked stdin. A stdin-TTY / stdout-redirected run that hit EOF leaked the raw ExitPromptError instead of the #1479 "rerun with --yes" guidance. Classification now also counts a redirected stdout, matching how the prompt mode is chosen. (isInteractive, used broadly elsewhere, is left untouched.) - readYesNo never listened for the readline/input 'error' event, so a stdin error would hang the promise (and go unhandled). It now settles with the underlying fault, guarded so the promise resolves or rejects exactly once. Tests: TTY-stdin/redirected-stdout EOF is classified non-interactive; an erroring input stream rejects instead of hanging; the archive usable-terminal test now models a full terminal (both streams TTY). Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(archive): gate the change picker on a TTY and tidy the reader Follow-ups from a second review round: - selectChange (the no-argument change picker) called @inquirer's `select` unconditionally. `select` writes ANSI escapes to stdout even when redirected — the same #1526 mechanism the confirm prompts were fixed for — so `openspec archive > log.txt` with no change name still spewed cursor moves into the capture before blocking. Refuse before rendering when either stream is not a TTY, with the same "pass a change name / --yes" guidance the caught ExitPromptError already gives. A new test asserts the picker is never reached in a non-terminal run. - readYesNo now removes its input-stream 'error' listener on every settle path (it lives on the long-lived process.stdin) and closes the readline interface on error too, so nothing accumulates across archive's sequential prompts. - troubleshooting.md now notes the picker also stays clean. Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(changeset): add patch changeset for the archive non-TTY fix (#1526) User-facing patch note for the archive ANSI/disk-fill fix. Also drops an unnecessary optional-chain on the non-nullable readline handle in readYesNo (the listener is only attached after the interface exists). Refs #1526 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
83be9d113e |
feat(validate): add --archived to lint task completion of archived changes (#1604)
* feat(validate): add --archived to lint task completion of archived changes `openspec validate --archived` scans every change under changes/archive/ and fails (exit 1) if any has unchecked tasks in tasks.md. This catches changes archived with unfinished work — which the normal validate flow never sees, since it only looks at active changes — and is meant for a pre-commit or CI hook. It is a standalone, opt-in scope: it returns before any existing bulk path, so no current `validate` invocation changes behavior, and it does not re-validate already-applied spec deltas. Reuses getTaskProgressForChange (the same counter status/list/archive use) so task counting never forks, and reads root.archiveDir so it is store-aware. Closes #205 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(validate): fail loudly on archive read errors and unreadable task files Address adversarial + CodeRabbit review of `validate --archived`: - listArchivedChangeIds now returns [] only for ENOENT (missing archive dir) and rethrows permission/I/O/ENOTDIR errors, so a real archive-read failure exits 1 instead of silently reading as "no archived changes". - Add getTaskProgressDetailForChange, which reports task files that exist but cannot be read; --archived turns those into an ERROR (naming the file) rather than silently counting them as zero tasks. The shared getTaskProgressForChange now wraps it and drops the detail, so status/list/archive totals are byte-identical. - Start the spinner after listing so a thrown listing error never leaves a spinner running. Adds regression tests (archive path is a file; archived tasks.md is unreadable) and unit tests for the new detail variant. Docs: align the --archived table verb and add a troubleshooting one-liner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(validate): address CodeRabbit nits on archived-task tests - Build the unreadable-fixture path from separate path.join components instead of a hard-coded Unix-separator string. - Assert the reported unreadable path (canonicalized with realpathSync.native), not just the count, so a wrong path can't pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(validate): address round-3 review of --archived From three fresh adversarial reviews (scale/perf, flag/output-shape, filesystem/security): - perf: memoize schema→glob resolution across archived changes via a run-scoped SchemaGlobCache, so the same schema.yaml isn't re-parsed once per change (the archive is append-only and can hold thousands). Threaded as an optional arg; existing callers are unchanged. Loop stays sequential by design (per-change work is synchronous) — now documented. - output shape: issue `path` now follows validate's convention — 'tasks.md' for incomplete tasks, and the POSIX root-relative file path for an unreadable file (one issue per file) instead of the bare 'tasks'. - plain output: print `change/<id>` (matching the JSON `type` and bulk validation) instead of `archived/<id>`. - docs: correct the "Never throws" docstrings (glob resolution can throw on a malformed/unsafe schema; the caller guards it) and note the load-bearing projectRoot override for the archive path depth. Store-mode resolution confirmed correct by review. Tests updated + a memo regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
13e213e00f |
feat(tools): add Atlassian Rovo Dev CLI as a first-class tool (#1516)
* feat(tools): add Atlassian Rovo Dev CLI as a first-class tool Rovo Dev CLI loads project Agent Skills from `.rovodev/skills/<name>/SKILL.md` (Atlassian docs), the same SKILL.md format OpenSpec generates. It was usable only via the generic "Shared .agents skills" fallback; this makes it a named, selectable target in `openspec init`. Rovo has no slash-command surface, so it is registered as an adapterless skills-only tool (like CodeArts/ForgeCode/Hermes) — no command adapter. Closes #212 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tools): reference Rovo skills by natural language, not dead slash commands Rovo Dev CLI has no slash-command surface — it matches skills automatically or by prompt, and `/skills` only manages them. The generated skills and the getting-started hint still advertised `/openspec-*` slash commands (18 references across the skill bodies plus the "Start your first change" hint), so every one was a dead command. Adds a natural-language skill-reference path for no-slash tools: `/opsx:<id>` now renders as "the openspec-<skill> skill" for rovodev, in both skill bodies and the init hint. Other tools are unchanged. - src/utils/command-references.ts: NATURAL_LANGUAGE_SKILL_TOOLS + usesNaturalLanguageSkillReferences(); getSkillReferenceTransformer returns the prose transformer for rovodev. - src/core/init.ts: phrase the skills-only hint as an instruction for no-slash tools ("ask Rovo Dev CLI to use the openspec-propose skill…"). - docs/supported-tools.md: correct the Rovo row (was "use skill-based /openspec-* invocations"). - tests: assert generated Rovo skills contain no /openspec-* or /opsx slash tokens, the hint advertises no dead command, and the transformer emits prose. Addresses alfred-openspec review on #1516. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
622c509a13 |
fix(telemetry): honor telemetry.enabled in global config (#1513)
* fix(telemetry): honor telemetry.enabled in global config Honor the documented global config opt-out while preserving environment and CI overrides. Keep runtime-managed telemetry identity fields intact and apply the same privacy setting to update checks. AI: agentic * docs(telemetry): address automated review feedback Document the full opt-out behavior in the changeset and describe the new test helper so automated documentation coverage meets the project threshold. AI: agentic --------- Co-authored-by: Marcus Don <marcus.don@team.blue> |
||
|
|
06b310bf57 |
fix(templates): restore intentional apply skill/command separation (#1514)
* fix(templates): restore intentional apply skill/command separation Revert the deduplication from #1153. Skills and commands are different ways to invoke the apply workflow: commands reference /opsx:*, while skills reference other skills by name and avoid /opsx: (a skill may be installed without the commands). Teams choose skills-only, commands-only, or both through profiles, so generating both is intentional, not drift. #1153 collapsed getApplyChangeSkillTemplate() and getOpsxApplyCommandTemplate() into one shared body and added a test asserting they are byte-identical, erasing four deliberate differences (change-name example, contextFiles note, blocked-state pointer, and completion hint). This restores the two separate templates and removes the identical-body assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(templates): keep apply skill invocations transformable per target Address alfred's review on #1514. A plain revert of #1153 restored the skill template's bare `openspec-continue-change` prose and dropped the archive/input invocations. The generator only rewrites canonical `/opsx:<id>` tokens, so bare prose is dead text for skills-only targets: skills.sh, Codex, and Kimi lost valid continue/apply/archive invocations. Keep the skill and command templates split (no shared constant, no identical-body assertion — the design separation #1153 erased stays reverted), but author the skill's three invocation references as transformable `/opsx:*` tokens. The generator now emits the correct per-target skill invocation: `/openspec-continue-change` (default), `$openspec-continue-change` (Codex), `/skill:openspec-continue-change` (Kimi) — i.e. "invoked as skills," spelled for each tool. Regenerated the static SKILL.md and parity hashes, and added default/Codex/Kimi generation regressions that pin the apply skill's per-target invocations so this break can't recur silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
59bfb27a76 |
fix(codex): install skills in canonical agents directory (#1511)
* fix(codex): install skills in canonical agents directory * fix(codex): preserve shared agents compatibility * fix(codex): harden shared skill migration * fix(codex): preserve customized legacy skills * fix(codex): reject malformed generated versions |
||
|
|
9cd845fc45 |
fix(security): keep paths on a short leash (#1499)
* fix(security): keep paths on a short leash * fix(security): tighten linked path handling * test(security): prove schema escape rejection * fix(security): close remaining trust boundary gaps * fix(security): close review-found read windows * fix(security): preserve safe linked workflows * fix(schema): preserve fork failure details |
||
|
|
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> |
||
|
|
427abf40ac |
fix(tasks): count indented sub-tasks in task progress (#1486)
Both checkbox parsers anchored the bullet at column 0, so an indented sub-task was invisible to `openspec list`/`view` progress, to the apply task list, and to archive's incomplete-task check. A change whose sub-tasks were unfinished reported "✓ Complete" and archived with no warning. One shared `parseTaskLines()` now backs both surfaces and allows leading whitespace. It matches every line the two patterns it replaces matched, and more - including a tab or non-breaking space inside the brackets, which the old counting pattern accepted - so task counts can rise but never fall: no change starts reporting less work than before, and archive's gate can only get stricter. Checkboxes still count wherever they sit, including inside a code fence. Skipping fenced ones was implemented and dropped: every rule for deciding which fence is real has an input where a stray or unbalanced ``` swallows genuine tasks, which is the silent failure this fix exists to remove. Verified differentially against a build of main over hand-built fixtures and the repo's own 120 tasks.md files: 0 files count fewer tasks, 0 lose an incomplete-task warning. Closes #1485 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
17af60c66e |
fix(archive): make the scenario-drift check fence-aware, plus release-audit follow-ups (#1475)
* fix(archive): make the scenario-drift check fence-aware parseScenarioBlocks matched #### Scenario: headers on raw lines while the validator's countScenarios masks fenced code blocks (#1151). The drift check (#1391) inherited the raw scan, so a fenced scenario example in the current spec aborted an archive that validate had passed, and a fenced name in the MODIFIED block counted as keeping a scenario the block had actually dropped. Build the shared code-fence mask and skip masked lines in both the header scan and the block-end scan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(update): tear down the redirected request when the budget expires The overall request budget was armed inside the first send() and its callback closed over that hop's request. After a redirect the timer destroyed the already-dead first request, so a redirect target that trickled bytes kept resetting its idle timeout and held the socket open until the body-size cap. Track the in-flight request and have the budget timer destroy whichever one is open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(release): add changesets for user-facing changes missing from the 1.7.0 notes 18 feat/fix commits merged since v1.6.0 without a changeset, so the pending Version Packages PR would have released them silently: five tool integrations (ZCode, Hermes, CodeArts, Kimi Code rename, Codex skills-only), skills.sh distribution, symlinked schema dirs, nested spec discovery, drift multiplicity, checkbox markers, Windows welcome input, npx avoidance, doctor store drift, local dates, missing-core-workflows warning, store-aware main specs, open-questions guidance, and spec content guidance. Plus changesets for this branch's two fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(adapters): escape TOML-active characters in Gemini command files The gemini adapter interpolated the description into a TOML basic string and the body into a multiline basic string with no escaping. Every current template value happens to be safe; the first description with a double quote or backslash would silently produce invalid TOML for all Gemini command files. Escape both contexts (#1447 fixed the same class for the YAML adapters but scoped itself to YAML). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(update): harden install detection and redirect handling Three follow-ups from the release audit: - A path segment literally named volta (a user or project directory) classified the install as volta-managed and swallowed the upgrade offer. The undotted spelling now requires volta's own tools/image layout, matching how pnpm and yarn already demand corroboration. - The Windows npm-ownership fallback checked that the npm prefix exists, which is true of any X\node_modules\pkg tree, hand-copied ones included. Corroborate with the openspec.cmd shim npm actually writes. - A https registry redirecting to plain http was followed; a MITM on that reply controls the newer-version answer. Refuse the downgrade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(cli): export zcodeAdapter from the barrel and sync a completion description zcode was registered but missing from the adapters barrel (its test imported the module directly), and the completion registry still carried the pre-#1062 description for the instructions command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(parser): strip a UTF-8 BOM before parsing specs and deltas A BOM-prefixed delta spec (Windows editors, PowerShell Out-File) failed validate and archive with 'No delta sections found' because the first line never matched '## ADDED Requirements'. Strip the BOM in both normalizers, the same way tool detection already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): reject over-long change names with a validation message A 300-character change name surfaced two raw ENAMETOOLONG errno dumps from stat and mkdir. Bound the name at 200 characters in validateChangeName so the failure is a normal validation error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): finish the early-sync no-op rules for MODIFIED and RENAMED Two asymmetries left over from the #1376/#1386/#1437 no-op work: - MODIFIED counted every delta as applied even when the block was byte-equal to the main spec, so a fully early-synced change rewrote the file (normalization churn), printed '~ N modified', and reported specsUpdated: true where its ADDED/REMOVED/RENAMED twins print 'Specs already in sync; no files changed.' Count only real replacements. - RENAMED's already-synced skip (source gone, target present) had no near-miss guard: a case/whitespace variant of the source still in the spec means a typo'd header, and REMOVED already hard-aborts on that signal. Apply the same guard, excluding the target itself so a case-only rename still no-ops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validate): stop reporting an unreadable specs dir as 'no deltas' The delta-validation loop swallowed every error as 'if no specs dir, treat as no deltas', so an EACCES capability folder produced the misleading 'Change must have at least one delta' while archive let the same error propagate. Tolerate only ENOENT and ENOTDIR (a stray specs file); anything else stays loud, matching discoverSpecFiles' documented fail-loud contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(update): say when commands-only delivery leaves a tool with nothing Under delivery: commands, update removed the skills of adapterless skills-only tools (Hermes, Kimi Code, Vibe, CodeArts, ForgeCode) without a word — leaving zero OpenSpec artifacts while the tool's detection dir kept re-suggesting an init that would also generate nothing. Print the same per-tool configuration correction init already prints, pointing at 'openspec config set delivery both'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(completion): honor $ZSH and $ZSH_CUSTOM for Oh My Zsh installs The installer used a set $ZSH only as an is-installed signal and then wrote to ~/.oh-my-zsh regardless, so a custom OMZ location got a freshly created ~/.oh-my-zsh tree that no shell ever loads — and isInstalled/uninstall looked in the same wrong place. Route every path through the $ZSH/$ZSH_CUSTOM-aware helpers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(init): make the static welcome screen wait for the Enter it asks for The static branch printed 'Press Enter to select tools...' and returned immediately, so the Enter landed in the tool picker and submitted the pre-selected set sight-unseen. #1462 routed reduced-motion, OPENSPEC_NO_ANIMATION, --no-animation, NO_COLOR, and narrow-terminal users onto this path. Wait in a TTY; drop the prompt line when there is no TTY to wait on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(feedback): keep the manual fallback on every gh failure Only missing-gh and unauthenticated flows showed the formatted feedback and pre-filled submission URL; issues-disabled, network, or rate-limit failures printed gh's stderr and discarded the path to submit what the user had already typed. Route those through the same manual fallback, preserving gh's exit code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(update): model the npm shim in the Windows prefix fixture The ownership corroboration now checks for the openspec.cmd shim npm writes beside node_modules; the Homebrew-prefix fixture built the layout without it, so the test failed on windows-pwsh. Write the shim in the fixture and pin the inverse: the same shape with nothing npm wrote (a hand-copied portable tree) is not an npm install. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(update): require volta's full tools/image layout for the undotted spelling The corroboration used has('tools', 'image'), which is some() — volta AND (tools OR image) — so /srv/volta/tools/apps/... still classified as a Volta install and swallowed the upgrade offer. Require both segments, matching the real %LOCALAPPDATA%\Volta\tools\image layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(adapters): escape control characters in Gemini multiline prompts escapeTomlMultilineBasicString handled backslashes and quote-triples but not the C0 controls that are as invalid in a multiline basic string as in a single-line one. Reuse TOML_CONTROL_CHARS, applied last so the escapes it introduces are not re-doubled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(completion): finish the $ZSH_CUSTOM support and isolate it in tests The fpath verification advice still grepped the literal custom/completions, which a relocated $ZSH_CUSTOM need never contain — grep the actual directory instead. The installer tests cleared only $ZSH, so on a machine exporting $ZSH_CUSTOM they would have written into (and deleted from) the developer's real OMZ custom dir — the same leakage class #1400 fixed for $ZSH. Clear/restore both, and pin the custom-location paths with two new tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(release): correct the hermes and zcode changeset wording Hermes is skills-only (no command adapter), and zcode's namespaced commands register /opsx:<id>, not /opsx-* — the release notes must not reintroduce the invocation-spelling confusion #1471 removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(feedback): pin the manual fallback on a non-label gh failure The new reportGhFailure output (formatted feedback + pre-filled URL) had no coverage; the network-failure test now asserts it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(completion): match fpath entries as literal strings in the OMZ guidance The verification advice interpolated the completions dir into grep "<dir>" where regex metacharacters make the check unreliable and quotes could break the displayed command. Print one fpath entry per line and match with grep -F on a shell-quoted literal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(adapters): never emit a bare carriage return in Gemini TOML prompts A lone CR is illegal in a multiline basic string — Python 3.13 tomllib rejects the file — and the control-char pass deliberately skipped it on the assumption it only appears as CRLF. Normalize CRLF to LF and escape any remaining CR as \r. The escaping guarantee is now parser-backed: smol-toml (new devDependency) round-trips every hostile body in the regression matrix (lone CR, CRLF, CR before a quote run, NUL/VT/FF, trailing backslash, four- and five-quote runs), and the same outputs were verified against Python tomllib. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * build(nix): update the pnpm deps hash for the smol-toml devDependency The lockfile changed, so the fixed-output derivation hash moved; value taken from the CI mismatch report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6a5171e186 |
fix(validate): allow numeric-prefixed change names (#1435)
* fix(validate): allow numeric-prefixed change names `validateChangeName` required a leading letter, so `openspec new change 100-add-feature` or `00001-add-auth` failed with "Change name must start with a letter". This contradicted the rest of OpenSpec: the shared kebab-id grammar in src/core/id.ts (store ids, workset names, change metadata ids) already allows a leading digit, and archive explicitly supports `YYYY-MM-DD-` prefixed change names as a convention (#1309). Reuse the canonical `isKebabId` grammar for change names so numeric prefixes work, keeping the tailored error messages for the other failure cases. Fully backward-compatible: every previously valid name still validates (`[a-z]` ⊂ `[a-z0-9]`), and consecutive/leading/trailing hyphens, uppercase, spaces, underscores and other characters are still rejected. Closes #850 Closes #1169 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs, changeset, tests for numeric-prefixed change names Address review of the numeric-prefix change: - add the required changeset (patch) - fix docs/cli.md which still said names "cannot start with a number" and advised prefixing ticket IDs with a word (website copy regenerates from this file at build time) - pin the all-numeric case (`100`) so accepting it is a conscious decision Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: name the tiered-prefix test for what it covers CodeRabbit noted the 101-01-fix-auth fixture contains letters, so the old title 'all digits and hyphens' was inaccurate. Rename it to describe the tiered numeric-prefix case (#850); the dedicated all-numeric case is the separate '100' test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
26f009d940 |
fix(change): resolve changes by directory instead of requiring proposal.md (#1433)
* fix(show): resolve changes by directory instead of requiring proposal.md `openspec show <change>` and shell completion resolved a change only when `openspec/changes/<name>/proposal.md` existed. Every sibling command -- `list`, `status`, `instructions`, `validate` -- resolves a change by its directory (`getAvailableChanges`). The two rules disagree the moment a change is created: `openspec new change <name>` scaffolds only `.openspec.yaml`, so `list` showed the change while `show` reported `Unknown item`. A custom schema that defines no proposal artifact was never resolvable at all (#1161). Resolve by directory in `getActiveChangeIds`/`getArchivedChangeIds`, and report a change that exists without a proposal accurately -- pointing at `openspec status --change <name>` -- rather than as missing. The deprecated `openspec change list` keeps its own proposal-backed scan; its JSON output parses proposal.md per change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(show): offer proposal-less changes in the no-name selector too `ChangeCommand.show` resolved a named proposal-less change but still built its no-name selector (and the non-interactive "Available IDs" hint) from the proposal-gated scan, so a scaffolded change could not be picked. Use directory-based discovery there as well. `list` keeps the local proposal-backed scan: its --json output parses proposal.md per change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(change): unify list/show discovery and report a missing proposal honestly Adversarial review of the first two commits surfaced four defects. `change list` still used a private proposal-gated scan, so the deprecated alias reported a different set than `openspec list` -- the command its own deprecation warning tells you to use -- while the `show` selector beside it offered the wider set. Move it to `getActiveChangeIds` and drop the now unused helper and its ARCHIVE_DIR constant. Widening that list exposed three follow-on bugs, all fixed here: - Task counts were computed inside the proposal try block, so a change with tasks but no proposal.md reported 0/0. Task progress is independent of the proposal; resolve it first. - `--long` printed "(unable to read)" and `--json` "Unknown" for a change that is simply not written yet. Distinguish a missing proposal from an unreadable one by testing existence, not by sniffing error codes. - `show` reported a stray file under changes/, or a traversing name such as `../..`, as a change awaiting its proposal, pointing the user at a `status --change` call that cannot work. Require a directory that is a direct child of changes/. Also drops a duplicated proposal.md read in the --long path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(change): contain change lookup and stop guessing at unreadable proposals Second review round. `isDefinitelyMissing` replaces the plain existence check: fs.access can fail with EACCES or an I/O error, and treating that as "no proposal.md yet" hid a real read failure behind an ordinary-looking state. Only ENOENT counts as absent; anything else falls through to the existing unreadable handling. `show` now rejects a name that is not a direct child of changes/ before touching the filesystem. This closes a pre-existing traversal on main: `openspec change show ../..` resolved openspec/changes/../../proposal.md and printed a file from outside the changes directory. Both new tests fail without the guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a874d1d671 |
chore(security): create test temp dirs with mkdtemp and override two pinned CVEs (#1432)
* test: create temp dirs with fs.mkdtemp
Every one of these suites built its temp directory by hand:
testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
await fs.mkdir(testDir, { recursive: true });
That check-then-create is what CodeQL's js/insecure-temporary-file flags,
and it accounted for 452 of the repo's 467 dismissed code-scanning alerts.
Dismissing them one by one is a treadmill: every new suite that copies the
idiom mints fresh alerts, and a real finding is easy to lose in that volume.
fs.mkdtemp creates the directory atomically at mode 0700 with a random
suffix, so there is no window to pre-empt and no name to guess. The suites
that already used it are the evidence this silences the rule: 33 of the 34
files calling mkdtemp carry zero alerts. The lone exception, archive.test.ts,
was scanned one commit before its own mkdtemp fix landed and is left to that
change rather than conflicting with it.
The dirs named on Date.now() alone were genuinely predictable; the randomUUID
ones were not, but they trained the same copy-paste. Both are gone now.
Behavior is unchanged: mkdtemp creates the root the old mkdir created, and
no assertion depended on the root being absent. Verified with the full suite
(2196 tests, 111 files).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): override postcss and sharp in the website
Two advisories on the docs site have no Dependabot PR and never will:
Next pins postcss at 8.4.31 as a direct dependency and sharp at ^0.34.5 as
an optional one, so Dependabot cannot raise either without a Next release
that does it first. 16.2.11 does not — it still pins both. Left alone these
sit open indefinitely.
postcss 8.4.31 -> 8.5.22 GHSA-qx2v-qp2m-jg93 (XSS via unescaped </style>)
sharp 0.34.5 -> 0.35.3 GHSA-f88m-g3jw-g9cj (4 libvips CVEs)
A version-ranged selector (`postcss@<8.5.10`) was the first instinct, since it
lapses on its own once Next moves past it. It is the wrong tool: it pins the
override to one advisory's floor, and silently stops applying when the next
advisory raises that floor. GHSA-6g55-p6wh-862q landed while this branch was
open and moved postcss's patched floor to 8.5.12 — under the ranged selector a
dependency pinning 8.5.11 would have resolved to 8.5.11 and stayed vulnerable.
A plain floor cannot under-match, so that is what this uses.
The floor also covers the new advisory: 8.5.22 is past 8.5.12. postcss dedupes
to the single copy the site already had for Tailwind.
These are the last two open Dependabot alerts on the repo. `pnpm audit` on
website/ goes from 2 advisories to "No known vulnerabilities found". The site
builds clean, OG image generation included, and the package set grows by
exactly two platform-gated wasm32 binaries that never install on CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(security): audit the docs site too
Both `pnpm audit` steps run at the repo root. The docs site keeps its own
lockfile and is not a workspace member, so neither step could see it — the
root audit passed all the way through while postcss and sharp sat open in
website/. That blind spot is why they needed a manual override to find.
Blocking rule copied from the published-dependency audit: advisory on pull
requests, blocking on the weekly schedule and on pushes to main. An
always-advisory step would only relocate the blind spot — the sweep would stay
green with a live advisory and someone would have to read the log of a passing
run to notice.
`!cancelled()` because the two audits above can fail hard. Without it a root
advisory would skip this step entirely, in exactly the situation where the
site's own state matters most.
Verified against the pre-fix lockfile — the step reports the two advisories it
would have caught:
2 vulnerabilities found
Severity: 1 moderate | 1 high
and reports "No known vulnerabilities found" against the fixed one. Confirmed
it reads website/pnpm-lock.yaml and not the root: with a vulnerable website
lockfile and a clean root, it exits 1; a missing website lockfile is a hard
ERR_PNPM_AUDIT_NO_LOCKFILE rather than a false clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
27b22ab4cb |
feat(validate): accept zero-delta changes that declare skip_specs (#1399)
Squashed for rebase; see PR #1399 for the full commit history. |
||
|
|
a84ae70e8c |
fix(init): use skill references for tools without a command adapter (#1404)
* fix(init): use skill references for tools without a command adapter Adapterless tools (kimi, vibe, hermes, forgecode, codeartsagent, agents) skip command generation even under the default 'both' delivery, but their generated SKILL.md files still told agents to run /opsx:* commands that were never created, and the init summary suggested /opsx:propose. Route the existing skill-reference transform by command-surface capability so these tools get /openspec-* references, and point the getting-started hint at the skill when no selected tool got commands. Fixes #1155 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): address adversarial review findings for adapterless skill references - transform the committed skills.sh distribution too: pass transformToSkillReferences in generate-skillssh.mjs and the parity test, regenerate skills/ (that channel installs SKILL.md files only, so /opsx:* commands never exist there) - key the getting-started hint purely on whether any selected tool got commands, so the delivery=commands + adapterless corner can no longer print /opsx:propose - make the one-time profile-migration message capability-aware for projects whose detected tools have no command adapter - import CommandSurfaceCapability type-only instead of duplicating the union inline (a value import would close a module cycle) - cover the update path: the kimi migration test now asserts refreshed skills contain no /opsx references Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): honor Kimi Code's documented /skill: invocation syntax Per review: the blanket /openspec-* rewrite contradicted Kimi's documented invocation contract (/skill:openspec-*, see docs/supported-tools.md). Skill-reference transforms are now selected per tool via getSkillReferenceTransformer, with Kimi mapped to /skill:<name> and every other tool keeping the documented /<name> form; the getting-started hint and migration message use the same per-tool syntax. End-to-end Kimi assertions cover generated skill content, the refreshed update path, and the hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): gate the getting-started hint on a generated surface Per review: with delivery=commands and only adapterless tools selected, init generated neither skills nor commands yet still advertised an invocation. Print a configuration correction instead, with the exact 'openspec config set delivery both' remedy, covered by an end-to-end commands-only adapterless test. Also from the adversarial review round: mixed selections that disagree on invocation syntax (kimi + vibe) now fall back to the default /openspec-* form in the shared hint and migration message instead of picking the first tool's syntax; add the missing changeset; correct the codex doc comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): suppress the restart hint when no surface was generated From the third adversarial review round: the 'Restart your IDE for slash commands' line printed directly after the message saying nothing was generated. Gate it on an actually generated surface and pin that in the commands-only adapterless test. Also: use randomUUID() for init test temp dirs (matches update.test.ts, removes a theoretical Date.now collision), and clarify the changeset wording about the skills.sh channel's default reference form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): print one usable getting-started hint per invocation syntax Per review: the mixed-syntax fallback advertised /openspec-propose, which Mistral Vibe accepts but Kimi Code does not. Group successful tools by their transformed reference and print one labeled hint line per distinct form, so every advertised instruction is usable by the tool it names; the mixed-tool test asserts exactly that. The migration message compares transformed outputs instead of function identities (also per review) and stays syntax-neutral ('the openspec-propose skill') when detected tools disagree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): keep codex hints syntax-neutral (skills-invocable, no slash surface) Codex has no slash-command surface: docs direct users to .codex/skills/openspec-*. The getting-started hint and the one-time migration message now name the skill ('the openspec-propose skill') instead of advertising a /openspec-* form Codex does not accept, and the restart line only claims slash commands when commands were generated. Hint lines are also limited to tools that actually got skills: under delivery=commands, codex+kimi previously advertised /skill:openspec-propose for Kimi while .kimi-code was never created. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(init): advertise a usable instruction for every configured tool Adversarial-review round fixes: - Mixed adapter-backed + skill-only selections (claude+kimi, claude+codex) printed a single unlabeled /opsx:propose hint that the skill-only tool cannot use; hints are now derived per tool from its generated surface and labeled when the selection disagrees. - The delivery=commands configuration correction keyed on the global aggregate, so a tool that got zero artifacts lost its correction as soon as any other tool generated something; it is now per-tool. - The migration message advertised /opsx:propose under an explicit 'delivery: skills' config where commands will never exist; the command form is now gated on the effective delivery. - Migration-message coverage extended (kimi, codex+kimi, delivery=skills, commands-installed); profile-describe init tests use randomUUID temp dirs like the first describe block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(update): derive migration and legacy-upgrade references per tool surface The one-time migration message collapsed mixed command + skill-only selections to /opsx:propose (Claude commands + a Kimi skill told the Kimi user to run a command it cannot invoke); the reference is now computed per detected tool and falls back to the syntax-neutral form on disagreement. The legacy-upgrade getting-started menu had the same capability blindness with hard-coded /opsx:new/continue/apply — a legacy Codex upgrade advertised commands Codex lost in #1283; menu lines are now derived the same way (byte-identical for command-tool upgrades). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b7c85c741c |
fix: use skill references in SKILL.md for skills-only delivery (#1194)
* fix(skills): use skill references in skills-only delivery mode When delivery is configured as 'skills', generated SKILL.md files contained hardcoded /opsx:* command references pointing to commands that were never generated, breaking cross-skill workflows. Add transformToSkillReferences() with the explicit command-to-skill mapping (kept in sync with WORKFLOW_TO_SKILL_DIR) and wire it in init and update wherever skill content is generated, following the approach outlined in #881. Closes #881 Closes #879 Generated with Claude (Cowork) using claude-fable-5; verified with the full vitest suite (1683 tests passing). * fix(skills): wire skill references in workspace skill generation Review follow-up for the skills-only delivery fix: workspace skill setup (src/core/workspace/skills.ts) generates SKILL.md via the same generateSkillContent path but was not wired with transformToSkillReferences, leaving dangling /opsx:* references when delivery is 'skills'. Wire both call sites, add a regression test (verified to fail against the unwired code), strengthen the update skills-only test with content assertions, and correct the COMMAND_TO_SKILL_REFERENCE comment (WORKFLOW_TO_SKILL_DIR exists in both profile-sync-drift.ts and init.ts). Generated with Claude (Cowork) using claude-fable-5; verified with eslint and targeted vitest suites (144 tests passing). * refactor(skills): extract transformer selection into getTransformerForTool Address CodeRabbit review: the tool/delivery transformer selection was duplicated at five call sites across init.ts, update.ts, and workspace/skills.ts. Extract it into a documented helper in command-references.ts with unit tests locking the selection matrix (opencode/pi precedence, skills-only delivery, default). Generated with Claude (Cowork) using claude-fable-5; verified with eslint, tsc, and targeted vitest suites (147 tests passing). * fix(skills): prioritize skill references over hyphen commands in skills-only delivery Address review: getTransformerForTool returned transformToHyphenCommands for opencode/pi before checking delivery, so skills-only delivery still emitted /opsx-* references to commands that were never generated. Check delivery === 'skills' first so skill references win for every tool, and keep the hyphen transform for opencode/pi only when commands are generated. Add unit coverage for the opencode/pi selection matrix and an opencode skills-only init integration test asserting no /opsx: or /opsx- references remain. Generated with Claude (Cowork) using claude-fable-5; verified with eslint, tsc, and targeted vitest suites (148 tests passing). * docs(skills): add docstrings to init/update generation entry points * test(skills): reject stale hyphenated references in skills-only assertions * fix(skills): add missing update entry to command-to-skill reference map COMMAND_TO_SKILL_REFERENCE was missing the update workflow, so a /opsx:update reference in any skill template would survive skills-only transformation untouched. Align the map with the canonical 12-entry WORKFLOW_TO_SKILL_DIR and assert the generated openspec-update-change skill carries no raw command references. |
||
|
|
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> |
||
|
|
3fdd2f2f7b |
fix(specs): discover nested spec paths recursively across parse, apply, and archive (#1355)
* fix(specs): discover nested spec paths recursively across parse, apply, and archive Delta discovery previously read only specs/<name>/spec.md one directory level below a change's specs root, so nested layouts like specs/<area>/<capability>/spec.md were silently skipped: show reported deltaCount 0, and archive/apply completed without merging the delta into the main specs directory. Main-spec discovery had the same one-level assumption, so nested capabilities were also invisible to list, show, view, and validate. Introduce a shared recursive discoverSpecFiles() helper and use it in the change parser, findSpecUpdates (apply/sync/archive), archive's delta detection, and main-spec discovery (item-discovery, list, spec list, view). Capability ids are the directory path relative to the specs root, forward-slash separated on every platform, and apply/archive preserve the relative path when writing the target spec. Symlinks are not followed and dot-directories are skipped. Fixes #1353 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(specs): surface non-ENOENT errors in spec discovery discoverSpecFiles swallowed every readdir error, so an unreadable (EACCES/EIO) capability directory silently vanished from validate/show/ archive/apply — recreating the data-loss class #1353 is closing, now on the merge path. Suppress only the expected missing-root ENOENT and rethrow everything else. Adds regression tests for non-ENOENT (ENOTDIR + guarded EACCES) and the documented symlink-not-followed behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(specs): sort discovered specs by code point, not locale localeCompare follows the process's ICU locale, so ordering could vary by OS/CI. Code-point comparison guarantees the deterministic output the docstring promises. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f58b445692 |
fix(completion): install the right completions for fish users (#1364)
* fix(completion): detect the interactive shell from the parent process `openspec completion install` read only $SHELL, the login shell, so users whose interactive shell differs (e.g. fish users on distros where the login shell is bash) got bash completions installed by default (#1197). Detection now consults the parent process via `ps` before falling back to $SHELL. It only trusts a parent that maps to a supported shell, so npx/npm and other non-shell parents still fall back cleanly; Windows is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(completion): match shell basename exactly and pin platform in parent-process tests Two fixes from CI and review on #1364: - matchSupportedShell now matches the executable basename exactly (stripping a login-shell leading dash) instead of substring matching, so parents like fish-lsp or bash-language-server no longer get mistaken for the shell (CodeRabbit review). - The parent-process tests pin process.platform to linux so they exercise the ps path on Windows CI, where detection otherwise short-circuits and the tests failed. Adds regression tests for -zsh login shells and fish-lsp fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
41ceebe2d8 |
fix(ci+installers): harden permission checks and guard completion/profile writes (#1247)
* fix: harden permission checks in root CI * fix: close permission guard review gaps * test: address coderabbit installer comments |
||
|
|
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
|
||
|
|
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 |
||
|
|
93f7b797cf | fix: prefer native realpath for canonical paths (#972) | ||
|
|
62d4391268 |
fix: improve Windows compatibility in tests (#646)
- Replace Unix 'cat' command with fs.readFile in spec.test.ts - Replace 'mkdir -p' and 'bash' commands with fs.mkdir/writeFile in validate.enriched-output.test.ts - Skip symlink test on Windows (requires admin privileges) in file-system.test.ts |
||
|
|
697738bc9b |
fix(opencode): transform command references from colon to hyphen format (#626)
* Add OpenCode files to gitignore * docs(changes): add opencode-command-references change artifacts * fix(opencode): transform command references from colon to hyphen format |
||
|
|
12a7224dc6 |
chore: remove TDD schema and all references (#586)
* chore: remove TDD schema and all references TDD was an internal test example that should not be in user-facing docs. This removes: - The schemas/tdd directory and all its templates - All TDD references from documentation - TDD examples from skill templates and source code comments * test: update tests to remove TDD schema references All tests that referenced the removed TDD schema have been updated to use spec-driven or custom-schema names instead. * chore: remove accidentally committed files |
||
|
|
39bebefcc4 |
feat(cli): merge init and experimental commands (#565)
* feat(core): add legacy cleanup detection functions for init migration Implement src/core/legacy-cleanup.ts with detection and cleanup functions for all legacy OpenSpec artifact types: Detection functions: - detectLegacyConfigFiles() - checks for config files with OpenSpec markers (CLAUDE.md, CLINE.md, CODEBUDDY.md, COSTRICT.md, QODER.md, IFLOW.md, AGENTS.md, QWEN.md) - detectLegacySlashCommands() - checks for old /openspec:* command directories and files across all 21 tool integrations - detectLegacyStructureFiles() - checks for openspec/AGENTS.md and openspec/project.md (project.md preserved for migration hint) - detectLegacyArtifacts() - orchestrates all detection Utility functions: - hasOpenSpecMarkers() - checks if content has OpenSpec markers - isOnlyOpenSpecContent() - checks if file is 100% OpenSpec content - removeMarkerBlock() - surgically removes marker blocks from mixed content Cleanup functions: - cleanupLegacyArtifacts() - orchestrates removal with proper edge cases: - Deletes files that are 100% OpenSpec content - Removes marker blocks from files with mixed content - Deletes legacy slash command directories and files - Preserves openspec/project.md (shows migration hint only) Formatting functions: - formatDetectionSummary() - formats what was detected before cleanup - formatCleanupSummary() - formats what was cleaned up after This is task 1.1 for the merge-init-experimental change. * feat(utils): add removeMarkerBlock() for surgically removing marker blocks - Add removeMarkerBlock() function to file-system.ts that properly handles inline marker mentions by using findMarkerIndex/isMarkerOnOwnLine - Refactor legacy-cleanup.ts to use the shared utility - Export removeMarkerBlock from utils/index.ts for reusability - Add comprehensive tests for inline marker mention edge cases - Add tests for shell-style markers and various whitespace scenarios The new implementation correctly ignores markers mentioned inline within text and only removes actual marker blocks that are on their own lines. * feat(core): add formatProjectMdMigrationHint() for migration messaging - Add standalone formatProjectMdMigrationHint() function for reusable migration hint output directing users to migrate project.md content to config.yaml's "context:" field - Update formatDetectionSummary() to include the migration hint when project.md is detected (not just in cleanup summary) - Refactor formatCleanupSummary() to use the new function for consistency - Add unit tests for the new function and updated behavior * test(init): rewrite init tests for experimental workflow approach Rewrites the init command tests to verify the new experimental workflow implementation. The new tests cover: - OpenSpec directory structure creation (specs, changes, archive) - config.yaml generation with default schema - 9 Agent Skills creation for various tools (Claude, Cursor, Windsurf, etc.) - 9 slash commands generation using tool-specific adapters - Multi-tool support (--tools all, --tools none, specific tools) - Extend mode (re-running init) - Tool-specific adapters (Gemini TOML, Continue .prompt, etc.) - Error handling for invalid tools and permissions Removes old tests for legacy config file generation (AGENTS.md, CLAUDE.md, project.md, etc.) as the new init command uses Agent Skills instead. * test(update): rewrite tests for skills/commands refresh behavior Update the update command tests to match the new implementation that refreshes skills and opsx commands instead of config files. Changes: - Remove old ToolRegistry import (deleted module) - Rewrite tests to verify skill file updates - Rewrite tests to verify opsx command generation - Add tests for multi-tool support (Claude, Cursor, Qwen, Windsurf) - Add tests for error handling and tool detection - Fix test assertions to match actual skill template names The update command now: - Detects configured tools by checking skill directories - Updates SKILL.md files with latest skill templates - Generates opsx commands using tool-specific adapters * docs(readme): update documentation for new init behavior - Replace tool list with simplified supported tools section (skills-based) - Update init instructions to document --tools flag, --force, and legacy cleanup - Replace project.md with config.yaml documentation - Update workflow examples to use /opsx:* commands instead of /openspec:* - Add command reference table for slash commands - Update Team Adoption and Updating sections for new workflow - Replace Experimental Features with Workflow Customization section * refactor(cli): remove legacy configurators and merge experimental into workflow - Delete src/core/configurators/ directory (ToolRegistry, all config generators) - Delete legacy templates (agents-template, claude-template, project-template, etc.) - Move experimental commands to src/commands/workflow/ with cleaner structure - Remove experimental setup.ts and index.ts (functionality merged into init) - Update CLI to register workflow commands directly instead of through experimental - Update openspec update command to refresh skills/commands instead of config files - Update tests for new command structure * refactor: extract shared modules and move AGENTS.md to root - Move AGENTS.md from openspec/ to project root - Add shared module with tool-detection and skill-generation utilities - Update legacy-cleanup with improved cleanup logic - Enhance update.ts with additional functionality - Add comprehensive tests for shared modules * fix(ui): update welcome screen tagline Change from experimental reference to reflect the merged workflow. * fix: improve Windows cross-platform compatibility - Handle both forward and backward slashes in path parsing - Normalize paths before regex matching for legacy artifact detection - Use regex split for both path separators in tool directory extraction - Handle CRLF line endings when cleaning up multiple blank lines - Add retry logic for test file cleanup to handle Windows file locking * fix(init): use dynamic counts for skills and commands in success message Replace hard-coded "9 skills and 9 commands" with dynamic values from getSkillTemplates().length and getCommandContents().length to prevent the message from diverging from reality when skills/commands change. * fix: various small improvements across init, cleanup, and file handling - Remove shell prompt characters from README bash examples (MD014) - Show actual config filename (config.yaml vs config.yml) in init output - Include hasProjectMd in hasLegacyArtifacts to show migration hint - Add existence check before AGENTS.md deletion to avoid spurious errors - Preserve leading whitespace and original newline style in file operations - Use dynamic tool list from CommandAdapterRegistry in tests |
||
|
|
20714c1c28 |
feat(config): add project-level configuration via openspec/config.yaml (#499)
* feat(config): add project-level configuration via openspec/config.yaml Adds openspec/config.yaml support for project-level customization without forking schemas. Teams can now: - Set a default schema (used when --schema flag not provided) - Inject project context into all artifact instructions - Add per-artifact rules (e.g., proposal rules, specs rules) Key changes: - New `src/core/project-config.ts` with Zod schema and resilient parsing - New `src/core/config-prompts.ts` for interactive config creation - Updated schema resolution order: CLI → change metadata → config → default - Updated instruction generation to inject <context> and <rules> XML sections - Integrated config creation prompts into `artifact-experimental-setup` Schema resolution precedence: 1. --schema CLI flag (explicit override) 2. .openspec.yaml in change directory (change-specific) 3. openspec/config.yaml schema field (project default) 4. "spec-driven" (hardcoded fallback) * test(config): add e2e tests and performance benchmarks for project config - Add project config integration tests in artifact-workflow.test.ts: - Test new change uses schema from config - Test CLI schema overrides config schema - Test context and rules injection in instructions - Test backwards compatibility without config file - Test immediate reflection of config changes - Add performance benchmark tests in project-config.test.ts: - Typical config (1KB): <20ms target - Large config (50KB): <50ms target - Repeated reads consistency check - Missing config fast-path test - Add project configuration documentation in experimental-workflow.md: - Config fields reference (schema, context, rules) - Schema precedence explanation - Artifact IDs by schema - Troubleshooting guide - Mark all tasks complete in tasks.md * refactor(config): remove benchmark tests, document decision in source Remove performance benchmark tests from project-config.test.ts and document the results directly in the source code instead. Benchmarks showed config reads are fast enough (~0.5ms typical) that caching is unnecessary. * fix(config): resolve three issues in project config feature - Remove nested <template> tags: generateInstructions() no longer wraps template content since printInstructionsText() already handles XML structure - Fix schema message accuracy: newChangeCommand() now uses the resolved schema returned from createChange() instead of hardcoded fallback - Add TTY check: artifact-experimental-setup skips interactive prompts in non-TTY environments (CI, automation) to prevent hangs |
||
|
|
c4a54a8d54 |
fix: skip Windows-specific permission tests that rely on chmod() (#464)
fs.chmod() on directories doesn't restrict write access on Windows since Windows uses ACLs that Node.js doesn't control. Additionally, admin users and CI runners can bypass read-only attributes. Skip these tests on Windows and use platform-specific invalid paths in cross-platform tests. Fixes #401 (bash/pwsh completion commit breaking Windows e2e tests). |
||
|
|
38d2356836 |
feature/bash_fish_power_shells_completions (#401)
* added CLI completions support for: bash, fish and powershell
* Add bash/fish/powershell completions
* Archive extend-shell-completions
* Archive extend-shell-completions
* Fix canWriteFile control flow and add tests
* Fix bash completion fallback and security escaping
- Add _init_completion fallback for systems without bash-completion
- Fix command injection escaping in Fish/PowerShell generators
- Add Bash command name escaping for security
- Add comprehensive security tests for all generators
- Fix test placement issues in bash/powershell test files
* refactor: extract completion templates and standardize naming
Extract static template literals from generators into separate template files.
Standardize naming to {SHELL}_STATIC_HELPERS and {SHELL}_DYNAMIC_HELPERS.
- Create bash/fish/powershell/zsh template files
- Rename constants: BASH_HELPERS → BASH_DYNAMIC_HELPERS,
FISH_HELPER_FUNCTIONS → FISH_STATIC_HELPERS,
POWERSHELL_HELPERS → POWERSHELL_DYNAMIC_HELPERS
- Update generator imports
- Remove ~99 lines of boilerplate from generators
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* docs: update spec to reflect multi-shell support
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add all shells to zsh completion suggestions
* feat: add --yes flag to completion uninstall
* fix: remove bash-completion dependency from fallback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: use printf instead of echo for Fish tab output
Fish's echo doesn't interpret escape sequences, so \t outputs
literally instead of as a tab character. Use printf for proper
tab-separated completion output.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: make UX messages shell-aware
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add Homebrew paths for bash-completion detection
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: support both PowerShell Core and Windows PS 5.1
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: preserve colon handling in bash completion
Add -n : option to _init_completion to prevent colons from being
treated as word separators. This is important for spec/change IDs
that may contain colons.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: update completion tests to match implementation changes
Updated bash-generator test to expect `-n :` flag in _init_completion call.
Updated powershell-installer tests to match refactored implementation that
supports both PowerShell Core and Windows PowerShell 5.1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
|
||
|
|
1786684af6 |
feat: add per-change schema metadata (.openspec.yaml) (#443)
This feature enables workflow schema auto-detection for changes: - Add ChangeMetadataSchema Zod schema to types.ts - Create change-metadata.ts with writeChangeMetadata(), readChangeMetadata() - Update createChange() to accept optional schema param and write metadata - Modify loadChangeContext() to auto-detect schema from .openspec.yaml - Add --schema option to openspec new change command - Update status/instructions commands to auto-detect schema from metadata Schema resolution order: 1. Explicit --schema flag (if provided) 2. Schema from .openspec.yaml in change directory 3. Default 'spec-driven' |
||
|
|
4f4af5708d |
feat: add change creation utilities (#408)
* proposal: add change manager - extract + new functionality Slice 2 of the artifact tracker POC. Creates ChangeManager module that: **Extracts existing functionality:** - `listChanges()` from ListCommand + ChangeCommand.getActiveChanges() - `changeExists()` from inline fs.access() checks - `getChangePath()` from inline path.join() calls - `isInitialized()` from ListCommand directory check **Adds new functionality:** - `createChange(name, description?)` - create change directory + README - `validateName(name)` - enforce kebab-case naming **Refactors CLI commands to be thin wrappers:** - ListCommand delegates to ChangeManager - ChangeCommand delegates to ChangeManager Also updates docs/artifact_poc.md to reflect XDG decisions from Slice 1. * proposal: simplify to utility functions only Remove extraction/refactor scope. Just add: - createChange(projectRoot, name, description?) - validateChangeName(name) Simple utility functions in src/utils/change-utils.ts. No class, no abstraction layer. * docs: update artifact_poc.md for simplified Slice 2 - Rename ChangeManager to change-utils (simple utility functions) - Remove extracted methods (listChanges, getChangePath, etc.) - Keep only new functionality: createChange(), validateChangeName() - Update component diagram and summary table * docs: clarify existing vs new functionality in artifact_poc.md Audit artifact_poc.md against existing codebase to mark what already exists vs what's genuinely new: - Slice 4 CLI table: Added Status column (NEW/EXISTS) - Added "Existing CLI commands" section listing what's not in scope - Updated Implementation Order Slice 4 with explicit new vs existing - Summary table: Updated status + added "What already exists" section Key finding: The document was already well-simplified. Only truly new functionality (createChange, validateChangeName, InstructionLoader, artifact graph CLI commands) is proposed. * rename: change-manager -> change-creation capability The capability name "change-manager" implied a manager class abstraction, but the simplified proposal uses only utility functions. Renamed to "change-creation" to accurately reflect what the capability provides. * feat: implement change creation utilities Add createChange() and validateChangeName() functions for programmatic change directory creation with kebab-case validation. - createChange(projectRoot, name) creates openspec/changes/<name>/ - validateChangeName() enforces kebab-case naming conventions - Comprehensive test coverage (21 tests) * chore: archive add-change-manager change Move change to archive and create change-creation spec with requirements for createChange() and validateChangeName(). * docs: update change-creation spec purpose |
||
|
|
9ac6330430 |
fix(cli): respect --no-interactive flag in validate command (#395)
* fix(cli): respect --no-interactive flag in validate command The validate command's spinner was starting regardless of the --no-interactive flag, causing hangs in pre-commit hooks. Changes: - Pass noInteractive option to runBulkValidation - Handle Commander.js --no-* flag syntax (sets interactive=false) - Only start ora spinner when in interactive mode - Add CI environment variable check to isInteractive() for industry standard compliance * test: add unit tests for interactive utilities and CLI flag - Export resolveNoInteractive() helper for reuse - Add InteractiveOptions type export for testing - Refactor validate.ts to use resolveNoInteractive() - Add 17 unit tests for isInteractive() and resolveNoInteractive() - Add CLI integration test for --no-interactive flag This prevents future regressions where Commander.js --no-* flag parsing is not properly handled. |
||
|
|
1adf3cea88 |
feature/oh-my-zsh-completions (#289)
* shell completions for zsh * after code review changes * expose only postinstall.js script * Replace _openspec "$@" with compdef in zsh-generator.ts to prevent execution during load * Update test/commands/completion.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * - Fix dispatcher to use $words instead of $line for subcommand routing - Add __complete endpoint with tab-separated output for safe parsing - Replace brittle awk parsing with __complete in completion helpers - Add uninstall confirmation prompt with --yes flag to skip - Prefer $ZSH env var for Oh My Zsh detection before dir check - Add fpath verification guidance for OMZ installations - Update cli-completion spec to document generate subcommand * improve shell detection and installation handling - Return structured result from detectShell() with shell and detected name - Detect already-installed completions and skip reinstall - Add update detection with automatic backup of previous version - Add debug logging to silent catch blocks for diagnostics - Quote fpath directories to handle paths with spaces - Verify Oh My Zsh fpath configuration and add to .zshrc if needed - Show helpful error for detected but unsupported shells - Update all tests for new detection API --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com> |
||
|
|
9c9e57daa1 |
Ensure slash command paths resolve on Windows platforms (#144)
* Ensure slash command paths work on Windows * Add Linux home path coverage for joinPath |
||
|
|
f8eca37796 |
feat(init): slim root agent instructions (#98)
* feat(init): slim root agent instructions * Fix marker updates to ignore inline mentions * styling updates * update instructions * fix tests |
||
|
|
20b2fee749 | feat(init): support multi-select extend flow |