Commit Graph

241 Commits

Author SHA1 Message Date
Pascal THUET
490566847c feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver (#3186)
* feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver

The shell resolver honors SPECIFY_INIT_DIR (#2892), but the Python CLI did
not: it resolved the project as Path.cwd() + a .specify/ check and never read
the override. So setup-plan.sh respected it while `specify integration install`
ignored it, and you still had to cd into the member project.

Route project resolution through a shared _resolve_init_dir_override() that
applies the shell resolver's validation rules (relative to cwd, must exist and
contain .specify/, hard error, no fallback, same error strings). It's wired into
_require_specify_project() — the chokepoint for every project-scoped subcommand
(integration/extension/workflow/preset/...) — and the `workflow run <file>`
standalone path, which re-applies its symlinked-.specify guard on the override
branch too. init is unchanged: it creates .specify/, so the must-pre-exist rule
doesn't apply.

The resolver canonicalizes symlinks via Path.resolve() while the shell keeps the
logical path; they agree for non-symlinked paths (documented in the resolver).

Tests in tests/test_init_dir_cli.py mirror the strict cases from test_init_dir.py
through the CLI; conftest now strips SPECIFY_* for the whole suite so a stray
export can't perturb the now-env-reading resolver. Docs note the CLI applies the
same rules.

Discussion: github/spec-kit#2834

(Disclosure: I used an AI coding agent to audit the call sites and resolver,
draft the change, and run an adversarial code review; reviewed by me.)

* fix(cli): honor SPECIFY_INIT_DIR for bundle commands

Assisted-by: Codex (model: GPT-5, autonomous)

* fix(bundler): refuse symlinked .specify on the SPECIFY_INIT_DIR override path

find_project_root refuses a symlinked .specify (following it could read/write
outside the tree, and a test pins that), but the SPECIFY_INIT_DIR override added
for bundle commands returned early and skipped that guard:
_resolve_init_dir_override validates .specify with is_dir(), which follows
symlinks. So `specify bundle` accepted via the override a layout the cwd path
rejects. Re-check the override result with the same guard, plus a regression test.

(Disclosure: found via an AI code review and fixed with an AI coding agent;
reviewed by me.)

* fix(cli): keep SPECIFY_INIT_DIR strict for bundles

Treat an explicit symlinked SPECIFY_INIT_DIR project as a hard bundle error instead of returning no project, which could initialize the current directory. Align the docs with the actual unset resolver behavior.

Assisted-by: Codex (model: GPT-5, autonomous)

* docs(core): note symlinked .specify handling differs across CLI surfaces

A symlinked .specify is followed by integration/extension/workflow (matching the
shell resolver) but refused by bundle and workflow run <file> (write
confinement). Document the asymmetry so it reads as intentional.

(Disclosure: AI-assisted; reviewed by me.)

* docs(core): reframe symlinked .specify note around the override invariant

Per maintainer feedback on #3186: SPECIFY_INIT_DIR relocates where the project
is, not how a surface treats symlinks. Each surface keeps its cwd-path stance
(write surfaces refuse a symlinked .specify, read/config surfaces follow it),
so the split is one policy relocated, not an inconsistency.

* docs: address Copilot review on resolver docstrings

- _project.py: the error messages "mirror" the shell wording rather than
  "match" it (the CLI renders a Rich `Error:` line, the shell a plain `ERROR:`).
- find_project_root: document that honoring SPECIFY_INIT_DIR when start is None
  can raise typer.Exit / BundlerError, so the Path | None signature isn't
  surprising to direct callers.

* docs(bundler): note require_project_root inherits the override raise behavior

find_project_root can raise typer.Exit / BundlerError under the SPECIFY_INIT_DIR
override (start=None); require_project_root inherits that, so document it
alongside its own BundlerError-on-missing-project.

* docs: clarify symlinked project root behavior

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* Address SPECIFY_INIT_DIR review feedback

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* Route workflow JSON errors to stderr

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
2026-07-01 15:55:18 -05:00
darion-yaphet
810d6fcfe1 refactor: move workflow command handlers to workflows/_commands.py (PR-8/8) (#3159)
* refactor: move workflow command handlers to workflows/_commands.py (PR-8/8)

Final PR of the __init__.py split. Moves the workflow command group out of
__init__.py into the existing workflows/ package, completing the domain-dir
layout established in PR-5 (integrations), PR-6 (presets) and PR-7
(extensions).

- New workflows/_commands.py holds the four Typer apps (workflow / catalog /
  step / step-catalog), all 25 command handlers, the six workflow-only
  helpers (_parse_input_values, _workflow_run_payload, _emit_workflow_json,
  _stdout_to_stderr_when, _validate_step_id_or_exit,
  _resolve_steps_base_dir_or_exit), and a register(app) entry point.
- workflows is already a package, so no rename is needed; intra-package
  imports change from `.workflows.x` to `.x`. The only root-helper dep
  (_require_specify_project) is reached through a call-time shim so test
  monkeypatching of specify_cli._require_specify_project keeps working.
- __init__.py drops ~1445 lines (2066 -> 621); the workflow group is
  re-attached via register(app). Dead `contextlib` import removed.
- tests/test_workflows.py: import the now-relocated _stdout_to_stderr_when
  helper from its new home (workflows._commands) instead of the package root.

No behavior change. Full suite green (3847 passed), ruff clean.

* Prevent workflow state writes through symlinked storage

Workflow commands persist run state under .specify/workflows/runs, so the command-local project shim now rejects symlinked workflow storage before any workflow command proceeds. The standalone YAML path uses the same guard because it intentionally bypasses the normal project requirement while still creating workflow state under the current directory.

Constraint: Local YAML workflow runs do not require an existing .specify project directory but still create .specify/workflows/runs state

Rejected: Guard only .specify in the file-source path | .specify/workflows and runs can independently redirect writes

Confidence: high

Scope-risk: narrow

Directive: Keep workflow storage symlink checks centralized before constructing WorkflowEngine

Tested: .venv/bin/python -m pytest tests/test_workflow_run_without_project.py tests/test_workflows.py::TestWorkflowAddSymlinkGuard -v

Tested: .venv/bin/python -m py_compile src/specify_cli/workflows/_commands.py tests/test_workflow_run_without_project.py tests/test_workflows.py

Not-tested: Ruff lint; ruff is not installed in the repo virtualenv

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* fix(workflows): pass github_hosts allowlist to GHES release asset resolver

workflow add resolved GitHub release download URLs without forwarding the
github_provider_hosts() allowlist, so resolve_github_release_asset_api_url
never treated any host as GHES. This regressed GitHub Enterprise Server
release asset resolution and diverged from presets/extensions, which already
pass github_hosts. Forward github_provider_hosts() at both the direct-URL and
catalog install call sites. The allowlist remains the anti-SSRF gate.

* fix(workflows): reject symlinked/traversal <id> dir on workflow install

Local/URL and catalog installs wrote to .specify/workflows/<id>/workflow.yml
without guarding the <id> segment. A pre-planted symlink at <id> or
<id>/workflow.yml let mkdir+copy/download follow it and write outside the
project root; a non-directory <id> made mkdir raise unhandled.

Add _safe_workflow_id_dir() to reject path traversal, symlinked or
non-directory <id>, and a symlinked workflow.yml leaf before any write.
Fold the catalog branch's existing traversal check into the helper.

* fix(workflows): harden _safe_workflow_id_dir output and leaf checks

- Reorder symlink/non-directory check before resolve() so a symlinked
  <id> reports as symlinked instead of misleading "Invalid workflow ID"
- Reject a pre-existing <id>/workflow.yml that is not a file, avoiding an
  unhandled IsADirectoryError on later write/copy2
- Escape workflow_id in Rich output to prevent markup injection; escape
  the repr (not the raw id) so repr-added backslashes cannot re-expose
  brackets, matching extensions/_commands.py hardening
- Add tests for workflow.yml-as-directory and markup-escaped invalid id

* Avoid stale lint failures from config helper imports

Move PyYAML loading into the helpers that read and write agent-context configuration, and replace the broad Any annotation with object. The runtime behavior stays the same while the module no longer exposes top-level imports that can be flagged as unused when CI analyzes a narrower code shape.

* Prevent workflow commands from targeting reserved storage

Workflow install and removal paths are derived from workflow IDs before any catalog download, local copy, or directory deletion. Validate that IDs are single workflow-id path segments and reject names reserved for workflow runtime storage so commands cannot target .specify/workflows/runs or .specify/workflows/steps.
2026-06-30 11:03:54 -05:00
Dyan Galih
6fb7e77b3e fix: allow prerelease spec-kit versions in compatibility checks (#2695)
* docs: generate integrations reference from catalog

* refactor: integrate table rendering into specify integration search --markdown

- Remove standalone scripts/generate_integrations_reference.py
- Strip doc injection machinery from catalog_docs.py; keep only table rendering
- Wire render_integrations_table() into existing --markdown flag of integration search
- Remove old simple markdown table block from integration_search (was Name|ID|Version|Description|Author)
- Simplify tests: drop subprocess/doc-path tests, keep table rendering and metadata tests
- Clean up docs/reference/integrations.md: remove generated markers, update note

* fix: address Copilot review feedback on catalog_docs and integration_search

- Warn when --markdown is combined with filters (query/--tag/--author) which are
  silently ignored; catch ValueError/FileNotFoundError and surface clean error
  via console instead of raw traceback (r3244821516)
- Add coverage enforcement in list_integrations_for_docs(): raises ValueError
  with actionable message if any registry key is missing from INTEGRATION_DOC_URLS,
  preventing silently incomplete doc tables (r3244821589)
- Rename test to accurately reflect sources: label derives from registry config,
  URL comes from INTEGRATION_DOC_URLS doc map — not solely from registry (r3244821607)
- Simplify test dict construction to idiomatic dict comprehension (r3244821619)

* fix: add sync test, INTEGRATIONS_REFERENCE_PATH constant, and fix naming

* revert: restore docs/reference/integrations.md to upstream/main; remove sync test (GH Actions job will handle)

* fix: remove dead INTEGRATIONS_REFERENCE_PATH, drop URL-length padding, fix docstring, drop FileNotFoundError

* fix: send --markdown warnings/errors to stderr, rename test for clarity

* fix: detect stale doc-map keys, test _render_cell escaping, strengthen header assertion

* refactor: promote _render_cell to public render_cell function

* test: mock registry and doc maps to avoid brittle live registry coupling

* refactor: flatten patches, remove unused imports, fix trailing whitespace, optimize missing calculation

* refactor: make validation non-fatal, fix context manager syntax, add CLI tests

* fix: improve docstring clarity, test robustness, and exception handling

* fix: improve test assertions, disable warnings by default, enhance exception handling

* fix: make CLI tests deterministic and improve config access resilience

* fix: remove extra blank line, add stale keys validation, add regression test for docs sync

* Fix 5 remaining feedback items:
- Rename _get_mocked_cli_runner() to _get_catalog_docs_patches() for clarity
- Use ExitStack context manager for guaranteed patch cleanup
- Add explicit UTF-8 encoding to file reads
- Skip doc sync test gracefully when docs aren't present
- Remove exception chaining from typer.Exit to avoid noisy tracebacks

* address all outstanding copilot review feedback on PR 2563

* Address Copilot feedback: escape URLs in markdown links, deduplicate cell rendering, fix table parser for escaped pipes

* Address 3 new Copilot feedback: add URL escaping test, fix parse_first_markdown_table for escaped pipes, guard community tests with skip

* Address 3 new Copilot feedback: escape id field, remove unused alias, escape integration URLs

* Address 3 new Copilot feedback: fix comment name, include all integrations in list

* Fix architectural issue: escape raw fields before composing Markdown to prevent double-escaping

* Deduplicate _escape_url_for_markdown_link and add URL escaping test

* Address 4 new Copilot feedback: add trailing newline, fix test helper ExitStack, update warning message

* Address 4 new Copilot feedback: make escape function public, fix error message, validate test rows, prevent double newline

* Update error message in test_missing_catalog_file for clarity

* Remove obsolete integrations sync test

* keep integrations docs in sync

* fix: allow prerelease spec-kit versions in compatibility checks

Allow prerelease/dev builds to satisfy extension and preset compatibility
checks when their version number falls within the required specifier range.
Also harden the integrations docs rendering helpers and add regression
coverage for the markdown table parsing and version gating paths.

Tests: pytest -q; python3 -m compileall -q .; black/flake8 unavailable
Reference: branch 002-generate-integrations-docs; source patch /tmp/spec-kit-changes.patch

* fix: isolate prerelease compatibility gate changes

Keep the prerelease/version compatibility fix on its own branch and remove
the unrelated integrations docs updates that belong with PR 2563.

Tests: full suite passed on the prerelease branch before splitting; docs branch covered by targeted docs tests
Reference: upstream/main; source patch /tmp/spec-kit-changes.patch

* Address PR 2695 feedback: Centralize prerelease policy and add boundary test

* Address remaining Copilot PR feedback: revert docs and add preset prerelease tests

* Remove unreachable raise CompatibilityError

* Fix PEP8 E302 and E303 formatting issues
2026-06-30 09:41:57 -05:00
Manfred Riem
53d9543355 feat: make agent-context extension a full opt-in (#3097)
* docs: add Spec Kit spec for agent-context full opt-in

Use Spec Kit's own specify workflow to author the spec that makes the
agent-context extension a full opt-in, removing all agent-context
configuration/support from the Python codebase and removing the
deprecation message. Force-added despite specs/ being gitignored; the
generated artifact will be purged prior to merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add Spec Kit plan artifacts for agent-context full opt-in

Phase 0/1 of the SDD plan workflow: plan.md, research.md, data-model.md,
quickstart.md, and contracts/cli-behavior.md. Constitution Check is a
documented no-op (repo has no ratified constitution). Force-added despite
specs/ being gitignored; generated artifacts will be purged prior to merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: correct Constitution Check against ratified v1.0.0

Earlier draft wrongly treated the gate as a no-op; the fork's main is 16
commits behind upstream/main, which carries .specify/memory/constitution.md.
Re-evaluate the feature against Principles I-V (all PASS) and note that
Principle I mandates keeping context_file as a declared class attribute,
validating the R1 metadata decision.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: refresh plan artifacts against synced upstream/main

After syncing fork main to upstream and rebasing, re-scan the current
agent-context surface. Upstream generalized the single context_file into a
plural context_files concept with new resolver helpers
(_resolve_context_files, _resolve_context_file_values,
_format_context_file_values) and upsert/remove now loop over multiple
files. Update research.md, data-model.md, contracts, quickstart grep
guards, and the plan summary to cover the expanded removal scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add Spec Kit tasks for agent-context full opt-in

Phase 2 of SDD: dependency-ordered tasks.md (30 tasks) organized by the
three user stories, with mandatory test tasks (Constitution Principle II)
and a foundational phase decoupling __CONTEXT_FILE__ resolution from the
extension config. Includes the extension self-seeding task (T015) and a
static guard test (T002) enforcing zero agent-context references in the CLI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat!: remove agent-context lifecycle from the Specify CLI

Make the agent-context extension a full opt-in. The CLI no longer
installs the extension during init, writes agent-context-config.yml,
or creates/updates/removes the managed Spec Kit section in agent
context files. Context-section upsert/remove, marker resolution,
extension-enabled gating, the config helpers, and the obsolete inline
deprecation warning are all removed. Integration context_file stays as
inert metadata; __CONTEXT_FILE__ now resolves from registry metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(agent-context): self-seed context file from the active integration

When agent-context-config.yml has no context_file/context_files, the
bundled bash and PowerShell update scripts now resolve the context file
from the active integration in .specify/init-options.json via the
integration registry, so the extension no longer depends on the CLI
writing its config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test+docs: update suite and docs for agent-context opt-in

Update integration/extension tests to expect no agent-context install,
config, or context-section writes during init. Add a static guard test
(test_agent_context_cli_free.py) asserting the CLI source is free of
agent-context lifecycle symbols, plus backward-compatibility tests for
legacy projects. Refresh AGENTS.md, the extension README, and add a
CHANGELOG entry describing the opt-in behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(agent-context): warn on self-seed failure, correct docs, speed up guard test

Address PR review feedback:
- Self-seed scripts (bash + PowerShell) now emit an actionable warning when
  an active integration is configured but specify_cli cannot be imported by
  the chosen Python (e.g. pipx installs), or when the integration declares no
  context file, instead of silently falling through to 'nothing to do'.
- Correct the extension README disable note: command rendering never reads the
  extension config; __CONTEXT_FILE__ is always substituted from integration
  metadata, so a stale context_files value cannot affect rendering.
- Cache CLI source reads in the static guard test via a module-scoped fixture
  so the directory walk happens once instead of once per forbidden symbol.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(agent-context): ship self-owned per-agent context-file defaults

The extension now bundles agent-context-defaults.json (key→context_file
map) and self-seeds from it, dropping any dependency on the Specify CLI
registry. Both the bash and PowerShell update scripts read the bundled
JSON map keyed by the active integration from init-options.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat!: remove all agent-context state from the Specify CLI

Strip every context_file reference from the CLI: the field on all 35
integration classes, the IntegrationBase plumbing (process_template
param/step, _context_file_display, docstrings), the __CONTEXT_FILE__
resolution in agents.py, the legacy context_file/context_markers
popping in _helpers.py, and the context_file template in
integration_scaffold.py. Also drop the Agent context update step and
__CONTEXT_FILE__ placeholder from templates/commands/plan.md.

The agent-context extension now solely owns all context-file knowledge,
including the per-agent default mapping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: drop context_file coverage and guard against CLI reintroduction

Remove CONTEXT_FILE attrs and context_file assertions across the base
mixins, all 35 per-integration test files, shared integration tests, and
conftest stubs. Rewrite the base-mixin context tests to assert no managed
section is written and no __CONTEXT_FILE__ placeholder survives. Extend
the CLI-free static guard to forbid context_file, __CONTEXT_FILE__, and
_context_file_display in src/specify_cli, and have the extension tests
copy the bundled defaults JSON so self-seed runs without the CLI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: reflect full removal of agent-context state from the CLI

Update AGENTS.md (integration examples, required-fields table, context
behavior section, pitfalls), CHANGELOG, and the SDD spec artifacts
(FR-007, SC-002, data-model) to state that the CLI carries no
context_file and the extension fully owns the per-agent default mapping
via agent-context-defaults.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: align SDD artifacts with full context_file removal

Update research.md (R1, R2, R4, summary table), contracts/cli-behavior.md
(C3, C5), tasks.md (Phase 2, T026, notes), plan.md (Principle I, source
map), and checklists/requirements.md so the spec artifacts reflect the
implemented decision: the CLI carries no context_file attribute or
__CONTEXT_FILE__ resolution, and the per-agent defaults map lives in the
extension. Resolves PR review #4548130110.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: scrub stale context-file mentions from CLI docstrings

Update the multi_install_safe docstring (drop the removed "context file"
invariant), the RovoDev setup docstring (no longer upserts a context
section), the Copilot module docstring (drop the context-file line), and
tighten the _update_init_options_for_integration note. Pure docstring
changes — no behavioral impact. Resolves PR review #4548237085.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test+docs: harden agent-context test helper and fix stale docs

- base.py: document multi_install_safe as an optional subclass attribute
  in the IntegrationBase docstring.
- test_cli.py: clarify the init-options assertion is guarding against
  leftover legacy agent-context keys, not relocation.
- test_extension_agent_context.py: _install_agent_context_config now
  asserts the bundled agent-context-defaults.json exists and always
  copies it, so self-seeding tests fail loudly instead of silently
  skipping when the map is missing.
- test_integration_cursor_agent.py: drop Path/IntegrationManifest imports
  left unused after removing the context-section frontmatter tests.

Resolves PR review #4548293116.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove gitignored SDD artifacts from specs/

The specs/001-agent-context-full-optin/ artifacts were force-added for
dogfooding visibility, but specs/ is gitignored and these were always
intended to be purged before merge. Remove them so merging does not add
an intentionally-untracked directory to repo history.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: keep CHANGELOG.md identical to upstream

CHANGELOG.md is auto-generated at release time, so the branch should not
carry a manual entry. Restore it to match upstream/main exactly.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve Cursor .mdc frontmatter in agent-context updater scripts

The bundled agent-context updater scripts wrote the managed section as
plain text. For Cursor-style `.mdc` targets this dropped the required
`---\nalwaysApply: true\n---` frontmatter, reintroducing the rule-loading
bug originally fixed in #1699. Port the `_ensure_mdc_frontmatter` logic
into both the bash and PowerShell updaters: prepend frontmatter when
missing, repair `alwaysApply` when set to the wrong value, and leave
non-`.mdc` targets untouched. Add regression tests covering both shells.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: scope CLI-free guard to agent-context-specific symbols

Drop the bare "context_file" substring from FORBIDDEN_SYMBOLS so the
guard no longer fails on unrelated future CLI fields named context_file.
The list still covers agent-context-specific identifiers (__CONTEXT_FILE__,
_context_file_display, _resolve_context_files, _resolve_context_file_values).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: harden agent-context bash self-seed against malformed init JSON

Two robustness fixes in the embedded Python self-seed logic:
- Coerce the integration value from init-options.json to a string only when
  it is actually a string; otherwise treat it as unset so a corrupted
  dict/list value degrades to the existing nothing-to-do behavior instead of
  breaking the agents-map lookup.
- Normalize agent-context-defaults.json: only use 'agents' when both the JSON
  root and the 'agents' value are dicts, so a wrong-shaped (but valid) JSON
  falls back to the warning path instead of raising on .get.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: correct PowerShell hyphenated key lookup and regex replace count

- Self-seed now reads the defaults mapping via
  $defaults.agents.PSObject.Properties[$integrationKey].Value instead of
  member access ($defaults.agents.$integrationKey), which parsed hyphenated
  keys like 'cursor-agent'/'kiro-cli' as subtraction and failed to resolve.
- Replace the static [regex]::Replace(..., 1) call, whose trailing 1 was
  interpreted as RegexOptions.IgnoreCase rather than a replacement count, with
  an instance Regex whose Replace(input, replacement, 1) limits to the first
  match as intended.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: make bash .mdc frontmatter guard case-insensitive

The bash updater only injected Cursor .mdc frontmatter when ctx_path ended
in lowercase '.mdc', so a mixed/upper-case extension (e.g. specify-rules.MDC)
was skipped and Cursor would not auto-load the rule file. Compare against the
casefolded path. The PowerShell variant already uses -match, which is
case-insensitive by default, so no change is needed there.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: document separator-agnostic agent-context update invocation

The README hard-coded the dot-notation slash command
(/speckit.agent-context.update), which hyphen-separator agents like Forge and
Cline do not recognize. Document the canonical command ID plus both slash
invocations so users copy the form their agent accepts.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-29 15:27:26 -05:00
Si Zengyu
1add20341d fix(extensions,presets,workflows): resolve private GHES release assets via /api/v3 (#3157)
* feat(auth): add github_provider_hosts() to enumerate GHES hosts from auth.json

Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)

* fix(extensions): resolve GHES release assets via /api/v3

Generalizes resolve_github_release_asset_api_url to GitHub Enterprise
Server hosts (gated by auth.json github hosts), fixing private GHES
extension/preset downloads. github/spec-kit#3147

Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)

* fix(extensions,presets): pass auth.json github hosts into release resolver

Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)

* docs(auth): document GHES private catalog + release-asset auth

Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)

* fix(presets,workflows): pass auth.json github hosts into remaining release resolvers

Wires preset add --from and workflow add through github_provider_hosts()
so private GHES release assets resolve via /api/v3 there too. github/spec-kit#3147

Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)

