Commit Graph

149 Commits

Author SHA1 Message Date
Quratulain-bilal
442a581358 fix(cli): pin UTF-8 encoding on init-options and .extensionignore I/O (#2686)
* fix(cli): pin UTF-8 encoding on init-options and .extensionignore I/O

``Path.read_text`` / ``Path.write_text`` default to the system locale
codec, which is cp1252 / gb2312 / cp932 on Windows. Two user-facing
file paths in spec-kit were calling them without an explicit
``encoding=`` argument:

  - ``src/specify_cli/__init__.py:400,412`` —
    ``save_init_options`` / ``load_init_options`` for
    ``.specify/init-options.json``. A peer machine with a different
    default locale (or a UTF-8 Unix CI runner reading a file written on
    a cp1252 Windows host) cannot decode the file, raising
    ``UnicodeDecodeError``. ``UnicodeDecodeError`` is a subclass of
    ``ValueError`` — not ``OSError`` / ``json.JSONDecodeError`` — so
    the existing fall-back ``except`` tuple in ``load_init_options``
    also misses it and the error propagates raw to the CLI.

  - ``src/specify_cli/extensions.py:764`` — ``.extensionignore``
    pattern reader. The very next line already normalises
    backslashes "so Windows-authored files work", proving the codebase
    expects Windows authors to write this file. Multibyte UTF-8
    patterns (Chinese filenames, accented directory names) silently
    mojibake when the host locale is not UTF-8, so the patterns fail
    to match and unintended files are shipped with the extension.

The sibling integration-catalog reader at
``src/specify_cli/integrations/catalog.py:150,156,193,202,374``
already pins ``encoding="utf-8"`` everywhere. PR #2280 fixed the
symmetric PowerShell-template BOM bug. This change brings the two
remaining drifted paths in line with that precedent.

Regression tests:

  - ``tests/test_presets.py::TestInitOptions`` — parametrized non-ASCII
    round-trip (CJK, Latin-1, Greek, emoji) plus a corrupted-file case
    that asserts the existing "fall back to {}" contract still holds
    when a peer file contains bytes invalid as UTF-8.
  - ``tests/test_extensions.py::TestExtensionIgnore`` — Japanese
    (``ドキュメント/``) and Latin-1 (``café/``) ignore patterns
    correctly exclude their directories during install.

* fix(cli): wrap .extensionignore decode error and tighten UTF-8 contract

Addresses Copilot review feedback on this PR.

Three issues, three fixes:

1. ``save_init_options`` now writes JSON with ``ensure_ascii=False``.
   Without that flag, ``json.dumps`` emits ASCII-only ``\uXXXX``
   escapes, which means the ``encoding="utf-8"`` pin on the
   surrounding ``Path.write_text`` makes no observable difference for
   any value we currently write. Flipping ``ensure_ascii`` makes the
   non-ASCII bytes hit the file directly, so the encoding pin becomes
   the thing that decides between cp1252 garbage and clean UTF-8 on
   Windows. The comment above the call now describes the real reason
   instead of the previously-misleading rationale Copilot flagged.

2. ``test_save_load_round_trip_preserves_non_ascii`` was a no-op under
   the old ``ensure_ascii=True`` writer (Copilot's second comment).
   Added ``test_save_writes_real_utf8_bytes`` that asserts the on-disk
   bytes contain the UTF-8 encoding of ``café`` (``0xC3 0xA9``), not
   its JSON escape form ``é``. Removing either
   ``ensure_ascii=False`` or ``encoding="utf-8"`` from the writer now
   breaks this test — the contract is pinned.

3. ``.extensionignore`` reader wraps ``UnicodeDecodeError`` as
   ``ValidationError`` with a pointer to the offending byte
   (Copilot's third comment). Mirrors
   ``ExtensionManifest._load_yaml``'s existing handler for
   ``extension.yml``. Adds
   ``test_extensionignore_invalid_utf8_raises_validation_error``
   asserting installation aborts with the wrapped error instead of a
   raw Python traceback.
2026-06-02 07:19:11 -05:00
WOLIKIMCHENG
14da893e4f fix(copilot): resolve active spec template (#2765)
Co-authored-by: root <kinsonnee@gmail.com>
2026-06-01 14:49:02 -05:00
Manfred Riem
39925ac084 fix: add missing agent-context extension entries to Cline _expected_files (#2797)
TestClineIntegration._expected_files() overrides the base-class version but
was not updated when the bundled agent-context extension files were added to
test_integration_base_markdown.py, causing test_complete_file_inventory_sh
and test_complete_file_inventory_ps to fail.

Fixes #2796
2026-06-01 14:31:25 -05:00
Pedro Barbosa
44aac9f6e4 feat: add native Cline integration (#2508)
* test: strip ansi to make asserts work

* feat: add native Cline integration
2026-06-01 11:20:48 -05:00
Manfred Riem
089feca75f fix: move URL install confirmation prompt before spinner (#2783) (#2784)
* fix: move URL install confirmation prompt before spinner (#2783)

The typer.confirm() prompt inside console.status() was overwritten by
Rich's spinner animation, making extension add --from <url> appear hung.

Move URL validation and the default-deny confirmation prompt before the
spinner block so the user can see and respond to the [y/N] prompt.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: guard prompt with not dev, escape from_url in Rich markup

Address PR review feedback:
- Gate URL confirmation prompt on 'not dev' so --dev + --from does not
  show a confusing prompt for a URL path that will be ignored.
- Escape from_url with rich.markup.escape() in both the warning panel
  and the download message to prevent markup injection via crafted URLs.

* fix: remove unused import, reuse safe_url, add regression tests

Address second round of PR review:
- Remove unused urllib.request import from URL install path
- Remove redundant re-import of rich.markup.escape; reuse safe_url
  computed before the spinner for download and error messages
- Add test_add_from_url_prompts_before_spinner: asserts typer.confirm
  fires before console.status spinner to prevent #2783 regression
- Add test_add_from_url_cancel_exits_cleanly: asserts declining the
  prompt exits with code 0 and prints Cancelled

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 07:50:03 -05:00
Copilot
50da3a0f77 Extract agent context updates into bundled agent-context extension (#2546)
* 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>
2026-05-30 06:37:18 -05:00
Copilot
cc3d828227 Add support for SPECKIT_WORKFLOW_RUN_ID override (#2742)
* Initial plan

* feat: support SPECKIT_WORKFLOW_RUN_ID override

* docs: clarify run_id env var precedence wording

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-29 10:50:00 -05:00
Copilot
b4e5a1c3be feat: support SPECKIT_INTEGRATION_<KEY>_EXECUTABLE env var (#2743)
* Initial plan

* feat: support SPECKIT_INTEGRATION_<KEY>_EXECUTABLE env var override

Adds `IntegrationBase._resolve_executable()` which reads
`SPECKIT_INTEGRATION_<KEY>_EXECUTABLE` (hyphens→underscores, uppercased)
and falls back to `self.key` when unset or whitespace-only.

All concrete `build_exec_args()` implementations now call
`self._resolve_executable()` instead of hard-coding `self.key` or
`"agy"` as the first argv token:
- MarkdownIntegration, TomlIntegration, SkillsIntegration (base classes)
- CodexIntegration, DevinIntegration, OpencodeIntegration, HermesIntegration, AgyIntegration
- CopilotIntegration (overrides `_resolve_executable()` to fall back to
  the platform-specific `_copilot_executable()` default; also updates
  `dispatch_command()` to use `_resolve_executable()`)

Tests added to tests/integrations/test_extra_args.py covering:
- default (unset) falls back to key
- env var replaces first argv token
- whitespace-only env var is a no-op
- key hyphen→underscore normalisation
- cross-integration scoping (wrong key ignored)
- all override integrations (Codex, Devin, Opencode, Copilot)
- Copilot dispatch_command path
- EXECUTABLE and EXTRA_ARGS can be set simultaneously

See issue #2596."

* fix: complete docstring sentence in _resolve_executable

* test: generalize extra-args test comments for override coverage

* Fix stale Codex executable comment

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-29 10:19:31 -05:00
Huy Do
f50839a928 feat(integrations): support SPECIFY_<KEY>_EXTRA_ARGS env var for agent subprocess flags (#2596)
* feat(integrations): support SPECIFY_<KEY>_EXTRA_ARGS env var for agent subprocess flags

Read a per-integration env var (SPECIFY_<KEY>_EXTRA_ARGS) inside
`SkillsIntegration.build_exec_args`, `MarkdownIntegration.build_exec_args`,
and `TomlIntegration.build_exec_args` and append the parsed flags to the
spawned agent's argv, gated per integration key.

Operators can now opt into extra CLI flags (e.g.
`SPECIFY_CLAUDE_EXTRA_ARGS=--dangerously-skip-permissions`) without
modifying any SKILL or workflow YAML. Useful in CI / non-interactive
contexts where the spawned `<agent> -p ...` would otherwise hang on
an internal permission or input prompt invisible to the parent
`specify workflow run` process.

Key normalization: `kiro-cli` → `SPECIFY_KIRO_CLI_EXTRA_ARGS` (hyphen
replaced with underscore, then uppercased).

Default (env var unset or whitespace-only) is byte-identical to
previous behaviour. Extra args are inserted between `-p prompt` and
the model / output-format flags so they cannot clobber canonical
Spec Kit args.

Implementation: a single helper `IntegrationBase._apply_extra_args_env_var`
encapsulates the env-var read + shlex parsing; each of the three
concrete `build_exec_args` implementations calls it.

Closes #2595

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

* fix(integrations): wire SPECIFY_<KEY>_EXTRA_ARGS into Codex/Devin/Opencode/Copilot

Four integrations override `build_exec_args` and were silently
ignoring the env-var hook introduced in the previous commit:

- CodexIntegration (`codex exec ...`)
- DevinIntegration (`devin -p ...`)
- OpencodeIntegration (`opencode run ...`)
- CopilotIntegration (`copilot -p ...`)

Each now calls `self._apply_extra_args_env_var(args)` between the
base argv and the canonical Spec Kit flags (matching the placement
in `MarkdownIntegration`, `TomlIntegration`, and `SkillsIntegration`),
so operator-injected flags cannot clobber `--model` / `--output-format`
/ `--json`.

Adds 4 parameterized override-integration tests locking the wiring
per agent. Also cleans up an inline `__import__("os").environ` in the
fixture to a top-of-file `import os`.

Drive-by typing fix: guard `self.registrar_config.get(...)` in
`CopilotIntegration.add_commands` against the `None` case, matching
the pattern already used in `base.py` for the same access.

Addresses Copilot review on #2596.

* fix(integrations): apply Opencode extra-args before prompt-derived --command

When the Opencode prompt starts with `/`, `build_exec_args` injects
`--command <X>` derived from the prompt. The previous placement of
`self._apply_extra_args_env_var(args)` appended operator-injected
args AFTER that `--command`, so a user setting
`SPECIFY_OPENCODE_EXTRA_ARGS="--command override"` could redirect the
command under typical last-wins repeated-flag CLI semantics.

Move the hook to immediately after `args = [self.key, "run"]`, before
the prompt-parsing block. The operator's `--command override` (if
any) now precedes the Spec Kit-derived `--command speckit`, so the
canonical choice wins.

Adds `test_opencode_extra_args_cannot_clobber_prompt_derived_command`
locking the ordering. Also corrects the module docstring to describe
the hook as living in `IntegrationBase` (not `SkillsIntegration`) and
to acknowledge that this file covers Codex/Devin/Opencode/Copilot in
addition to SkillsIntegration stubs.

Addresses Copilot review on #2596.

* fix(integrations): honour SPECIFY_COPILOT_EXTRA_ARGS in dispatch_command

`CopilotIntegration` is the only integration that overrides
`dispatch_command` — it builds `cli_args` inline rather than going
through `build_exec_args`. The previous commit wired
`_apply_extra_args_env_var` into `build_exec_args` but workflow
execution calls `dispatch_command`, so `SPECIFY_COPILOT_EXTRA_ARGS`
was silently ignored at runtime.

Add the hook in `dispatch_command` immediately after
`cli_args = ["copilot", "-p", prompt]`, mirroring the placement in
`build_exec_args` (between `-p prompt` and the canonical
`--agent` / `--yolo` / `--model` / `--output-format` flags).

`IntegrationBase.dispatch_command` already delegates to
`build_exec_args`, so Codex, Devin, and Opencode continue to honour
their respective env vars through inheritance — no further changes
needed for them.

Adds two end-to-end tests that monkeypatch `subprocess.run` and
assert the env-var args reach the executed argv:
- `test_copilot_dispatch_command_includes_extra_args` locks the
  bypass fix on the overridden path.
- `test_codex_dispatch_command_includes_extra_args` locks the
  inherited-base-dispatch path for the other three integrations.

Addresses Copilot review on #2596.

* refactor(integrations): rename env var to SPECIFY_INTEGRATION_<KEY>_EXTRA_ARGS

Per maintainer request: scope the operator-injected env var to the
integration subsystem by prepending `INTEGRATION_` to the key
segment, so it does not collide with other Spec Kit env-var
namespaces.

Renames everywhere it appears:
- Helper `IntegrationBase._apply_extra_args_env_var` env_name
  format and docstring (`base.py`).
- Inline comment in `CopilotIntegration.dispatch_command`.
- All `monkeypatch.setenv(...)` calls, docstrings, and the
  autouse fixture's scope filter in
  `tests/integrations/test_extra_args.py`.

No behaviour change beyond the variable name. Default (env var
unset) still byte-identical to before this PR.

Addresses review on #2596.

* fix(integrations): raise actionable error on malformed EXTRA_ARGS quoting

Wrap `shlex.split` in `_apply_extra_args_env_var` so an unmatched quote
in `SPECIFY_INTEGRATION_<KEY>_EXTRA_ARGS` surfaces a clear `ValueError`
naming the offending env var and showing the invalid value, instead of
crashing workflow dispatch with a bare shlex traceback. Adds a new test
locking the actionable error path.

Addresses Copilot review feedback on #2596.

* test(integrations): use `_copilot_executable()` in Copilot extra-args test

`test_copilot_integration_honours_extra_args` hardcoded `"copilot"`
in the expected argv, but `CopilotIntegration.build_exec_args` calls
`_copilot_executable()` which returns `"copilot.cmd"` on Windows
(`os.name == "nt"`). The test passed on Linux/macOS and failed on
all three Windows-latest matrix entries.

Resolve by importing `_copilot_executable` alongside `CopilotIntegration`
and using it as the first expected argv token. The companion
`test_copilot_dispatch_command_includes_extra_args` already uses
`index()` lookups rather than full-argv equality so it was unaffected.

* fix(integrations): couple Codex executable to self.key + cover base classes

Two Copilot findings on the latest pass:

1. `CodexIntegration.build_exec_args` hardcoded the executable name
   as the literal `"codex"` while the env-var lookup derives from
   `self.key`. The two should stay coupled (matching Devin/Opencode,
   which both use `self.key` already). Replace the literal with
   `self.key` so the argv and env-var scoping cannot drift.

2. `tests/integrations/test_extra_args.py` covered the
   `SkillsIntegration` mechanism (via stubs near the top) and the
   four `build_exec_args` overrides (Codex/Devin/Opencode/Copilot)
   end-to-end, but did not exercise the `MarkdownIntegration` or
   `TomlIntegration` base implementations directly. Add bare
   `_MarkdownAgentStub` and `_TomlAgentStub` test stubs and a test
   each — the most common integration pattern (Amp, Auggie, Generic,
   Gemini, Tabnine, …) inherits without overriding, so the base
   wiring is now locked.

Full suite: 3011 passed (was 3009), 40 skipped, no regressions.

Addresses Copilot review feedback on #2596.

* fix(integrations): rename env var to SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS

Renames the env-var hook prefix from `SPECIFY_INTEGRATION_*` to
`SPECKIT_INTEGRATION_*` to match the established codebase
convention for integration-subsystem env vars
(`SPECKIT_INTEGRATION_CATALOG_URL` in `integrations/catalog.py`,
`SPECKIT_COPILOT_ALLOW_ALL_TOOLS` in `integrations/copilot/__init__.py`).

The `SPECIFY_*` prefix is reserved for user-facing
feature-resolution variables (`SPECIFY_FEATURE`,
`SPECIFY_FEATURE_DIRECTORY`); reusing it for integration-subsystem
scoping would introduce a second integration namespace under a
different prefix, confusing operators who already set
`SPECKIT_INTEGRATION_CATALOG_URL`.

Also reverts the unrelated defensive `arg_placeholder` /
`registrar_config is None` guard in
`CopilotIntegration.setup_skills_mode` — it was a drive-by pyright
cleanup mixed into this PR. Every concrete integration sets
`registrar_config` so the guard never fires in practice; the
typing issue belongs in a focused follow-up rather than this
env-var-hook PR.

Updates everywhere the old prefix appeared:
- `IntegrationBase._apply_extra_args_env_var` helper + docstring
- `CopilotIntegration.dispatch_command` inline comment
- All `monkeypatch.setenv(...)` calls in `tests/integrations/test_extra_args.py`
- The autouse fixture scope filter
- Test module docstring

Full suite: 3011 passed, 40 skipped, no regressions.

Addresses Copilot review feedback on #2596.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:00:15 -05:00
Manfred Riem
ad62357015 docs: consolidate Community sections in README (#2736)
* docs: consolidate Community sections in README

Replace four separate Community sections (Extensions, Presets,
Walkthroughs, Friends) with a single consolidated section containing
a bullet list, one shared disclaimer, and both publishing guide links.

* fix: broken community anchor links and missing Hermes hook note injection

- Update README.md and extensions/README.md to point community
  extension links to the docs site instead of removed section anchor
- Add post_process_skill_content() call in Hermes setup() so hook
  command notes are injected into generated skills
- Add Hermes test override for test_hook_sections_explain_dotted_command_conversion
  with Path.home() monkeypatch
2026-05-28 11:32:56 -05:00
WOLIKIMCHENG
57a518a583 Fix shared script command hints for integration separators (#2627)
* fix shared script command refs for integration separators

* Fix integration use shared infra refresh hint

* Clarify shared infrastructure force wording

---------

Co-authored-by: root <1647273252@qq.com>
Co-authored-by: root <kinsonnee@gmail.com>
2026-05-28 10:02:27 -05:00
darion-yaphet
6d25d869b3 feat(agy): enhance Google Antigravity CLI integration (#2689)
* feat(agy): enhance Google Antigravity CLI integration

- Set requires_cli=True and install_url for CLI tool detection
- Implement build_exec_args() for non-interactive execution via agy --print
- Add dot-to-hyphen hook command note injection in generated SKILL.md files

* fix(agy): add --ignore-agent-tools to TestAgyAutoPromote tests

Tests verify file layout and setup warnings, not CLI presence.
agy requires_cli=True causes CI failures when agy is not installed.
2026-05-28 09:51:19 -05:00
NgoQuocViet2001
9307093d8a Fix --dev extension agent symlinks (#2554)
* Fix dev extension agent symlinks

* Address dev symlink review feedback

* fix: handle dev symlink relpath failures

* fix: fall back when dev cache writes fail

* test: cover dev symlink fallback without privileges
2026-05-28 09:29:17 -05:00
Puneet Dixit
5a678c552e Share skills hook note post-processing (#2679)
* fix(integrations): share skills hook note post-processing

* fix(integrations): tighten skill post-processing

Apply skill content post-processing before the initial write, use an exact hook-note sentinel for idempotence, and route Copilot skill post-processing through the shared helper before adding mode frontmatter.

* Make hook note injection per instruction

* Deduplicate Codex hook note processing

---------

Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com>
Co-authored-by: Puneet Dixit <puneetdixit200@users.noreply.github.com>
2026-05-28 09:08:48 -05:00
Dave Majors Stark
5a50b75adb feat: add Hermes Agent integration (with review fixes) (#2651)
* feat: add Hermes Agent integration

* feat: add Hermes Agent integration

* feat: add Hermes Agent integration

* feat: add Hermes Agent integration (with review fixes)

- Full SkillsIntegration subclass with dual install strategy
  (project-local .hermes/skills/ + global ~/.hermes/skills/)
- CLI fix: integration_uninstall now calls integration.teardown()
  instead of manifest.uninstall() directly, allowing custom cleanup
- Fix Copilot review issues:
  - Docstring now reflects both -Q (quiet) and -q (query) flags
  - Empty command guard prevents passing empty skill names
- Add catalog entry for hermes in integrations/catalog.json

Co-authored-by: Zhaoxiaoguang001 <3357983213@qq.com>

* feat: write Hermes skills directly to global ~/.hermes/skills/

Hermes loads skills from the global ~/.hermes/skills/ directory,
not from project-local paths.  The old dual-install strategy copied
SKILL.md files to both locations — project-local (for manifest
tracking) and global (for Hermes discovery).

This change removes the project-local copies entirely:
- setup() writes directly to ~/.hermes/skills/speckit-*/SKILL.md
- An empty .hermes/skills/ marker directory is created in the
  project so extension commands (e.g. git) can detect Hermes
  as an active integration via register_commands_for_all_agents()
- teardown() cleans both the global speckit-* dirs and the local
  marker
- import yaml moved to local import inside setup()

Tests updated: Hermes-specific tests now assert global skill
location, and shared SkillsIntegrationTests that assumed
project-local files are overridden with Hermes-appropriate
assertions.

Co-authored-by: Zhaoxiaoguang001 <3357983213@qq.com>

* fix: address Copilot review feedback on Hermes integration

Addresses all 6 review comments from copilot-pull-request-reviewer:

1. Hard-fail on missing integration key → fall back to
   manifest.uninstall() with a warning instead of raising an error.
   Allows users to always remove stale integration files even when
   the integration class is missing from the registry.

2. HOME isolation in tests → every test that calls setup() or
   CliRunner now monkeypatches Path.home() to a temp directory,
   keeping the test suite hermetic and non-destructive.

3. HermesIntegration.teardown() now delegates to
   manifest.uninstall() for project-local tracked files
   (scripts, manifest), merging results with global cleanup.

4. Global skills cleanup gated behind force=True to avoid destroying
   speckit-* skills shared across multiple Spec Kit projects when
   running 'specify integration uninstall hermes' without --force.

5. Line 160 isolation (CLI test test_complete_file_inventory_sh).

6. Line 258 isolation (Path.home assertion in
   test_ai_hermes_without_ai_skills_auto_promotes).

* fix: address second Copilot review round — 6 remaining observations

- Move  to module scope (was inside per-template loop)
- Add  safety checks in setup() matching standard
- Fix docstrings: global skills always removed on uninstall (standard)
- Fix removal tracking: only report after successful rmtree
- Override shared test_modified_file_survives_uninstall with Hermes-appropriate
  behaviour (global skills always removed, no hash tracking)
- Update PR description to match implementation (global-only skills + marker)

* fix: add first-class global/home-based agent dir support in CommandRegistrar

Resolves Copilot HIGH concern (discussion_r3312194525):
HermesIntegration.registrar_config.dir was '.hermes/skills' (project-
relative), but skills live in ~/.hermes/skills/ (global). Extensions
and presets registering commands for the 'hermes' agent via
CommandRegistrar would write to the project-local marker directory
instead of the real global skills directory, making those commands
invisible to Hermes.

Fix consists of three parts:

1. CommandRegistrar._resolve_agent_dir now supports '~/'-prefixed and
   absolute paths in agent_config['dir']. Relative paths still resolve
   against project_root as before — zero change for existing agents
   (Claude, Codex, Gemini, etc.).

2. HermesIntegration.registrar_config.dir changed from '.hermes/skills'
   to '~/.hermes/skills', so extensions/presets write directly to the
   global directory Hermes searches at runtime.

3. Two inline project_root / agent_config['dir'] calls in the extension
   update backup/restore paths (src/specify_cli/__init__.py) now delegate
   to _resolve_agent_dir, giving them the same global-dir support plus
   the legacy_dir fallback they were missing (improvement for all agents).

Test side-effect: test_update_failure_rolls_back_registry_hooks_and_commands
was constructing verification paths with project_dir / '~/.hermes/skills'
(literal tilde) — fixed to use _resolve_agent_dir and monkeypatch
Path.home() so Hermes' global dir doesn't leak into the real filesystem.

* fix: address remaining 3 Copilot review observations (round 3)

- teardown docstring: clarify marker removal is conditional (if empty)
- test_pre_existing_skills_not_removed: now actually calls teardown()
  to verify foreign skills survive uninstall (was only running setup)
- integration_switch Phase 1: replaced old_manifest.uninstall() +
  remove_context_section() with current_integration.teardown(),
  matching the pattern already used in integration_uninstall.
  This ensures custom teardown logic (e.g. Hermes global skills
  cleanup) runs during switches.

* fix: address Copilot round 4 — home-relative dir resolution + project-local detection

1. _resolve_agent_dir(): expand ~/... via Path.home() + slice instead of
   expanduser(), so tests that monkeypatch Path.home() properly isolate
   the home directory (Copilot r3312731595, r3312731729)

2. Add detect_dir field to registrar_config: Hermes declares
   detect_dir='.hermes/skills' (project-local marker). CommandRegistrar
   checks detect_dir before resolving the output dir, preventing global
   dirs like ~/.hermes/skills from causing false detection in every
   project (Copilot r3312731682)

3. test_update_failure_rolls_back: no additional changes needed — the
   _resolve_agent_dir fix makes the existing Path.home() monkeypatch
   effective, so ~/.hermes/skills is not found in the fake home and
   Hermes is properly skipped.

Tests: 2236 passed (2009 integration + 195 extension + 32 hermes)

---------

Co-authored-by: Zhaoxiaoguang001 <3357983213@qq.com>
Co-authored-by: majordave <majordave@users.noreply.github.com>
2026-05-28 09:04:03 -05:00
Huy Do
c6afe4cde1 feat(workflows): expose {{ context.run_id }} template variable (#2664)
* feat(workflows): expose `{{ context.run_id }}` template variable

Closes #2590.

Surfaces the engine-assigned run id (the same 8-character hex
string Spec Kit prints as `Run ID:` at the end of
`workflow run`) as a workflow template variable so YAML
authors can reference it from shell `run:`, command
`input.args:`, switch `expression:`, and any other field that
already evaluates `{{ ... }}` templates.

### Why

The run id is the natural join key between a Spec Kit workflow
run and downstream artifacts, telemetry, or per-run scratch
state. Today the operator sees it in stdout but workflows
themselves cannot reference it — there was no way to stamp a
log line, name a scratch directory, or tag an artifact with
the same id Spec Kit assigned.

The three motivating use cases from the issue:

1. Telemetry / observability — stamp logs and events with the
   run id so external systems can join workflow runs to
   downstream artifacts.
2. Per-run scratch / isolation — interactive operator commands
   that need their own state directory under
   `/tmp/run-<id>/`.
3. Run-id in artifact metadata — stable join key from artifact
   back to the producing run.

### Implementation

`StepContext.run_id` is already populated by `WorkflowEngine`
in both `execute()` and `resume()`. The only gap was the
template namespace builder.

`_build_namespace` (in `workflows/expressions.py`) now adds a
`context` key alongside the existing `inputs`, `steps`,
`item`, and `fan_in` namespaces:

```python
ns["context"] = {"run_id": run_id}
```

The value is always present (even outside a run) and falls
back to an empty string when no run is active. Workflows
referencing `{{ context.run_id }}` therefore never error — a
hard requirement from the issue's acceptance criteria for
dry-run, validation, and ad-hoc evaluator usage.

### Default behaviour preserved

Workflows that do not reference `{{ context.run_id }}` are
byte-equivalent to before this change. The `context`
namespace is added unconditionally to keep template
resolution branch-free, but its presence has no observable
effect when nothing references it.

### Tests

`TestExpressions` (unit-level) gains three tests:

- `test_context_run_id_resolves` — direct lookup against a
  `StepContext(run_id=...)`.
- `test_context_run_id_defaults_to_empty_when_unset` —
  graceful default outside a run context.
- `test_context_run_id_string_interpolation` — mixed
  template (e.g. `"RUN_ID={{ context.run_id }}"`).

`TestContextRunId` (end-to-end) covers the three step types
the acceptance criteria called out:

- `test_shell_run_resolves_run_id` — `run:` field
  substitution, verified via captured stdout.
- `test_command_input_args_resolves_run_id` — `input.args:`
  resolution, captured in step output even when CLI dispatch
  is unavailable (the artifact-metadata use case).
- `test_switch_expression_matches_on_run_id` — switch
  matches against the resolved value, proving the run id is a
  first-class value in the expression engine, not just an
  interpolation token.
- `test_workflow_without_context_reference_unchanged` —
  locks the byte-equivalent default required by the issue.

### Docs

`workflows/README.md` gains a "Runtime Context" subsection
under "Expressions" documenting the new namespace and the
three canonical use patterns (telemetry, per-run scratch,
artifact metadata).

* test(workflows): drop inline double-quotes in run_id shell tests

`test_shell_run_resolves_run_id` and
`test_switch_expression_matches_on_run_id` used
`run: 'echo "RUN_ID={{ context.run_id }}"'` with inner double-quotes
around the echo argument. Bash/sh strips those quotes before invoking
echo, but cmd.exe (used on Windows when `shell=True`) treats them
as literal characters and emits `"RUN_ID=abc12345"` — failing the
exact-match assertion. Linux passed; all three Windows-latest matrix
entries failed with `assert '"RUN_ID=abc12345"' == 'RUN_ID=abc12345'`.

Resolve by dropping the inner double-quotes (the value has no spaces
or shell metacharacters) and wrapping the YAML scalar in plain
double-quotes the same way other shell-step tests in this file do
(e.g. `run: "echo b-saw-..."`). Behaviour-equivalent on POSIX,
portable to cmd.exe.
2026-05-27 13:00:58 -05:00
Huy Bui Minh
66884db85b fix: resolve __SPECKIT_COMMAND_*__ refs in preset skill rendering (#2717) (#2718)
* fix: resolve __SPECKIT_COMMAND_*__ refs in preset skill rendering (#2717)

The preset skill layer mirrors command templates into SKILL.md files but
only ran resolve_skill_placeholders(), leaving command cross-references as
raw __SPECKIT_COMMAND_<NAME>__ placeholders instead of rendering them as
/speckit-<cmd> the way CommandRegistrar.register_commands() does. As a
result, presets that override core commands under the agent skill layer
(e.g. Claude --ai-skills) leaked the raw tokens into SKILL.md.

Add a shared PresetManager._resolve_skill_command_refs() helper that maps
the agent's invoke separator to IntegrationBase.resolve_command_refs(), and
call it right after resolve_skill_placeholders() in every preset
skill-rendering path: _register_skills() (install), the _reconcile_skills()
override-restoration block, and both _unregister_skills() restore paths.
This mirrors register_commands() and addresses the path divergence flagged
in #1976.

Add regression tests covering the install and restore paths.

AI assistance: authored with Claude Code (Anthropic) — analysis, patch, and
tests. Verified via the existing pytest suite plus a manual CLI install and
remove cycle on a Claude --ai-skills project.

* test: cover reconcile-override and extension restore command-ref paths (#2718 review)

Copilot review flagged that the install and core-template restore paths
gained regression tests, but the reconcile project-override branch and the
extension-backed restore branch were uncovered. Add focused tests for both:

- test_reconcile_override_skill_resolves_command_refs: a project override
  wins after preset removal; _reconcile_skills must render command refs.
- test_extension_restore_resolves_command_refs: a skill restored from an
  extension command body must also render command refs.

Both fail on main and pass with the fix in 8dd93c0.
2026-05-27 12:49:54 -05:00
Manfred Riem
3227b9660e fix: paths-only skips branch validation, setup-plan preserves existing plan (#2672)
* fix: paths-only skips branch validation, setup-plan preserves existing plan (#2653)

- check-prerequisites.sh/ps1: move branch validation after --paths-only
  early exit so --paths-only returns paths without requiring a spec branch
- setup-plan.sh/ps1: skip template copy when plan.md already exists to
  prevent overwriting user-authored plans on reruns
- setup-plan.sh: send status messages to stderr in --json mode so stdout
  remains parseable JSON
- Add tests for both fixes (bash + PowerShell)

* fix: remove trailing whitespace in PowerShell scripts

* fix: route PS skip message to stderr in -Json mode, add PS JSON assertions

Address review: setup-plan.ps1 Write-Output polluted stdout in -Json
mode when plan.md already existed. Use [Console]::Error.WriteLine()
when -Json is set. Add json.loads + stderr assertions to the PS rerun
test to catch regressions.

* fix: use Test-Path -PathType Leaf for plan existence check

Bare Test-Path matches directories too, which would silently skip plan
creation if a directory existed at the plan.md path.
2026-05-27 07:17:34 -05:00
Manfred Riem
e54653efcc fix: create skills directory on demand during extension/preset install (#2711)
* fix: create skills directory on demand during extension/preset install

_get_skills_dir() in both extensions.py and presets.py returned None
when the skills directory did not yet exist on disk, even though skills
were enabled in init-options. This caused extension skill registration
to silently produce an empty registered_skills list and skip writing
SKILL.md files.

Replace the is_dir() bail-out with mkdir(parents=True, exist_ok=True)
so the directory is created on demand when ai_skills is enabled.

Update the existing test expectation and add a parametrized regression
test (claude + codex) that installs an extension before the skills
directory exists and asserts SKILL.md files and registry entries are
created.

Fixes #2682

* test: assert skills dir is NOT created when skills are disabled

Strengthen negative tests to verify _get_skills_dir does not create the
directory on disk when ai_skills is false or init-options.json is absent.

* fix: add symlink/containment check and preserve Kimi existence gate

Address PR review feedback:

- Use _ensure_safe_shared_directory() instead of raw mkdir() to prevent
  symlink-following writes outside the project root.
- Restore the is_dir() existence gate for the Kimi native-skills
  fallback (ai_skills=false): only create the directory on demand when
  ai_skills is explicitly enabled.
- Update docstrings to reflect the on-demand vs existence-gate behavior.
- Reuse resolve_skills_dir helper in tests instead of manually
  reconstructing paths from AGENT_CONFIG.

* refactor: extract resolve_active_skills_dir shared helper

Deduplicate the _get_skills_dir logic that was nearly identical in
ExtensionManager and PresetManager into a single module-level
resolve_active_skills_dir() function in __init__.py.

The shared helper wraps _ensure_safe_shared_directory errors with
skills-specific messages so users see 'agent skills directory' instead
of 'shared infrastructure directory' in error output.

Both class methods now delegate to the shared helper.

* fix: preserve original error reason in skills dir safety check

Include the original exception message from _ensure_safe_shared_directory
in the re-raised ValueError so the user sees the specific reason (symlink,
not-a-directory, path escape, etc.) instead of a generic message.

* fix: handle skills dir safety errors gracefully during install

Catch ValueError/OSError from _get_skills_dir() inside
_register_extension_skills() so a symlink or permission error logs a
warning and returns [] instead of aborting mid-install and leaving a
partially-installed extension without a registry entry.

Also document OSError in resolve_active_skills_dir() docstring.

* fix: catch errors in _get_skills_dir and use _print_cli_warning

Move the ValueError/OSError catch from _register_extension_skills into
_get_skills_dir itself so all callers (install, uninstall, reconcile)
are protected from unsafe-path exceptions.

Replace logging.getLogger().warning with _print_cli_warning for
consistent Rich-formatted user output.

* fix: use context-aware error messages for skills directory safety

Add a 'context' parameter to _ensure_safe_shared_directory (defaults to
'shared infrastructure directory' for backward compat). The skills dir
caller passes context='agent skills directory' so error messages say
e.g. 'Refusing to use symlinked agent skills directory' instead of
'Refusing to use symlinked shared infrastructure directory'.

Simplify resolve_active_skills_dir by removing the now-unnecessary
try/except wrapper.

* fix: validate Kimi native-skills directory for symlink/containment

The Kimi fallback path (ai_skills=false) used is_dir() which follows
symlinks, so a symlinked .kimi/skills could cause writes outside the
project root. Now validates with _ensure_safe_shared_directory(create=
False) before returning the directory.
2026-05-26 15:10:59 -05:00
Copilot
c7e0cacaff fix: PS 5.1 compat — replace non-ASCII chars in shipped PowerShell scripts (#2709)
* Initial plan

* fix: replace non-ASCII chars in PS1 files, add encoding regression tests, fix ANSI stripping in tests, update docs

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-26 13:18:34 -05:00
LahkLeKey
0ae451f697 Add util for windows sub-process (#2598)
* Add util for windows sub-process

* Use platform-aware Copilot executable in subprocess calls

* Update test_workflows.py
2026-05-26 08:25:15 -05:00
darion-yaphet
7f33dca87c refactor: create commands/ package and move init handler (PR-4/8) (#2615)
* refactor: create commands/ package and move init handler (PR-4/8)

- Extract agent configuration constants (AGENT_CONFIG, AI_ASSISTANT_HELP,
  SCRIPT_TYPE_CHOICES, etc.) to _agent_config.py to avoid circular imports
- Create commands/ package skeleton with stub modules for each command group
- Move init command handler (~670 lines) from __init__.py to commands/init.py
  using the register(app) pattern; lazy imports inside the handler body
  prevent circular dependencies with __init__.py
- Re-export AGENT_CONFIG, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES from
  __init__.py for backward compatibility
- Add tests/test_commands_package.py to verify package structure

* fix(tests): update patch targets after moving init handler to commands/init.py

_stdin_is_interactive and select_with_arrows are now bound in
specify_cli.commands.init, not specify_cli directly.

* fix(lint): remove unused imports and mark re-exports in __init__.py

- Remove shutil, shlex top-level imports (used lazily inside functions)
- Remove rich.live.Live import (moved to commands/init.py)
- Mark select_with_arrows and _locate_bundled_workflow as explicit
  re-exports to satisfy ruff F401

* chore: add from __future__ import annotations to new modules

Aligns with the project convention established in _console.py, _assets.py,
_utils.py, and other modules.

* docs(cli): align init help with bundled scaffolding

Potential fix for pull request finding

Update command package documentation and init help text to reflect the current implementation: init uses bundled assets and integration setup, while placeholder command modules are import anchors until extracted. Remove the unused tracker-active flag assignment that had no reader in the codebase.

Constraint: --offline is hidden/no-op and init no longer downloads templates from GitHub releases

Rejected: Add no-op register functions to placeholder modules | would imply extracted command groups are implemented there

Confidence: high

Scope-risk: narrow

Directive: Keep CLI help text aligned with the actual init scaffolding path

Tested: uv run specify init --help; uv run pytest tests/test_commands_package.py tests/test_agent_config_consistency.py -q; uv run pytest tests/test_commands_package.py tests/test_console_imports.py tests/integrations/test_cli.py -q

Not-tested: full test suite

* fix(init): align preset failure reporting with _print_cli_warning helper

Use the _print_cli_warning helper (introduced in main) for preset install
failures so that output matches the expected format:
  "Failed to install preset '<name>': ..."
  "Continuing without the optional preset."

* fix(init): remove unused lazy imports

The init command imported CLI error formatting helpers through its circular-dependency-safe lazy import block, but the module does not use them. Remove those imports so ruff does not report F401.

Constraint: uvx ruff check src/ must pass.

Rejected: Wire the helpers into init error handling | Existing preset warnings already use _print_cli_warning, and changing behavior is unnecessary for this lint fix.

Confidence: high

Scope-risk: narrow

Directive: Keep lazy import blocks limited to names consumed in the importing module.

Tested: uvx ruff check src/

Not-tested: Full pytest suite

Co-authored-by: OmX <omx@oh-my-codex.dev>

---------

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-26 08:06:35 -05:00
Pascal THUET
dca81b90de fix init-options speckit version refresh (#2647) 2026-05-26 06:51:06 -05:00
Manfred Riem
616eba6a57 fix: while/do-while loop condition reads stale iteration-0 step output (#2662)
* fix: while/do-while loop condition reads stale iteration-0 step output

After executing namespaced loop body steps, copy each iteration's
results back to the original unprefixed step key so that
evaluate_condition() sees the latest values instead of stale
iteration-0 data.

Fixes #2592

* address review: cross-platform tests, preserve iteration-0 history

- Rewrite shell scripts in tests to use Python via script files
  instead of POSIX syntax, so they pass on Windows CI.
- Snapshot iteration-0 nested-step results under a namespaced key
  (parent:child:0) before the first copy-back overwrite, preserving
  complete per-iteration history for debugging.

* address review: skip copy-back on paused/failed iterations

Move the status check before the copy-back so that partial results
from paused or failed nested steps (e.g., a gate awaiting input)
do not overwrite the unprefixed key. This preserves correct resume
behavior.

* address review: quote paths in test shell commands

Quote both the Python executable and script file paths in the
run: commands to handle spaces in paths on Windows.

* address review: execute loop body with original IDs

Instead of namespacing step IDs for execution and copying results
back, execute the loop body with original (unprefixed) step IDs so
results naturally land at the right keys.  Snapshot previous
iteration results to namespaced keys (parent:child:N) for history
only.

This fixes multi-step loop bodies where step B references step A's
output within the same iteration — previously step B would see
stale data until the copy-back ran after the entire iteration.

* address review: namespaced execution with per-step copy-back

Revert to namespaced step IDs for execution (preserving unique
log entries and state keys per iteration) but copy each step's
result back to the unprefixed key immediately after it completes.

This preserves backward compatibility (same namespaced key format,
same log IDs) while fixing both the condition evaluation bug and
inter-step references within multi-step loop bodies.

* address review: alias after status check, add multi-step body test

- Move per-step aliasing below the PAUSED/FAILED/ABORTED status
  check so partial results from incomplete steps are not aliased
  back to the unprefixed key.
- Add test_while_loop_multi_step_body_inter_step_refs to exercise
  a multi-step loop body where step B reads step A's output within
  the same iteration, verifying per-step aliasing works correctly.

Addresses feedback from @doquanghuy (items 2 & 4) and Copilot
review on commit 9d0a222.

* address review: stable fallback IDs, expression-based inter-step test

- Use enumerate() for stable fallback IDs when loop body steps lack
  an explicit id (step-0, step-1, etc. instead of always step-0).
- Rewrite multi-step body test so step B uses expression
  substitution ({{ steps.step-a.output.stdout }}) instead of
  reading the counter file directly, making it a true regression
  test for per-step aliasing.
2026-05-21 12:25:03 -05:00
Quratulain-bilal
0dee2faf11 fix(catalogs): reject boolean priority in extension and preset catalog readers (#2589)
`bool` is a subclass of `int` in Python, so `int(True)` silently returns
`1`. The extension- and preset-catalog config readers coerced priority
with a bare `int(item.get("priority", idx + 1))`, which meant a YAML
config like:

    catalogs:
      - name: mine
        url: https://example.com/catalog.json
        priority: yes     # parses to True

was silently accepted as a valid priority of 1, quietly reordering the
catalog stack instead of raising the same `Invalid priority` error a
typo of `priority: not-a-number` already raises.

The sibling integration-catalog reader in `src/specify_cli/catalogs.py`
already guards this case (see `catalogs.py:137`). This change mirrors
that pattern in `extensions.py` and `presets.py` so the three catalog
validators stay consistent, and adds regression tests for both readers
matching the existing `test_load_catalog_config_rejects_boolean_priority`
template in `tests/integrations/test_integration_catalog.py`.
2026-05-21 08:21:13 -05:00
Pascal THUET
b4b83be51b feat: add self-check tip to check output (#2574)
* feat: add self-check tip to check output

* style: drop trailing period from self-check tip

Aligns the new tip with the other `Tip:` lines in `specify check`,
which don't end in a period. Per Copilot review feedback on #2574.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:21:11 -05:00
darion-yaphet
3d50f85875 fix(cli): clarify exception diagnostics (#2602)
Consolidate the CLI diagnostic plan, implementation, and test hardening into one reviewable change. The CLI now reports phase and target context for broad failure paths while preserving existing fail-fast behavior for real setup failures and warning-only behavior for optional best-effort work.

The workflow unit tests also avoid discovering real local agent CLIs, so developer machines with tools such as gemini installed do not hang pytest during metadata-only assertions.

Constraint: CLI setup failures must remain fail-fast, while optional preset and cleanup paths should continue with clear warnings.

Rejected: Replace broad handlers across the whole codebase in one pass | too broad for a targeted CLI diagnostic fix

Rejected: Add runtime timeouts to workflow agent dispatch | dispatch may legitimately be long-running and the observed hang was test isolation

Confidence: high

Scope-risk: moderate

Directive: Keep future best-effort CLI warnings tied to the failed phase and target so users can diagnose setup state.

Tested: uvx ruff check src/; uv run pytest tests/integrations/test_cli.py -v; uv run pytest tests/test_workflows.py::TestCommandStep::test_step_override_integration tests/test_workflows.py::TestPromptStep::test_execute_with_step_integration tests/test_workflows.py::TestPromptStep::test_execute_with_model -vv; uv run pytest

Not-tested: Real Nacos/PG/Redis-style external service failure injection; real interactive workflow dispatch against installed gemini CLI
2026-05-20 21:19:48 -05:00
Chao Z
9735145289 fix(codex): inject dot-to-hyphen hook command note in Codex skills (#2503)
* fix(codex): inject dot-to-hyphen hook command note in Codex skills

Hook commands in `.specify/extensions.yml` use dotted ids like
`speckit.git.commit`, but Codex skills are named with hyphens
(`speckit-git-commit`). The Claude integration handles this via an
explicit instruction injected into each generated SKILL.md by
`ClaudeIntegration.post_process_skill_content`, but the Codex
integration had no such override, so Codex would emit
`/speckit.git.commit` (which does not resolve) instead of
`/speckit-git-commit`.

This adds the same `_inject_hook_command_note` helper and a
`post_process_skill_content` override to `CodexIntegration`, plus a
small `setup()` override that applies the post-process to each
generated SKILL.md (mirroring the pattern in `ClaudeIntegration`).

Also widens the existing
`test_non_claude_post_process_is_identity` test to use `agy`
(another `SkillsIntegration` with no override), since asserting
identity behavior on Codex would now incorrectly fail.

Tests:
- New `TestCodexHookCommandNote` class mirrors
  `TestClaudeHookCommandNote`: setup-level injection, no-op when
  no hook block is present, idempotency, and indentation
  preservation.
- `pytest tests/` → 2866 passed, 34 skipped.

Signed-off-by: Chao Zhang <1175468+picklebento@users.noreply.github.com>

* fix(codex): handle empty eol when instruction is final line without newline

The hook-note injection regex allowed end-of-string matches via ``$``,
which left the captured ``eol`` empty. When the matched indent was also
empty, the substitution concatenated the note onto the same line as the
instruction. Default ``eol`` to ``\n`` when the capture is empty.

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

---------

Signed-off-by: Chao Zhang <1175468+picklebento@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:04:47 -05:00
WOLIKIMCHENG
51e6a140e2 refactor: migrate extension catalog stack parsing to shared base (#2576)
Co-authored-by: root <1647273252@qq.com>
2026-05-18 07:02:18 -05:00
Quratulain-bilal
409ec59704 fix(workflow): support integration: auto to follow project's initialized AI (#2421)
* fix(workflow): support integration: auto to follow project's initialized AI

Closes #2406

(squashed)

* fix(workflow): combine JSONDecodeError and UnicodeDecodeError handling

Address Copilot feedback: UnicodeDecodeError can be raised by both
read_text() and json.loads(), so combining the handlers ensures both
cases produce a consistent, clear error message.

* fix(workflows): honor integration_state schema guard and modern state in 'integration: auto'

Three Copilot follow-ups on PR #2421:

1. engine.py:799 — `_load_project_integration` was bypassing the same
   schema guard `_read_integration_json` enforces. It now reads the
   schema field directly, returns None on a future schema (so the
   workflow falls back to the literal 'auto' default rather than
   guessing), and routes through `normalize_integration_state` /
   `default_integration_key` so modern installs that record
   `default_integration` / `installed_integrations` (without the
   legacy top-level `integration` field) resolve correctly.

2. test_workflows.py — added two regression cases:
   - `integration: auto` resolves a modern normalized state file
   - `integration: auto` falls back when the state file declares a
     newer `integration_state_schema` than this CLI supports

3. test_cli.py — added a CLI-level regression for the `UnicodeDecodeError`
   branch in `_read_integration_json` to match the existing
   malformed-JSON coverage.

* refactor(integration): extract shared try_read_integration_json helper

Address Copilot review on PR #2421:

Both `_read_integration_json` (CLI) and `_load_project_integration`
(workflow engine) were parsing `.specify/integration.json` independently,
duplicating the schema guard and risking drift between the two readers.

Extract the parse + schema validation into a single low-level helper
`try_read_integration_json` in `integration_state.py` that returns either
the normalized state or a structured `IntegrationReadError`. Both callers
now delegate to this helper:

- CLI keeps its loud-fail UX: each error kind ("decode", "os",
  "not_object", "schema_too_new") is translated into the existing console
  message + typer.Exit(1).
- Engine keeps its silent fallback: any error simply returns None so
  `integration: auto` falls back to the workflow's literal default.

This eliminates the divergence Copilot flagged without changing observable
behavior for either caller.

* fix(integration): distinguish missing file from non-regular path

Address Copilot review on PR #2421:

`try_read_integration_json` was collapsing two distinct cases into a
single `(None, None)` return:

1. `.specify/integration.json` truly missing — silent fallback is correct.
2. Path exists but is a directory, socket, or other non-regular file —
   this is a misconfiguration the CLI should surface loudly.

Split the check: `exists()` falsey returns `(None, None)`; existing-but-
not-a-regular-file returns `(None, IntegrationReadError(kind="os", ...))`
so the CLI's loud-fail path produces an actionable error while the
engine still treats it as a fallback to the workflow's literal default.

* docs(workflow): clarify version pin, advisory integrations list, enum exemption

- workflow.yml: fix comment that said 0.8.3 was first release with auto
  resolution; the pin is >=0.8.5 so the comment now matches the pin.
- workflow.yml: clarify that requires.integrations.any is an advisory,
  non-exhaustive compatibility hint, not a closed set.
- engine.py: clarify that the auto-sentinel exemption only skips enum
  membership; declared type is still enforced through _coerce_input.

* fix(workflow): resolve auto sentinel for provided values; report stat errors

Two Copilot findings fixed:

1. _resolve_inputs only resolved the ``integration: auto`` sentinel when it
   came from the input default. A caller explicitly providing
   ``{"integration": "auto"}`` (which the workflow prompt advertises as a
   valid value) bypassed _resolve_default and the literal "auto" leaked
   to dispatch. Provided values now go through the same resolution path
   as defaults, and the enum-membership exemption applies in both cases.
   Regression test added.

2. try_read_integration_json used Path.exists() / Path.is_file() as a
   pre-check. Both return False on some OSErrors (e.g. permission errors
   during stat), which silently treated an unreadable-but-present file
   as missing — the engine fell back without warning and the CLI failed
   to surface the loud error. The pre-check is gone: read_text() is
   attempted directly, FileNotFoundError means missing (silent fallback),
   IsADirectoryError and other OSErrors become loud IntegrationReadError.

* fix(workflow): enforce declared type for string inputs, reject bool-as-number

Two Copilot findings fixed:

1. _coerce_input previously coerced/validated only ``number`` and
   ``boolean`` types, so ``type: string`` silently accepted any Python
   value (numbers, lists, dicts). A YAML authoring mistake like
   ``type: string`` + ``default: 5`` slipped through. Strings are now
   required to actually be strings; non-strings raise ValueError, which
   surfaces as an ``invalid default`` error from validate_workflow.

2. ``type: number`` accepted ``default: true`` because ``bool`` is a
   subclass of ``int`` (``float(True) == 1.0``). Bools are now rejected
   explicitly in the number path so the YAML mistake fails fast. The
   boolean path is also tightened to reject non-bool / non-string
   values for symmetry.

Comment on the auto-sentinel enum exemption updated to reflect the
stronger guarantee. Regression tests added for both rejections.

* fix(cli): drop unused normalize_integration_state import to satisfy ruff

CI's `uvx ruff check src/` flagged this as F401: the symbol was imported
under a private alias but never referenced. Tests stay green after
removal.
2026-05-15 16:03:33 -05:00
darion-yaphet
d947fda96f refactor: extract _version.py from __init__.py (PR-3/8) (#2550)
* refactor: extract _version.py from __init__.py (PR-3/8)

Move version-checking helpers and `specify self` sub-commands into a
focused `_version.py` module.

Moved symbols:
- GITHUB_API_LATEST — GitHub releases API endpoint constant
- _get_installed_version — importlib.metadata-based version lookup
- _normalize_tag — strip leading 'v' from release tag strings
- _is_newer — PEP 440 version comparison
- _fetch_latest_release_tag — single outbound call to GitHub API
- self_app — Typer sub-app for `specify self`
- self_check, self_upgrade — `specify self check/upgrade` commands

Dependency rule: _version.py imports only stdlib + packaging + ._console.

Backward compatibility: GITHUB_API_LATEST, self_check, self_upgrade
remain importable from specify_cli via re-exports in __init__.py.

Update test_upgrade.py to import helpers from specify_cli._version and
patch at the correct module path (specify_cli._version.*).
Add test_version_imports.py as regression guard.

* fix(tests): update _fetch_latest_release_tag import path in test_authentication.py

PR-3 moved _fetch_latest_release_tag from specify_cli into
specify_cli._version. test_upgrade.py was updated at the time, but
test_authentication.py::TestFetchLatestReleaseTagDelegation still
imported from the old location, causing ImportError on all three
delegation tests. Update all three inline imports to the correct
module path.
2026-05-15 13:12:24 -05:00
Nimra Akram
f684305e51 fix(powershell): ensure UTF-8 templates are written without BOM (#2280)
* fix(powershell): strip BOM from templates and ensure No-BOM output

* fix: address review feedback on encoding and naming for all ps scripts

* fix: address copilot feedback (encoding detection and variable naming)

* fix: remove duplicate comments in setup-plan.ps1

* test: verify spec.md is written without UTF-8 BOM

* test: also verify BOM-free output under Windows PowerShell 5.1

* fix

* fix: resolve merge conflict with main, add TestDescriptionQuoting

* fix: resolve TestDescriptionQuoting string quoting conflict with main

* test: restore PowerShell prefix-stripping parity test in TestGetFeaturePathsSinglePrefix

* fix: remove trailing whitespace from module docstring blank lines

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test: seed ps_git_repo with BOM-prefixed template to exercise WriteAllText fix

* fix: remove duplicate ps_git_repo fixture, restore ext_ps_git_repo

* fix: remove unrelated TestDescriptionQuoting and restore original test_ps_specify_feature_prefixed_resolves_by_prefix

---------

Co-authored-by: Nimraakram22 <nimra.akram123451@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 07:39:17 -05:00
WOLIKIMCHENG
be382804c7 Fix preset skill description precedence (#2538)
* Fix preset skill description precedence

* Fix skill description precedence during restore

---------

Co-authored-by: root <1647273252@qq.com>
2026-05-14 14:59:38 -05:00
Pascal THUET
c87081a50a fix(integration): clarify multi-install guidance (#2549) 2026-05-14 13:46:48 -05:00
Pascal THUET
e6afba9429 feat: add version feature reporting (#2548) 2026-05-14 12:52:14 -05:00
Dyan Galih
59fdca5997 fix(cli): harden extension registration and discovery workflows (#2499)
* chore: update community catalog with latest extension versions

- Update memory-md from 0.7.9 to 0.8.0
- Update architecture-guard from 1.6.7 to 1.8.0

* fix(cli): harden extension registration with project-level tracking in extensions.yml

* test(cli): add comprehensive unit tests for extension registration logic

* chore: remove out-of-scope catalog changes

* refactor: address PR feedback for extension registration hardening

* fix: harden extension registration defensive logic and add comprehensive unregister_hooks tests

- Add dict guard to register_hooks() to handle corrupted extensions.yml (non-dict root)
- Add 5 comprehensive tests for unregister_hooks() workflow:
  * Full workflow with hooks + installed list removal
  * Resilience when config has no 'hooks' key
  * Corrupted YAML handling
  * Multiple extension scenarios
  * All 11 tests passing

* fix: sanitize installed to strings, guard unregister_hooks dict, handle null hook values

- register_extension(): filter non-string entries from installed before sort
- register_hooks(): normalize hooks to {} when missing or not a dict
- unregister_hooks(): add isinstance(config, dict) guard before key checks
- unregister_hooks(): coerce null/scalar hook lists to [] before iteration
- tests: add 3 regression tests for no-hooks manifest, mixed-type installed, null hook values
- All 14 tests passing

* fix(cli): persist sanitization results and harden hook registration

* Harden extension registration to always persist sanitization results

* Hardening extension registration: support mapping entries, improve persistence, and fix update rollback

* fix(cli): harden extension update and unregistration workflows

* fix(cli): move update sentinels outside try block to prevent NameError on rollback

* fix(cli): sanitize hook event lists in register_hooks to prevent crashes

* fix(cli): deduplicate hook entries and harden rollback hooks-restore guards

* test(cli): add regression tests for extension update and rollback hardening

* fix(cli): deduplicate installed list by id in register_extension

* fix(cli): consolidate and harden extension update rollback logic

* fix(cli): initialize backup_registry_entry before try block to prevent UnboundLocalError on rollback

* fix(tests): return Path from download_extension mock and add Path import

* fix(cli): normalize get_project_config() return to dict; deduplicate in unregister_extension()

* fix(cli): normalize hooks/installed/settings in get_project_config(); use tmp_path-scoped zip in tests

* fix(cli): set modified=True on hook coercion in rollback; sanitize hook event values in get_project_config(); harden test assertions

* fix(cli): filter non-dict hook entries in get_project_config(); remove dead MISSING sentinel

* fix(cli): gate extensions.yml rollback on backup_hooks is not None; update stale comment

* fix(cli): move _AgentReg import outside try block; assert result.exception is None in tests

* fix(extensions): consistent key order in default config; deep-copy backup_installed

* test: fix misleading comment; assert exit_code==1 in rollback test

* test: clean up duplicate imports in hardening tests

* refactor(extensions): extract _sanitize_installed_list helper; strengthen hook unregister assertion

* fix(extensions): validate extension IDs in _sanitize_installed_list; clarify test comment
2026-05-13 12:02:01 -05:00
darion-yaphet
2fb9d3bb4b refactor: extract _assets.py and _utils.py from __init__.py (PR-2/8) (#2543)
* refactor: extract _assets.py and _utils.py from __init__.py

Move bundle path resolution and version lookup into _assets.py (stdlib only,
zero internal imports), and system utilities (subprocess, tool detection,
file operations) into _utils.py (imports only from ._console). Re-export all
moved symbols from __init__.py for backward compatibility. Update
test_check_tool.py to patch both specify_cli and specify_cli._utils namespaces
since constants are now defined in _utils.

* style: apply PR-1 review patterns to _assets.py and _utils.py

- Add module docstring to _assets.py (stdlib-only, zero internal imports)
- Add blank line after `from __future__ import annotations` in both files
- Replace `Optional[X]` with `X | None` throughout _utils.py (PEP 604)
- Remove unused `Optional` import from _utils.py
- Use explicit re-export form (`X as X`) for public symbols in __init__.py
- Remove unused `subprocess` and `tempfile` imports from __init__.py (moved to _utils.py)
2026-05-13 11:20:36 -05:00
Marcus Burghardt
9732a4d092 fix(opencode): use commands/ directory (plural) to match OpenCode docs (#2453)
* 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>
2026-05-13 09:55:56 -05:00
darion-yaphet
4f51e066c3 refactor: extract _console.py from __init__.py (PR-1/8) (#2474)
* refactor: extract _console.py from __init__.py

Move Rich UI primitives (BANNER, TAGLINE, StepTracker, get_key,
select_with_arrows, console, BannerGroup, show_banner) into a new
src/specify_cli/_console.py module. Re-export all symbols from
__init__.py to preserve the public API. Add regression guard tests.

* refactor(console): improve type annotations and add guard for empty options

- Add module-level docstring documenting the console layer's purpose and
  the dependency-layering rule (no imports from other specify_cli modules)
- Tighten select_with_arrows() signature: options typed as dict[str, str]
  and default_key as str | None to align with repo typing style
- Add early ValueError guard when options is empty, preventing downstream
  ZeroDivisionError / IndexError inside the Live loop

* refactor(console): improve type safety and code quality in _console.py

- Add Callable import from collections.abc for precise callback typing
- Annotate StepTracker._refresh_cb as Callable[[], None] | None
- Add parameter/return types to attach_refresh()
- Use explicit keyword form typer.Exit(code=1) across all error exits
- Add blank line between StepTracker class and get_key() (PEP 8)
- Add regression test for select_with_arrows() raising ValueError on
  empty options dict

* style(cli): add __all__ declaration to fix Ruff F401 lint warnings

- Add explicit __all__ for intentional re-exports (BANNER, TAGLINE, get_key)
- Prevent F401 unused import errors in CI lint checks
- Maintain backward compatibility for external imports

* Preserve public console imports

The CLI package intentionally re-exports console helpers for compatibility, so __all__ must track that public surface instead of narrowing star imports to a partial set.

Constraint: Existing tests import console helpers directly from specify_cli

Rejected: Remove __all__ entirely | keeping an explicit export list documents the intended compatibility surface

Confidence: high

Scope-risk: narrow

Directive: Keep __all__ synchronized when adding or removing specify_cli public re-exports

Tested: uv run pytest tests/test_console_imports.py -q

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* style(cli): use explicit re-export syntax to fix ruff F401 warnings

Use `X as X` form for BANNER, TAGLINE, and get_key imports
to mark them as intentional public re-exports and silence
ruff F401 lint errors.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-13 08:57:15 -05:00
Eldar Shlomi
cba00ab9a5 fix(kiro-cli): replace literal $ARGUMENTS with prose fallback (#2482)
* fix(kiro-cli): replace literal $ARGUMENTS with prose fallback

Kiro CLI file-based prompts do not natively substitute any
argument placeholder (kirodotdev/Kiro#4141, kiro.dev/docs/cli
manage-prompts), so the literal "$ARGUMENTS" set in
KiroCliIntegration.registrar_config["args"] reached the model
verbatim and broke the prompt — every parameterized SpecKit
command under Kiro CLI was unusable.

Replace the placeholder with a prose fallback that instructs
the model to take its argument from the user's next message,
mirroring the convention used by other integrations whose
target CLI lacks native argument injection.

Add two regression tests in TestKiroCliIntegration:
  - test_rendered_prompts_do_not_contain_raw_arguments
  - test_rendered_prompts_contain_kiro_arg_placeholder
and override the inherited test_registrar_config so it does
not require args == "$ARGUMENTS".

Fixes #1926

* test(kiro-cli): tighten args regression guard + document quirk

Address review feedback on PR #2482.

Two changes that bracket the original bug fix from both sides — code AND
documentation:

1. Test layer (Copilot finding at lines 27, 56)

The previous test_registrar_config asserted only that args != "$ARGUMENTS"
and that args is truthy. That would silently pass if a future change
swapped $ARGUMENTS for $INPUT, {{userMessage}}, <args>, or any other
unsubstituted placeholder syntax — defeating the regression guard for
issue #1926.

Replace with a dual-layer guard:

  - test_registrar_config_args_is_exact_prose_fallback pins args to the
    imported _KIRO_ARG_FALLBACK constant. Wording drift now requires a
    deliberate paired commit (production constant + test).

  - test_registrar_config_args_does_not_look_like_a_placeholder_token is
    an independent regression guard built on a 7-pattern regex set
    covering Bash ($X, ${X}, ${X:-default}), Mustache/Handlebars/Jinja
    ({{X}}, {{{X}}}), Liquid/Jinja control ({% %}), Python str.format /
    .NET ({0}, {var}), angle-bracket (<X>), and Windows (%X%). Patterns
    are anchored to the full string so legitimate prose mentioning a
    placeholder ("the {{magic}} of placeholders") is not flagged.

Also fix the line-56 tautology by importing _KIRO_ARG_FALLBACK directly
into test_rendered_prompts_contain_kiro_arg_placeholder, instead of
reading the constant back from registrar_config["args"]. The test now
verifies the FALLBACK STRING reaches the rendered output, independent
of the integration's own config staying correct.

2. Docs layer (mnriem CHANGES_REQUESTED)

The Kiro CLI row in docs/reference/integrations.md only documented its
alias. Update the notes column to lead with the limitation — Kiro CLI
does not substitute $ARGUMENTS in file-based prompts, so Spec Kit ships
a prose fallback at render time — with inline links to upstream Kiro
"Manage prompts" docs and issue #1926. Style follows the Pi row
("limitation first, alias preserved at end").

Refs #1926
2026-05-12 07:48:25 -05:00
Quratulain-bilal
bf47e89249 test(presets): silence expected UserWarnings in self-test composition… (#2373)
* test(presets): silence expected UserWarnings in self-test composition tests

The self-test preset that ships with the repo provides a wrap-strategy
command (speckit.wrap-test) intentionally without a corresponding core
base layer, exercising the 'no base layer' branch of
_reconcile_composed_commands().

Eighteen tests across TestSelfTestPreset and TestPresetSkills install
this preset and trigger an expected UserWarning. Running the suite with
-W error::UserWarning surfaces them as test noise that could obscure
unrelated warnings.

Add class-level pytest.mark.filterwarnings filters to acknowledge the
two known messages ('Cannot compose command speckit.wrap-test' and
'Post-install reconciliation failed for self-test') so other UserWarning
sources still propagate normally.

Fixes #2363

* test(presets): scope filterwarnings to UserWarning category

Address Copilot review on #2373: the previous filterwarnings entries
omitted the warning category, so any warning class with a matching
message would have been silenced. Append :UserWarning to the four
filters so only the deliberately-emitted UserWarnings from
_reconcile_composed_commands() are ignored.

* test(presets): narrow self-test warning filter to install helper only

Address Copilot feedback: the class-level @pytest.mark.filterwarnings on
TestPresetSkills was too broad. The 'Post-install reconciliation failed'
filter could mask real reconciliation regressions, since that warning is
only emitted when _reconcile_composed_commands/_reconcile_skills raises.

Tests in TestPresetSkills already call install_self_test_preset(), which
scopes a narrow filter to the expected wrap-strategy 'Cannot compose'
warning. The class-level filters are redundant for those calls and unsafe
elsewhere, so they are removed.

* test(presets): align TestSelfTestPreset docstring with helper-based filtering

Address Copilot feedback: docstring referred to 'filters above', but the
fix uses warnings.filterwarnings inside install_self_test_preset rather
than class-level decorators. Updated the docstring to describe the actual
mechanism.

* test(presets): remove extra blank line between helper and class (PEP 8)

Address Copilot feedback: PEP 8 expects two blank lines between top-level
definitions; reduce the three blank lines between install_self_test_preset
and TestSelfTestPreset to two.
2026-05-11 15:16:55 -05:00
Quratulain-bilal
947b4398c7 fix(integration): refresh shared infra on integration switch (#2375)
* 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>
2026-05-11 10:49:48 -05:00
Copilot
f0998348be feat: Config-driven opt-in authentication registry with multi-platform support (#2393)
* Initial plan

* feat: add authentication provider registry (GitHub + Azure DevOps)

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/da7ecfd0-e1c9-48dc-b692-27be0879e976

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* feat: add try-each-provider HTTP helper and wire all catalog fetches through auth registry

- Add authentication/http.py with open_url() that tries each configured
  provider in registry order, falling through on 401/403 to the next,
  and finally to unauthenticated
- Add build_request() for one-shot request construction
- Add configured_providers() to registry __init__
- Remove api_base_url() from AuthProvider ABC (unused)
- Remove hosts attribute from providers (no host matching)
- Replace _github_http.py usage in ExtensionCatalog and PresetCatalog
- Wire IntegrationCatalog and WorkflowCatalog through open_url (were unauthenticated)
- Wire _fetch_latest_release_tag() through open_url
- Wire all inline --from-url downloads through open_url
- Fix unused stub variable flagged by code-quality bot
- 49 auth tests (positive + negative), 1805 total tests passing

* fix: address review — fix stale docstrings, restore Accept header, add extra_headers to open_url

- Fix _open_url() docstrings in extensions.py and presets.py that
  incorrectly claimed redirect stripping behavior
- Add extra_headers parameter to open_url() so callers can pass
  additional headers (e.g. Accept) that persist across retries
- Restore Accept: application/vnd.github+json header in
  _fetch_latest_release_tag() via extra_headers

* feat: config-driven opt-in auth via ~/.specify/auth.json

Security-first redesign: no credentials are sent unless the user
explicitly creates ~/.specify/auth.json mapping hosts to providers.

- Add authentication/config.py: loads and validates auth.json with
  host-to-provider mappings, supports token/token_env/azure-ad/azure-cli
- Refactor AuthProvider ABC: auth_headers(token, scheme) + resolve_token(entry)
- Refactor GitHubAuth: bearer scheme only, token from config entry
- Refactor AzureDevOpsAuth: 4 schemes (basic-pat, bearer, azure-cli, azure-ad)
  with dynamic token acquisition for azure-cli and azure-ad
- Rewrite authentication/http.py: host matching, redirect stripping,
  provider fallthrough on 401/403, unauthenticated fallback
- Add docs/reference/authentication.md with full reference and template
- 1823 tests passing (67 auth-specific)

* fix: address review — unused imports, host normalization, provider+scheme validation, security hardening

- Remove unused imports (os, field, Any) in config.py
- Normalize hosts during load (strip + lowercase)
- Validate token/token_env are non-empty strings during load
- Validate provider+scheme compatibility during load
- Fix extra_headers order: auth headers applied last, cannot be overridden
- Remove unused 'tried' variable in http.py
- Warn (once) on malformed auth.json instead of silent fallback
- URL-encode OAuth2 client credentials body in azure_devops.py
- Update 403 message to mention auth.json configuration
- Fix registry leak in test_register_duplicate (try/finally)
- Fix import style consistency in test_authentication.py
- Add azure-cli and azure-ad token acquisition tests (mock subprocess/urlopen)
- Add autouse fixture to isolate upgrade tests from real auth.json
- 1829 tests passing

* fix: reject unknown providers, validate azure-ad fields, strip Authorization from extra_headers

- Reject unknown provider keys during auth.json load with clear error message
- Validate azure-ad tenant_id/client_id/client_secret_env as non-empty strings
- Strip Authorization from extra_headers in both build_request and open_url
  to prevent accidental or intentional bypass of provider-configured auth
- Add tests for unknown provider and incompatible scheme validation
- 1831 tests passing

* fix: extract shared auth test helpers, global config isolation, align docstring

- Move _inject_github_config / make_github_auth_entry to tests/auth_helpers.py
  to eliminate duplication across test_extensions, test_presets, test_upgrade
- Move auth config isolation fixture to global conftest.py (autouse) so ALL
  tests are isolated from ~/.specify/auth.json, not just test_upgrade
- Align load_auth_config docstring with actual behavior: ValueError may be
  caught by higher-level HTTP helpers that warn and continue unauthenticated
- 1831 tests passing

* fix: preserve auth header across multi-hop redirect chains

- Read Authorization from both headers and unredirected_hdrs in
  _StripAuthOnRedirect to survive multi-hop chains within allowed hosts
- Add test_multi_hop_redirect_within_hosts_preserves_auth
- 1832 tests passing

* fix: use resolved config path in warning/error messages and patch build_opener in no-network test

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/86df9557-54f1-4fe4-a25f-9501cb2356cf

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: assert full resolved config path in rate-limit output test

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/86df9557-54f1-4fe4-a25f-9501cb2356cf

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: close HTTPError on 401/403, remove _VALID_AUTH_SCHEMES, catch TimeoutExpired, skip POSIX test on Windows, remove unused import

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/a1e29737-dd6e-4287-96c1-509e0c96fb21

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: use stable ~/.specify/auth.json in rate-limit message, skip POSIX permission check on Windows

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/4636bcdb-87ae-45d6-9545-a40e4effd617

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: validate host patterns, cache auth config per-process

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/889b58a7-7f8c-47e2-8056-931ebcc671cc

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: clarify _is_valid_host_pattern docstring, clean up test sentinel type

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/889b58a7-7f8c-47e2-8056-931ebcc671cc

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: improve _is_valid_host_pattern docstring and test observability

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/889b58a7-7f8c-47e2-8056-931ebcc671cc

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

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 <175728472+Copilot@users.noreply.github.com>
2026-05-07 12:51:20 -05:00
natelastname
cd44dc2147 fix(goose): Declare args parameter in generated recipes (#2402) 2026-05-06 17:21:48 -05:00
qiyang.yuan
f5b675e9ee feat: Add lingma support (#2348)
* add lingma support

* fix

* fix context file

* Update CONTEXT_FILE path in test integration

* fix IntegrationOption.default

* fix IntegrationOption.defaultfix

* fix: address Copilot review feedback

- Add blank line after __future__ import (PEP 8)
- Remove trailing whitespace at end of lingma/__init__.py
- Bump integrations/catalog.json updated_at timestamp
- Add Lingma to supported agent list in README.md

* fix: address Copilot review feedback (round 4)

- Reword module docstring: Lingma is a brand-new skills-only integration
  with no prior command-mode history, so 'deprecated since v0.5.1'
  wording (copied from Trae) was misleading
- Remove Lingma from README CLI-tool check list: Lingma is IDE-based
  (requires_cli=False) and is explicitly skipped by specify init /
  specify check tool detection
2026-05-06 16:12:13 -05:00
Andrii Furmanets
2d5e63005d fix: default non-interactive init to copilot integration (#2414)
* fix: default non-interactive init integration

* chore: clarify non-interactive init default integration

* Address non-interactive init review feedback

* Fix interactive init test after fallback
2026-05-06 12:48:50 -05:00
Eric Rodriguez Suazo
793632089a fix(forge): use hyphen notation for command refs in Forge integration (#2462)
* fix(forge): use hyphen notation for command refs in Forge integration

- Add invoke_separator = "-" class attribute to ForgeIntegration so
  effective_invoke_separator() returns "-" for shared-template installs
- Add "invoke_separator": "-" to ForgeIntegration.registrar_config so
  agents.py CommandRegistrar can resolve refs with the correct separator
- Pass invoke_separator to process_template() in ForgeIntegration.setup()
  so all .forge/commands/*.md bodies use /speckit-foo notation
- Replace literal /speckit.specify with __SPECKIT_COMMAND_SPECIFY__ in
  extensions/git/commands/speckit.git.feature.md so every agent resolves
  the reference through its own separator
- Apply resolve_command_refs re.sub in agents.py register_commands() after
  argument-placeholder substitution so extension commands registered for
  Forge get /speckit-foo refs; all other agents continue to get /speckit.foo

Fixes ZSH compatibility: dot-notation command invocations (/speckit.specify)
are misinterpreted by ZSH as file-path operations; hyphen notation
(/speckit-specify) works correctly in all shells.

* fix(agents): propagate invoke_separator from integration class into AGENT_CONFIGS

Skills-based agents (claude, codex, kimi, …) inherit invoke_separator="-"
from SkillsIntegration but do not repeat it in their registrar_config dicts.
_build_agent_configs() was copying registrar_config verbatim, so
register_commands() fell back to "." when resolving __SPECKIT_COMMAND_*__
tokens for those agents — emitting /speckit.specify instead of the correct
/speckit-specify for extension commands like speckit.git.feature.

Fix: after copying registrar_config, inject invoke_separator from the
integration's class attribute when it is not already declared explicitly.
This makes the integration class the single source of truth for all agents,
without requiring each SkillsIntegration subclass to duplicate the field.

Also replace the inline re.sub in register_commands() with a call to
IntegrationBase.resolve_command_refs() (deferred import to avoid the
existing circular dependency) so token-resolution logic is not duplicated.

Adds two tests in test_agent_config_consistency.py:
- test_skills_agents_have_hyphen_invoke_separator_in_agent_configs: asserts
  every /SKILL.md agent has invoke_separator="-" in AGENT_CONFIGS.
- test_skills_agent_command_token_resolves_with_hyphen: end-to-end check via
  CommandRegistrar that the git extension's speckit.git.feature command is
  installed for Claude with /speckit-specify (not /speckit.specify).

Addresses review comment on PR #2462.
2026-05-06 12:19:10 -05:00
Copilot
b4060d5620 Load constitution context in /speckit.implement to enforce governance during implementation (#2460)
* Initial plan

* fix implement command to load constitution context

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/05663d9d-149b-4c13-a22d-2552b3fa619c

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

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 <175728472+Copilot@users.noreply.github.com>
2026-05-06 06:56:59 -05:00
Ayesha Aziz
30e6fa9e32 fix: validate URL scheme in build_github_request (#2449)
* fix: validate URL scheme in build_github_request

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test: add missing hostname validation test for build_github_request

* fix: update docstring and fix import grouping per Copilot feedback

* fix: sort imports and simplify url validation in build_github_request

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 13:28:29 -05:00
Pascal THUET
a7201c183e fix(workflows): require project for catalog list (#2436) 2026-05-04 12:00:28 -05:00