Conversation
…Fission-AI#550) * chore(nix): improve flake with dynamic version and source filtering - Read version dynamically from package.json instead of hardcoding - Add lib.fileset source filtering to exclude node_modules and build artifacts - Update update-flake.sh to support dynamic version pattern - Add hash change detection to skip unnecessary rebuilds - Improve error handling with automatic rollback on failure - Update specs to reflect dynamic version behavior * chore(ci): bump Nix actions to latest versions - nix-installer-action: v13 → v21 - magic-nix-cache-action: v8 → v13 - Update validation message for unchanged flake.nix * chore: add changeset for Nix improvements * fix(nix): make update-flake.sh portable to macOS - Fix grep pattern on line 37 to include opening parenthesis - Replace GNU grep -oP with portable sed alternatives (lines 53, 68, 70) - Ensures script works on both Linux and macOS (BSD sed/grep) * fix(nix): properly check build verification exit status Fix logic bug where build failures were incorrectly reported as success. The script now: - Captures build exit code and output separately - Fails fast if build returns non-zero exit code - Only checks for 'dirty tree' warning if build succeeded This addresses CodeRabbit review feedback on line 101-107. --------- Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
…ission-AI#615) Implements a new `openspec dashboard` command that serves a local HTTP server with a web-based dashboard for exploring changes, specs, and archive. Features include: - Three-tab navigation for Changes, Specifications, and Archive - Click artifacts to view rendered markdown in a detail panel - Domain-grouped specs with requirement counts - Task progress tracking for active changes - Artifact status indicators (proposal, specs, design, tasks) - Archive pagination with reverse chronological sorting - Zero external dependencies (Node.js built-in http module) - Port auto-increment (3000-3010) with --port override - Cross-platform browser opening (macOS, Linux, Windows) - Path traversal prevention on artifact API Includes comprehensive tests for markdown renderer, data gathering, and API security (43 tests, all passing).
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
…AI#197) (Fission-AI#605) On Windows, fs.rename() often fails with EPERM when moving non-empty directories. Fall back to recursive copy then rm when rename throws EPERM or EXDEV so 'openspec archive' succeeds where Move-Item works. Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
* fix windsurf workrules * fix a missing update --------- Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
…-AI#603) When artifacts or tasks are complete, the command templates now suggest specific slash commands (/opsx:apply, /opsx:archive) instead of generic guidance, helping users discover the next workflow step. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
* feat: support global paths for Codex command generation Codex custom prompts live in ~/.codex/prompts/ (global, not per-project). Update the Codex adapter to return absolute paths via os.homedir(), handle absolute paths in init/update writers, and update docs and specs to reflect the change. * fix: address review feedback on Codex global paths - Guard against empty CODEX_HOME resolving to CWD by trimming the env var - Loosen test regex to not depend on .codex prefix (resilient to custom CODEX_HOME) - Clarify non-goal wording in design.md to avoid contradictory phrasing
…owsing (Fission-AI#615)" (Fission-AI#623) This reverts commit f45ba73.
…Fission-AI#624) The test expected path.join('/custom/codex-home', ...) but the implementation uses path.resolve() which adds the drive letter on Windows (e.g. D:\). Align the test expectation with the implementation.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…at (Fission-AI#626) * Add OpenCode files to gitignore * docs(changes): add opencode-command-references change artifacts * fix(opencode): transform command references from colon to hyphen format
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…-AI#632) * fix: use Skill tool for sync invocation in archive templates Update archive skill templates to properly instruct the AI to use the Skill tool to invoke sync commands instead of executing command logic directly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use Task tool subagent for sync in archive templates Skill tool terminates after completion and doesn't return control to the caller, causing archive to not continue after sync. Changed to spawn a subagent via Task tool which properly returns control after completion. Also updated opsx:sync references to openspec-sync-specs for semantic coherence across templates. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…test (Fission-AI#637) The onboarding preflight used `openspec status --json` to detect if a project was initialized, but that command requires an existing change to succeed. After a fresh `openspec init` (no changes yet), it always failed — causing the onboarding to incorrectly tell users to run init again. Replace with two targeted checks: - `openspec --version` to verify the CLI is installed - `test -f openspec/config.yaml` to verify project initialization
…ission-AI#638) * fix(onboard): replace broken preflight check and add Windows compatibility The onboarding preflight used `openspec status --json` to detect if a project was initialized, but that command requires an existing change to succeed. After a fresh `openspec init` (no changes yet), it always failed — causing the onboarding to incorrectly tell users to run init again. Replace with `openspec --version` to verify the CLI is installed. Also add Windows PowerShell alternatives for all platform-specific shell commands in the onboarding skill: - `2>&1 ||` → `; if ($LASTEXITCODE -ne 0) {}` - `2>/dev/null` → `2>$null` - `mkdir -p` → `New-Item -ItemType Directory -Force` * fix(onboard): address PR review feedback for PowerShell commands Use Get-Command for robust CLI detection instead of $LASTEXITCODE (which stays stale when a command isn't found), and use forward slashes in PowerShell paths for consistency with Unix commands.
- 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
…on-AI#676) * docs: clarify GitHub Copilot CLI does not support custom prompt files GitHub Copilot's .github/prompts/*.prompt.md files are only recognized as custom slash commands in IDE extensions (VS Code, JetBrains, Visual Studio). The Copilot CLI does not support them (github/copilot-cli#618). This updates the docs to clarify the limitation and point users to the .github/agents/ workaround. Closes Fission-AI#671 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: use distinct footnote markers for Codex and Copilot Addresses review feedback: the shared `*` marker was ambiguous across Markdown renderers. Now uses `*` for Codex and `**` for Copilot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
* refactor: split skill templates into workflow modules * fix: align template index exports and parity docs * fix: add standard metadata to feedback skill template * fix: add ff command guardrail for context and rules * spec: add unified template generation pipeline proposal
Add Kiro (AWS AI IDE) as a supported tool with command adapter that writes to .kiro/prompts/ with YAML frontmatter. - Add Kiro to AI_TOOLS registry in config.ts - Create kiro.ts adapter (GitHub Copilot pattern) - Register adapter in index.ts and registry.ts - Add legacy cleanup path for migration - Update supported-tools.md documentation Generated with Kiro CLI using Claude Opus 4.6 Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
…#716) * chore: bulk archive completed changes and normalize specs * docs: finalize spec purposes and align init workflow scenarios * test: guard source specs against placeholders and delta headers * docs: resolve remaining spec review nits
…it (Fission-AI#719) * feat: add change proposal for simplified skill installation Introduces a change proposal to simplify the init flow and skill installation: - Zero-question init with sensible defaults (core profile, both delivery) - Auto-detect AI tools from existing directories (.claude/, .cursor/, etc.) - Profile system: core (4 workflows), extended (11 workflows), custom - Delivery config: both, skills, commands - New `propose` workflow combining new + ff - Fix tool selection UX (space to select, enter to confirm) Key design decisions: - Extend existing global config (~/.config/openspec/config.json) - Profile install/uninstall immediately mutates filesystem - Safe deletion via SKILL_NAMES and COMMAND_IDS constant lookups - Filesystem as truth for installed workflows Also adds rules to openspec/config.yaml to prevent overengineering (explicit lookups over pattern matching). * chore: add missing .openspec.yaml metadata file * fix: address PR review feedback Issues fixed: - Clarify workflow count: extended = existing 10 + new propose = 11 - Rename spec: tool-auto-detection → available-tools (matches proposal) - Change "identical" to "functionally equivalent" in propose spec - Add profile change notification when install/uninstall changes profile - Specify edge case: uninstall workflow from current non-custom profile - Specify behavior when --apply-profile confirmation is declined - Fix section numbering in design.md (6, 6a, 6b, 8) - Add scaffolding verification tasks (verify .openspec.yaml exists) - Specify case sensitivity mechanism: use fs.existsSync, let OS handle it * fix: address CodeRabbit review comments - Add language specifiers to fenced code blocks in proposal.md - Add COMMAND_IDS update for propose in modified files list - Make init success message tool-aware (colon vs hyphen syntax) - Fix grammar: "Skills-only" and "Commands-only" in delivery-config - Specify config get delivery output when field absent: "both (default)" - Add profile set scenarios: config-only vs --apply-profile with filesystem mutation - Add error scenarios for invalid profile name and unknown workflow - Add scenario for existing config without profile field - Mark active profile in profile list output - Enumerate artifacts in propose basic scenario - Fix propose equivalence to use skill syntax consistently - Specify continue/create new branches in propose - Remove out-of-scope command assertion from skill-generation spec - Reference SKILL_NAMES constant instead of vague "existing templates" - Fix design.md: SKILL_NAMES AND COMMAND_IDS (not "only") - Specify overwrite semantics for refresh/update - Add task 6.8: propose to COMMAND_IDS - Fix function name: getAvailableTools() not detectInstalledTools() * refactor: simplify skill installation design based on review - Update design to use existing CLAUDE.md mechanisms - Add cli-update spec for managing skill updates - Clarify profile system and user config interactions - Add explorations directory with design notes - Update docs with clearer concepts * docs: rename zero-question init to smart defaults init Clarify that init auto-detects tools and asks for confirmation, rather than being completely question-free. Update examples to show the tool confirmation UI. * docs: add explore workflow tasks and UX exploration - Add tasks to update explore.ts references to /opsx:propose - Create exploration note for deeper explore → propose UX questions - Captures open questions about exploration artifacts, lifecycle, context handoff, and transition smoothness * fix: address PR review feedback from 1code-async - Add ## Purpose sections to all 10 spec files (required by schema) - Add specs/ to propose workflow's first-time user guidance scenario - Add --tools flag scenario for interactive mode in cli-init/spec.md - Clarify that profile changes take effect on next init/update - Fix design snippet to use AI_TOOLS config instead of TOOL_DIRS constant - Add explicit Windsurf detection scenario to available-tools/spec.md - Mark tasks 10.2-10.3 as follow-up work (out of scope) - Fix capability name: init → cli-init in proposal.md
…ission-AI#726) * feat: implement simplified skill installation with profiles and smart defaults Introduces a profile system (core/custom) to reduce the default workflow count from 10 to 4, auto-detects AI tools during init, adds a new `propose` workflow combining new+ff, fixes multi-select keybindings, and adds backwards-compatible migration for existing users. * feat: harden update config drift, command-only detection, and init profile validation Address post-implementation review findings: update now detects profile/delivery drift even when template versions are current, recognizes command-only installs as configured tools, init validates --profile values and applies delivery cleanup on re-init. Specs and design docs updated with new scenarios and rationale. * fix: address AI reviewer feedback on config, detection, and test cleanup - Add error handling for execSync in config profile apply and use static import - Fix config list showing "(explicit)" for core profile workflows misleadingly - Add missing 'openspec-onboard' to SKILL_NAMES for parity with COMMAND_IDS - Remove unused fsSync import in init tests - Fix configTempDir leak in test afterEach cleanup * docs: add qa smoke harness change proposal
…ces (Fission-AI#733) * Add OpenSpec change proposals for stacking and scope * Address review feedback across change proposals * Preserve legacy install-scope behavior with migration path * Address remaining review threads across spec proposals * Address latest review feedback on split and command-surface specs * Clarify split and composition semantics from latest review
…#736) * Improve profile sync flows and add coverage for commands-only edge cases * Fix migration workflow preservation and add coverage
* feat: add support for Pi (pi.dev) coding agent Add Pi as a supported tool in OpenSpec with full adapter implementation. Changes: - Create pi.ts adapter for command generation - Register adapter in registry and export from index - Add Pi to AI_TOOLS config with .pi skills directory - Add tests for piAdapter following existing patterns - Update supported-tools.md documentation Pi uses: - Skills: .pi/skills/ (Agent Skills standard) - Prompts: .pi/prompts/*.md (with description frontmatter) Closes Fission-AI#732 * fix: add Pi to LEGACY_SLASH_COMMAND_PATHS for test compliance * style: add trailing newline to pi.ts * fix: correct legacy cleanup pattern for Pi (opsx-*.md not openspec-*.md) * fix: add YAML escaping for Pi adapter to handle special characters in descriptions - Add escapeYamlValue() function to properly escape YAML special characters - Apply escaping to description field in frontmatter - Add tests for YAML special character escaping (colons, quotes, newlines) This follows the same pattern used by cursor, claude, and windsurf adapters. * fix: remove Pi from LEGACY_SLASH_COMMAND_PATHS Pi was never supported in pre-1.0 versions, so no legacy cleanup is needed. Per reviewer feedback: this is only for tools from pre-1.0 OpenSpec. * test: relax legacy-cleanup registry coverage invariant --------- Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com> Co-authored-by: TabishB <tabishbidiwale@gmail.com>
… run (Fission-AI#1492) LEGACY_SLASH_COMMAND_PATHS lists artifacts older OpenSpec versions left behind, so init and update remove whatever matches. Two entries named paths the current adapters still write to. `costrict` was a whole-directory entry for `.cospec/openspec/commands`, the folder its adapter writes `opsx-<id>.md` into, so every run deleted the directory and everything in it — including files the user put there — under a heading reading 'No user content to preserve'. It is now a file pattern for `.cospec/openspec/commands/openspec-*.md`: the only files that folder ever held before the opsx rename were openspec-proposal.md, openspec-apply.md and openspec-archive.md, written by the slash configurator added in Fission-AI#240 and dropped in Fission-AI#565. `junie` listed `.junie/commands/opsx-*.md`, its adapter's own output, next to `openspec-*.md`. Both halves arrived in Fission-AI#853 one file apart, so the entry has collided with itself since day one. Cleanup runs before migrateIfNeeded, so on a config with no `profile` key yet — the state after a first init — the deleted command files make inferDelivery read the project as skills-only and write that to the global config. The files are not regenerated, and the delivery preference changes for every other project too. The entry is removed rather than narrowed. Junie support landed in Fission-AI#853, months after Fission-AI#565 deleted the slash configurators that wrote `openspec-*` files, and no junie configurator ever existed — so `.junie/commands/openspec-*.md` is a shape OpenSpec never produced. The same reasoning already keeps `.devin/` off the list two lines above. The regression test is an invariant rather than a fixture — it writes every registered adapter's output for every workflow into a temp project and asserts detection reports nothing — and it names codex, the one legacy id with no adapter, instead of silently skipping it. Nothing else changes. The surviving `openspec-*` globs stay as broad as they have always been, since narrowing them to the three ids the pre-opsx configurators actually wrote is a separate, uniform change, and qwen's `opsx-*.toml` pair stays because its adapter emits Markdown now.
…-dependencies group (Fission-AI#1494) * chore(deps-dev): bump eslint in the development-dependencies group Bumps the development-dependencies group with 1 update: [eslint](https://github.com/eslint/eslint). Updates `eslint` from 10.7.0 to 10.8.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.7.0...v10.8.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> * fix(nix): refresh pnpm dependency hash --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com>
Bumps the website-dependencies group in /website with 9 updates: | Package | From | To | | --- | --- | --- | | [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.11.5` | `16.12.1` | | [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.2.0` | `15.2.1` | | [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.11.5` | `16.12.1` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.25.0` | `1.27.0` | | [next](https://github.com/vercel/next.js) | `16.2.11` | `16.2.12` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.17` | `19.2.18` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.2.4` | | [postcss](https://github.com/postcss/postcss) | `8.5.22` | `8.5.25` | Updates `fumadocs-core` from 16.11.5 to 16.12.1 - [Release notes](https://github.com/fuma-nama/fumadocs/releases) - [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.11.5...fumadocs@16.12.1) Updates `fumadocs-mdx` from 15.2.0 to 15.2.1 - [Release notes](https://github.com/fuma-nama/fumadocs/releases) - [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs-mdx@15.2.0...fumadocs-mdx@15.2.1) Updates `fumadocs-ui` from 16.11.5 to 16.12.1 - [Release notes](https://github.com/fuma-nama/fumadocs/releases) - [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs@16.11.5...fumadocs@16.12.1) Updates `lucide-react` from 1.25.0 to 1.27.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/lucide-react) Updates `next` from 16.2.11 to 16.2.12 - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](vercel/next.js@v16.2.11...v16.2.12) Updates `@types/node` from 26.1.1 to 26.1.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.2.17 to 19.2.18 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@types/react-dom` from 19.2.3 to 19.2.4 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `postcss` from 8.5.22 to 8.5.25 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.5.22...8.5.25) --- updated-dependencies: - dependency-name: fumadocs-core dependency-version: 16.12.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: website-dependencies - dependency-name: fumadocs-mdx dependency-version: 15.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: fumadocs-ui dependency-version: 16.12.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: website-dependencies - dependency-name: lucide-react dependency-version: 1.27.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: website-dependencies - dependency-name: next dependency-version: 16.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: "@types/node" dependency-version: 26.1.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: "@types/react" dependency-version: 19.2.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: "@types/react-dom" dependency-version: 19.2.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: website-dependencies - dependency-name: postcss dependency-version: 8.5.25 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: website-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* docs(workflows): add lifecycle diagrams * docs(workflows): clarify optional archive paths * docs(workflows): correct lifecycle diagrams * docs(website): render Mermaid diagrams * fix(website): preserve Mermaid label text
* 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
…I#1484) * fix(archive): retire a capability when a change removes its last requirement A delta whose REMOVED entries cover every requirement rebuilt the main spec empty, and an empty spec fails validation ("Spec must have at least one requirement"), so the archive aborted with no way forward. Pre-deleting the main spec did not help: the delta was then treated as a create and landed on the same empty spec. Archive now treats an emptied capability as retired. It deletes the capability's spec.md and any directory the deletion leaves empty, stopping short of the specs root, and reports the removals in the totals. Nothing is deleted unless this run actually removed a requirement, so a re-applied or already-synced delta still leaves the file alone. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): decide retirement from the validator and contain the deletion Adversarial review found the original rule unsound. It retired whenever no canonical `### Requirement:` blocks were left, but the validator counts requirements differently: MarkdownParser accepts any `###` heading under `## Requirements`, while the delta block parser indexes only canonical headers and sweeps the rest into the preamble, which survives into the rebuilt spec. A strict-valid spec could therefore be deleted on an archive that previously succeeded. Retirement is now decided by putting the rebuilt spec to the validator and retiring only when its sole error is that it has no requirements, which makes "this spec could not have been written anyway" true by construction. Also fixed: - The directory prune walked string prefixes, but path.resolve does not resolve symlinks and readdir/rmdir both follow them, so a symlinked capability directory let it delete directories outside the repository. Pruning is now bounded by real paths and refuses to descend through a symlink. - A spec that was already requirement-less and lost nothing this run is no longer skipped past validation; it aborts exactly as it did before. - Deletions are deferred until every spec write has succeeded, so a later failure cannot leave a spec already deleted. - Retirement is recorded in `warnings`, naming any other sections the deleted file held, so JSON consumers and humans can both see what went. - Totals carry every applied operation; a rename applied on the way to the removal was being dropped. - bulk-archive guidance, the sync/archive skill specs, and the docs that described archive as never deleting a spec. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): close the retirement gaps a second review round found Five adversarial reviews, mutation testing and CodeRabbit went at the reworked retirement. The findings, all verified by repro before fixing: - The archive-name collision check ran AFTER the spec merge, so archiving twice in one day deleted the capability's spec and then failed, leaving the change unarchived and the file gone. The destination depends only on the change name, so it is now settled before any spec is written or deleted - which also closes the same, older window for ordinary writes. - `--no-validate` retired too, but the whole safety argument is the validator's verdict, and that path produces none. It now writes the spec exactly as it did before this feature existed, leaving no exception to the claim that nothing previously working changes. - The validator can be talked out of seeing a requirement: a stray `### Requirements` under Purpose captures its section lookup, so a spec still holding a real requirement reported "no requirements" and was deleted. Any `###` heading left under `## Requirements` now vetoes retirement outright - a reader is not fooled by the stray heading even when the parser is. - A dangling symlink made `update.exists` false (`fs.access` follows links, `unlink` does not), skipping the "removed something this run" guard: a run that removed nothing deleted an entry and reported a removal. The no-target case is now an explicit branch that never deletes, instead of an ENOENT probe. - `findOtherSections` reported `## ` headings that were inside HTML comments and listed duplicates; it now masks comments like every other structural scan here and dedupes. The warning also names the `## Purpose`, which the deletion always takes, and the resolved path when a symlink puts the file outside the repo. - A failed `unlink` surfaced a bare errno; it now says what was being attempted and what to do. Tests grew from 19 to 33, killing every surviving mutant the review found: deferral proven against a failing write (not just a failing validation), the warnings payload, the already-gone path's output, multi-level pruning, the `+ path.sep` boundary, a symlinked specs root, two retirements in one archive, and `isRetirableSpec` unit-tested directly - including the two-error shape that proves `every` rather than `some`. Agent guidance, the three living specs and the docs now state the same conditions the CLI applies, so a sync agent cannot delete a spec archive keeps. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): make the write-failure test platform-neutral and the path note meaningful Windows CI and CodeRabbit each caught one: - `chmod 0o555` is not a write barrier on Windows, so the test that proves deletions are deferred until every write succeeds never failed a write there: the archive completed, the spec was retired, and the assertion blew up. It now puts a directory where the second spec's file belongs, which fails the write on every platform. Verified it still kills the reordering mutant. - The "resolved to" note compared a canonicalized path against a merely resolved one, so any symlinked ancestor - the platform's own /var -> /private/var is enough - decorated an ordinary retirement with a path that says nothing. It now fires only when the spec really lived outside the specs tree, which is the fact the nominal path hides. Both directions are pinned by tests. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): make the residual-heading veto position-independent A third review round, scoped to the code the earlier rounds never saw. The veto that is supposed to stop a retirement deleting hand-written content only worked when that content sat ABOVE the first requirement. `parts.preamble` is by definition the text before the first `### Requirement:` header; anything after the last one belongs to that block's raw and is discarded with it, so the rebuilt-body scan never saw it. Identical content, different position: one aborted, the other was deleted silently. The veto now reads the original Requirements section - preamble plus every block - so position does not matter. Also: - `realpath` follows a symlinked `spec.md` but `unlink` removes the link, so the warning declared it had deleted a file outside the repo that was still there. The note is now skipped when the target is itself a symlink. - `findHeadings` masked HTML comments before code fences, so an unterminated `<!--` inside a fenced example blanked the rest of the document and truncated the very list of sections the deletion was reporting. Fence first, then comments. - Moving the collision check before the merge widened the window between it and the move, where a claimed destination surfaced as a raw ENOTEMPTY and degraded to `archive_error`. `moveDirectory` now reports that as `archive_target_exists`, the same diagnostic the pre-flight check gives. And a simplification the review asked for: the overlapping `retirable` / `deletes` / `retired` booleans are now one `decideSpecOutcome()` returning 'write' | 'delete' | 'skip'. Behavior is identical - same clauses, same order - but the fourth state that existed only as a comment is now a visible return. Both guards were kept: the review constructed inputs where each is the sole thing preventing a data-losing delete. Two tests the review found wanting are gone or rewritten: one killed no unique mutant, and one assertion straddled two editable message fragments and could have gone vacuously true. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(archive): canonicalize both negative path assertions CodeRabbit caught that `expect(warnings).not.toContain(shared)` passed vacuously: on macOS the temp root lives under /var, whose realpath is /private/var, so the warning would print a form the assertion never compared against. The sibling assertion on `tempDir` had the same flaw. Both now canonicalize first, and both were confirmed to fail against a mutant - dropping the lstat guard, and forcing the resolved-path note on - which neither did before. Closes Fission-AI#1302 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): move a retired capability's spec into the archive instead of deleting it Retiring a capability was the first case where archiving deleted a file under `openspec/specs/`. Nothing in the repo had ever removed spec content before, so the blast radius of a wrong verdict was a lost file with only the reflog to recover it. The spec now moves instead. It is staged into the change directory, which the archive step renames onto the archive path moments later, so it comes to rest at `<archive>/retired-specs/<capability>/spec.md` beside the proposal and tasks that retired it. `git` records a rename, and bringing a capability back is a `git mv` from the archive. Staged into the change rather than written to the archive path after the move, because the archive path must not exist yet and the ordering is safer: if a later step fails, the spec sits in a change that is still active and a rerun carries it through, versus stranding the live specs tree without a spec it still needs. A symlinked `spec.md` is copied by content and its link removed, rather than moved: relocating the link itself would archive a relative path that no longer resolves from where it landed. A spec already staged by an earlier aborted run is never overwritten - it is the only copy once the live one moves. The retirement verdict, its guards, and the deferral until every write has succeeded are all unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): clean up staging directories when a retirement move fails The staging directories are created before the move, so any failure left an empty `retired-specs/<capability>/` behind. That folder then rode into the archive with the change, where it reads as a retirement that never happened - a spec was supposedly retired here, and there is nothing to show for it. The failure path now prunes back up to the change directory. Only empty directories go, so a capability the same run already staged next to the failing one is untouched, and the guard that refuses to overwrite a staged spec still stops at a non-empty destination. Both cases are covered by tests that fail without the prune: a dangling symlink is the reproducible post-staging failure, since lstat sees a file and the copy then follows the link and finds nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(archive): say "moved" where the retirement path still said "deleted" Three leftovers from the deletion version: the `residualRequirementHeadings` comment, `pruneEmptyDirs`'s `mainSpecsDir` parameter - now a boundary that is the change directory on the cleanup path, not the specs root - and a sentence in writing-specs.md that used "deleted" for the requirement and then again for the file, two lines apart. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): roll back a staged copy when the live spec cannot be removed Both non-atomic retirement routes - a symlinked main spec, and the EXDEV/EPERM rename fallback - copy the spec into staging first and remove the original second. A copy that landed before an `unlink` that failed left the spec in TWO places, and the staged one then tripped the "already staged" guard on every rerun. The error told the caller to rerun the archive, and the rerun could never work. Reproduced at the previous head with a symlinked `spec.md` in a read-only capability directory: `copyFile` succeeded, `unlink` returned EACCES, and both copies remained. The failure path now deletes the destination this attempt created, so the capability is left exactly as the attempt found it and the rerun works. The rollback is gated on a flag set only after the destination is proven free, so a spec staged by an EARLIER run is never the thing removed - the overwrite guard still fires ahead of it and rolls nothing back. A partially written copy is cleaned by the same call. The message no longer promises more than it delivers: it reports that the spec is still in place, or names the leftover copy when the rollback itself failed. Regression tests cover both routes and assert the rerun succeeds, not just that the copy is gone. Both fail without the rollback. The cross-device route injects EXDEV, which cannot be provoked inside one temp directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(archive): run the rename-fallback rollback case on Windows too The two post-copy rollback cases shared one `skipIf(win32)`, inherited from the symlink case, which needs privileges Windows does not grant by default. The rename-fallback case uses regular files and spies only, and the sibling errno it stands in for - EPERM - is the Windows case, so skipping it there left that route untested on the platform that produces it. Skipping is now per-case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): claim the retirement destination atomically `fs.access` followed by a write is not an ownership claim. Two concurrent retirements both saw the destination free and both set `destIsOurs`; one moved the spec into staging, and the other - equally convinced the file was its own - rolled it back out. The source and the staged copy both ended up gone. Reproduced at the previous head in 36 of 40 iterations. The claim and the content now arrive in one syscall: `copyFile` with `COPYFILE_EXCL` fails with EEXIST rather than overwriting, so exactly one caller can ever own the path. That is also the check that refuses to clobber a spec an earlier aborted run staged, now decided atomically rather than by a separate look beforehand. The losing caller fails two ways, and both used to destroy the winner's file. EEXIST is the obvious one. ENOENT is not: `copyFile` opens the source first, so a loser that arrives after the winner removed the source fails before creating anything - and treating that as "a partial copy of mine" unlinked the winner's file. Neither errno now claims ownership. Fixing only EEXIST left 4 of 40 iterations still losing both copies. Copying rather than renaming is what makes the claim possible: `rename` overwrites silently on every platform, so it cannot tell "I created this" from "I destroyed someone else's". It also crosses filesystems, which retires the EXDEV/EPERM fallback, and reads a symlink's content rather than moving the link - so the two routes collapse into one shape. Regression asserts the invariant over 25 rounds: exactly one caller retires, the spec survives once and intact, and the source is gone. It fails against the old access-then-write shape. Not crash-safe, which is a weaker promise and now documented: a process killed between the copy and the unlink leaves the spec in both places, and the next run refuses rather than guessing which to keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): take retirement ownership from an exclusive create, not an errno Claiming the destination with `copyFile(..., COPYFILE_EXCL)` closed the concurrent race but kept reading ownership out of a failure code, and that cannot be made correct however the errnos are partitioned. An errno says what went wrong, not what was created: a source-side EACCES is indistinguishable from a partial copy of our own, so the cleanup deleted a recovery copy an earlier run had staged - the last remaining copy of a spec whose live file could not even be read. Reproduced at the previous head with an unreadable `spec.md` and a pre-existing `retired-specs/legacy/spec.md`: the staged file was destroyed. Ownership now comes from `open(dest, 'wx')`. O_CREAT|O_EXCL returns a handle exactly when it created the file, so the question is answered by the syscall instead of inferred afterwards, and every failure path leaves the flag false. EEXIST remains the refusal that protects an earlier run's copy, now decided by the same operation. Content is written through the claimed handle, as bytes, and the handle is closed before any rollback so Windows can unlink it. The regression uses real mode bits, skipped on Windows and under root: the defect was a source-side errno being read as proof about the destination, and stubbing a JS-level read cannot reproduce it, because the copy it has to fool never went through one. Verified it fails against the errno- inference version. All three findings on this path now hold together: the pre-existing copy survives, 0 of 120 racing iterations lose a spec, and a post-copy unlink failure still rolls back and reruns cleanly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): keep the staged copy when the source is already gone The rollback exists for a copy that landed while the source survived - the two-places state that blocks every rerun. It must not fire once the source is gone: at that point the staged copy holds the only remaining content, and the end state the retirement was reaching for is already reached. An external delete landing between the read and the unlink produced exactly that, and the rollback destroyed the spec outright - `retired: false`, no live file, no staged copy, content gone. `unlink` returning ENOENT is now a success rather than a failure to roll back. Every other errno still throws: the source is still sitting there, and leaving the staged copy beside it is the state that blocks a rerun. Found reviewing the finished path rather than reported - the same class as the three review findings before it, all of them the rollback reaching a copy it should not have. Regression verified against the unconditional unlink. Also corrects a doc line that still credited the copy with claiming the destination; the claim is the exclusive create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(archive): gate retirement on a declared marker, drop retired-specs/ Reworks Fission-AI#1302 to follow the design that already exists instead of adding one. The move-into-the-archive approach introduced two things OpenSpec did not have: capability retirement as a lifecycle state, and `retired-specs/` as an on-disk convention no schema declares - which a future unarchive command would have to know about. Its whole justification was preserving content that two existing mechanisms already preserve: the archived change carries the delta naming every REMOVED requirement with its Reason and Migration, and git carries the file. The approach even conceded the point by advertising `git mv` as the recovery path. The issue itself proposed neither. It asked for a delete, or an explicit retirement marker. This does both: archive deletes the emptied spec, and only when the change declares `retire_capabilities: true` in its `.openspec.yaml`. `skip_specs` is the precedent. The marker reader is the same function, parameterised by key, so the two can never drift apart on what counts as honorable metadata - a marker in unparseable YAML, or one whose schema does not load, is not a marker in either case. An explicit `false` is not an unhonorable marker, it is simply undeclared. Without the marker nothing changes: the unwritable spec aborts the archive exactly as before, except the abort now names the marker as the way out - and says nothing about it when retiring would not have made the spec writable anyway, so it never sends an author after the wrong fix. Applying REMOVED already deletes requirement content from a main spec, so deleting the spec once nothing is left is that same operation carried to its end. Every guard survives: the validator's verdict, the residual-heading veto, something-removed-this-run, and never under --no-validate. What goes is the exclusive claim, the rollback, the staging directories, and the four data-loss windows they created across four review rounds. Net 307 lines smaller than the move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: regenerate parity hashes over the merged sync-specs template Fission-AI#1482 and this branch both edit the sync-specs template, so the merged template needs its own hash - neither side's committed value describes it. * docs(archive): correct claims the redesign left false, and bump to minor Review findings, all verified before fixing: - `pruneEmptyDirs`'s doc claimed "two callers, two boundaries", naming the change directory as the second. That was the staging walk from the move design; there is one caller. The boundary stays a parameter, and the comment now says why. - Three comments still described the retirement as moving the file somewhere. It deletes it. - The sync skill told agents the retirement condition includes "no other `###` headings or prose" and then claimed "openspec archive draws exactly these lines". It does not draw the prose line: a main spec with loose prose under `## Requirements` retires and is deleted, and the prose is not named in the warning, which reports `## ` sections only. Verified against the built CLI. The condition now states what the CLI enforces, and the template tells the agent to read that prose back to the user, since the CLI cannot see it for the agent. - `docs/concepts.md`'s `.openspec.yaml` field list omitted the new marker - the one place a user goes to learn what that file may hold. - `docs/cli.md`'s `--no-validate` row did not mention that it disables retirement, though the row two lines down documents retirement. - Bumped patch -> minor. `skip_specs`, the marker this one mirrors, shipped as a minor change in 1.7.0 (Fission-AI#1399); this adds a metadata field and an archive outcome on the same footing. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): refuse to retire a spec with a second Requirements section Four review agents ran against this branch. Two data-loss findings, both reproduced before fixing. 1. A spec with a SECOND `## Requirements` section was deleted even though it passed `validate --strict` with zero issues, and the report named only `Purpose`. `extractRequirementsSection` binds to the FIRST `## Requirements`, so everything after it rides through the merge untouched: the residual-heading veto never sees it, `findOtherSections` filters it out by title, and the validator's own section lookup stops there too - which is why a second section holding a `SHALL` with a scenario reads as valid and then died with the file. The earlier round made that veto position-independent WITHIN the section; this is the same evasion one level up. Retirement is now refused outright for such a spec, so the archive aborts as it did before Fission-AI#1302. The abort's marker hint takes the same conjunct, so it never advises a marker that would not have helped. 2. The recovery line promised `git checkout HEAD -- <path>` unconditionally, and the path was wrong twice over. Verified failures: an UNTRACKED spec - the ordinary case, since an earlier `openspec archive` creates the main spec and nobody has committed it yet - is deleted and the printed command errors, so the file is gone for good; under a store-selected root the nominal `openspec/specs/...` path does not exist in the caller's repo; and a symlinked capability directory puts the file somewhere else entirely. The line now names the path the file actually lived at, and is phrased as the condition it really is rather than a promise archive cannot keep. Regressions for both, plus the three fail-closed branches on the deletion authorisation path that no test observed: a marker in unparseable YAML, and a failing unlink. Each verified against a mutation - removing the veto, restoring the unconditional promise, swallowing the unlink error, and honouring a marker in broken YAML each fail their test. Also pins the sync skill's retirement guidance by content rather than by golden hash, since a hash proves only that it matches its source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(archive): note that retiring a capability strands an in-flight MODIFIED A capability's main spec is the base Fission-AI#1482's scenario-loss check compares a MODIFIED block against. Retire the capability and that check goes silent by design (a missing main spec is the sister-change-in-flight case), so a change that modifies the retired capability keeps validating clean and then refuses to archive with "target spec does not exist". Nothing is lost - there are no scenarios left to drop - but nothing connects the refusal back to the retirement either, so the changeset says it up front. Found by testing this PR against the three that merged into main today. * fix(archive): veto retirement on any heading past the merged section A sixth data-loss defect, from a second round of review agents. Reproduced before fixing: a `validate --strict`-clean spec was deleted with a live SHALL requirement in it, and the report named only "Purpose". The cause is a mask disagreement. `extractRequirementsSection` - the function that decides where the Requirements section ENDS - masks fenced blocks only. `findHeadings`, which both retirement vetoes were built on, masks HTML comments as well. So a multi-line comment holding a `## ` line terminates the section for the merge while being invisible to the scan that had to notice it: everything below became a tail no guard could see. The round-five guard counted `## Requirements` headings, which the same trick skins straight past. The veto is now asked of the tail itself - does anything `###`-shaped sit past the boundary the merge actually chose - read with the fence-only mask, so it answers the question whatever produced that boundary. That subsumes the multiple-Requirements-sections case it replaces and every comment variant. Also from this round: - The recovery command is derived from the path that was unlinked, not rebuilt from the capability id. On a case-insensitive filesystem the id and the real directory differ in case, git is case-sensitive, and the printed command was one git rejects. - An absolute recovery path now says which checkout to run it in - for a selected store, the file is not under the directory archive was run from. - A declared marker refused by the tail veto says why, instead of dropping the author who did what the docs asked back into the bare Fission-AI#1302 abort. - Corrected "draws exactly these four lines" in the sync skill, a claim added two commits ago that was false when written: the CLI checks two more. Both regressions are mutation-verified. Reverting the veto to the narrow multi-section count fails the comment-boundary test. One reported finding was NOT actioned, because its premise does not hold: a residual `###` heading INSIDE the section still counts as a requirement to the validator, so that spec is valid and simply gets written - there is no silent dead end there to explain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(archive): say the marker needs the schema key beside it `.openspec.yaml` requires `schema:`, so a file holding only `retire_capabilities: true` is not honorable metadata and the marker does nothing. The docs and the abort hint both described adding one line, which sends anyone creating that file from scratch into a dead end. The message did explain itself once you were there ("schema: Invalid input: expected string, received undefined"), but it should not need to. Pre-existing shared behavior - `skip_specs` has the same requirement - so this is wording, not a behavior change. * chore: merge main (Fission-AI#1483) and keep both archive test suites Fission-AI#1483 landed while this branch was in review. Three conflicts: - `archive.ts`: one import line, both sides' imports kept. - `skill-templates-parity.test.ts`: hash constants, resolved by key-union and then regenerated from the merged source, which is the only authority once two branches have edited the same template. - `archive.test.ts`: the trap this repo documents. Both branches appended a DIFFERENT describe block at the same place - `capability retirement (Fission-AI#1302)` here, `non-interactive prompts (Fission-AI#1479)` on main - so taking either side would have dropped 16 or 133 tests with a green suite. Both are kept. The conflict boundary also cut the retirement describe's last two closing braces, which `tsc --noEmit` accepted and only esbuild caught as "Unexpected end of file". Restored by brace-balance against both parents. Verified after: every one of main's 91 archive titles and 19 parity titles is present, Fission-AI#1483's describe still holds its 16 tests, and its own non-interactive repro still behaves as it does on main. * fix(archive): only print a recovery command that would actually run Both blockers from the last review. The recovery line offered `git checkout HEAD -- <path>` for every retirement, including ones where the file never lived under the directory archive was run from: a selected store, or a symlinked capability directory. Git rejects an absolute path from a different worktree however it is quoted, and an unquoted path containing a space splits when pasted - a real store path reproduced both. Those cases now say where the file was and leave recovery to the reader, rather than handing them a command that cannot work. The ordinary case still gets the command, quoted when the path needs it, via the portable quoting Fission-AI#1483 already established for change names. And `openspec/specs/specs-sync-skill/spec.md` still authorised deletion from the four original conditions, with no mention of the tail-heading veto the CLI gained - so the living spec permitted something the code refuses. It now carries that condition, and a parity test pins it in the generated guidance so the two cannot drift apart again. Both fixes are mutation-verified: restoring the unconditional command fails the escaped-path regression, and rewording the veto out of the template fails the guidance test. * fix(archive): retire only what the merge can account for Replaces the tail-heading veto with a rule that does not read Markdown at all. Six review rounds each found a different way to dress content so a heading scan would miss it: a second `## Requirements` section, a `##` inside an HTML comment ending the section early, a three-space indent, a setext underline. Every fix was another regex approximating a parser, and every round found the next skin. `extractRequirementsSection` has already split the file into the parts this merge understands. So instead of asking "does anything here look like a requirement" - a question a regex and a renderer answer differently - the guard now asks where content ended up: anything non-blank between the `## Requirements` header and the first requirement, or after the section ends, is content the merge carried through without understanding, and a retirement that would delete the file is refused. There is no second opinion to disagree with the first, because there is no second parse. The in-block heading guard stays, and its comment now says why: a `###` heading that is not a requirement header is absorbed into the block above it, so it never reaches the preamble or the tail. Folding that into the rule above needs a parser that ends a block at any `###` heading, which belongs in the parser. This narrows the feature: a spec carrying an authored section beyond Purpose can no longer be retired automatically. That is deliberate. The abort names the lines that stood in the way, and deleting a file whose contents this merge cannot enumerate is exactly the case a person should decide. Depends on Fission-AI#1490 for indented requirement headers, which are swallowed by the block parser before any of this runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): account for the whole spec, not two slices of it Defect eight, same class as the seven before it. The guard asked where content landed, which was the right question, but it only read two of the five slices `extractRequirementsSection` produces: the preamble and the tail. Content simply moved somewhere nobody looked. Reproduced: a hand-written migration runbook and a table written below a requirement's scenarios live inside that requirement's `raw` - the block runs to the next header the parser RECOGNISES - so removing the requirement deleted them, and the report said "Its section(s) went with it: Purpose". Not silence: a false statement the reader can act on. The same hole covered anything written above the `## Requirements` section. And because the abort hint is gated on the same checks, an unmarked run RECOMMENDED adding the marker that destroys it. The audit now covers the whole file. Expected: the title, the `## Purpose` section, the `## Requirements` header, and inside each block a requirement's own parts - its header, its statement, its scenarios' bullets. Every other non-blank line is reported and refuses the retirement. That folds in the `###`-heading guard, which was a patch on this same leak using the technique the rewrite was meant to abandon. One reported shape is deliberately not a case: prose between `## Purpose` and `## Requirements` IS the Purpose body, since the section runs to the next `##`, and the warning already names Purpose as going with the file. The test says so. Both regressions fail against the two-slice version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(specs): keep content absorbed into a removed requirement A requirement block's `raw` runs to the next header the parser RECOGNISES, so a heading it does not - one indented by the 0-3 spaces CommonMark allows, or a plain `### Notes` - is absorbed into the requirement above it. Removing that requirement deleted the absorbed content with it. Silently: nothing counted it, so nothing warned, and the spec left behind still validated. Reproducible on main with no marker and no capability retirement involved. Anything from the first `#`/`##`/`###` heading after a removed block's own header is now kept in place. `####` is excluded deliberately - a requirement's `#### Scenario:` headings are its own and go with it. This replaces an earlier attempt on this branch that widened every heading pattern in both parsers to accept indentation. That was wrong twice over. It reclassified content, so a spec that was valid became invalid - commented-out and indented examples started parsing as real requirements, taking `list` from 1 requirement to 3. And it did not even fix the bug: moving the line out of the block only meant the reconstruction dropped it at a different step, since `rebuilt` is assembled from `before + header + kept blocks + after` and anything skipped is simply gone. So nothing is reclassified now. An indented heading is still not a requirement, exactly as before; it just survives its neighbour's removal, which is all this ever needed to do. The repo's own corpus produces byte-identical `list`, `validate --specs --strict` and `validate --changes --strict` output. Four regressions, each mutation-verified: removing the salvage fails the three absorbed-content cases, and counting `####` as a boundary fails the scenario case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(specs): keep notes absorbed into a modified or removed requirement A slow audit of the previous commit found the fix covered one of three paths. A requirement block absorbs anything below it that the parser does not read as a new header - a note indented by the 0-3 spaces CommonMark allows, say - so that content rides inside the block. The previous commit salvaged it when the requirement was REMOVED and missed MODIFIED entirely: that path rebuilds the block from the delta, which never carried the note, so it was dropped exactly as before. Verified against the real CLI: main loses it on both paths. RENAMED was the opposite trap. It rewrites the original block's header line in place, so the note is already there - but it also deletes the original key from the block map, which made the requirement look REMOVED to the salvage and produced a duplicate. Tracking which operation applied is therefore not reliable at this point in the merge, so the salvage now asks the assembled result instead: re-insert a note only when nothing else in the rebuilt section already carries it. That is correct for all three paths by construction. Salvaged content also keeps its position now, next to the requirement it was written beside, rather than being appended at the end of the section. Six regressions, three of them mutation-verified against this logic: never re-inserting fails four, always re-inserting duplicates on rename, and appending at the end loses the position. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(specs): decide salvage by identity, not by matching text Another audit pass, another defect in my own fix. Deciding whether a note survived by searching the rebuilt section for its text is wrong when two requirements carry the same note: the first copy is found, and the second is dropped. Reproduced - two removed requirements each followed by an identical `### Notes`, one note destroyed. Survival is a question about the block, not about text. An untouched block is the same object the parser produced and still carries its note; a replaced one is a different object and does not. The RENAMED path previously blurred that by copying the whole raw, so it now carries only the requirement's own lines and the salvage puts the note back like every other path. With every replacement uniformly lacking the tail, `replacement !== block` decides it exactly, and no text is compared at all. Four properties, each mutation-verified: matching text instead of identity loses the duplicate note, always re-inserting doubles an untouched block's note, letting RENAMED keep the tail doubles it on rename, and counting `####` as a boundary severs a requirement from its scenarios. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(specs): warn when a note absorbed into a requirement will be deleted An adversarial review found the previous approach was worse than the bug. Salvaging the "foreign tail" out of a requirement block relied on a positional rule: everything after the first heading-shaped line is not the requirement's. That is not true. A `# comment` inside a scenario bullet, or a markdown example, matches the same shape - and on MODIFIED the old text was then spliced back in after the new, so the spec asserted both. The validator called the result valid, and re-applying the same delta grew the file every time. Reproduced end to end. It also turned a working archive into a hard abort: preserving an unindented `### Notes` made the rebuilt spec fail validation as a scenario-less requirement, so changes that archived cleanly on main stopped archiving, with an error that never mentioned the note. Measured before choosing: 3 of 742 requirement blocks in this repo contain a heading-shaped line, and the repro shows those are false positives. Trading a rare silent deletion for silent corruption on the most common operation is a bad trade. So the merge is left exactly as it was - byte-identical output, verified against main - and the loss is reported instead. That fixes the part of the bug that actually hurt: it was silent. A wrong warning costs a line of output; acting on a wrong answer rewrites the spec. Eight tests. Dropping the warning fails three; ignoring the fence mask fails one - the fence case the previous version left unpinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): scope a scenario's bullets, and stop refusing ordinary prose Defect nine, plus the over-refusal it exposed. Every bullet counted as a scenario's own, anywhere in the block. So an operational note bulleted below the last scenario - "IMPORTANT: escrow keys live in the legacy vault" - was deleted with the file, on a spec that passes `validate --strict`, and the report named only "Purpose". A scenario's bullets run unbroken beneath its header; a blank line after them ends the run, and bullets past that point are the author's own note. Measuring the guard against this repo's 36 specs then showed the opposite failure was already there: 7 of them could never be retired, almost entirely because every fenced line inside a requirement was treated as foreign. A code example inside a scenario is that requirement's own content - a `### Requirement:` inside a fence is not a heading to any reader - so fenced lines are now accounted for, as are numbered lists and a statement that opens with inline code. One ambiguity is left deliberately unresolved: a scenario whose bullets are split by a blank line reads exactly like a note bulleted below it, and no line-based rule separates them. Those specs are REFUSED, never deleted. The abort quotes the lines, and the author moves them or removes the file by hand. Refusing costs a message; the alternative costs the file. Two regressions: the bulleted note must refuse, and a requirement using a numbered list, a fenced example and an inline-code statement must still retire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): a section is not only an ATX heading Defect nine, from a deep adversarial pass, and it is the same species as the eight before it: the guard decided what a section IS by one syntax while a reader recognises three. Once `## Purpose` was seen, every later line in the pre-requirements slice was accepted as its body until the next ATX `##`. But a setext underline turns the line above it into a heading, and raw HTML says so outright - a reader sees a sibling of `## Purpose`, not more of it. So a whole authored section could sit between Purpose and Requirements, pass `validate --specs --strict`, and be deleted with the file while the report said only "Purpose". On main the same archive aborts and loses nothing. Reproduced with a `Data Migration Notes` section underlined with dashes: the capability retired, the notes gone, unnamed. Now refused, with the lines quoted. Two path defects from the same review, one fix: the reported path was rebuilt from the capability id, so on a case-insensitive filesystem it differed in case from the file actually unlinked and git rejected the printed command; and a capability directory symlinked to a sibling deleted one spec while naming another. `retireSpec` now always returns the path it unlinked, and archive reports that. Whether to print a command at all is decided against the REAL repo root, so a symlink that stays inside the repo still gets a working command and only a path that genuinely leaves it falls back to prose. Also pins `!skipValidation` in isolation. The existing --no-validate test passed for the wrong reason - its fixture was blocked by the content guard - so the conjunct itself was unpinned. Four regressions, all mutation-verified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(archive): close remaining capability retirement gaps * fix(archive): close final transaction safety gaps * fix(archive): close retirement race windows * fix(archive): preserve retirement authorization * fix(archive): verify complete fallback copies * fix(archive): preserve transactional safety Reject structurally ambiguous or symlinked inputs before mutation, serialize archive claims safely, and preserve permissions during verified fallback moves. Keep retired specs as inode-preserving backups until the archive commits, restore them on rollback, and retain any backup changed concurrently instead of deleting user data. * fix(archive): preserve replaced claims on Windows Add a per-claim nonce and verify stable claim contents before unlinking because Windows file IDs may not distinguish a replacement lock entry. * test(archive): respect Windows deferred deletion Skip the POSIX unlink-and-recreate claim simulation on Windows, where deletion of an open file remains pending until the original handle closes. * test(archive): align symlink fixtures with path boundaries --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(validate): allow non-English requirements * test(validate): cover non-English change deltas * test(validate): distinguish missing bodies from guidance
* fix(templates): correct generated workflow guidance * fix(templates): address workflow review feedback * test(templates): pin store-aware commands * fix(templates): harden generated workflow guidance * test(templates): align parity hashes after rebase
* fix(propose): stop before implementation * fix(propose): require explicit implementation request * fix(propose): hand implementation to apply * test(propose): align parity hashes after rebase
* fix(propose): honor explicit schema selection * fix(propose): harden schema selection guidance * fix(propose): preserve selected store * fix(propose): respect store flag support * fix(propose): resolve schema discovery root * fix(propose): preserve rootless schema discovery * test(propose): align schema parity after rebase
* fix(status): clarify planning completion * test(status): cover skipped planning artifacts * fix(workflows): gate archive guidance on implementation * fix(status): clarify human completion message * fix(status): make completion guidance stage-neutral * test(status): align parity hashes after rebase
…#1503) * fix(explore): scaffold changes before capturing artifacts * fix(explore): harden artifact capture guidance * fix(explore): evaluate conditional prerequisites * fix(explore): retain store during artifact capture * fix(explore): propagate store in follow-ups * test(explore): align parity hashes after rebase
* fix(workflows): preserve nested spec paths * fix(workflows): key conflicts by capability path * fix(workflows): preserve full paths in examples * fix(workflows): clarify nested path inputs * test(workflows): align parity hashes after rebase
Fission-AI#1510) * fix(security): patch fast-uri, postcss, and brace-expansion advisories Resolve the two open Dependabot alerts plus a third high-severity advisory the repo's own audit surfaces but Dependabot had not filed, all via version-ranged pnpm overrides (they lapse once the upstream tree moves past them): - fast-uri 3.1.4 -> 3.1.5 (website): GHSA-7p8r-x3mc-p8w7, high. Host confusion via backslash authority introducer. Pulled in transitively by ajv@8.18.0; bounded to ^3.1.5 so it stays on the 3.x line ajv expects. - postcss 8.5.22 -> 8.5.25 (root): GHSA-fxqj-rqcc-2cmp, moderate. Arbitrary .map file read via attacker-controlled sourceMappingURL. Pulled in by vite (dev/test tooling). - brace-expansion 5.0.8 -> 5.0.9 (website): GHSA-rgw5-rvv9-x895, high. DoS via unbounded recursion. The existing override capped at >=5.0.8, and 5.0.8 is itself vulnerable under this newer advisory; the root already resolved to 5.0.9. Root and website audits are clean at --audit-level high (and any-severity for the website). Full test suite: 3662 passing. * harden(security): bound overrides, scope release perms, add website lockfile drift check, document archive TOCTOU intent Hardening pass over the security fixes, from a parallel review of the dependency, CI, archive, and adjacent-code surfaces. Each item is low-risk and verified; resolved dependency versions are unchanged. - deps: bound the three security overrides to their current major (brace-expansion ">=5.0.9 <6", postcss ">=8.5.23 <9"). A bare ">=X" pin would take a future major on the next lockfile regen without review; the website already models the caret-bounded idiom. - ci: scope release-prepare.yml permissions per job. The top-level block dropped "pull-requests: write"; only the "prepare" job (which opens the Version Packages PR) now holds it. The "beta" job only tags/releases and publishes via OIDC, so it inherits the narrower default (least privilege). - ci: add a "Website Lockfile Drift" job to security.yml. The website keeps its own lockfile and is never installed in CI, so a website override that stops resolving would go unnoticed and `pnpm audit` would scan a stale graph. A `pnpm install --frozen-lockfile --ignore-scripts --dir website` fails fast on that drift (root drift is already caught in ci.yml). - archive: add intent comments at the 7 js/file-system-race sites in src/core/archive.ts. The stat->read->re-stat pattern is a deliberate concurrent-change detector; the comments record why, so no future refactor (human or scanner-driven) collapses it to fd I/O and blinds the guard. Verified: 3662 tests pass, build clean, website build clean, root+website audits clean at --audit-level high, and the new frozen-lockfile check passes locally. * chore(nix): refresh pnpmDeps hash for the lockfile change The root pnpm-lock.yaml changed (postcss + brace-expansion overrides), which stales the fixed-output pnpmDeps hash and fails Nix Flake Validation. Repin to the value CI computed from the new lockfile.
* Add pnpm-workspace.yaml to allow esbuild build scripts pnpm 10+ blocks all dependency build scripts by default unless explicitly approved via allowBuilds or onlyBuiltDependencies in pnpm-workspace.yaml. esbuild (transitive dependency of vitest -> vite) has a postinstall script that downloads a platform-specific native binary. Without this config, pnpm install exits non-zero with [ERR_PNPM_IGNORED_BUILDS], breaking any downstream packaging (AUR, Nix, Docker) or local setup using pnpm >=10. Refs: Fission-AI#1195 * fix(build): declare pnpm workspace root * fix(build): harden pnpm workspace policies --------- Co-authored-by: Clay Good <hi@claygood.com>
…copilot) (Fission-AI#1274) * feat: generate copilot cloud agent files when github-copilot tool is selected When `openspec init` or `openspec update` is run with the github-copilot tool selected, two additional files are now generated in the user's project: 1. `.github/workflows/copilot-setup-steps.yml` - A GitHub Actions workflow that pre-installs the OpenSpec CLI in the Copilot coding agent's ephemeral environment (required for the agent to use `openspec` commands). 2. `.github/agents/openspec.agent.md` - A custom agent definition that instructs the GitHub Copilot coding agent how to use the OpenSpec CLI, including all agent-compatible commands with `--json` output, workflow patterns, and best practices. These files are only written if they don't already exist (to preserve user customizations). The generation is non-fatal — if it fails, init/update still completes successfully. New module: src/core/github-copilot/cloud-agent.ts Tests: test/core/github-copilot-cloud-agent.test.ts * fix: wire up removeCopilotCloudFiles in update flow When github-copilot is not in the configured tools during update, remove the cloud agent files (copilot-setup-steps.yml and openspec.agent.md) if they exist. * fix: refresh Copilot cloud agent restore * fix: address Copilot cloud review feedback * fix: recognize legacy Copilot cloud files * fix: harden Copilot legacy file matching * fix(copilot): harden cloud agent file management * fix(copilot): harden cloud agent file handling --------- Co-authored-by: Clay Good <hi@claygood.com>
…sion-AI#1153) * fix(templates): deduplicate apply skill and command instructions Extract shared APPLY_INSTRUCTIONS constant so skill and command templates reference the same string. Eliminates content drift reported in Fission-AI#1139. * fix(templates): update parity hashes after parameterizing apply instructions * test(templates): add normalized body parity assertion for apply skill vs command * docs(templates): add JSDoc to getApplyInstructions * docs(templates): add JSDoc to all functions in apply-change * fix(templates): parameterize /opsx:apply examples and add regression tests for skill /opsx: references --------- Co-authored-by: Clay Good <hi@claygood.com>
* feat: add MiniMax Code skills support to OpenSpec * fix: separate init skill and command output summaries * feat(minimax): add global skills support --------- Co-authored-by: showms <showms@users.noreply.github.com> Co-authored-by: Clay Good <hi@claygood.com>
…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
…ission-AI#1514) * fix(templates): restore intentional apply skill/command separation Revert the deduplication from Fission-AI#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. Fission-AI#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 Fission-AI#1514. A plain revert of Fission-AI#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 Fission-AI#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>
…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>
* docs(stores): add multi-repo implementation flow * docs(stores): qualify project pointer precedence --------- Co-authored-by: Clay Good <hi@claygood.com>
…d command (Fission-AI#1515) * refactor(templates): share one apply instruction body across skill and command The apply skill and command templates each carried a full ~150-line copy of the same instruction body, differing in exactly one line (the `contextFiles` note). Two near-identical copies invite silent drift. Author the body once in `getApplyInstructions(contextFilesNote)` and render it per surface, passing each surface's own note. The single intentional wording difference stays explicit as a named constant, and further per-surface parameters can be added here as the surfaces evolve — the skill and command remain distinct templates. Pure refactor: the generated skill and command output is byte-identical to before (SKILL.md and all parity hashes unchanged). Added a contract test that fails both if the shared body drifts between surfaces and if the intentional contextFiles difference is flattened away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(templates): unify apply instruction body into one shared core Builds on the shared-core extraction: the apply skill and command still each carried a slightly different `contextFiles` note (skill spelled out example artifact sets, command said only "varies by schema"). That difference was long-standing accidental drift between the two copies, not an intentional surface distinction — the surfaces are meant to differ only in how they are invoked, which the generation transformers already handle downstream by rewriting `/opsx:<id>` tokens per surface. Resolve the drift by unifying on the more informative note, so both surfaces render one shared `getApplyInstructions()` body with no per-surface text. Skill output is unchanged; the command's contextFiles note gains the example artifact sets. Updated the contract test to assert both surfaces render the shared core (no silent template-level drift), and regenerated the command function hash accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…n-AI#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 Fission-AI#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 Fission-AI#1516. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(copilot): make cloud coding-agent files opt-in Selecting the `github-copilot` tool auto-generated a GitHub Actions workflow (.github/workflows/copilot-setup-steps.yml) plus an agent file. Writing into a user's CI on init/update is invasive, benefits only the narrow set of Copilot *cloud* coding-agent users, and couples us to GitHub's externally-owned custom-agent format. Cloud files are now opt-in: - `openspec init` prompts before generating them (default No) and records the choice in openspec/config.yaml (`githubCopilot.cloudAgent`). - `--copilot-cloud` / `--no-copilot-cloud` decide non-interactively. - `openspec update` never prompts; it only refreshes files for projects that opted in, or that already have generated cloud files (so existing setups keep working — the migration path). The pre-existing content-matching guarantees are unchanged and now proven by regression tests: a user-customized cloud file is never overwritten or deleted. Opt-in state is persisted via the YAML document model so the user's hand-authored config comments and formatting survive untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(copilot): polish the cloud opt-in — safety, UX, and docs Follow-up hardening driven by a five-agent review swarm over the opt-in. Correctness: - persistCopilotCloudOptIn no longer throws on a scalar/`null` config file (reproduced crash); it starts a fresh map while preserving comment-only and empty files. - Explicit opt-out (`--no-copilot-cloud` / `cloudAgent: false`) now removes OpenSpec-managed cloud files on both init and update, instead of orphaning them. Customized files are still never touched. - `--copilot-cloud` / `--no-copilot-cloud` warns when github-copilot isn't among the selected tools, instead of silently no-opping. UX / discoverability: - init prints whether cloud files were written or, when skipped for want of a signal, how to enable them (`--copilot-cloud`). - When the user opts in but already has their own copilot-setup-steps.yml or agent file, init/update say it was left untouched and that the OpenSpec install step must be added by hand — the direct answer to "will this affect my existing Copilot cloud agent?". - Clearer interactive prompt (names both files; distinguishes the GitHub-hosted cloud agent from Copilot in the editor); a dim, interactive-only, decision- gated hint on `openspec update`; tightened flag help text. Docs (the feature was undocumented): new "GitHub Copilot cloud coding agent" section in supported-tools.md; init flags in cli.md; the githubCopilot.cloudAgent key in customization.md. Tests: interactive prompt (accept/decline), opt-out removal + customized-file preservation, config.yml variant, scalar-config regression, collision reporting, flag-ignored warning, re-init honoring persisted opt-in, and the config parse/warn branches. 2763 tests pass; the only failures are pre-existing and unrelated (completion mocks, adapters loader, one config-profile PATH case, one experimental-alias case), verified identical on clean main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): make init cloud-file output honest; harden config guard Final hardening pass (adversarial review of the opt-in polish). - init's success line listed both cloud-file paths from the *decision* to write, not from what was written — so it claimed files that a write skipped (user already owns them) or that the alternate-agent path removed. It now lists only OpenSpec-managed files that actually exist after the write (listManagedCloudFiles), keeps the "left untouched" caveat for user-owned files, and reports opt-out removals in the normal output block. - persistCopilotCloudOptIn's non-map guard used isCollection, which is also true for sequences, so a YAML list at the config root still made setIn throw. Gate on isMap so scalars AND sequences fall back to a fresh document; empty/comment-only files still round-trip with comments intact. - Fixed a misleading catch comment on the opt-out removal path. Tests: success-line accuracy over a user-owned file, sequence-root config regression, and listManagedCloudFiles coverage. 318 tests pass across the touched suites; build + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): replace a non-map githubCopilot node before setIn Addresses alfred review on Fission-AI#1517. The prior guard only fixed a non-map config *root*; a valid top-level map whose `githubCopilot` value is itself a scalar/null/sequence (`githubCopilot: false`, `null`, or a list) still made `setIn(['githubCopilot','cloudAgent'], ...)` throw, which init swallowed — so the explicit opt-in/out was never saved. Now the intermediate node is replaced with an empty map before descending, keeping the rest of the config and its comments intact. Regression covers all three reproduced cases (false/null/sequence). Full suite: 2770 pass; only the pre-existing unrelated failures remain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(copilot): never throw persisting into an unparseable config Deeper pass on persistCopilotCloudOptIn (the function alfred flagged), driven by an exhaustive input-shape check. Two malformed inputs still threw at toString(): a multi-document YAML stream and a tab-indented (syntactically invalid) file. Such a file can't be edited without corrupting it, so persist now detects parse errors and leaves it untouched (no throw, no clobber) — it is already invalid, so readProjectConfig ignores it regardless. With this the function is throw-free across every shape exercised: empty, comment-only, scalar/sequence root, a non-map githubCopilot value, anchors, CRLF, BOM, and the two malformed cases (now skipped byte-identical). Regression added for the multi-document case. Touched suites: 314 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ission-AI#1518) * chore(release): add catch-up changeset for Rovo, Codex dir, status Cover three user-facing PRs that merged without changesets so they appear in the v1.8.0 CHANGELOG: - Fission-AI#1516 Atlassian Rovo Dev CLI (new tool) - Fission-AI#1511 Codex skills move to shared .agents directory - Fission-AI#1505 openspec status separates planning from implementation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(release): correct isPlanningComplete wording in changeset Skipped planning artifacts count as satisfied without being written; say "every non-skipped planning artifact exists" to match the CLI and agent-contract docs (alfred/CodeRabbit review). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Contributor
|
Important Review skippedToo many files! This PR contains 872 files, which is 572 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (872)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.