* test(presets): use module-level io.BytesIO in GHES preset test

Addresses Copilot review on PR #3157: drop unnecessary __import__("io")
in test_preset_add_from_ghes_release_url_resolves_via_api_v3 since io is
already imported at module level.

* fix(github-http): pass through GHES asset API URLs by path shape

Addresses Copilot review on PR #3157. A direct GHES /api/v3 release asset
URL was only returned as already-resolved when its host was in the
allowlist; otherwise the resolver returned None and the caller downloaded
the same URL without 'Accept: application/octet-stream', fetching JSON
metadata instead of the binary.

Gate the passthrough on path shape alone, mirroring the github.com case.
This is safe: passthrough returns the input URL unchanged and the caller
fetches it either way, so no new request to an arbitrary host is induced;
the token stays independently gated by auth.json in open_url. The
allowlist remains the anti-SSRF gate on the tag-lookup resolving path.

Add test_passthrough_for_unlisted_ghes_api_asset_url.
2026-06-25 10:44:30 -05:00
darion-yaphet
826e193cee refactor: move extension command handlers to extensions/_commands.py (PR-7/8) (#3014)
* refactor: move extension command handlers to extensions/_commands.py (PR-7/8)

Convert the flat extensions.py module into an extensions/ package and
extract all extension_app and catalog_app command handlers plus their
private helpers (_resolve_installed_extension, _resolve_catalog_extension,
_print_extension_info) out of __init__.py into the new
extensions/_commands.py, mirroring the domain-dir layout used for
presets/_commands.py (PR-6) and integrations/_commands.py (PR-5).

- extensions.py -> extensions/__init__.py (pure rename, 99%); intra-module
  relative imports bumped from `.x` to `..x` since they reference root
  siblings.
- Root helpers (_require_specify_project, _locate_bundled_extension,
  load_init_options, _display_project_path) are reached through thin shims
  that re-fetch from the parent package at call time, so test
  monkeypatching of specify_cli.<helper> keeps working unchanged.
- __init__.py drops ~1444 lines (3511 -> 2067); CLI surface preserved via
  register(app).

No behavior change. Full suite failure set is identical before/after
(82 pre-existing env failures, 0 new).

* fix(extensions): preserve per-command path in update backup for skills agents

Skills agents (extension == "/SKILL.md") name every command file SKILL.md,
each in its own per-command subdir (e.g. speckit-plan/SKILL.md). The update
backup keyed the backup path on cmd_file.name alone, so all of an agent's
skill files collided onto a single backup path — each shutil.copy2 overwrote
the previous one, and rollback restored one skill's content over all the
others, corrupting or losing the rest.

Mirror the real on-disk layout by using cmd_file.relative_to(commands_dir),
keeping each backup path unique. This also makes backed_up_command_files
values unique so restore copies the correct content back to each command.

Add a regression test asserting two distinct skill files survive a
backup -> failed-update -> rollback cycle with their own content.

* style(extensions): use yaml.safe_dump when writing catalog config

The catalog add/remove handlers wrote the integration catalog config with
yaml.dump. Switch to yaml.safe_dump to align with the SafeDumper used by the
presets commands and to refuse emitting !!python/object tags if a non-basic
value ever reaches the config dict.

Output is unchanged for the current basic-type payload (str/int/bool/dict/
list) — this is a defensive/consistency change, not a behavioral fix.

* fix(extensions): correct _print_cli_warning import path in skill registration

register_enabled_extensions_for_agent imported _print_cli_warning from `.` (the extensions package), but the helper lives in the parent specify_cli package. The wrong level raised ImportError inside the error handlers, aborting extension/skill registration on the first failure instead of warning and continuing. Use `..` to match the other parent-package imports.

* fix(extensions): escape untrusted values in Rich markup output

User-provided arguments and extension/catalog metadata (names, descriptions, versions, IDs, paths) were interpolated into Rich markup strings without escaping. Values containing markup sequences (e.g. [red]...) would be parsed as markup, allowing output injection that could corrupt or mislead CLI messages.

Wrap all such interpolations with rich.markup.escape across the extension/catalog command handlers: list, search, info (_print_extension_info), add (including --dev paths), remove, enable, disable, set-priority, update, and the ambiguous-match resolvers (error strings and Table rows). Reuse the already-computed safe_extension where available.

Escaping is a no-op for benign strings, so normal output is unchanged.

* Prevent Rich markup injection in extension CLI output

User-controlled catalog URLs and extension IDs are rendered through Rich-enabled console paths, so every remaining output-only interpolation now escapes markup while leaving stored values and filesystem behavior unchanged. Regression tests cover catalog add, install hints, remove hints, and state command messages with bracketed markup-like values.

* Prevent markup injection from exception text

Rich markup remains enabled for styled CLI messages, so exception text and config path labels must be escaped before rendering. YAML parser errors, URL validation failures, download errors, and extension validation errors can include user-controlled catalog or manifest values.

Constraint: Preserve existing exception handling and user-facing error paths

Rejected: Disable Rich markup for these messages | existing output intentionally uses markup for labels and styling

Confidence: high

Scope-risk: narrow

Directive: Escape user-controlled exception text before interpolating into Rich-rendered strings

Tested: .venv/bin/python -m pytest tests/test_extensions.py -q

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

* Prevent path and manifest review regressions

Catalog path labels are rendered through Rich markup and downloaded update manifests are trusted long enough to validate extension IDs. Escape displayed project paths before rendering, and reject non-mapping extension.yml payloads before ID validation so bad archives fail with a clear rollback reason.

---------

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-06-22 13:40:57 -05:00
Austin Z.
bbdf1b8f40 fix(agent-context): support multiple context files safely (#2969)
* fix(agent-context): support multiple context files safely

* fix(agent-context): harden context file validation

* fix(agent-context): preserve disabled context target

* fix(agent-context): address review follow-ups

* fix(agent-context): dedupe PowerShell context files

* fix(agent-context): align context file dedupe

* fix(agent-context): align bash context file dedupe

* fix(agent-context): preserve disabled display target

* fix(agent-context): require yaml-capable updater python

* fix(agent-context): preserve context files config

* fix(agent-context): align context file fallbacks

* fix(agent-context): share context file resolution

---------

Co-authored-by: AustinZ21 <AustinZ21@users.noreply.github.com>
2026-06-22 12:10:55 -05:00
Huy Do
f5f76160a3 feat: surface gate detail in the workflow run/resume --json payload (#2965)
* feat: surface gate detail in the workflow run/resume --json payload

A paused run was indistinguishable from any other pause in the
machine-readable outcome, and the gate's prompt/options/choice never
left the human-facing stream. Record each step's type in the run
state's step results (one engine line) and, when the run sits at a
gate, add a gate block (step_id/message/options/choice) to the payload
so orchestrators can drive review gates without parsing stdout.

Reference implementation for the proposal in #2964.

Addresses #2964

* fix(workflow): only surface gate detail in --json when the run is paused

Address review (#2965): _gate_outcome() emitted a gate block whenever current_step_id pointed at a gate step. Since RunState.current_step_id is never cleared on completion, a completed/failed run whose last step was a gate leaked stale gate detail in run/resume/status --json. Guard on status == paused. Also assert CLI success in the _run_json test helper before JSON-parsing, and add direct coverage for the suppression guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(workflows): surface gate block on aborted runs; stabilize message

Address Copilot review:
- `_gate_outcome` now also surfaces the gate block when a run is `aborted`
  by a gate rejection (`on_reject: abort`), not only when `paused`. Abort
  is the only path that sets ABORTED and it leaves current_step_id on the
  gate, so an orchestrator can read the recorded `choice` for the stop.
- Coerce `message` to a string (it may be a non-string YAML literal that
  GateStep only coerces for interpolation) so the JSON schema stays stable.
- Tests: add a CLI-level aborted-path test, a message-coercion test, and
  extend the suppression test to allow `aborted`; share the run helper via
  `_invoke_json` to avoid duplicating the invoke boilerplate.

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

* test(workflows): assert clean exit in gate-abort JSON test

Address Copilot review: the gate-abort test parsed stdout without first
asserting the CLI exited cleanly, so an invoke failure would surface as an
opaque JSON decode error. Route it through `_run_json` (which asserts
exit_code == 0 before parsing) and drop the now-redundant `_invoke_json`
helper — a gate abort emits the payload and returns, so the run exits 0.

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

* fix: use result.output in run-helper assert; document step_data shape

Address Copilot review:
- `_run_json` asserted with `result.stdout` in the message, but under
  `--json` step output is redirected off stdout — the useful diagnostics
  live on `result.output`. Switch the assertion message to `result.output`
  (the JSON parse still reads stdout), matching the other CLI tests.
- `StepContext.steps` documented a 5-key entry shape; the engine now also
  persists `type` and `status`. Update the docstring to the canonical
  7-key shape so step authors/debuggers see the real record.

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

* test(workflows): align gate-abort JSON test with aborted→exit-1

After rebasing onto main, a gate abort now emits the --json payload and
then exits non-zero (`_run_outcome_exit_code` maps aborted → 1, from the
merged exit-code work). Give `_run_json` an `expected_exit` parameter
(default 0) so the abort case asserts exit 1 while the paused/completed
cases stay at 0 — keeping a single shared helper rather than duplicating
the invoke boilerplate.

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

* fix(workflows): backward-compat gate detection + normalize gate options

Address Copilot review:
- A run paused by an older version has no persisted step `type`, so
  `_gate_outcome` would never surface its gate block on resume. Add
  `_is_gate_step`: prefer the `type` field, but when it is absent fall back
  to the gate's unique output signature (`on_reject`, written only by
  GateStep). A record with a different known `type` is still not a gate.
- Normalize `options` to a list of strings (mirroring the `message`
  coercion) so an unvalidated workflow with non-string options can't
  destabilize the JSON schema.
- Tests: options coercion, type-less gate detection, and a type-less
  non-gate negative case.

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

* fix(workflows): normalize non-list gate options to a stable list[str]

Address Copilot review: the prior options normalization only mapped a
`list`, returning the raw value for any other shape (scalar/tuple), which
contradicted the "stable list[str]" intent. Extract `_normalize_gate_options`:
None stays None; list/tuple maps each element through str; any other scalar
becomes a single-element list (a bare string is one option, never iterated
character-by-character). The emitted schema is now always list[str] | None.
Extend the options test to cover list, tuple, bare string, numeric scalar,
and None.

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

* fix(workflows): normalize gate choice to str; portable plain-gate test

Address Copilot review:
- `_gate_outcome` normalized `message` and `options` but passed `choice`
  through as-is; an unvalidated gate can record a non-string `choice`,
  which contradicts the stable-schema rationale. Coerce `choice` to
  `str | None` (None still means "no decision yet"), consistent with the
  other two fields. Adds a focused choice-coercion test.
- The plain (no-gate) test workflow used `run: "true"`, which fails under
  cmd.exe on Windows (ShellStep uses shell=True). Use the cross-platform
  `run: "exit 0"` (matching the exit-code suite's workflows).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 07:05:54 -05:00
Manfred Riem
487af97864 feat: add specify bundle command (#3070)
* docs: dogfood Spec Kit — bundler SDD artifacts + constitution

Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:

- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
  dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(bundler): add `specify bundle` subcommand for role-based setups

Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).

Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.

Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(contributing): document running the full test suite via project .venv

Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(bundler): wire real in-process primitive install + local-artifact install

Closes the two follow-ups left after the initial bundler landing.

T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
  (install_from_directory / install_from_zip); bundled assets install fully
  offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
  command callables in-process (project root as cwd), avoiding any duplicated
  download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
  network-only kinds refuse with an actionable message rather than silently
  reaching out.

T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.

Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(bundler): append Phase 8 convergence tasks from converge assessment

Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.

Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Implement Phase 8 convergence tasks (T046–T052)

Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:

- T046: add docs/reference/bundles.md documenting the full `specify bundle`
  command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
  online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
  preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
  project via the existing `specify init` machinery, choosing the integration by
  precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
  refresh path in install_bundle, preserving primitive-level overrides.

Adds unit/contract/integration coverage (107 tests pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* converge: append Phase 9 (T053) — surface bundle trust indicator

Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Implement T053 — surface bundle trust indicator in discovery

`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.

Adds contract coverage; 108 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: dogfood Spec Kit — bundler SDD artifacts + constitution

Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:

- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
  dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(bundler): add `specify bundle` subcommand for role-based setups

Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).

Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.

Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(contributing): document running the full test suite via project .venv

Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(bundler): wire real in-process primitive install + local-artifact install

Closes the two follow-ups left after the initial bundler landing.

T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
  (install_from_directory / install_from_zip); bundled assets install fully
  offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
  command callables in-process (project root as cwd), avoiding any duplicated
  download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
  network-only kinds refuse with an actionable message rather than silently
  reaching out.

T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.

Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(bundler): append Phase 8 convergence tasks from converge assessment

Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.

Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Implement Phase 8 convergence tasks (T046–T052)

Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:

- T046: add docs/reference/bundles.md documenting the full `specify bundle`
  command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
  online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
  preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
  project via the existing `specify init` machinery, choosing the integration by
  precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
  refresh path in install_bundle, preserving primitive-level overrides.

Adds unit/contract/integration coverage (107 tests pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* converge: append Phase 9 (T053) — surface bundle trust indicator

Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Implement T053 — surface bundle trust indicator in discovery

`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.

Adds contract coverage; 108 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address PR review — annotations, Windows paths, HTTPS, errors, reproducible builds

Resolves automated review feedback on github/spec-kit#3070:

- validator: drop redundant string-quoting on ReferenceChecker's
  `str | None` return so the annotation evaluates as a real union under
  `from __future__ import annotations`.
- adapters: normalize Windows drive-letter paths (e.g. C:\...) to the
  local-file branch so offline file catalogs resolve on Windows.
- adapters: enforce HTTPS (HTTP only for localhost) and require a host on
  remote catalog URLs before any network call, mirroring
  specify_cli.catalogs URL validation (MITM/downgrade protection).
- adapters: pass `origin` to loads_json for local files and HTTP payloads
  so JSON parse errors name the real source instead of <string>.
- manifest: parse component `priority` defensively, raising an actionable
  BundlerError on non-integer values instead of a raw ValueError.
- packager: write zip members with a fixed timestamp + permissions so
  identical inputs yield byte-for-byte identical artifacts (genuinely
  reproducible builds), and strengthen the determinism test accordingly.

Adds regression tests for priority validation, plain-HTTP/host rejection,
and byte-level artifact reproducibility (111 bundler tests pass; ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address PR review round 2 — nested output dir + file:// URLs

- packager: when --output points inside the bundle directory, exclude the
  whole output subtree from collection so previously-built artifacts are
  never re-packaged (prevents broken reproducibility and unbounded growth).
- adapters: resolve file:// catalog URLs via url2pathname and preserve
  netloc, so Windows file URLs (file:///C:/...) and UNC shares
  (file://server/share) resolve correctly instead of dropping the host or
  producing /C:/x.

Adds regression tests for nested-output exclusion and file:// resolution
(113 bundler tests pass; ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address PR review round 3 — discovery UX + hardening

- bundle search/info: fall back to the built-in/user catalog stack instead of
  requiring a Spec Kit project, so discovery works in a fresh directory (and
  the README/quickstart examples now match actual behavior). install still
  auto-initializes a project as before.
- packager: traverse with os.walk(followlinks=False) and prune symlinked
  directories before descending, so a symlink-to-dir can no longer pull in
  out-of-tree files (which previously turned "skip symlinks" into a hard
  ensure_within() failure and did extra filesystem work).
- records: parse contributed-component priority defensively, raising an
  actionable BundlerError on a corrupt records file instead of leaking a raw
  ValueError/traceback.
- installer: give install_bundle's manifest parameter an explicit
  BundleManifest | None type for a clearer, safer service API.

Adds regression tests for project-less search/info, symlinked-dir pruning,
and corrupt-priority records (117 bundler tests pass; ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address PR review round 4 + markdownlint exclusions

Review fixes:
- bundle info: expand the manifest regardless of install policy so
  discovery-only bundles remain inspectable (only install is refused).
- _download_manifest: handle local .zip download_url by extracting bundle.yml
  (via _local_manifest_source), and add a real remote HTTPS fetch path using
  the shared authenticated, redirect-validated open_url client (HTTPS enforced
  on the initial URL and every redirect; offline still refuses).
- _run_init: thread the --offline flag through to the init callback so
  `bundle install/init --offline` never performs network init.
- conflict.ConflictReport: use field(default_factory=list) and drop the
  None + __post_init__ workaround.
- CatalogSource.from_dict: parse priority defensively, raising an actionable
  BundlerError naming the source + offending value instead of a raw ValueError.

markdownlint:
- Exclude .specify/, .github/, and specs/ (and their subdirectories) from
  markdownlint so the in-flight dogfooding scaffolding doesn't trip the linter.

Adds regression tests for discovery-only info, local-zip download_url, and
non-integer catalog priority (120 bundler tests pass; ruff clean; the PR's own
markdown lints clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address PR review round 5 + ignore generated files in whitespace check

Review fixes:
- packager: exclude any prior build artifact for this bundle (matching
  <id>-*.zip), not just the current output path, so older artifacts next to
  bundle.yml are never re-packaged.
- docs(bundles): correct the note — `search` and `info` work without a project
  (they fall back to the built-in/user catalog stack); only list/update/remove/
  catalog require an initialized project.

CI / generated files:
- .gitattributes: mark the generated dogfooding scaffolding (.specify/**, the
  speckit .github agent/prompt files, copilot-instructions.md, specs/**) with
  -whitespace so `git diff --check` (the Lint workflow's whitespace gate) stops
  flagging emitted trailing whitespace. These files are produced by
  `specify init` and are scrubbed before merge.

Adds a regression test for prior-artifact exclusion (121 bundler tests pass;
ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): collision-resistant catalog ids, canonical local paths, explicit uninstalled result

Addresses review round 6 (PR #3070):
- catalog_config._derive_id now combines host label with the URL path stem so
  multiple catalogs from the same host get distinct, stable default ids.
- add_source canonicalizes local file paths to absolute before persisting, so
  project config no longer depends on the caller's cwd.
- InstallResult gains a dedicated `uninstalled` list; remove_bundle no longer
  overloads `installed` for removals, and the CLI prints from `uninstalled`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): confine config writes, guard indeterminate integration, fix validate docs

Addresses review round 7 (PR #3070):
- save_records and catalog_config._write now pass within=project_root to
  dump_json/dump_yaml, refusing symlinked .specify paths that escape the
  project (defense-in-depth, matching the rest of the codebase).
- resolve_install_plan now fails when a bundle pins an integration but the
  project's active integration cannot be determined and no explicit
  --integration override was given, instead of silently adopting the bundle's
  required integration (FR-019 guard). CLI passes integration_explicit.
- docs/reference/bundles.md: corrected the validate semantics to describe the
  actual best-effort online behavior (unreachable catalogs warn, not fail).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): Windows path handling + review round 8 hardening

Fix Windows CI failures:
- is_safe_relpath now rejects POSIX-absolute (/abs) and Windows drive-absolute
  (C:\x, UNC) paths on every OS, instead of passing them through on Windows
  where os.path.isabs('/abs') is False and Path('/abs').parts yields '\\'.
- _download_manifest treats a Windows drive-letter download_url (C:\bundle.yml,
  which urlparse reads as scheme 'c') as a local file, fixing the empty
  component set in `bundle info` on Windows.

Address review round 8 (PR #3070):
- Bundled workflows now install under --offline (locate via
  _locate_bundled_workflow) instead of being refused unconditionally.
- bundle update preserves the original installed_at timestamp on refresh
  (import find_record; reuse the existing record's timestamp).
- _derive_id lowercases the host label so 'Example.com' and 'example.com'
  produce the same deterministic id.
- CatalogEntry.from_dict validates 'tags' is a list and 'verified' is a real
  boolean, raising BundlerError on invalid untrusted shapes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): normalize SemVer prerelease spellings before version parsing

Addresses review round 9 (PR #3070): parse_version and is_semver now apply the
same prerelease normalization (mirroring specify_cli._version._normalize_tag)
so SemVer spellings like 1.2.3-rc1 / 1.2.3-alpha1 validate and compare
consistently across is_semver, parse_version, and satisfies. Leading 'v' is
also stripped. Keeps the manifest validator and constraint checks in agreement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): no collateral removal + enforce manifest-pinned versions

Addresses review round 10 (PR #3070):
- install_bundle records only the components this bundle actually contributed:
  freshly-installed components, plus pre-existing ones already owned by this
  bundle (refresh) or a sibling bundle (shared/refcounted). A component that is
  installed on disk but tracked by no bundle was installed independently and is
  no longer attributed, so `bundle remove` won't uninstall it (FR-022).
- preset/extension/workflow install paths now verify the active catalog's
  advertised version matches the manifest-pinned component.version before
  downloading/installing, raising BundlerError on mismatch so bundles stay
  reproducible. When a catalog advertises no version the pin can't be enforced
  and installation proceeds.

Added regression tests: independent pre-existing component survives removal;
version-mismatch refusal (helper + workflow path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root (#2892)

* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root

Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.

- Single resolver in core; the git feature-branch script inherits it by
  sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
  succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
  propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
  SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
  feature-axis composition, the no-core guard, and a PowerShell mirror.

* fix: guard SPECIFY_INIT_DIR with stale core scripts

* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording

* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver

Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).

Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.

* test: normalize bash path expectations on Windows

* docs: clarify SPECIFY_INIT_DIR root helpers

* chore: sync dogfooded .specify core scripts with SPECIFY_INIT_DIR

Mirror the SPECIFY_INIT_DIR resolver (resolve_specify_init_dir in
common.sh) into the committed dogfooding .specify/scripts/bash copies so
the git extension's create-new-feature-branch.sh finds an up-to-date
common.sh instead of failing with "requires updated Spec Kit core
scripts". Fixes the test_init_dir.py CI failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): harden remote catalog fetch and config parsing

- adapters: route catalog HTTP fetches through the shared authenticated
  client (authentication.http.open_url) so auth.json tokens apply and the
  Authorization header is stripped on cross-host/downgrade redirects.
  Reject any redirect that leaves HTTPS via a redirect_validator and
  re-validate the final URL after redirects, closing the urlopen
  auto-redirect MITM/downgrade gap.
- catalog_config._read: raise an actionable BundlerError when the config
  top level is not a mapping, 'catalogs' is not a list, or an entry is
  not a mapping, instead of letting list(<str>) produce a downstream
  AttributeError.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): tighten record read confinement, policy gate, and precedence

Addresses review 4534504799:

- records.load_records: confine the read via ensure_within(project_root,
  ...) so a symlinked/traversal-escaping .specify cannot read arbitrary
  files outside the project (matches the write path's within= guard).
- catalog_config._slug: lowercase so derived catalog ids are
  deterministic across platforms and case-variant duplicates can't slip
  past the case-sensitive dup check.
- installer.install_bundle: reword the docstring's misleading "atomic on
  failure" claim to describe the real scoped guarantee (record written
  only on full success; rollback limited to newly-installed components).
- bundle update: enforce the source install_policy like install, refusing
  to update from a discovery-only source (FR-025).
- catalog source precedence: the CLI now passes ~/.specify as the user
  config dir so project > user > built-in precedence is actually
  reachable (previously the user scope was silently ignored).
- .gitattributes: scope the specs whitespace exemption to the generated
  dogfooding feature dir (specs/001-spec-kit-bundler/**) instead of all
  of specs/**.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): no collateral refresh, catalog id integrity, loud info

Addresses review 4534571362:

- installer: in refresh mode (bundle update) only re-apply already-
  installed components that this bundle (or a sibling) owns. Components
  installed independently and tracked by no bundle are now skipped, never
  refreshed, so update cannot make collateral changes (FR-022).
- catalog.load_catalog_payload: validate each entry's own id is present
  and matches its enclosing bundles key, rejecting catalogs that would
  otherwise list a spoofed or unresolvable id.
- bundle info: stop swallowing manifest download failures. If the
  manifest can't be resolved (e.g. --offline against an https download_url
  or a download failure), surface the error and exit non-zero instead of
  silently degrading to catalog `provides` counts, preserving the "info
  == what install applies" guarantee.

Added regressions: refresh leaves independently-installed components
untouched, catalog id key/field mismatch + missing id rejection, and
info exits non-zero when the manifest is unresolvable offline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): confine catalog-config and integration-marker reads

Addresses review 4534716790: two more state reads bypassed the
symlink/path-escape confinement that records and the write paths already
enforce.

- catalog_config._read: validate the config path with
  ensure_within(project_root, ...) before exists()/read, so a symlinked
  .specify resolving outside project_root is rejected instead of read.
- lib.project.active_integration: confine the .specify/integration.json
  read the same way; an out-of-tree escape is treated as "not
  determinable" (returns None) rather than followed.

Added regressions covering both via a symlinked .specify pointing
outside the project root.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): validate manifest tags, disambiguate derived ids by full host

Addresses review 4534768419:

- manifest.from_dict: reject a non-list `tags` (e.g. a bare string) instead
  of splitting it character-by-character, matching the catalog parser and
  the schema contract (tags = list of strings).
- catalog_config._derive_id: derive ids from the full host (TLD included)
  so example.com and example.net no longer collide on the same id. Updated
  the affected id assertions.
- CHANGELOG: call out the new `specify bundle` command group in the
  unreleased section (the PR's headline user-facing feature).
- .gitattributes: clarify the specs whitespace exemption — the dogfooding
  feature dir is scrubbed before merge (not retained), so it doesn't weaken
  checks for kept docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(gitattributes): retain whitespace exemption for constitution.md

The project constitution (.specify/memory/constitution.md) is the one
dogfooding artifact carried forward past the pre-merge scrub. Give it its
own standalone whitespace exemption so it survives removal of the broader
.specify/** generated-scaffolding exemption.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): accurate uninstall count, confine catalog read, safe bundle id

Addresses review 4534812056:

- installer.remove_bundle: only count a component as uninstalled when
  installer.remove() actually ran; components already absent on disk are
  reported as skipped, keeping the uninstalled count accurate.
- catalog.load_source_stack: confine the project-scoped .specify config read
  with ensure_within, so a symlinked .specify/ resolving outside the project
  root is refused (consistent with the bundler's other guarded reads).
- manifest: enforce a filesystem-safe slug for bundle.id in structural
  validation; packager.build_bundle adds an ensure_within defense-in-depth
  check so a crafted id can never push the artifact outside the output dir.

Also reverts the CHANGELOG entry (the changelog is updated separately).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): validate requires/provides shapes in manifest and catalog

Addresses review 4534855443:

- manifest: validate requires.tools and requires.mcp as list-of-strings via
  a shared _parse_str_list helper (also reused for tags), so a bare string
  like `tools: docker` is rejected with an actionable BundlerError instead of
  being split character-by-character.
- catalog.CatalogEntry.from_dict: validate that `requires` and `provides` are
  mappings before accessing them, so an untrusted catalog payload with
  `requires: "..."` raises a named BundlerError rather than escaping as a raw
  AttributeError traceback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): require README.md when building a bundle artifact

Addresses review 4534938014: build_bundle now fails early with an
actionable error when README.md is missing, matching the documented
artifact contract (manifest + README) instead of silently producing a
bundle with no human-facing description.

Also reverts CHANGELOG.md to the upstream/main copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): validate record shapes; drop stale install --refresh claim

Addresses review 4534969692:

- records.InstalledBundleRecord.from_dict: hard-error when
  contributed_components is not a list, instead of iterating a corrupt
  bare string character-by-character.
- records.load_records: validate the top-level 'bundles' field is a list and
  fail with a clear BundlerError when a corrupt file makes it a mapping/string.
- PR description: remove the inaccurate "supports --refresh" note from
  `bundle install` (refresh is the `bundle update` path); docs already omit it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): refuse symlinked .specify, reject bad url schemes, IPv6 ids

Addresses review 4534997724:

- lib.project.find_project_root: a symlinked .specify is no longer accepted
  as a project root (is_dir() follows symlinks), matching the confinement the
  rest of the CLI applies and avoiding confusing downstream failures.
- catalog_config.add_source: reject unsupported url schemes (ssh://, ftp://,
  ...) up front instead of silently treating them as local paths; local paths
  containing ':' but not '://' are still allowed.
- catalog_config._derive_id: derive the host via urlparse().hostname so IPv6
  literals, credentials, and ports no longer corrupt the derived id.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): strict semver, narrow artifact skip, preserve priority 0

Addresses review 4535084048:

- versioning.is_semver: enforce a full MAJOR.MINOR.PATCH SemVer (with optional
  pre-release/build) via a dedicated regex, instead of accepting any
  packaging.version.Version-parseable string (e.g. "1", "1.0"). This makes
  BundleManifest.structural_errors() reject non-semver versions.
- packager: narrow the prior-artifact skip pattern to semver-named zips
  (<id>-<x.y.z>.zip) so legitimate assets like <id>-assets.zip are still
  packaged.
- primitives (preset + extension install): use an explicit `is None` check so
  an intentional priority of 0 is preserved instead of being replaced by the
  default.

Adds regressions: non-semver rejection ("1"/"1.0"/"1.2.3.4"), asset-not-
excluded vs semver-artifact-excluded, and priority-0 pass-through.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): artifact regex for prerelease+build; clarify integration/priority docs

Addresses review 4535132279:

- packager: the prior-artifact skip regex now matches semver names carrying
  both a prerelease and build-metadata segment (e.g. 1.0.0-rc1+build5), so such
  an existing artifact is excluded rather than re-packaged — keeping builds
  bounded/deterministic, consistent with is_semver().
- docs/reference/bundles.md: correct the install integration wording.
  --integration selects the integration when initializing a new project and
  confirms the target when a pinned bundle's active integration can't be
  determined; it does NOT override a bundle that targets a specific integration
  (a mismatch aborts with no changes).
- examples/security-researcher README: reword the preset priority note in terms
  of the numeric comparison (ascending priority order) to avoid inverting the
  meaning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): --integration can't bypass clash guard; honest rollback docs

Addresses review 4535159341:

- bundle install: for an already-initialized project, the project's recorded
  active integration is now authoritative. --integration no longer overrides it
  (which let a copilot project install a claude-pinned bundle via
  `--integration claude`, bypassing the FR-019 clash guard). The override still
  selects the integration at init time and confirms the target only when the
  active integration cannot be determined.
- docs/reference/bundles.md: reword the install guarantee to match the
  implementation — no provenance record is written unless the install fully
  succeeds, and rollback of this run's components is best-effort (removal errors
  are swallowed, so partial on-disk state may remain). Dropped the inaccurate
  "atomic / rolls back everything" claim.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): validate component kind/id when loading records

Addresses review 4535194606: _component_from_dict now rejects a contributed
component whose 'kind' is not a supported component kind or whose 'id' is
empty, raising a BundlerError that explicitly flags the records file as
corrupt. Previously such a record loaded successfully and only failed later
(e.g. in primitive_manager() during bundle remove/update) with a less
actionable error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): address review 4535234003 (7 findings)

- versioning: tolerate an uppercase `V` prefix in `_normalize_semver` and
  `is_semver`, mirroring specify_cli._version tag normalization (V -> v) so
  `V1.2.3` parses and validates consistently.
- validator: import BundlerError and narrow the speckit_version constraint
  except clause to `BundlerError` only, so programming errors are no longer
  masked behind an "invalid constraint" message.
- bundle update: accept `--integration` and thread it through
  resolve_install_plan the same way `bundle install` does (override used only
  when the active integration can't be auto-detected), so integration-pinned
  bundles can be updated where `.specify/integration.json` is missing/unreadable.
- bundle validate: fold reference warnings into `report.warnings` so the
  ValidationReport is the single warning channel at the CLI layer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(bundler): make update --integration help assertion ANSI-safe

Rich can split the "--integration" option label with ANSI escape codes
between the two leading dashes, so the literal substring check failed under
CI's terminal settings. Match the un-split option word instead, mirroring how
test_bundle_help_lists_all_commands checks bare command names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): preserve exec bits in artifacts; document install-time pins

Addresses review 4535280786:

- packager.build_bundle: no longer forces every ZIP member to 0644, which
  stripped the executable bit from bundled scripts (e.g. extension hook
  scripts) and could break them after extraction. Permissions are now
  normalized reproducibly to 0755 when the source file has any execute bit
  set, otherwise 0644 — identical inputs still yield byte-for-byte identical
  artifacts.
- installer.install_bundle + docs/reference/bundles.md: document that version
  pins are enforced install-time only. Because primitive is_installed checks
  are id-based (not version-aware), an already-present component is skipped
  during install without comparing its on-disk version to the manifest pin;
  pins are guaranteed applied only on a real install or `bundle update` refresh.

Added a regression asserting executable sources map to 0755 and plain files to
0644 in the built artifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(bundler): skip exec-bit packager test on Windows

Windows filesystems do not carry Unix execute bits, so chmod(0o755) is a no-op
and the source file reports no execute bit — the packager then correctly stores
the member as 0644. The assertion that an executable source maps to 0755 is only
meaningful on POSIX, so skip it on nt rather than asserting platform-specific
behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): normalize prerelease spellings inside version constraints

Addresses review 4535327154: parse_version() normalized SemVer prerelease
spellings (e.g. 1.2.3-rc1 -> 1.2.3rc1) but parse_constraint() passed the
constraint to packaging.SpecifierSet unmodified, so ">=1.2.3-rc1" raised
InvalidSpecifier even though the same spelling is accepted for installed
versions. parse_constraint() now normalizes the version portion of each
comma-separated clause via the shared _normalize_semver helper, so prerelease
handling is consistent across versions and constraints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(bundler): validate schema versions and required record identity fields

Addresses review 4535351596:

- records.load_records: validate the on-disk 'schema_version' (required;
  forward-compatible across same-major minor bumps) and fail fast with an
  actionable error on a missing/unknown version, rather than silently parsing a
  possibly-incompatible format and risking incorrect bundle attribution/removal.
- records.InstalledBundleRecord.from_dict: treat missing 'bundle_id' or
  'version' as corruption and raise BundlerError, instead of coercing them to
  empty strings that let later list/remove/update operations behave
  unpredictably.
- catalog_config._read: validate 'schema_version' when present (same-major
  compatibility) and fail fast on an unsupported version so an incompatible
  future config shape can't be mis-parsed into a wrong effective catalog stack.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(bundler): scrub generated dogfooding scaffold before merge

The bundler feature was developed by dogfooding Spec Kit on itself. Now that
the work is complete, remove all generated scaffolding so it does not land in
the repository on merge:

- specs/001-spec-kit-bundler/** (spec, plan, research, data-model, contracts,
  quickstart, tasks, checklists)
- .specify/** (extensions, integrations, scripts, templates, workflows,
  feature/init/integration metadata)
- .github/agents/speckit.*.agent.md, .github/prompts/speckit.*.prompt.md, and
  .github/copilot-instructions.md (Copilot integration scaffold)

Retained: .specify/memory/constitution.md — the single dogfooding artifact
carried forward — with its whitespace exemption in .gitattributes.

.gitattributes and .markdownlint-cli2.jsonc are reverted to the upstream
baseline (plus the constitution whitespace exemption), dropping the now-moot
exemptions for the removed scaffold.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Pascal THUET <pascal.thuet@arte.tv>
2026-06-19 17:07:20 -05:00
Ben Buttigieg
0c29d890ab feat: add /speckit.converge command (#3001)
* Add /speckit.converge SDD artifacts and project scaffolding

Dogfood the converge feature through Spec Kit's own workflow:

- spec.md, plan.md, tasks.md, research, data-model, contracts, quickstart
- requirements checklist for the feature
- ratified constitution v1.0.0 (.specify/memory)
- Specify project scaffolding (.specify/, .github agent + prompt files)

Defines a built-in /speckit.converge command that assesses spec/plan/tasks
against the codebase and appends remaining work as new tasks (no git, no
change tracking, append-only). Implementation not yet started.

Excludes unrelated working-tree changes to agents.py, extensions.py,
test_extensions.py, catalog.community.json, and README.md.

* Implement /speckit.converge command

Add the built-in converge command that assesses the codebase against a
feature's spec.md, plan.md, and tasks.md and appends remaining unbuilt work
as new traceable tasks to tasks.md (append-only; no git, no change tracking).

- templates/commands/converge.md: full command body (load artifacts, assess
  code, classify findings missing/partial/contradicts/unrequested, append
  '## Phase N — Convergence' tasks with source-ref + gap-type, read-only
  guardrails, converged branch, handoff, before/after_converge hooks)
- Register converge as a core command across all enumeration sites
  (SKILL_DESCRIPTIONS, _FALLBACK_CORE_COMMAND_NAMES, ARGUMENT_HINTS, and the
  integration test command lists incl. copilot/generic file inventories)
- init.py Next Steps panel + README Core Commands table
- tasks.md: T001-T024 complete (T025 manual quickstart pending)

Full suite green: 2343 passed.

* Record quickstart validation results for /speckit.converge (T025)

All six quickstart scenarios validated (GitHub Copilot agent, macOS/zsh):
S1 gap->appended traceable task, S2 implement+re-converge, S3 converged leaves
tasks.md unchanged, S4 read-only boundaries, S5 missing-prereq stop, S6 cross-
integration install (copilot + windsurf). Automated suite: 2343 passed.

* Record 2026-06-16 re-verification results for /speckit.converge (T025)

* 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>

* Fix integration upgrade deleting settings.json and dropping script +x

Two upgrade-path bugs surfaced during converge E2E validation:

- copilot upgrade stale-deleted .vscode/settings.json because setup() only tracks the file when it creates it; on upgrade the pre-existing file is merged and left untracked, so Phase 2 stale cleanup removed it. Add an integration-level stale_cleanup_exclusions() hook (CopilotIntegration returns {.vscode/settings.json}) and subtract it from stale_keys.

- shared .specify/scripts/*.sh lost their execute bit because the managed refresh rewrites them with the bundled source mode (often 0o644) and nothing restored perms. Call ensure_executable_scripts() after the managed-refresh block (POSIX only).

Add regression tests in TestIntegrationUpgrade covering both fixes (validated to fail without the fixes).

* fix: resolve markdownlint errors in PR files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: clean up runtime state files from PR

Remove .specify state files that are per-project runtime artifacts:
- feature.json, init-options.json, integration.json
- manifest files, extension registry, bug artifacts

These are generated by 'specify init' and should not be committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: fold converge artifacts from #3003 and #3005

- Add speckit.converge Copilot agent and prompt files (#3003)
- Add regression test for Claude argument hints (#3005)
- Remove invalid converge entry from Claude argument hints
- Fix documentation removing branch-prefix fallback claims

Supersedes: #3003, #3005

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove non-converge specify scaffolding from PR

Remove .specify/ artifacts, non-converge .github/agents and prompts,
and copilot-instructions.md that were generated by 'specify init'
and are not part of the converge command feature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove SDD spec artifacts from PR

Remove specs/001-converge-command/ — the spec/plan/tasks/research SDD
artifacts produced while building this feature. spec-kit does not track
a specs/ directory on main (those are outputs of running the workflow on
the repo, not part of the shipped tool).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove generated Copilot converge command files

Remove .github/agents/speckit.converge.agent.md and
.github/prompts/speckit.converge.prompt.md — these are generated by
'specify init --integration copilot' from templates/commands/converge.md
(all __SPECKIT_COMMAND_*__/{SCRIPT} tokens are resolved). main tracks no
.github/agents or .github/prompts files; the template is the source of truth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: split out unrelated integration-upgrade fix

Move the stale_cleanup_exclusions / executable-bit upgrade fix
(base.py, copilot, _migrate_commands.py, test_integration_subcommand.py)
out of this PR into its own change. This PR is now scoped purely to the
/speckit.converge command.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add converge to core command template ordering

converge is a core command in SKILL_DESCRIPTIONS but was missing from
_CORE_COMMAND_TEMPLATE_ORDER, so it sorted with the fallback rank. Add it
after 'implement' to keep core-command ordering consistent across
integrations.

Addresses review feedback on #3001.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: make converge findings example neutral

Replace the self-referential sample evidence text in the Convergence
Findings table with a neutral placeholder so agents are less likely to copy
nonsensical template-specific findings into real output.

Co-authored-by: Copilot <223556219+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>

* docs: clarify converge scope and hook outcome wording

- Remove FR-specific parenthetical from code-scope rule so it doesn't imply
  a hard FR-001 reference exists in every feature
- Replace unsupported 'pass outcome to hook context' instruction with explicit
  in-session outcome reporting before hook listing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: align converge task example with tasks format

Use  (no colon) in the convergence task example so it
matches tasks-template formatting and downstream expectations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarification of usage

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>

* docs: align converge phase/task-id format with tasks template

- Use  (colon) for consistency with tasks template
- Clarify appended task IDs must be zero-padded ( style)
- Update checklist example to a concrete zero-padded ID ()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: standardize converge phase heading format

Use  consistently in converge.md (including the
append-only contract section) to match Step 7 and tasks template style.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 14:47:00 -05:00
Huy Do
1ee2b626a8 fix: non-zero exit code when a workflow run ends failed or aborted (#2959)
* fix: non-zero exit code when a workflow run ends failed or aborted

workflow run and workflow resume printed Status: failed (or emitted the
--json payload) and exited 0, so scripts and CI could not rely on the
process exit code. Map terminal outcomes: failed|aborted -> 1,
completed|paused -> 0, on both the text and --json paths.

The previous exit-0-on-failed behavior was pinned by
test_workflow_run_failing_yaml_without_project; the pin is updated to
the new contract.

Fixes #2958

* test: portable exit-code step commands + cover resume failed->exit-1

Address review (#2959): replace non-portable run: 'true'/'false' with 'exit 0'/'exit 1' (Windows cmd.exe has no true/false builtins under shell=True), and add an end-to-end 'workflow resume' test asserting a resumed failed run exits non-zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-17 08:02:00 -05:00
Copilot
c52ccd7dc7 Add workflow step catalog — community-installable step types (#2394)
* Initial plan

* Add workflow step catalog: StepRegistry, StepCatalog, CLI commands, and tests

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/2885e646-477d-4df8-b9a3-06d8cb29e748

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

* Potential fix for pull request finding 'An assert statement has a side-effect'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Address PR review: path traversal, cache robustness, collision check, failed-to-load display

- Add resolve()+relative_to() path traversal guards in workflow_step_add and
  workflow_step_remove to prevent directory escape via step_id
- Harden _is_url_cache_valid in both StepCatalog and WorkflowCatalog to
  coerce fetched_at to float and catch TypeError/ValueError
- Check STEP_REGISTRY and StepRegistry before installing to prevent
  collisions with built-in step types or already-installed steps
- Show 'Custom (installed, failed to load)' section in workflow step list
  for steps in the registry that failed to load into STEP_REGISTRY

* Fix StepRegistry shape validation and StepCatalog empty-YAML handling

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/0dca6393-f5a9-40de-bb5c-77ba6af033d2

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

* Polish: rename _default to default_registry, strengthen unreadable-file test

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/0dca6393-f5a9-40de-bb5c-77ba6af033d2

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

* Address PR review: atomic install, hostname validation, cache resilience, no dynamic imports in list/info

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/3e18fef0-e2e6-4b3e-9e8d-9adb1e5e464e

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

* Fix shutil.move with existing step_dir: remove before move to avoid subdirectory nesting

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/3e18fef0-e2e6-4b3e-9e8d-9adb1e5e464e

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

* Call load_custom_steps at execution time; enforce hostname in _safe_fetch and _validate_url

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/73865880-fb25-4061-a43e-4e4b4d1c4de6

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

* Wrap YAML parsing in try/except; atomic step install via os.rename() under same fs

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/ff915bc5-ec7e-4e6a-b505-35f5795250df

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

* Validate YAML root is a dict in _load_catalog_config and workflow_step_add; fix WorkflowCatalog hostname validation

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

* Fix load_custom_steps() package imports and add reserved step ID validation

* Move _re/_sys imports out of loop and _RESERVED_STEP_IDS to module level

* Address review: collision-resistant module names, extra_files support, remove orphan dir

* Harden extra_files: warn on non-dict, resolve symlinks in path traversal check

* Switch _safe_fetch and StepCatalog._fetch_single_catalog to use open_url for auth consistency

* Harden step_id validation against path-segment tricks; raise on StepRegistry.save() OSError

* Clean up sys.modules on broken step packages; handle StepValidationError in step add/remove

* Address review thread: int-coerce priorities, sys.modules cleanup, _require_specify_project, registry-first remove

* fix: normalize workflow step catalog metadata fallbacks

* fix: address latest workflow step and catalog review findings

* Handle non-string extra_files keys in workflow step add

* Harden StepRegistry symlink reads and extra_files path/URL validation

* Harden custom step loader and step remove against symlinks and OSError

* Fix StepCatalog.search() to coerce non-string fields before joining

* Fix WorkflowCatalog YAML parsing error handling and isinstance checks

* Harden step registry save and custom step/catalog ID handling

* Harden cache validation and staging OSError handling

* Address review: reorder symlink guard and split mixed test

- Move symlink-parent check before is_dir() in load_custom_steps() so
  we never stat an external target through a symlink
- Split test_get_merged_steps_normalizes_list_ids_to_strings into two
  focused tests: one for list-id normalization, one for get_step_info
  return values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: symlink-before-stat in loader, restore registry on rmtree failure

- load_custom_steps(): check is_symlink() before is_dir() on step
  directories so symlinked entries are skipped without statting external
  targets
- workflow_step_remove: restore the registry entry when shutil.rmtree()
  fails so filesystem and registry state stay consistent and a future
  'step add' isn't blocked

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden step_id validation and file-write error handling

- _validate_step_id_or_exit: reject whitespace-only/padded IDs,
  Windows-invalid characters (<>:"|?*), control characters, trailing
  dots/spaces, and Windows reserved device names (con, nul, etc.)
- Wrap step.yml/__init__.py staging writes in OSError handler
- Wrap extra_files disk writes (mkdir + write_bytes) in OSError handler
  that names the failing relative path
- Registry rollback on rmtree failure: restore verbatim metadata and
  emit a warning if the restore itself fails

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: cache symlink guard, verbatim registry rollback, Windows test fix

- StepCatalog: add _is_cache_path_safe() guard that checks for symlinks
  in .specify/workflows/steps/.cache path; skip cache reads and writes
  when any component is symlinked to prevent writes outside project root
- Registry rollback: write metadata directly to registry.data['steps']
  and call save() instead of using add() which overwrites timestamps
- temp_dir fixture: use ignore_errors=True on Windows to avoid flaky
  teardown from locked file handles (WinError 32)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify exec_module call by removing redundant nested try/except

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix empty YAML tolerance in WorkflowCatalog.add_catalog, scope ignore_errors to Windows

- WorkflowCatalog.add_catalog(): treat None from yaml.safe_load() (empty
  file) as an empty mapping instead of raising 'corrupted'
- temp_dir fixture: limit ignore_errors to sys.platform == 'win32' so
  real cleanup issues surface on non-Windows platforms

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Chain exceptions in _load_catalog_config for both catalog classes

Add 'from exc' to preserve root cause in tracebacks while keeping
clean user-facing messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Make default catalog tests hermetic by isolating HOME

Monkeypatch Path.home() to project_dir and clear catalog env vars so
tests don't break on machines with a real ~/.specify/step-catalogs.yml
or ~/.specify/workflow-catalogs.yml.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix falsy ID handling in _get_merged_steps for list-based catalogs

Check for None explicitly instead of using 'or' which drops valid
falsy IDs like 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Compare reserved step IDs case-insensitively for filesystem safety

On case-insensitive filesystems (Windows, common macOS), variants like
STEP-REGISTRY.JSON would collide with the actual registry file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add explanatory comments to intentional empty except blocks

Document why cache-read failures are silently ignored in both
WorkflowCatalog and StepCatalog _fetch_single_catalog methods.

Co-authored-by: Copilot <223556219+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 <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 18:03:45 -05:00
darion-yaphet
f20e8ee6f7 refactor: move preset command handlers to presets/_commands.py (PR-6/8) (#2826)
* refactor(presets): convert presets.py module to presets/ package

Pure structural move to mirror integrations/. presets.py becomes
presets/__init__.py with relative imports rebased one level deeper.
No behavior change; public import surface (from .presets import ...)
preserved. Prepares for co-locating preset command handlers in PR-6/8.

* refactor: move preset command handlers to presets/_commands.py (PR-6/8)

Cut the preset_app / preset_catalog_app Typer groups and all 12 command
handlers out of __init__.py into presets/_commands.py, exposing register(app)
— mirrors the integration co-location from PR-5. __init__.py now registers
via _register_preset_cmds(app), dropping ~620 lines (3282 -> 2663).

Handlers lazy-import root helpers (_require_specify_project, get_speckit_version,
_locate_bundled_preset, _display_project_path) via 'from .. import' so test
monkeypatching of specify_cli.<helper> keeps working. _locate_bundled_preset
kept as an explicit re-export in __init__.py for that resolution path.

CLI surface and public imports unchanged. Full suite: 3162 passed, 40 skipped.
2026-06-16 14:52:12 -05:00
darion-yaphet
36ad3cde1b fix(presets): harden preset URL installs against unsafe redirects (#2911)
* Harden preset URL installs against unsafe redirects

Preset URL installs already rejected non-HTTPS source URLs, but the authenticated opener follows redirects. Validate the final response URL before writing the ZIP, preserve GitHub release asset URL resolution after the preset command module split, stream the response to disk, and keep catalog config serialization on safe YAML output.

Constraint: open_url follows redirects, so source URL validation alone does not constrain the downloaded target

Rejected: Keep response.read() for simplicity | large preset downloads should not be buffered entirely in memory

Confidence: high

Scope-risk: narrow

Directive: Keep preset URL policy aligned with workflow installer redirect validation

Tested: uvx ruff check src/specify_cli/__init__.py src/specify_cli/presets/__init__.py src/specify_cli/presets/_commands.py tests/test_presets.py

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

Not-tested: Real network redirect integration against a live HTTP server

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

* Reject malformed preset download URLs

Preset downloads should fail early when a URL lacks a hostname, even if the scheme is HTTPS. The redirect error now describes any disallowed target instead of implying that only non-HTTPS redirects are blocked.

* Prevent credentialed preset redirects from downgrading transport

Preset URL downloads already checked the final URL after urllib followed redirects, but that was too late for authenticated requests because same-host redirects could preserve Authorization during the redirect itself. The authenticated HTTP helper now supports an opt-in redirect validator, and preset downloads use it to reject disallowed redirect targets before following them. The redirect auth handlers also stop preserving credentials across HTTPS to non-HTTPS downgrades as defense in depth.

* test(presets): 修复 URL 解析测试 mock 缺少 redirect_validator 参数

重定向安全加固为 open_url 新增 redirect_validator 参数,
两处 fake_open_url mock 签名未同步导致 TypeError。
补齐参数后全部 3717 个测试通过。

---------

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-06-11 07:21:50 -05:00
Manfred Riem
40e48ed22c feat: add category and effect as first-class fields in extension schema (#2899)
* feat: add category and effect as first-class fields in extension schema

Add `category` and `effect` as optional fields in the extension schema
(`extension.yml`) and community catalog (`catalog.community.json`).

Schema changes:
- Valid categories: docs, code, process, integration, visibility
- Valid effects: read-only, read-write
- Both fields are optional (backward-compatible with existing extensions)
- Validation raises ValidationError for invalid values when present

Propagation:
- Added `category` and `effect` to all 108 entries in catalog.community.json
  (populated from the existing docs/community/extensions.md table)
- Updated extension template with commented category/effect fields
- Updated add-community-extension skill with new JSON template fields
- Updated `specify extension info` CLI output to display category/effect
- Added properties to ExtensionManifest class

Tests:
- test_valid_category: all 5 category values pass
- test_valid_effect: both effect values pass
- test_invalid_category: invalid value raises ValidationError
- test_invalid_effect: invalid value raises ValidationError
- test_category_and_effect_optional: omitting fields still works

Closes #2874

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: make category free-form, keep effect validated

Category is a free-form string (only validated as non-empty when present),
while effect remains restricted to 'read-only' or 'read-write'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR review feedback

- Add type guard before 'in' check for effect to prevent TypeError on
  unhashable YAML values (list/dict)
- Comment out category/effect in template so authors must opt in
- Use VALID_EFFECTS constant in test instead of hard-coded values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: update category docstring to reflect free-form semantics

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: clarify canonical extension effect values

---------

Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-10 07:44:27 -05:00
Copilot
927f54feea feat: make git extension opt-in and remove --no-git at v0.10.0 (#2873)
* feat(init)!: make git extension opt-in and remove --no-git at v0.10.0

- Remove --no-git parameter from specify init command
- Remove git extension auto-installation from init flow
- Git repository initialization (git init) still runs when git is available
- Remove --no-git from all test invocations across the test suite
- Update docs to reflect opt-in git extension behavior
- Replace TestGitExtensionAutoInstall with TestGitExtensionOptIn tests

BREAKING CHANGE: specify init no longer auto-installs the git extension.
Use `specify extension add git` to install it explicitly.
The --no-git flag has been removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scripts): remove git operations from core scripts

Git functionality is now entirely managed by the git extension.
Core scripts only handle directory-based feature creation and numbering.

- Remove has_git(), check_feature_branch(), git branch creation from core
- Simplify number detection to use only spec directory scanning
- Remove HAS_GIT output from get_feature_paths()
- Remove git remote fetching and branch querying
- Keep BRANCH_NAME output key for backward compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: remove all git operations from core

- Remove is_git_repo() and init_git_repo() dead code from _utils.py
- Remove --branch-numbering from init command
- Remove git from 'specify check' (now extension-only)
- Update docs: git is optional prerequisite, check command description
- Fix tests to reflect no-git-in-core reality (fallback to main)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scripts): remove directory scanning and branch fallback from core

Core scripts now resolve feature context exclusively from:
1. SPECIFY_FEATURE env var (set by git extension)
2. .specify/feature.json (persisted by specify command)

Removed find_feature_dir_by_prefix() and directory scanning heuristics —
these are the git extension's responsibility. Scripts error clearly when
no feature context is available.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: introduce feature_numbering, deprecate branch_numbering in init-options

- specify command template now reads feature_numbering (preferred) with
  fallback to branch_numbering (deprecated) from init-options.json
- Git extension reads git-config.yml > feature_numbering > branch_numbering
- init now writes feature_numbering: sequential to init-options.json
- Deprecation warning emitted when branch_numbering is used as fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove trailing whitespace in common.ps1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(scripts): persist SPECIFY_FEATURE_DIRECTORY env var to feature.json

When SPECIFY_FEATURE_DIRECTORY is set, get_feature_paths() now writes the
value to .specify/feature.json so future sessions without the env var can
still resolve the feature directory. The write is idempotent — it skips
when the file already contains the same value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address review feedback — error messages and docs

- Update error messages in common.sh and common.ps1 to reference
  SPECIFY_FEATURE_DIRECTORY instead of SPECIFY_FEATURE (which no longer
  resolves feature directories)
- Fix get_current_branch comment (returns empty string, not error)
- Update upgrade.md to reference SPECIFY_FEATURE_DIRECTORY with correct
  example paths
- Update local-development.md troubleshooting: replace stale 'Git step
  skipped' row with actionable git extension guidance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scripts): harden feature.json persistence

- Use json_escape in printf fallback when jq is unavailable (common.sh)
- Replace utf8NoBOM encoding with UTF8Encoding($false) for PowerShell
  5.1 compatibility (common.ps1)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scripts): remove dead feature_json_matches_feature_dir functions

These guards are no longer needed since the branch-name validation they
protected against has been removed from check-prerequisites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(git-ext): rename create-new-feature to create-new-feature-branch

The git extension's script only creates the git branch — rename it to
reflect that responsibility. The core create-new-feature.sh/.ps1 handles
feature directory creation and feature.json persistence.

Also includes fixes from review feedback:
- common.sh: _persist_feature_json uses json_escape fallback
- common.ps1: Save-FeatureJson uses UTF8Encoding for PS 5.1 compat
- common.ps1: case-sensitive path stripping on non-Windows
- create-new-feature.sh/ps1: output both SPECIFY_FEATURE and
  SPECIFY_FEATURE_DIRECTORY
- setup-tasks.sh: fix stale 'Validate branch' comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests): update references to renamed git extension scripts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests): remove duplicate EXT_CREATE_FEATURE assignments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 06:13:07 -05:00
Copilot
7106858c4e feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags (0.10.0) (#2872)
* Initial plan

* feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags at 0.10.0

* refactor(tests): rename stale test_ai_help_* methods to test_agent_config_*

* fix: address review — derive agent folder for generic integration and remove redundant test

- Security notice now falls back to integration_parsed_options['commands_dir']
  when AGENT_CONFIG folder is None (generic integration).
- Remove test_agent_config_includes_kiro_cli which duplicates the assertion
  in test_runtime_config_uses_kiro_cli_and_removes_q.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: scrub all remaining --ai flag references from source and tests

- Remove dead AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, and
  _build_ai_assistant_help() from _agent_config.py
- Update comments/docstrings in extensions.py, presets.py, and
  integration subpackages to reference 'skills mode' or
  '--integration' instead of the removed flags
- Fix catalog.json generic integration description
- Update test docstrings/comments in test_extension_skills.py,
  test_extensions.py, and test_presets.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: remove legacy --ai flag rejection tests

The flags are fully removed from the CLI; typer handles unknown options
generically. No custom rejection logic exists to test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* revert: remove manual CHANGELOG.md entry

CHANGELOG is generated automatically; manual edits should not be made.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: make generic catalog description self-explanatory

Include the required --commands-dir sub-option in the description so
readers don't need to look up integration docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests): rename duplicate test classes to avoid shadowing

The rename from Test*AutoPromote to Test*Integration collided with the
existing Test*Integration(SkillsIntegrationTests) base classes, causing
the shared test suites to be silently overwritten. Rename the CLI init
flow classes to Test*InitFlow instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 14:56:28 -05:00
lselvar
f512b8b0d1 fix: resolve GitHub release asset API URL for private repo preset and workflow downloads (#2855)
* fix: resolve GitHub release asset API URL for private repo preset and workflow downloads

- Add shared `resolve_github_release_asset_api_url` utility to `_github_http.py` for
  reuse across preset and workflow download paths
- Apply the same private-repo fix from PR #2792 (extensions) to:
  - `PresetCatalog.download_pack` — ZIP downloads via catalog `download_url`
  - `preset add --from <url>` — ZIP downloads from a direct URL
  - `workflow add <url>` — workflow YAML downloads from a direct URL
  - `workflow add <id>` (catalog) — workflow YAML downloads via catalog `url`
- For browser release URLs (`github.com/…/releases/download/…`), the asset is
  resolved via the GitHub REST API and downloaded with `Accept: application/octet-stream`
- Direct REST API asset URLs (`api.github.com/…/releases/assets/<id>`) are
  downloaded directly with `Accept: application/octet-stream`
- Auth is preserved end-to-end through the existing `open_url` infrastructure
- Update `test_download_pack_sends_auth_header` and add
  `test_download_pack_accepts_direct_github_rest_asset_url` to cover both paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: URL-encode tag in release API URL to handle special characters

Encode the tag as a path segment (using quote with safe='') when
building the releases/tags/<tag> API URL. This prevents malformed
URLs when tags contain reserved characters like '/' or '#'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: add CLI-level tests for preset add --from GitHub release URL resolution

Adds regression tests covering:
- resolve_github_release_asset_api_url unit tests (passthrough, resolution,
  network error, URL encoding of special chars in tags)
- CLI-level 'preset add --from <github-release-url>' end-to-end flow
- CLI-level 'preset add --from <api-asset-url>' direct passthrough

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: deduplicate release URL resolution; fix test issues

- ExtensionCatalog._resolve_github_release_asset_api_url now delegates
  to the shared helper in _github_http.py (also gains URL-encoding fix)
- Remove unused 'io' import from test_github_http.py
- Remove duplicate 'provides' dict keys accidentally added to test_presets.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: align resolver timeout with download timeout; add workflow CLI tests

- Pass timeout=30 to resolve_github_release_asset_api_url in both
  workflow add paths so worst-case latency matches the download timeout
- Add CLI-level regression tests for 'workflow add <url>' covering
  browser URL resolution and direct API asset URL passthrough

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove unused urllib.request import; add catalog workflow test

- Remove unused 'import urllib.request' in preset add --from path
- Add CLI test for catalog-based 'workflow add <id>' with GitHub
  release URL resolution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: remove unused MagicMock imports from tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 10:41:40 -05:00
Huy Do
141119efea feat(workflows): add JSON output for workflow run resume and status (#2814)
* feat(workflows): add --json output to workflow run, resume, and status

Adds an opt-in `--json` flag to `workflow run`, `workflow resume`, and
`workflow status` that emits a single machine-readable object (run_id,
workflow_id, status, current step; status also reports per-step states
and a runs list) for automation and external orchestrators.

JSON is written via a small `_emit_workflow_json` helper using plain
stdout, so Rich markup, highlighting, and line-wrapping can never alter
the emitted object. Default human-readable output and exit codes are
unchanged when `--json` is omitted. Reference docs updated.

Closes #2811.

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

* fix(workflows): keep --json stdout clean while steps write output

Suppressing the banner and the step-start callback was not enough to
guarantee a single parseable JSON object on stdout: individual steps still
write there while the engine runs. The gate step prints its prompt, and the
prompt step runs a CLI subprocess that inherits the process's stdout file
descriptor — either can corrupt the JSON stream for interactive runs or
integration-backed workflows.

Wrap engine.execute()/engine.resume() in a file-descriptor-level redirect
(dup2) when --json is set, so both Python-level writes and inherited-fd
subprocess output go to stderr while stdout carries only the emitted JSON.
Step progress stays visible on stderr. status does not run the engine, so
it is unaffected.

Tests cover both pollution channels (a Python print and a real subprocess)
via fd-level capture, and the inactive no-op path. Docs note the
stdout/stderr split.

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

* docs(workflows): fix stray escape sequence in --json redirect comments

The redirect helper's docstring and its test comment wrote ``print``\s,
which renders as "print\s" rather than "prints". Replace with plain
"prints".

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:11:39 -05:00
minbang
a9a759450d fix: recover active skills registration for extensions (#2803)
Extension command registration now resolves the active skills directory before writing command artifacts. This lets initialized skills-backed agents recover a missing active skills directory while preserving the existing preset registration behavior.

Add regression coverage for missing active skills directories, shared skills directories, and symlinked parent guards.

Fixes #2769.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-06-04 09:53:31 -05:00
Copilot
40d832f90a Allow specify workflow run to execute YAML files without a project (#2825)
* Initial plan

* feat: add --workflow option to init command for post-init workflow execution

* chore: remove unused import in test file

* refactor: allow workflow run without project when given a YAML file path

Instead of adding --workflow to init, make `specify workflow run ./file.yml`
work without requiring a .specify/ project directory. When the source is a
YAML file that exists on disk, cwd is used as the project root. When it's a
workflow ID, the .specify/ project requirement is preserved.

* Handle standalone workflow path edge cases

* Fix USERPROFILE env var portability and docs notation

* Fix workflow YAML path detection to require regular files

* Harden workflow run against unsafe .specify paths

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-04 07:09:54 -05:00
Grissiom.GuRui
659a41a6cc feat(extensions): add --force flag to extension add for overwrite reinstall (#2530)
* feat(extensions): add --force flag to extension add for overwrite reinstall

Add --force support to `specify extension add` that allows overwriting
an already-installed extension without manually removing it first.

- install_from_directory() and install_from_zip() accept force=True,
  automatically calling remove() before installation
- The --force CLI flag works with all install modes (--dev, --from URL,
  bundled, and catalog)
- Config files (*-config.yml) are preserved across force reinstall
- Error message suggests --force when extension is already installed
- 6 new tests covering unit and CLI force reinstall flows

* fix: address PR review feedback on --force implementation

- Remove unused `backup_config_dir` variable assignment (Ruff F841)
- Defer `remove()` until after `_validate_install_conflicts()` to prevent
  data loss if validation fails mid-reinstall
- Use `TemporaryDirectory` instead of `NamedTemporaryFile` in ZIP test
  to avoid Windows file-locking failures

* fix: only restore config backup when --force actually triggers a remove

When --force is used but the extension is not already installed, the
backup restore/cleanup should not run. Previously it could resurrect
stale config files from a previous removal and delete the backup
directory unnecessarily.

* fix: address Copilot review feedback on --force implementation

- Clear stale backup dir before remove() so only fresh backups are restored
- Restore only config files (*-config.yml, *-config.local.yml) from backup
- Remove trailing \n from --force console message (console.print adds newline)

* fix: handle non-directory paths in backup cleanup/restore

- Use is_dir() before rmtree/iterdir on backup path to avoid crashes
  when .backup/<id> exists as a file or symlink
- Remove unused manifest1 variable in test_install_force_reinstall

* fix: handle symlinks in backup cleanup/restore and correct CLI message

- Check is_symlink() before is_dir() in backup cleanup and restore:
  Path.is_dir() follows symlinks (returns True for symlink-to-dir) but
  shutil.rmtree() raises OSError on symlinks. Handle symlinks by
  unlinking them instead.
- Skip symlink entries during config file restore.
- Change --force dev-install message from "Reinstalling" to
  "Installing [...] (will overwrite if already installed)" because
  --force also works for first-time installs.
2026-06-03 16:34:24 -05:00
Huy Do
1732b9b62e feat(workflows): allow resume to accept updated workflow inputs (#2815)
`workflow resume` now accepts `--input key=value` (the same flag and
parsing as `workflow run`, via a shared `_parse_input_values` helper).
Supplied values are merged over the run's persisted inputs and
re-resolved through the existing typed-validation path
(`_resolve_inputs`), so a resumed/re-run step sees the updated inputs
and ill-typed values fail fast. Keys not supplied keep their persisted
values; resuming without `--input` is unchanged. Reference docs updated.

Distinct from #2405 (file-reference inputs at run time): this is about
supplying inputs at resume time, reusing the existing input model.

Closes #2812.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:04:07 -05:00
Rafael Figuereo
9e05195d24 fix(cli): force UTF-8 stdout/stderr on Windows to prevent UnicodeEncodeError (#2817)
On Windows, when stdout/stderr are not a UTF-8 TTY (output piped, redirected
to a file, or running under a legacy code page such as cp1252), Rich cannot
encode the banner and box-drawing glyphs, so the CLI aborts with a
UnicodeEncodeError traceback instead of printing. This breaks basic commands
like `specify --help` and `specify version` whenever their output is captured
rather than written to an interactive terminal.

Reconfigure sys.stdout/sys.stderr to UTF-8 with errors="replace" at the
main() entry point on win32 so output degrades gracefully instead of crashing.
The change is a no-op on POSIX, is guarded by try/except so it can never make
stream setup worse, and lives at the CLI entry point only -- importing
specify_cli as a library does not touch global streams.

Verified on Windows 11 (cp1252): `specify --help` piped and `specify version`
redirected to a file both render correctly and exit 0 without setting
PYTHONUTF8 / PYTHONIOENCODING.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:32:14 -05:00
darion-yaphet
ee17b04784 refactor(integrations): co-locate integration commands in integrations/ domain dir (PR-5/8) (#2720)
* refactor(integrations): co-locate integration commands in integrations/ domain dir

- Remove commands/ stubs (handlers will live in domain dirs)
- Move all integration CLI handlers out of __init__.py into integrations/
- Split into focused modules under integrations/:
    _helpers.py           (340 lines) — domain helpers
    _install_commands.py  (306 lines) — install / uninstall
    _migrate_commands.py  (487 lines) — switch / upgrade
    _query_commands.py    (442 lines) — list / use / search / info / catalog
    _commands.py           (34 lines) — app objects + register()
- __init__.py reduced by ~1400 lines; integration block replaced with register() call
- Fix patch paths in tests to new module locations

* fix(integrations): restore original integration list output in refactor

Preserve the CLI Required column, post-table default/installed summary,
and no-installed guidance that were dropped during the no-behavior-change
refactor of integration list into _query_commands.py.

* Potential fix for pull request finding

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

* fix(integrations): restore _clear/_update_init_options public imports

The refactor that split integration commands moved
_clear_init_options_for_integration and _update_init_options_for_integration
into integrations/_helpers.py, but tests still import them from the top-level
specify_cli package, causing ImportError. Re-export them with explicit aliases
at the end of __init__.py to preserve the public import surface.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-02 12:21:19 -05:00
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
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
Manfred Riem
b48b22379e Add confirmation prompt for URL-based extension installs (#2745)
Display a yellow warning panel and default-deny [y/N] prompt when
installing extensions via --from <url>, since this bypasses the
catalog trust boundary.
2026-05-28 14:49:08 -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
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
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
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
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
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
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
darion-yaphet
6322a4d429 docs: fix script name in directory tree examples (#2555)
* docs: fix script name in directory tree examples

Replace update-claude-md.sh with the actual filename setup-tasks.sh
in two directory tree examples (Steps 2 and 4 of the detailed walkthrough).

* docs: fix .specify/scripts layout to show bash/ and powershell/ subdirs

install_shared_infra() installs scripts under .specify/scripts/bash/
(script_type="sh") or .specify/scripts/powershell/ (script_type="ps"),
not directly under .specify/scripts/. Update docs/installation.md and
the _install_shared_infra docstring to reflect the actual on-disk layout.

* docs: update README tree examples to show scripts/bash/ subdirectory

Scripts are installed under .specify/scripts/bash/ (or powershell/)
not directly under .specify/scripts/. Fix both tree diagrams in the
Detailed Process walkthrough to match the actual on-disk layout.
2026-05-14 15:14:52 -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
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
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
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
Pascal THUET
a7201c183e fix(workflows): require project for catalog list (#2436) 2026-05-04 12:00:28 -05:00