Clear pre-existing lint debt flagged by repo-wide `ruff check` (the lint
config only scopes src/, so tests/ had drifted). No behavior change.
- F401/F541: drop unused imports and redundant f-string prefixes (autofix)
- E741: rename ambiguous `l` to `ln` in comprehensions
- E702: split semicolon-joined statements onto separate lines
- F841: drop unused bindings while keeping the side-effecting calls
(_minimal_feature, install_from_directory)
Full suite: 3344 passed, 40 skipped. ruff check (repo-wide): clean.
VS Code Copilot Agent Skills do not support the `mode:` frontmatter field.
The generated SKILL.md files included `mode: speckit.<stem>` injected by
CopilotIntegration.post_process_skill_content(), which had no effect in
VS Code and could cause confusion. Simplify post_process_skill_content to
delegate directly to _CopilotSkillsHelper without injecting mode:.
Update tests to assert mode: is absent from generated skill frontmatter.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(integrations): co-locate integration commands in integrations/ domain dir
- Remove commands/ stubs (handlers will live in domain dirs)
- Move all integration CLI handlers out of __init__.py into integrations/
- Split into focused modules under integrations/:
_helpers.py (340 lines) — domain helpers
_install_commands.py (306 lines) — install / uninstall
_migrate_commands.py (487 lines) — switch / upgrade
_query_commands.py (442 lines) — list / use / search / info / catalog
_commands.py (34 lines) — app objects + register()
- __init__.py reduced by ~1400 lines; integration block replaced with register() call
- Fix patch paths in tests to new module locations
* fix(integrations): restore original integration list output in refactor
Preserve the CLI Required column, post-table default/installed summary,
and no-installed guidance that were dropped during the no-behavior-change
refactor of integration list into _query_commands.py.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(integrations): restore _clear/_update_init_options public imports
The refactor that split integration commands moved
_clear_init_options_for_integration and _update_init_options_for_integration
into integrations/_helpers.py, but tests still import them from the top-level
specify_cli package, causing ImportError. Re-export them with explicit aliases
at the end of __init__.py to preserve the public import surface.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Initial plan
* Extract agent context updates into bundled agent-context extension
* Potential fix for pull request finding 'Unused import'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Potential fix for pull request finding 'Unused import'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: address review comments on agent-context extension
- bash: parse init-options.json with a single python3 invocation instead
of three separate read_json_field calls, for parity with the PowerShell
ConvertFrom-Json approach and to avoid divergent error semantics
- bash: use parameter expansion to strip PROJECT_ROOT prefix from plan
path instead of sed interpolation, avoiding special-character fragility
- powershell: limit Get-ChildItem to -Depth 1 so plan.md discovery matches
the bash glob specs/*/plan.md (one level deep) — fixes cross-platform
inconsistency with nested plan.md files
- powershell: replace Substring+Length relative-path with
[System.IO.Path]::GetRelativePath for robustness across case/PSDrive
differences
- __init__.py: move agent-context extension install to after
save_init_options so init-options.json is present when hooks run
- __init__.py: seed context_markers in init-options only when
context_file is truthy; avoids noise for integrations without a context
file
- integrations/base.py: narrow blanket except Exception in
_resolve_context_markers to ImportError / (OSError, ValueError) so
unexpected bugs surface instead of being silently swallowed
* fix: gate context_markers in _update_init_options_for_integration on context_file
Apply the same gating logic used during `specify init`: only write
context_markers to init-options.json when the integration actually has a
context_file set. When switching to an integration without a context file
the stale markers are removed, keeping the two init paths consistent.
* fix: move context_file/context_markers from init-options.json to agent-context extension config
* Potential fix for pull request finding 'Unused global variable'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: clarify local import comment in agents.py
* Fix remaining agent-context review findings
* Fix follow-up agent-context review issues
* Address review feedback: narrow except, improve PyYAML messaging, surface config-written note
* Fix double-space in PyYAML install hint message
* Potential fix for pull request finding 'Empty except'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Potential fix for pull request finding 'Empty except'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Address latest agent-context review feedback
* Harden bash config parse output handling
* Clarify ImportError-only fallback comment
* Apply review feedback: drop dead try/except, guard ext-config creation, explicit ConvertFrom-Yaml check
* Remove redundant $Options = $null in PS1 catch block
* Add constitution directives, deprecation warning, agent-context auto-install, and init flow fix
- Add constitution-loading directive to specify, clarify, tasks, checklist, taskstoissues commands
- Add deprecation warning (v0.12.0) in upsert_context_section()
- Auto-install agent-context extension during specify init
- Move context_file from init-options.json to agent-context extension config
- Add tests: deprecation warning, corrupt config, constitution directives
- Update file inventories across all integration tests
* Address review: fix init ordering, test coverage, and hermes inventory
- Move agent-context extension install after init-options.json is saved
so skill registration can read ai_skills + integration key
- Write extension config after install (avoids template overwriting context_file)
- Fix test_defaults_when_markers_field_missing to truly test missing markers key
- Update hermes tests to allow extension-installed agent-context skill
* Address review: chmod ordering, preserve markers, PS1 Python check, YAML key order
- Move ensure_executable_scripts after agent-context extension install
so extension scripts get execute bits set
- Use preserve_markers=True on reinit to keep user-customized markers
- Add Python 3 version check in PowerShell fallback (matching bash behavior)
- Add sort_keys=False to yaml.safe_dump for stable config output
* Address review: path traversal guards and docstring fix
- Reject absolute paths and '..' segments in context_file in both bash and
PowerShell scripts to prevent writes outside the project root
- Fix docstring in _update_init_options_for_integration to accurately
describe marker preservation behavior
* Address review: strict enabled check, docstring, segment-level path traversal
- Use 'is not False' for enabled check so only literal False disables
- Update upsert_context_section docstring to mention disabled-extension return
- Fix path traversal guards to check actual path segments, not substrings
(allows filenames like 'notes..md' while rejecting '../' traversal)
* Address review: UnicodeError handling, missing extension warning
- Add UnicodeError to exception tuples in _load_agent_context_config and
_resolve_context_markers so garbled UTF-8 config files fall back to defaults
- Emit error (with reinstall command) instead of silent skip when bundled
agent-context extension is not found during init
* Address review: bash backslash traversal guard, wheel packaging
- Reject backslash separators and Windows drive-letter paths in bash
context_file validation (prevents traversal on Git-Bash/Windows)
- Add extensions/agent-context to pyproject.toml force-include so the
bundled extension is included in wheel builds
* Address review: write extension config before init-options.json
- Reorder writes in _update_init_options_for_integration so the
agent-context extension config is updated first; if it fails,
init-options.json remains consistent with the previous state
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix(opencode): use commands/ directory (plural) to match OpenCode docs
OpenCode documentation (https://opencode.ai/docs/commands/) uses
.opencode/commands/ (plural) as the canonical command directory.
The OpenCode runtime supports both .opencode/command/ and
.opencode/commands/ via a {command,commands} glob, but the
singular form was the original convention and is now outdated.
Update the OpenCode integration to write to .opencode/commands/
instead of .opencode/command/, aligning with the documented
standard and the OpenSpec fix (Fission-AI/OpenSpec#748).
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
Assisted-by: OpenCode (claude-opus-4-6)
* feat(registrar): add legacy_dir fallback for backward-compatible directory migration
Add _resolve_agent_dir() to CommandRegistrar that checks a
legacy_dir fallback when the canonical directory does not exist.
When legacy_dir is found, a deprecation warning directs users to
run "specify integration upgrade" to migrate.
The OpenCode integration declares legacy_dir: ".opencode/command"
so that extension and preset registration, as well as command
cleanup, continue working for projects that have not yet migrated
to .opencode/commands/.
The legacy_dir mechanism is opt-in: integrations that do not
declare it get no fallback and no behavioral change.
Add end-to-end test verifying that "specify integration upgrade
opencode" migrates commands from legacy .opencode/command/ to
canonical .opencode/commands/ and removes stale files.
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
Assisted-by: OpenCode (claude-opus-4-6)
* fix(registrar): address PR review feedback on legacy_dir handling
- Fix deprecation warning formatting: quote paths and remove trailing
'/.' that produced confusing '.opencode/commands/.' output
- Eliminate duplicate warnings: pass pre-resolved directory to
register_commands() via _resolved_dir parameter so
_resolve_agent_dir() is only called once per agent
- Fix unregister_commands() to clean both canonical and legacy dirs
when both exist, preventing orphaned command files after upgrade
- Add test_unregister_cleans_legacy_when_both_dirs_exist regression
test and tighten warning count assertion to exactly 1
Assisted-by: OpenCode (claude-opus-4-6)
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
---------
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
* fix(integration): refresh shared infra on integration switch
* fix(integration): address Copilot review on switch shared-infra refresh
- Clarify install_shared_infra docstring: force overwrites regular files
but always preserves symlinks (safe-destination check refuses to follow).
- Print refresh_hint only for preserved_user_files; skipped_files keeps
the generic remediation. Avoids misleading guidance when files were
merely skipped (not detected as customized).
- Catch ValueError from the safe-destination check and bucket the path
under a new symlinked_files warning instead of aborting the switch.
- Restore templates/constitution-template.md to upstream (drop accidental
leading blank lines).
* fix(integration): narrow symlink bucketing to dedicated exception
Address Copilot feedback on shared_infra.py:305 — _safe_dest_or_bucket
caught any ValueError as 'symlinked', which masked genuine safety errors
(path escape, parent-not-a-directory).
- Introduce SymlinkedSharedPathError(ValueError) raised only by the
symlink-specific branches in _ensure_safe_shared_*().
- _safe_dest_or_bucket() now catches only SymlinkedSharedPathError;
other ValueErrors propagate so the operation aborts with the real
cause instead of being silently bucketed.
- Wrap top-level dest_scripts/dest_variant/dest_templates mkdir calls
in the same bucket helper so a symlinked .specify/scripts or
.specify/templates is preserved with a warning rather than aborting
the switch (matches the documented 'preserve customizations' behavior).
- Update tests to expect the new bucket+warn behavior for leaf-level
symlinked destinations.
* fix(integration): tailor shared-infra warnings and rename preflight test
Address Copilot review on PR #2375:
- skipped_files hint now uses refresh_hint when refresh_managed=True
so integration switch suggests --refresh-shared-infra instead of the
generic init/upgrade flags.
- symlinked-files warning header says "path(s)" rather than "file(s)"
since symlinked directories (e.g. .specify/scripts/bash) are also
bucketed there.
- Rename test_shared_infra_install_preflights_before_writing to
test_shared_infra_install_buckets_unsafe_destinations_and_continues
to match the new bucket-and-continue semantics.
* test: rename symlink bucketing tests to reflect bucket-and-continue behavior
The two file-bucketing tests at line 300/320 were named *_refuses_*, but
the new behavior buckets symlinked file destinations with a warning while
safe destinations in the same install still complete. Rename to
*_buckets_* and update docstrings to match.
The remaining *_refuses_* tests (line 342/362/381) genuinely raise on
symlinked dirs/manifests and keep their names.
---------
Co-authored-by: Quratulain-bilal <quratulain.bilal@users.noreply.github.com>
* fix: migrate extension commands on integration switch
When switching integrations (e.g. kimi → opencode), extension commands
were not re-registered for the new agent, leaving the new agent without
extension support and orphaning files in the old agent's directory.
Changes:
- Add ExtensionManager.unregister_agent_artifacts() to clean up old
agent extension files and registry entries during switch
- Add ExtensionManager.register_enabled_extensions_for_agent() to
re-register all enabled extensions for the new agent
- Wire both into integration_switch() after uninstall/install phases
- Handle skills mode (Copilot --skills) correctly
- Add tests for kimi→opencode→claude migration, Copilot skills mode,
and disabled extension handling
Fixes extension commands not appearing after integration switch.
* Update src/specify_cli/extensions.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Initial plan
* Add specify integration subcommand (list, install, uninstall, switch)
Implements the `specify integration` subcommand group for managing
integrations in existing projects after initial setup:
- `specify integration list` — shows available integrations and installed status
- `specify integration install <key>` — installs an integration into existing project
- `specify integration uninstall [key]` — hash-safe removal preserving modified files
- `specify integration switch <target>` — uninstalls current, installs target
Follows the established `specify <noun> <verb>` CLI pattern used by
extensions and presets. Shared infrastructure (scripts, templates) is
preserved during uninstall and switch operations.
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/1cca6c84-3e12-465d-88b8-a646d3504f63
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Address review feedback: extract helper, fix return type annotation
- Extract _update_init_options_for_integration() to deduplicate init-options
update logic between install and switch commands
- Fix _parse_integration_options return type to dict[str, Any] | None
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/1cca6c84-3e12-465d-88b8-a646d3504f63
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Potential fix for pull request finding 'Unused import'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Address review feedback: validate script type, handle --flag=value, fix metadata cleanup
- Add _normalize_script_type() to validate script type against SCRIPT_TYPE_CHOICES
- Handle --name=value syntax in _parse_integration_options()
- Clear init-options.json keys in no-manifest uninstall early-return path
- Clear stale metadata between switch teardown and install phases
- Add 5 tests covering the new edge cases
* Block --force with different integration, persist script type in init-options
- --force on install now rejects overwriting a different integration; users must
use 'specify integration switch' instead
- _update_init_options_for_integration() now accepts and persists script_type
- Fix misleading test docstring for switch metadata test
- Add test_force_blocked_with_different_integration
* Remove --force from integration install, ensure shared infra on install/switch
- Remove --force parameter entirely from integration install; users must
uninstall before reinstalling to prevent orphaned files
- Auto-install shared infrastructure (.specify/scripts/, .specify/templates/)
when missing during install or switch
- Add test for shared infra creation on bare project install
* Remove redundant installed_key != key check
The == key case already returns above, so the != key guard is always true
at this point. Simplify to just 'if installed_key:'.
* Run shared infra unconditionally, defer metadata removal in switch
- Call _install_shared_infra() unconditionally on install and switch since it
merges without overwriting existing files
- Remove premature metadata cleanup between switch phases; metadata is now
only updated after successful Phase 2 install
* Add install rollback, graceful manifest errors, clear switch metadata
- Attempt teardown rollback on install/switch failure to avoid orphaned files
- Catch ValueError/FileNotFoundError on IntegrationManifest.load() in uninstall
with user-friendly recovery guidance
- Clear metadata immediately after switch teardown so failed Phase 2 doesn't
leave stale references to the removed integration
* Log rollback failures instead of silently suppressing them
* Handle corrupt manifest in switch, distinguish unknown vs missing manifest
- Wrap IntegrationManifest.load() in switch with ValueError/FileNotFoundError
handling, matching the pattern used in uninstall
- Split else branch to report 'unknown integration' vs 'no manifest' separately
* Clean up metadata on rollback, broaden init-options match in uninstall
- Remove integration.json in install/switch rollback paths so failed installs
don't leave stale metadata
- Match on both 'integration' and 'ai' keys when clearing init-options.json
during uninstall to handle partially-written metadata
* Fix recovery guidance for unreadable manifests, fix type annotations
- Recovery instructions now guide users through delete manifest → uninstall →
reinstall workflow that actually works
- Type annotations for optional CLI parameters changed from str to str | None
* Allow manifest-only uninstall for unknown/removed integrations
- Uninstall no longer requires the integration to be in the registry; falls back
to manifest.uninstall() directly when get_integration() returns None
- Switch Phase 1 similarly uses manifest-only uninstall for unknown integrations
instead of skipping teardown, preventing orphaned files
* Fail fast on corrupt integration.json, validate integration options
- _read_integration_json() now exits with an actionable error when
integration.json exists but is corrupt/unreadable
- _parse_integration_options() rejects unknown options, validates flag usage,
and requires values for non-flag options
* Validate integration.json is a dict, fail fast on missing manifest in switch
- _read_integration_json() validates parsed JSON is a dict, not a list/string
- Switch fails fast with recovery guidance when manifest is missing instead
of silently skipping teardown and risking co-existing integration files
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>