Compare commits

...

41 Commits

Author SHA1 Message Date
github-actions[bot]
3e9ac05007 chore: bump version to 0.8.0 2026-04-23 15:08:48 +00:00
Copilot
a067d4c2e3 feat(presets): Composition strategies (prepend, append, wrap) for templates, commands, and scripts (#2133)
* fix: rebase onto upstream/main, resolve conflicts with PR #2189

upstream/main merged PR #2189 (wrap-only strategy) which overlaps with
our comprehensive composition strategies (prepend/append/wrap). Resolved
conflicts keeping our implementation as source of truth:

- README: keep our future considerations (composition is now fully
  implemented, not a future item)
- presets.py: keep our composition architecture (_reconcile_composed_commands,
  collect_all_layers, resolve_content) while preserving #2189's
  _substitute_core_template which is used by agents.py for skill
  generation
- tests: keep both test sets (our composition tests + #2189's wrap
  tests), removed TestReplayWrapsForCommand and
  TestInstallRemoveWrapLifecycle which test the superseded
  _replay_wraps_for_command API; our composition tests cover equivalent
  scenarios
- Restored missing _unregister_commands call in remove() that was lost
  during #2189 merge

* fix: re-create skill directory in _reconcile_skills after removal

After _unregister_skills removes a skill directory, _register_skills
skips writing because the dir no longer passes the is_dir() check.
Fix by ensuring the skill subdirectory exists before calling
_register_skills so the next winning preset's content gets registered.

Fixes the Claude E2E failure where removing a top-priority override
preset left skill-based agents without any SKILL.md file.

* fix: address twenty-third round of Copilot PR review feedback

- Protect reconciliation in remove(): wrap _reconcile_composed_commands
  and _reconcile_skills in try/except so failures emit a warning instead
  of leaving the project in an inconsistent state
- Protect reconciliation in install(): same pattern for post-install
  reconciliation so partial installs don't lack cleanup
- Inherit scripts/agent_scripts from base frontmatter: when composing
  commands, merge scripts and agent_scripts keys from the base command's
  frontmatter into the top layer's frontmatter if missing, preventing
  composed commands from losing required script references
- Add tier-5 bundled core fallback to collect_all_layers(): check the
  bundled core_pack (wheel) or repo-root templates (source checkout) when
  .specify/templates/ doesn't contain the core file, matching resolve()'s
  tier-5 fallback so composition can always find a base layer

* fix: address twenty-fourth round of Copilot PR review feedback

- Use yaml.safe_load for frontmatter parsing in resolve_content instead
  of CommandRegistrar.parse_frontmatter which uses naive find('---',3);
  strip strategy key from final frontmatter to prevent leaking internal
  composition directives into rendered agent command files
- Filter _reconcile_skills to specific commands: use _FilteredManifest
  wrapper so only the commands being reconciled get their skills updated,
  preventing accidental overwrites of other commands' skills that may be
  owned by higher-priority presets

* fix: address twenty-fifth round of Copilot PR review feedback

- Support legacy command-frontmatter strategy: when preset.yml doesn't
  declare a strategy, check the command file's YAML frontmatter for
  strategy: wrap as a fallback so legacy wrap presets participate in
  composition and multi-preset chaining
- Guard skill dir creation in _reconcile_skills: only re-create the
  skill directory if the skill was previously managed (listed in some
  preset's registered_skills), avoiding creation of new skill dirs
  that _register_skills would normally skip

* fix: add explanatory comment to empty except in legacy frontmatter parsing

* fix: address twenty-sixth round of Copilot PR review feedback

- Unregister stale commands when composition fails: when resolve_content
  returns None during reconciliation (base layer removed), unregister
  the command from non-skill agents and emit a warning
- Load extension aliases during reconciliation: _register_command_from_path
  now checks extension.yml for aliases when the winning layer is an
  extension, so alias files are restored after preset removal
- Use line-based fence detection for legacy frontmatter strategy fallback:
  scan for --- on its own line instead of split('---',2) to avoid
  mis-parsing YAML values containing ---

* fix: address twenty-seventh round of Copilot PR review feedback

- Handle non-preset winners in _reconcile_skills: when the winning
  layer is core/extension/project-override, restore skills via
  _unregister_skills so skill-based agents stay consistent with the
  priority stack
- Update base_frontmatter_text on replace layers: when a higher-priority
  replace layer occurs during composition, update both top and base
  frontmatter so scripts/agent_scripts inheritance reflects the
  effective base beneath the top composed layer

* fix: address twenty-eighth round of Copilot PR review feedback

- Parse only interior lines in _parse_fm_yaml: use lines[1:-1] instead
  of filtering all --- lines, preventing corruption when YAML values
  contain a line that is exactly ---
- Omit empty frontmatter: skip re-rendering when top_fm is empty dict
  to avoid emitting ---/{}/--- for intentionally empty frontmatter
- Update scaffold wrap comment: mention both {CORE_TEMPLATE} and
  $CORE_SCRIPT placeholders for templates/commands vs scripts
- Clarify shell composition scope in ARCHITECTURE.md: note that bash/PS1
  resolve_template_content only handles templates; command/script
  composition is handled by the Python resolver

* fix: address twenty-ninth round of Copilot PR review feedback

- Fix TestCollectAllLayers docstring: reference collect_all_layers()
- Add default/unknown strategy handling in bash/PS1 composition: error
  on unrecognized strategy values instead of silently skipping
- Fix comment: .composed/ is a persistent dir, not temporary
- Fix comment: legacy fallback checks all valid strategies, not just wrap
- Cache PresetRegistry in _reconcile_skills: build presets_by_priority
  once instead of constructing registry per-command

* fix: address thirtieth round of Copilot PR review feedback

- Guard legacy frontmatter fallback: only check command file frontmatter
  for strategy when the manifest entry doesn't explicitly include the
  strategy key, preventing override of manifest-declared strategies
- Document rollback limitation: note that mid-registration failures may
  leave orphaned agent command files since partial progress isn't
  captured by the local vars

* fix: handle project override skills and extension context in reconciliation

* fix: add comment to empty except in extension registration fallback

* fix: filter extension commands in reconciliation and fix type annotation

* fix: filter extension commands from post-install reconciliation

Apply the same extension-installed check used in _register_commands to
the reconciliation command list, preventing reconciliation from
registering commands for extensions that are not installed.

* fix: skip convention fallback for explicit file paths and add stem fallback to tier-5

When a preset manifest provides an explicit file path that does not
exist, skip the convention-based fallback to avoid masking typos.
Also add speckit.<stem> to <stem>.md fallback in tier-5 bundled/source
core lookup for consistency with tier-4.

* fix: scan past non-replace layers to find base in resolve_content

The base-finding scan now skips non-replace layers below a replace
layer instead of stopping at the first non-replace. This fixes the
case where a low-priority append/prepend layer sits below a replace
that should serve as the base for composition.

* fix: add context_note to non-skill agent registration for extensions

Add context_note parameter to register_commands_for_non_skill_agents
and pass extension name/id during reconciliation so rendered command
files preserve the extension-specific context markers.

* fix: Optional type, rollback safety, and override skill restoration

- Fix context_note type to Optional[str]
- Wrap shutil.rmtree in try/except during install rollback
- Separate override-backed skills from core/extension in _reconcile_skills

* fix: align bash/PS1 base-finding with Python resolver

Rewrite bash and PowerShell composition loops to find the effective
base replace layer first (scanning bottom-up, skipping non-replace
layers below it), then compose only from the base upward. This
prevents evaluation of irrelevant lower layers (e.g. a wrap with
no placeholder below a replace) and matches resolve_content behavior.

* fix: PS1 no-python warning, integration hook for override skills, alias cleanup

- Warn when no Python 3 found in PS1 and presets use composition strategies
- Apply post_process_skill_content integration hook when restoring
  override-backed skills so agent-specific flags are preserved
- Unregister command aliases alongside primary name when composition
  fails to prevent orphaned alias files

* fix: include aliases in removed_cmd_names during preset removal

Read aliases from preset manifest before deleting pack_dir so alias
command files are included in unregistration and reconciliation.

* fix: add comment to empty except in alias extraction during removal

* fix: scan top-down for effective base in all resolvers

Change base-finding to scan from highest priority downward to find the
nearest replace layer, then compose only layers above it. Prevents
evaluation of irrelevant lower layers (e.g. a wrap without placeholder
below a higher-priority replace) across Python, bash, and PowerShell.

* fix: align CLI composition chain display with top-down base-finding

Show only contributing layers (base and above) in preset resolve
output, matching resolve_content top-down semantics. Layers below
the effective base are omitted since they do not contribute.

* fix: guard corrupted registry entries and make manifest authoritative

- Add isinstance(meta, dict) guard in bash registry parsing so corrupted
  entries are skipped instead of breaking priority ordering
- Only use convention-based file lookup when the manifest does not list
  the requested template, making preset.yml authoritative and preventing
  stray on-disk files from creating unintended layers

* fix: align resolve() with manifest file paths and match extension context_note

- Update resolve() preset tier to consult manifest file paths before
  convention-based lookup, matching collect_all_layers behavior
- Use exact extension context_note format matching extensions.CommandRegistrar
- Update test to declare template in manifest (authoritative manifest)

* revert: restore resolve() convention-based behavior for backwards compatibility

resolve() is the existing public API used by shell scripts and other
callers. Changing it to manifest-authoritative breaks backward compat
for presets that rely on convention-based file lookup. Only the new
collect_all_layers/resolve_content path uses manifest-authoritative
logic.

* fix: only pre-compose when this preset is the top composing layer

Skip composition in _register_commands when a higher-priority replace
layer already wins for the command. Register the raw file instead and
let reconciliation write the correct final content.

* fix: deduplicate PyYAML warnings and use self.registry in reconciliation

- Emit PyYAML-missing warning once per function call in bash/PS1 instead
  of per-preset to avoid spamming stderr
- Use self.registry.list_by_priority() in reconciliation methods instead
  of constructing new PresetRegistry instances to avoid redundant I/O
  and potential consistency issues

* fix: document strategy handling consistency between layers and registrar

Composed output already strips strategy from frontmatter (resolve_content
pops it). Raw file registration preserves legacy frontmatter strategy
for backward compat; reconciliation corrects the final state.

* fix: correct stale comments for alias tracking and base-finding algorithm

* security: validate manifest file paths in bash/PowerShell resolvers

Reject absolute paths and parent directory traversal (..) in the
manifest-declared file field before joining with the preset directory.
Matches the Python-side validation in PresetManifest._validate().

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
2026-04-23 10:07:52 -05:00
Copilot
8fefd2a532 feat(copilot): support --integration-options="--skills" for skills-based scaffolding (#2324)
* Initial plan

* feat(copilot): add --skills flag for skills-based scaffolding

Add --skills integration option to CopilotIntegration that scaffolds
commands as speckit-<name>/SKILL.md under .github/skills/ instead of
the default .agent.md + .prompt.md layout.

- Add options() with --skills flag (default=False)
- Branch setup() between default and skills modes
- Add post_process_skill_content() for Copilot-specific mode: field
- Adjust build_command_invocation() for skills mode (/speckit-<stem>)
- Update dispatch_command() with skills mode detection
- Parse --integration-options during init command
- Add 22 new skills-mode tests
- All 15 existing default-mode tests continue to pass

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/a4903fab-64ff-46c3-8eb8-a47f495a70c0

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

* docs(AGENTS.md): document Copilot --skills option

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/a4903fab-64ff-46c3-8eb8-a47f495a70c0

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

* Potential fix for pull request finding 'Unused local variable'

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

* fix: address PR #2324 review feedback

- Reset _skills_mode at start of setup() to prevent singleton state leak
- Tighten skills auto-detection to require speckit-*/SKILL.md (not any
  non-empty .github/skills/ directory)
- Add copilot_skill_mode to init next-steps so skills mode renders
  /speckit-plan instead of /speckit.plan
- Fix docstring quoting to match actual unquoted output
- Add 4 tests covering singleton reset, auto-detection false positive,
  speckit layout detection, and next-steps skill syntax
- Fix skipped test_invalid_metadata_error_returns_unknown by simulating
  InvalidMetadataError on Python versions that lack it

* fix: inline skills prompt in dispatch_command auto-detection path

build_command_invocation() reads self._skills_mode which stays False
when skills mode is only auto-detected from the project layout. Inline
the /speckit-<stem> prompt construction so dispatch_command() sends the
correct prompt regardless of how skills mode was detected.

Also strengthen test_dispatch_detects_speckit_skills_layout to assert
the -p prompt contains /speckit-plan and the user args.

---------

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>
2026-04-23 08:26:59 -05:00
sha0dow
b278d66b2c docs(install): add pipx as alternative installation method (#2288)
* docs(install): add pipx as alternative installation method

- Add pipx commands to README.md installation section
- Add note about pipx compatibility to docs/installation.md
- Mention pipx persistent installation in docs/quickstart.md
- Add pipx upgrade instructions to docs/upgrade.md
- Clarify that project has no uv-specific dependencies

Refs: https://github.com/github/spec-kit/discussions/2255

* docs(install): address Copilot feedback - update prerequisites and upgrade references for pipx

* Update docs/quickstart.md

markdownlint’s MD012 (enabled in this repo) flags multiple consecutive blank lines

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

* Update docs/upgrade.md

In the Quick Reference table, the label “pipx upgrade” is misleading because the command shown is `pipx install --force ...` (a reinstall). by copilot.

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 08:12:17 -05:00
Dyan Galih
709457cec2 Add Memory MD community extension (#2327) 2026-04-23 07:50:26 -05:00
Kevin Brown
9e259e1f8d Update version-guard to v1.2.0 (#2321)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 16:44:06 -05:00
Manfred Riem
3970855797 fix: --force now overwrites shared infra files during init and upgrade (#2320)
* fix: --force now overwrites shared infra files during init and upgrade

_install_shared_infra() previously skipped all existing files under
.specify/scripts/ and .specify/templates/, regardless of --force.
This meant users could never receive upstream fixes to shared scripts
or templates after initial project setup.

Changes:
- Add force parameter to _install_shared_infra(); when True, existing
  files are overwritten with the latest bundled versions
- Wire force=True through specify init --here --force and
  specify integration upgrade --force call sites
- Replace hidden logging.warning with visible console output listing
  skipped files and suggesting --force
- Fix contradictory upgrade docs that claimed --force updated shared
  infra (it didn't) and warned about overwrites (they didn't happen)
- Add 6 tests: unit tests for skip/overwrite/warning behavior, plus
  end-to-end CLI tests for both --force and non-force paths

Fixes #2319

* fix: improve skip warning to suggest specific commands

Address review feedback: the generic '--force' suggestion was
misleading when _install_shared_infra is called from integration
install/switch (which don't have a --force for shared infra).
Now points users to the specific commands that can refresh shared
infra: 'specify init --here --force' or 'specify integration
upgrade --force'.
2026-04-22 16:40:41 -05:00
Manfred Riem
f612e1a30d chore: release 0.7.5, begin 0.7.6.dev0 development (#2322)
* chore: bump version to 0.7.5

* chore: begin 0.7.6.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-22 16:26:44 -05:00
swithek
ecb3b94b43 fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi (#2313)
* fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi

* chore: remove unused NATIVE_SKILLS_AGENTS constant
2026-04-22 15:12:44 -05:00
김준호
c5c20134df feat(cli): add specify self check and self upgrade stub (#2316)
* feat(cli): add specify self check and self upgrade stub (#2282)

Introduce a new `specify self` Typer sub-app with two subcommands.

`specify self check` performs a read-only lookup against the GitHub Releases
API, compares the installed version to the latest tag with PEP 440 semantics,
and prints one of four verdicts (newer-available, up-to-date, indeterminate,
graceful-failure). When a newer stable release is available, the output
includes a copy-pasteable `uv tool install --force --from git+...@<tag>`
reinstall command. `GH_TOKEN` / `GITHUB_TOKEN` is attached as a bearer
credential when set so users behind shared IPs escape the anonymous 60/hour
rate limit.

`specify self upgrade` is a documented non-destructive stub in this release:
three-line guidance output, exit 0, no outbound call, no install-method
detection. The real destructive implementation is planned as follow-up work.

Failure categorization is a fixed three-entry enum (offline or timeout,
rate limited, HTTP <code>). Anything outside those three categories
propagates as a non-zero exit so bugs surface instead of being silently
swallowed. No machine-readable output, no retries, no caching in this
release — see issue #2282 discussion.

Tests mock `urllib.request.urlopen`; the suite performs zero real network
calls. Full regression suite: 1586 passed.

* fix(cli): disable Rich highlight for deterministic output

Rich's default `highlight=True` applies ANSI color to detected patterns
(integers, version strings, paths) whenever stdout is deemed a TTY.
This caused intermittent failures in existing pytest assertions in
tests/test_cli_version.py and tests/test_extensions.py::TestExtensionRemoveCLI
that compare plain-text output without passing through `strip_ansi()`.

Setting `Console(highlight=False)` globally makes all CLI output
deterministic and fixes the flake without modifying the affected tests.
The numeric cyan highlighting was not a documented part of the CLI
visual contract.

* fix: address copilot review feedback

* fix: tighten self-check token handling

* fix: align self-check helpers and script metadata

* fix: harden self-check version handling

* fix: guard self-check failure rendering
2026-04-22 13:33:03 -05:00
Kevin Brown
58f7a43ec3 Update version-guard to v1.1.0 (#2318)
- Version: 1.0.0 → 1.1.0
- Commands: 1 → 3 (check, load, validate)
- Hooks: 2 → 4 (before_plan, before_tasks, before_implement, after_implement)
- Added: persistent constraints artifact, two-channel model, CVE detection,
  decision record fallback for greenfield projects, skip artifact persistence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 13:27:56 -05:00
Manfred Riem
efb04e26eb docs: move community presets from README to docs/community (#2314)
Move the community presets table from the main README to a dedicated
docs/community/presets.md page, matching the pattern used for
walkthroughs and friends.

- Add docs/community/presets.md with the full presets table
- Add Claude AskUserQuestion preset (was in catalog but missing from table)
- Add Presets entry to docs/toc.yml under Community
- Replace inline README table with a short link to the docs page
2026-04-22 10:05:14 -05:00
TortoiseWolfe
c52ea23ba2 catalog: add wireframe extension (v0.1.1) (#2262)
* catalog: add wireframe extension

Adds https://github.com/TortoiseWolfe/spec-kit-extension-wireframe
(v0.1.0) to the community catalog. Provides a visual feedback loop
for spec-driven development: SVG wireframe generation, review, and
sign-off. Approved wireframes become spec constraints honored by
/plan, /tasks, and /implement.

Supersedes #1410 — the old PR predated the extension system
introduced in #2130 and proposed commands in templates/commands/,
which is no longer the right home for third-party commands.

* catalog: address review feedback (position + author)

Two changes per Copilot review:
- Move `wireframe` entry alphabetically between `whatif` and
  `worktree` (was appended after `worktrees`).
- Simplify `author` from "TortoiseWolfe (turtlewolfe.com)" to
  just "TortoiseWolfe" so the exact-match author filter in
  `ExtensionCatalog.search` finds the entry. Portfolio URL
  remains accessible via `homepage`/`repository`.

Thanks @Copilot, @mnriem for the review.

* docs(readme): add Wireframe Visual Feedback Loop row

Addresses @mnriem's follow-up: the README extension table also
needs an entry, not just the catalog JSON. Slots in alphabetically
between "What-if Analysis" and "Worktree Isolation" with category
`visibility` and Read+Write effect (since sign-off writes the
approved wireframe paths into spec.md).

* catalog: use speckit-prefixed command names in wireframe description

Address remaining Copilot review comment on PR #2262. The actual
commands are /speckit.plan, /speckit.tasks, /speckit.implement;
the unprefixed names would mislead catalog users.

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

* catalog: bump wireframe extension to v0.1.1

v0.1.1 of spec-kit-extension-wireframe ships the /speckit.-prefixed
command references in extension.yml and README.md. This updates the
catalog entry to point at the new release tag so `specify extension
add wireframe` installs the corrected version.

* catalog: set wireframe created_at to current timestamp

Per EXTENSION-PUBLISHING-GUIDE.md: newly added entries should use
the current timestamp for both created_at and updated_at. The 04-17
value reflected when I drafted the entry locally, not when the
catalog submission landed.

---------

Co-authored-by: TortoiseWolfe <jonpohlner@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:20:13 -05:00
Manfred Riem
d402a392c3 Move community walkthroughs from README to docs/community (#2312)
* Move community walkthroughs from README to docs/community

Extract the community walkthroughs section from README.md into its own
docs/community/walkthroughs.md file and replace it with a short summary
linking to the GitHub Pages URL.

* Address review: fix double-See phrasing, add walkthroughs to docs nav
2026-04-22 09:17:48 -05:00
Ash Brener
deb80956f3 docs(readme): list red-team in community-extensions table (#2311)
Follow-up to #2306 (merged). Per maintainer request
(https://github.com/github/spec-kit/pull/2306#issuecomment-4296655643),
adds the red-team entry to the alphabetically-ordered community-extensions
table in README.md so the extension is discoverable alongside the other
community entries — not only via catalog.community.json.

Slotted alphabetically between "Reconcile Extension" and "Repository
Index". Category: docs. Effect: Read+Write (produces a structured
findings-report file at specs/<feature-id>/red-team-findings-*.md; does
not modify specs — every resolution is maintainer-authorised).

Co-authored-by: Ash Brener <ashley@midletearth.com>
2026-04-22 09:03:06 -05:00
Ash Brener
4dcf2921d1 feat(catalog): add red-team extension to community catalog (#2306)
* feat(catalog): add red-team extension

Adds the `red-team` community extension to the catalog:

- Adversarial review of functional specs before /speckit.plan locks in
  architecture.
- Complements /speckit.clarify (correctness) and /speckit.analyze
  (consistency) with parallel adversarial lens agents.
- One command: speckit.red-team.run
- MIT licensed; requires spec-kit >= 0.7.0.

Origin: this extension was originally proposed as a core command
(github/spec-kit#2303). Per maintainer guidance (mnriem's comment on
that PR), it's been restructured as a community extension hosted at
https://github.com/ashbrener/spec-kit-red-team.

Dogfood-validated on a 500-line functional spec: 5 lens agents
dispatched in parallel returned 25 findings in ~1.5 min wall-clock,
19 of which met the meaningful-finding bar (severity >= HIGH AND
novel adversarial angle that clarify/analyze structurally cannot
catch). Full detail in the extension's CHANGELOG.

* catalog: shorten red-team description to fit <200 char schema limit

Resolves Copilot review comment on #2306. Previous description (259
chars) exceeded the extensions/EXTENSION-PUBLISHING-GUIDE.md Appendix
schema ceiling. Shortened to 188 chars, keeping the distinctive
value proposition (adversarial, complements clarify/analyze) and
moving the per-phase mechanics to the extension's own README.

* catalog: bump red-team to v1.0.1 (lower required spec-kit version)

Follow-up to v1.0.0 catalog entry:
- version: 1.0.0 -> 1.0.1
- download_url: points at v1.0.1 release asset
- requires.speckit_version: >=0.7.0 -> >=0.1.0

The v1.0.0 requirement was too strict and blocked installation on
common 0.6.x field versions (confirmed via local install attempt).
The extension uses no 0.7.x-specific APIs; matches community norm
(reconcile, refine, others use >=0.1.0).

* catalog: bump red-team to v1.0.2 (adds mandatory before_plan gate)

v1.0.2 ships a /speckit.red-team.gate command wired as a mandatory
before_plan hook so /speckit.plan auto-invokes it on every run against
qualifying specs. Non-qualifying specs return PROCEED silently; qualifying
specs without findings on record return HALT with explicit remediation
(run /speckit.red-team.run, or opt out via --skip-red-team-gate: <reason>
which is recorded as an Accepted Risk [red-team-skipped] in the plan).

Catalog metadata delta:
- version: 1.0.1 -> 1.0.2
- download_url: v1.0.2/red-team-v1.0.2.zip
- provides.commands: 1 -> 2 (adds speckit.red-team.gate)
- provides.hooks: 0 -> 1 (adds before_plan hook)

No breaking changes. Projects that do not want the gate simply do not
install the extension.

---------

Co-authored-by: Ash Brener <ashley@midletearth.com>
2026-04-22 08:33:08 -05:00
WangX
dd9c0b0500 Add superpowers-bridge community extension (#2309)
* Add superpowers-bridge community extension

Adds the superpowers-bridge extension by WangX0111 to the community
catalog and README table. This extension bridges spec-kit with
obra/superpowers (brainstorming, TDD, subagent-driven-development,
code-review) into a unified, resumable workflow with graceful
degradation and session progress tracking.

Extension details:
- ID: superpowers-bridge
- Repository: https://github.com/WangX0111/superspec
- Version: 1.0.0
- Commands: 5, Hooks: 3
- License: MIT

* Address Copilot review feedback

- Update top-level updated_at to 2026-04-22
- Shorten description to under 200 characters

---------

Co-authored-by: 乘浩 <wch453799@alibaba-inc.com>
2026-04-22 08:14:58 -05:00
Kennedy
22e76995c7 feat: implement preset wrap strategy (#2189)
* feat: implement strategy: wrap

* fix: resolve merge conflict for strategy wrap correctness

* feat: multi-preset composable wrapping with priority ordering

Implements comment #4 from PR review: multiple installed wrap presets
now compose in priority order rather than overwriting each other.

Key changes:
- PresetResolver.resolve() gains skip_presets flag; resolve_core() wraps
  it to skip tier 2, preventing accidental nesting during replay
- _replay_wraps_for_command() recomposed all enabled wrap presets for a
  command in ascending priority order (innermost-first) after any
  install or remove
- _replay_skill_override() keeps SKILL.md in sync with the recomposed
  command body for ai-skills-enabled projects
- install_from_directory() detects strategy: wrap commands, stores
  wrap_commands in the registry entry, and calls replay after install
- remove() reads wrap_commands before deletion, removes registry entry
  before rmtree so replay sees post-removal state, then replays
  remaining wraps or unregisters when none remain

Tests: TestResolveCore (5), TestReplayWrapsForCommand (5),
TestInstallRemoveWrapLifecycle (5), plus 2 skill/alias regression tests

* fix: resolve extension commands via manifest file mapping

PresetResolver.resolve_extension_command_via_manifest() consults each
installed extension.yml to find the actual file declared for a command
name, rather than assuming the file is named <cmd_name>.md.  This fixes
_substitute_core_template for extensions like selftest where the manifest
maps speckit.selftest.extension → commands/selftest.md.

Resolution order in _substitute_core_template is now:
  1. resolve_core(cmd_name) — project overrides win, then name-based lookup
  2. resolve_extension_command_via_manifest(cmd_name) — manifest fallback
  3. resolve_core(short_name) — core template short-name fallback

Path traversal guard mirrors the containment check already present in
ExtensionManager to reject absolute paths or paths escaping the extension
root.

* fix: add bundled core_pack as Priority 5 in PresetResolver.resolve()

resolve_core() was returning None for built-in commands (implement,
specify, etc.) because PresetResolver only checked .specify/templates/
commands/ (Priority 4), which is never populated for commands in a
normal project. strategy:wrap presets rely on resolve_core() to fetch
the {CORE_TEMPLATE} body, so the wrap was silently skipped and SKILL.md
was never updated.

Priority 5 now checks core_pack/commands/ (wheel install) or
repo_root/templates/commands/ (source checkout), mirroring the pattern
used by _locate_core_pack() elsewhere.

Updated two tests whose assertions assumed resolve_core() always
returned None when .specify/templates/commands/ was absent.

* fix: harden preset wrap replay removal

* fix: stabilize existing directory error output

* fix: track outermost_pack_id from contributing preset; use Path.parts in tests

- outermost_pack_id now updates alongside outermost_frontmatter inside
  the wrap loop, so it reflects the actual last contributing preset
  rather than always taking wrap_presets[0] (which may have been skipped)
- Replace str(path) substring checks in TestResolveCore with Path.parts
  tuple comparisons for correct behaviour on Windows (CI runs windows-latest)

* fix: guard against non-mapping YAML manifests; apply integration post-processing in replay

- ExtensionManifest._load raises ValidationError for non-dict YAML roots instead of TypeError
- PresetManager._replay_wraps_for_command calls integration.post_process_skill_content,
  matching _register_skills behaviour
- PresetResolver skips extensions that raise OSError/TypeError/AttributeError on manifest load
- Tests: non-mapping YAML, OSError manifest skip, and replay integration post-processing
2026-04-21 15:02:31 -05:00
김준호
569d18a59d fix(agents): block directory traversal in command write paths (#2229) (#2296)
Extend the alias containment guard from b67b285 to the two remaining
write paths that derive filenames from free-form command/alias names:

- Primary command write in CommandRegistrar.register_commands()
- CommandRegistrar.write_copilot_prompt()

Consolidate the check into a shared _ensure_inside() helper. Per
maintainer guidance on #2229, use a lexical
(os.path.normpath + Path.is_relative_to) containment check rather than
resolve() so `..` / absolute-path traversal is rejected while
intentionally symlinked sub-directories under an agent's commands
directory (e.g. .claude/skills/shared -> /team/shared-skills) keep
working for existing extension setups.

Add 22 parametrised regression cases covering traversal payloads on
primary commands, aliases, and the Copilot companion prompt, plus a
positive case that confirms symlinked sub-directories remain supported.
2026-04-21 12:06:09 -05:00
Manfred Riem
f10fd07481 chore: release 0.7.4, begin 0.7.5.dev0 development (#2299)
* chore: bump version to 0.7.4

* chore: begin 0.7.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-21 11:50:23 -05:00
Manfred Riem
370b5b4890 fix(copilot): use --yolo to grant all permissions in non-interactive mode (#2298)
* fix(copilot): use --yolo to grant all permissions in non-interactive mode

The Copilot CLI's --allow-all-tools flag only covers tool execution
permissions but does not grant path or URL access. When the Copilot
agent autonomously runs shell commands (e.g. npm run build) during
workflow execution, the CLI blocks path access and cannot prompt for
approval in non-interactive mode, producing:

  Permission denied and could not request permission from user

Replace --allow-all-tools with --yolo (equivalent to --allow-all-tools
--allow-all-paths --allow-all-urls) to grant all three permission types.

Rename the opt-out env var from SPECKIT_ALLOW_ALL_TOOLS to
SPECKIT_COPILOT_ALLOW_ALL to match the formal --allow-all alias and
scope it to the Copilot integration.

Fixes #2294

* review: deprecate SPECKIT_ALLOW_ALL_TOOLS, rename to SPECKIT_COPILOT_ALLOW_ALL_TOOLS

Address Copilot review feedback:

- Honour the old SPECKIT_ALLOW_ALL_TOOLS env var as a fallback with a
  DeprecationWarning so existing opt-outs are not silently ignored.
- Rename the new canonical env var to SPECKIT_COPILOT_ALLOW_ALL_TOOLS.
- New var takes precedence when both are set.
- Use monkeypatch in tests to avoid flakiness from ambient env vars.
- Add tests for deprecation warning, precedence, and opt-out paths.

* review: use UserWarning instead of DeprecationWarning

DeprecationWarning is suppressed by default in Python, so users relying
on the old SPECKIT_ALLOW_ALL_TOOLS env var would never see the
deprecation notice during normal CLI runs. Switch to UserWarning which
is always shown. Update test to also assert the warning category.
2026-04-21 11:41:52 -05:00
Parman Mohammadalizadeh
52afbea47a feat: add CITATION.cff and .zenodo.json for academic citation support (#2291)
* feat: add CITATION.cff and .zenodo.json for academic citation support

Adds a Citation File Format file (CITATION.cff) so GitHub surfaces a
native "Cite this repository" button, and a .zenodo.json metadata file
so Zenodo can pre-fill the DOI record once a maintainer enables the
integration at zenodo.org.

Closes #2269

* fix: address PR review feedback on citation metadata

- Fix 'a specify CLI' -> 'the Specify CLI' in both files
- Broaden description to include extensions, presets, and workflows
- Remove empty orcid fields from .zenodo.json creators
- Update date-released to 2026-04-17 (actual 0.7.3 release date)

* fix: correct docs URL in CITATION.cff to github.io domain
2026-04-21 08:46:43 -05:00
aeltayeb
85e00f63d6 Add spec-validate to community catalog (#2274)
* Add spec-validate to community catalog

- Extension ID: spec-validate
- Version: 1.0.1
- Author: Ahmed Eltayeb
- Description: Comprehension validation, review gating, and approval state for spec-kit artifacts

* Reorder spec-validate before speckit-utils (address Copilot feedback)

Lexicographically 'spec-validate' < 'speckit-utils' because '-' (0x2D)
sorts before 'k' (0x6B). Move the entry to match the alphabetical
ordering used in the 's' range of the catalog.
2026-04-20 18:27:25 -05:00
김준호
fc1cec9748 feat: register Ripple in community catalog (#2272)
* feat: add Ripple extension to community catalog

- Extension ID: ripple
- Version: 1.0.0
- Author: chordpli
- 3 commands (scan, resolve, check), 1 hook (after_implement)
- Delta-anchored side effect detection across 9 domain-agnostic categories

* fix: move ripple entry to correct alphabetical position

Moved ripple catalog entry from between reconcile/refine to between
review/security-review. Updated README table order to match.

* fix: correct ripple alphabetical position (review → ripple → scope)

* fix: align ripple entry timestamps with catalog top-level updated_at
2026-04-20 17:54:38 -05:00
Kevin Brown
ad46da552c Add version-guard to community catalog (#2286)
* Add version-guard to community catalog

- Extension ID: version-guard
- Version: 1.0.0
- Author: KevinBrown5280
- Description: Verify tech stack versions against live registries before planning and implementation

* Fix alphabetical ordering: move Version Guard after Verify rows
2026-04-20 17:12:44 -05:00
Kevin Brown
919f09283d Add spec-reference-loader to community catalog (#2285)
- Extension ID: spec-reference-loader
- Version: 1.0.0
- Author: KevinBrown5280
- Description: Reads the ## References section from the current feature spec and loads the listed files into context
2026-04-20 16:52:37 -05:00
Kevin Brown
8d99f030ce Add memory-loader to community catalog (#2284)
- Extension ID: memory-loader
- Version: 1.0.0
- Author: KevinBrown5280
- Description: Loads .specify/memory/ files before spec-kit lifecycle commands so LLM agents have project governance context
2026-04-20 16:46:47 -05:00
Ayesha Khalid
b4c4e86cbc fix(integrations): strip UTF-8 BOM when reading agent context files (#2283)
* fix(integrations): strip UTF-8 BOM when reading agent context files

* test(integrations): add BOM regression tests for context file read/write

* test(workflows): mock shutil.which in tests that assume CLI is absent

* test(integrations): remove unused manifest variable in BOM test
2026-04-20 16:00:20 -05:00
adaumann
dc057a2314 Preset fiction book writing1.6 (#2270)
* Update preset-fiction-book-writing to community catalog

- Preset ID: fiction-book-writing
- Version: 1.5.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc. V1.5.0: Support interactive, audiobooks, series, workflow corrections

* Add fiction-book-writing preset to community catalog

- Preset ID: fiction-book-writing
- Version: 1.6.0
- Author: Andreas Daumann
- Description: Added support for 12 languages, export with templates, cover builder, bio builder, workflow fixes

* Update presets/catalog.community.json

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

* fixed update_at for fiction-book-writing preset

* Update README.md

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

* fixed description for fiction-book-writing

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 14:14:43 -05:00
BachVQ
2568422085 fix(integrations): migrate Antigravity (agy) layout to .agents/ and deprecate --skills (#2276)
* refactor(agy): update storage directory from .agent to .agents

* feat: update Antigravity integration to use .agents/ directory layout and add version compatibility warnings

* Apply suggestion from @Copilot

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

* refactor: remove deprecated --skills flag from AgyIntegration and update related test assertions

* Update src/specify_cli/integrations/agy/__init__.py

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

* refactor: update Antigravity integration requirement to v1.20.5 and remove obsolete tests

* test: update skills directory path from .agent to .agents in preset restoration test

* Update tests/integrations/test_integration_agy.py

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

* Update tests/integrations/test_integration_agy.py

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 11:57:45 -05:00
Manfred Riem
c118c1c30f chore: release 0.7.3, begin 0.7.4.dev0 development (#2263)
* chore: bump version to 0.7.3

* chore: begin 0.7.4.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-17 14:33:38 -05:00
Manfred Riem
fc3d1244c0 fix: replace shell-based context updates with marker-based upsert (#2259)
* Replace shell-based context updates with marker-based upsert

Replace ~3500 lines of bash/PowerShell agent context update scripts
with a Python-based approach using <!-- SPECKIT START/END --> markers.

IntegrationBase now manages the agent context file directly:
- upsert_context_section(): creates or updates the marked section at
  init/install/switch time with a directive to read the current plan
- remove_context_section(): removes the section at uninstall, deleting
  the file only if it becomes empty
- __CONTEXT_FILE__ placeholder in command templates is resolved per
  integration so the plan command references the correct agent file
- context_file is persisted in init-options.json for extension access

The plan command template instructs the LLM to update the plan
reference between the markers in the agent context file.

Removed:
- scripts/bash/update-agent-context.sh (857 lines)
- scripts/powershell/update-agent-context.ps1 (515 lines)
- 56 integration wrapper scripts (update-context.sh/.ps1)
- templates/agent-file-template.md
- agent_scripts frontmatter key and {AGENT_SCRIPT} replacement logic
- update-context reference from integration.json
- tests/test_cursor_frontmatter.py (tested deleted scripts)

Added:
- upsert/remove context section methods on IntegrationBase
- __CONTEXT_FILE__ placeholder support in process_template()
- context_file field in init-options.json (init/switch/uninstall)
- Per-integration tests: context file correctness, plan reference,
  init-options persistence (78 new context_file tests)
- End-to-end CLI validation across all 28 integrations

* fix: search for end marker after start marker in context section methods

Address Copilot review: content.find(CONTEXT_MARKER_END) searched from
the start of the file rather than after the located start marker. If
the file contained a stray end marker before the start marker, the
wrong slice could be replaced.

Now both upsert_context_section() and remove_context_section() pass
start_idx as the second argument to find() and validate end_idx >
start_idx before performing the replacement.

* fix: address Copilot review feedback on context section handling

1. Fix grammar in _build_context_section() directive text — add commas
   for a complete sentence.

2. Resolve __CONTEXT_FILE__ in resolve_skill_placeholders() — skills
   generated via extensions/presets for codex/kimi now replace the
   placeholder using the context_file value from init-options.json.

3. Handle Cursor .mdc frontmatter — when creating a new .mdc context
   file, prepend alwaysApply: true YAML frontmatter so Cursor
   auto-loads the rules.

4. Fix empty-file leading newline — when the context file exists but
   is empty, write the section directly instead of prepending a blank
   line.

* fix: address second round of Copilot review feedback

1. Ensure .mdc frontmatter on existing files — upsert_context_section()
   now checks for missing YAML frontmatter on .mdc files during updates
   (not just creation), so pre-existing Cursor files get alwaysApply.

2. Guard against context_file=None — use 'or ""' instead of a default
   arg so explicit null values in init-options.json don't cause a
   TypeError in str.replace().

3. Clean up .mdc files on removal — remove_context_section() treats
   files containing only the Speckit-generated frontmatter block as
   empty, deleting them rather than leaving orphaned frontmatter.

* fix: address third round of Copilot review feedback

1. CRLF-safe .mdc frontmatter check — use lstrip().startswith('---')
   instead of startswith('---\n') so CRLF files don't get duplicate
   frontmatter.

2. CRLF-safe .mdc removal check — normalize line endings before
   comparing against the sentinel frontmatter string.

3. Call remove_context_section() during integration_uninstall() — the
   manifest-only uninstall was leaving the managed SPECKIT markers
   behind in the agent context file.

4. Fix stale docstring — remove 'agent_scripts' mention from
   test_lean_commands_have_no_scripts().

* fix: address fourth round of Copilot review feedback

1. Remove unused script_type parameter from _write_integration_json()
   and all 3 call sites — the parameter was no longer referenced after
   the update-context script removal.

2. Fix _build_context_section() docstring — correct example path from
   '.specify/plans/plan.md' to 'specs/<feature>/plan.md'.

3. Improve .mdc frontmatter-only detection in remove_context_section()
   — use regex to match any YAML frontmatter block (not just the exact
   Speckit-generated one), so .mdc files with additional frontmatter
   keys are also cleaned up when no body content remains.

* fix: handle corrupted markers and parse .mdc frontmatter robustly

1. Handle partial/corrupted markers in upsert_context_section() —
   if only the START marker exists (no END), replace from START
   through EOF. If only the END marker exists, replace from BOF
   through END. This keeps upsert idempotent even when a user
   accidentally deletes one marker.

2. Parse .mdc YAML frontmatter properly — new _ensure_mdc_frontmatter()
   helper parses existing frontmatter and ensures alwaysApply: true is
   set, rather than just checking for the --- delimiter. Handles
   missing frontmatter, existing frontmatter without alwaysApply, and
   already-correct frontmatter.

* fix: preserve .mdc frontmatter, add tests, clean up on switch

1. Rewrite _ensure_mdc_frontmatter() with regex — preserves comments,
   formatting, and custom keys in existing frontmatter instead of
   destructively re-serializing via yaml.safe_dump(). Inserts or
   fixes alwaysApply: true in place.

2. Add 6 focused .mdc frontmatter tests to cursor-agent test file:
   new file creation, missing frontmatter, preserved custom keys,
   wrong alwaysApply value, idempotent upserts, removal cleanup.

3. Call remove_context_section() during integration switch Phase 1 —
   prevents stale SPECKIT markers from being left in the old
   integration's context file. Also clear context_file from
   init-options during the metadata reset.

* fix: remove unused MDC_FRONTMATTER, preserve inline comments, normalize bare CR

1. Remove unused MDC_FRONTMATTER class variable — dead code after
   _ensure_mdc_frontmatter() was rewritten with regex.

2. Preserve inline comments when fixing alwaysApply — the regex
   substitution now captures trailing '# comment' text and keeps it.

3. Normalize bare CR in upsert_context_section() — match the
   behavior of remove_context_section() which already normalizes
   both CRLF and bare CR.

4. Clarify .mdc removal comment — 'treat frontmatter-only as empty'
   instead of misleading 'strip frontmatter'.

* fix: handle corrupted markers in remove, CRLF-safe end-marker consumption

1. Handle corrupted markers in remove_context_section() — mirror
   upsert's behavior: start-only removes start→EOF, end-only removes
   BOF→end. Previously bailed out leaving partial markers behind.

2. CRLF-safe end-marker consumption — both upsert and remove now
   handle \r\n after the end marker, not just \n. Prevents extra
   blank lines at replacement boundaries in CRLF files.

3. Clarify path rule in plan template — distinguish filesystem
   operations (absolute paths) from documentation/agent context
   references (project-relative paths).

* fix: only remove context section when both markers are well-ordered

remove_context_section() previously treated mismatched markers as
corruption and aggressively removed from BOF→end-marker or
start-marker→EOF, which could delete user-authored content if only
one marker remained. Now it only removes when both START and END
markers exist and are properly ordered, returning False otherwise.
2026-04-17 13:57:51 -05:00
Manfred Riem
518dc9ddad Add Community Friends page to docs site (#2261)
Move the Community Friends section from the main README into a dedicated
docs page at docs/community/friends.md, following the same structure as
the Reference section.

- New: docs/community/friends.md with content from README
- Updated: docs/toc.yml with Community section and Friends entry
- Updated: docs/docfx.json to include community/*.md in content glob
- Updated: README.md to link to the new docs page instead of inline list
2026-04-17 13:33:20 -05:00
Quratulain-bilal
13b614e9d5 Add Spec Scope extension to community catalog (#2172)
- Adds scope entry to catalog.community.json (between review and security-review)
- Adds Spec Scope row to community extensions table in README.md (between Spec Refine and Spec Sync)
- Bumps top-level updated_at to 2026-04-16T19:00:00Z
2026-04-17 11:40:19 -05:00
Ismael
3b82e0bcdd docs: add Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace to README (#2250)
* docs: add Claude Code / Copilot plugin installation option

Add Option 4 to README installation section documenting plugin-based installation via Claude Code and Copilot CLI marketplace commands

* docs(readme): move cc-spec-kit plugin to Community Friends

Relocate the cc-spec-kit plugin reference to the Community Friends
2026-04-17 09:54:08 -05:00
saram ali
ba9a8b8e59 fix: suppress CRLF warnings in auto-commit.ps1 (#2258)
* fix: suppress CRLF warnings in auto-commit.ps1 (#2253)

Replace 2> with 2>&1 redirection and  assignment to properly
suppress stderr output including CRLF warnings on Windows. Exit code
logic preserved for change detection.

Fixes #2253

* fix: use SilentlyContinue for CRLF stderr handling, add tests

The 2>&1 approach still raises terminating errors under
$ErrorActionPreference='Stop'.  Instead, temporarily set
SilentlyContinue around all native git calls that may emit
CRLF warnings to stderr (rev-parse, diff, ls-files, add, commit).

Adds 5 pytest tests (TestAutoCommitPowerShellCRLF) that set
core.autocrlf=true with LF-ending files.  On Windows runners
this triggers actual CRLF warnings; on other platforms the tests
pass trivially.

Fixes #2253

* refactor: address Copilot review feedback

- Use 'Continue' instead of 'SilentlyContinue' so error output is
  still captured in $out for diagnostics on real git failures.
- Wrap all three EAP save/restore blocks in try/finally to guarantee
  restoration even on unexpected exceptions.
- Fix CRLF test to commit a tracked LF file first, then modify it,
  so git diff --quiet HEAD actually inspects the tracked change and
  triggers the CRLF warning on Windows.

* test: assert CRLF warning fires on Windows

On Windows, probe git diff stderr before running the script to verify
the test setup actually produces the expected CRLF warning.  This
makes the regression test deterministic on the Windows runner.  On
non-Windows the probe is skipped (warnings don't fire there).

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
2026-04-17 09:27:45 -05:00
김준호
dedcae7cd8 feat: register Blueprint in community catalog (#2252)
* feat: add Blueprint extension to community catalog

- Extension ID: blueprint
- Version: 1.0.0
- Author: chordpli
- Repository: https://github.com/chordpli/spec-kit-blueprint

* fix: update catalog root updated_at to current timestamp

* fix: update hooks count to 1 (removed before_implement)

* fix: use canonical /speckit.implement command name in description
2026-04-17 08:14:24 -05:00
adaumann
2c11525be5 preset: Update preset-fiction-book-writing to community catalog -> v1.5.0 (#2256)
* Update preset-fiction-book-writing to community catalog

- Preset ID: fiction-book-writing
- Version: 1.5.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc. V1.5.0: Support interactive, audiobooks, series, workflow corrections

* Update presets/catalog.community.json

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

* Update README.md

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

* Update README.md

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

* Update presets/catalog.community.json

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-17 08:04:43 -05:00
dependabot[bot]
ca382992f7 chore(deps): bump actions/upload-pages-artifact from 3 to 5 (#2251)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 06:46:47 -05:00
Manfred Riem
669e253809 fix: add reference/*.md to docfx content glob (#2248)
Without this, the reference subdirectory pages are not included
in the docfx build and return 404 on the published site.
2026-04-16 14:12:25 -05:00
Manfred Riem
26fab003ee chore: release 0.7.2, begin 0.7.3.dev0 development (#2247)
* chore: bump version to 0.7.2

* chore: begin 0.7.3.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-16 14:03:59 -05:00
115 changed files with 6609 additions and 3727 deletions

View File

@@ -51,7 +51,7 @@ jobs:
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: 'docs/_site'

29
.zenodo.json Normal file
View File

@@ -0,0 +1,29 @@
{
"title": "Spec Kit",
"description": "Spec Kit is an open source toolkit for Spec-Driven Development (SDD) — a methodology that helps software teams build high-quality software faster by focusing on product scenarios and predictable outcomes. It provides the Specify CLI, slash-command templates, extensions, presets, workflows, and integrations for popular AI coding agents.",
"creators": [
{
"name": "Delimarsky, Den"
},
{
"name": "Riem, Manfred"
}
],
"license": "MIT",
"upload_type": "software",
"keywords": [
"spec-driven development",
"ai coding agents",
"software engineering",
"cli",
"copilot",
"specification"
],
"related_identifiers": [
{
"identifier": "https://github.com/github/spec-kit",
"relation": "isSupplementTo",
"scheme": "url"
}
]
}

View File

@@ -264,13 +264,13 @@ The base classes handle most work automatically. Override only when the agent de
| Override | When to use | Example |
|---|---|---|
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag, Copilot → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` (default) or `speckit-<name>/SKILL.md` (skills mode) |
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |
**Example — Copilot (fully custom `setup`):**
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. It also supports a `--skills` mode that scaffolds `speckit-<name>/SKILL.md` under `.github/skills/` using composition with an internal `_CopilotSkillsHelper`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
### 7. Update Devcontainer files (Optional)
@@ -391,6 +391,24 @@ Implementation: Extends `IntegrationBase` with custom `setup()` method that:
2. Generates companion `.prompt.md` files
3. Merges VS Code settings
**Skills mode (`--skills`):** Copilot also supports an alternative skills-based layout
via `--integration-options="--skills"`. When enabled:
- Commands are scaffolded as `speckit-<name>/SKILL.md` under `.github/skills/`
- No companion `.prompt.md` files are generated
- No `.vscode/settings.json` merge
- `post_process_skill_content()` injects a `mode: speckit.<stem>` frontmatter field
- `build_command_invocation()` returns `/speckit-<stem>` instead of bare args
The two modes are mutually exclusive — a project uses one or the other:
```bash
# Default mode: .agent.md agents + .prompt.md companions + settings merge
specify init my-project --integration copilot
# Skills mode: speckit-<name>/SKILL.md under .github/skills/
specify init my-project --integration copilot --integration-options="--skills"
```
### Forge Integration
Forge has special frontmatter and argument requirements:

View File

@@ -2,6 +2,81 @@
<!-- insert new changelog below this comment -->
## [0.8.0] - 2026-04-23
### Changed
- feat(presets): Composition strategies (prepend, append, wrap) for templates, commands, and scripts (#2133)
- feat(copilot): support `--integration-options="--skills"` for skills-based scaffolding (#2324)
- docs(install): add pipx as alternative installation method (#2288)
- Add Memory MD community extension (#2327)
- Update version-guard to v1.2.0 (#2321)
- fix: `--force` now overwrites shared infra files during init and upgrade (#2320)
- chore: release 0.7.5, begin 0.7.6.dev0 development (#2322)
## [0.7.5] - 2026-04-22
### Changed
- fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi (#2313)
- feat(cli): add specify self check and self upgrade stub (#2316)
- Update version-guard to v1.1.0 (#2318)
- docs: move community presets from README to docs/community (#2314)
- catalog: add wireframe extension (v0.1.1) (#2262)
- Move community walkthroughs from README to docs/community (#2312)
- docs(readme): list red-team in community-extensions table (#2311)
- feat(catalog): add red-team extension to community catalog (#2306)
- Add superpowers-bridge community extension (#2309)
- feat: implement preset wrap strategy (#2189)
- fix(agents): block directory traversal in command write paths (#2229) (#2296)
- chore: release 0.7.4, begin 0.7.5.dev0 development (#2299)
## [0.7.4] - 2026-04-21
### Changed
- fix(copilot): use --yolo to grant all permissions in non-interactive mode (#2298)
- feat: add CITATION.cff and .zenodo.json for academic citation support (#2291)
- Add spec-validate to community catalog (#2274)
- feat: register Ripple in community catalog (#2272)
- Add version-guard to community catalog (#2286)
- Add spec-reference-loader to community catalog (#2285)
- Add memory-loader to community catalog (#2284)
- fix(integrations): strip UTF-8 BOM when reading agent context files (#2283)
- Preset fiction book writing1.6 (#2270)
- fix(integrations): migrate Antigravity (agy) layout to .agents/ and deprecate --skills (#2276)
- chore: release 0.7.3, begin 0.7.4.dev0 development (#2263)
## [0.7.3] - 2026-04-17
### Changed
- fix: replace shell-based context updates with marker-based upsert (#2259)
- Add Community Friends page to docs site (#2261)
- Add Spec Scope extension to community catalog (#2172)
- docs: add Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace to README (#2250)
- fix: suppress CRLF warnings in auto-commit.ps1 (#2258)
- feat: register Blueprint in community catalog (#2252)
- preset: Update preset-fiction-book-writing to community catalog -> v1.5.0 (#2256)
- chore(deps): bump actions/upload-pages-artifact from 3 to 5 (#2251)
- fix: add reference/*.md to docfx content glob (#2248)
- chore: release 0.7.2, begin 0.7.3.dev0 development (#2247)
## [0.7.2] - 2026-04-16
### Changed
- docs: add core commands reference and simplify README CLI section (#2245)
- docs: add workflows reference, reorganize into docs/reference/, and add --version flag (#2244)
- docs: add presets reference page and rename pack_id to preset_id (#2243)
- docs: add extensions reference page and integrations FAQ (#2242)
- docs: consolidate integration documentation into docs/integrations.md (#2241)
- feat: update memorylint and superpowers-bridge versions to 1.3.0 with new download URLs (#2240)
- feat: Integration catalog — discovery, versioning, and community distribution (#2130)
- Add Catalog CI extension to community catalog (#2239)
- Added issues extension (#2194)
- chore: release 0.7.1, begin 0.7.2.dev0 development (#2235)
## [0.7.1] - 2026-04-15
### Changed

31
CITATION.cff Normal file
View File

@@ -0,0 +1,31 @@
cff-version: 1.2.0
message: >-
If you use Spec Kit in your research or reference it in a paper,
please cite it using the metadata below.
type: software
title: "Spec Kit"
abstract: >-
Spec Kit is an open source toolkit for Spec-Driven Development (SDD) —
a methodology that helps software teams build high-quality software faster
by focusing on product scenarios and predictable outcomes. It provides the
Specify CLI, slash-command templates, extensions, presets, workflows, and
integrations for popular AI coding agents.
authors:
- given-names: Den
family-names: Delimarsky
alias: localden
- given-names: Manfred
family-names: Riem
alias: mnriem
repository-code: "https://github.com/github/spec-kit"
url: "https://github.github.io/spec-kit/"
license: MIT
version: "0.7.3"
date-released: "2026-04-17"
keywords:
- spec-driven development
- ai coding agents
- software engineering
- cli
- copilot
- specification

View File

@@ -62,6 +62,10 @@ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@vX
# Or install latest from main (may include unreleased changes)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
# Alternative: using pipx (also works)
pipx install git+https://github.com/github/spec-kit.git@vX.Y.Z
pipx install git+https://github.com/github/spec-kit.git
```
Then verify the correct version is installed:
@@ -89,6 +93,7 @@ To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed inst
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z
# pipx users: pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z
```
#### Option 2: One-time Usage
@@ -195,6 +200,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
| Brownfield Bootstrap | Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally | `process` | Read+Write | [spec-kit-brownfield](https://github.com/Quratulain-bilal/spec-kit-brownfield) |
| Bugfix Workflow | Structured bugfix workflow — capture bugs, trace to spec artifacts, and patch specs surgically | `process` | Read+Write | [spec-kit-bugfix](https://github.com/Quratulain-bilal/spec-kit-bugfix) |
@@ -222,6 +228,8 @@ The following community-contributed extensions are available in [`catalog.commun
| MAQA Jira Integration | Jira integration for MAQA — syncs Stories and Subtasks as features progress through the board | `integration` | Read+Write | [spec-kit-maqa-jira](https://github.com/GenieRobot/spec-kit-maqa-jira) |
| MAQA Linear Integration | Linear integration for MAQA — syncs issues and sub-issues across workflow states as features progress | `integration` | Read+Write | [spec-kit-maqa-linear](https://github.com/GenieRobot/spec-kit-maqa-linear) |
| MAQA Trello Integration | Trello board integration for MAQA — populates board from specs, moves cards, real-time checklist ticking | `integration` | Read+Write | [spec-kit-maqa-trello](https://github.com/GenieRobot/spec-kit-maqa-trello) |
| Memory Loader | Loads .specify/memory/ files before lifecycle commands so LLM agents have project governance context | `docs` | Read-only | [spec-kit-memory-loader](https://github.com/KevinBrown5280/spec-kit-memory-loader) |
| Memory MD | Repository-native durable memory for Spec Kit projects | `docs` | Read+Write | [spec-kit-memory-hub](https://github.com/DyanGalih/spec-kit-memory-hub) |
| MemoryLint | Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution. | `process` | Read+Write | [memorylint](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint) |
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
@@ -234,27 +242,35 @@ The following community-contributed extensions are available in [`catalog.commun
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
| Red Team | Adversarial review of specs before /speckit.plan — parallel lens agents surface risks that clarify/analyze structurally can't (prompt injection, integrity gaps, cross-spec drift, silent failures). Produces a structured findings report; no auto-edits to specs. | `docs` | Read+Write | [spec-kit-red-team](https://github.com/ashbrener/spec-kit-red-team) |
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
| Retro Extension | Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions | `process` | Read+Write | [spec-kit-retro](https://github.com/arunt14/spec-kit-retro) |
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | `docs` | Read+Write | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | `code` | Read-only | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
| Ripple | Detect side effects that tests can't catch after implementation — delta-anchored analysis across 9 domain-agnostic categories | `code` | Read+Write | [spec-kit-ripple](https://github.com/chordpli/spec-kit-ripple) |
| SDD Utilities | Resume interrupted workflows, validate project health, and verify spec-to-task traceability | `process` | Read+Write | [speckit-utils](https://github.com/mvanhorn/speckit-utils) |
| Security Review | Comprehensive security audit of codebases using AI-powered DevSecOps analysis | `code` | Read-only | [spec-kit-security-review](https://github.com/DyanGalih/spec-kit-security-review) |
| SFSpeckit | Enterprise Salesforce SDLC with 18 commands for the full SDD lifecycle. | `process` | Read+Write | [spec-kit-sf](https://github.com/ysumanth06/spec-kit-sf) |
| Ship Release Extension | Automates release pipeline: pre-flight checks, branch sync, changelog generation, CI verification, and PR creation | `process` | Read+Write | [spec-kit-ship](https://github.com/arunt14/spec-kit-ship) |
| Spec Reference Loader | Reads the ## References section from the feature spec and loads only the listed docs into context | `docs` | Read-only | [spec-kit-spec-reference-loader](https://github.com/KevinBrown5280/spec-kit-spec-reference-loader) |
| Spec Critique Extension | Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives | `docs` | Read-only | [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) |
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
| Spec Scope | Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase | `process` | Read-only | [spec-kit-scope-](https://github.com/Quratulain-bilal/spec-kit-scope-) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| Spec Validate | Comprehension validation, review gating, and approval state for spec-kit artifacts — staged quizzes, peer review SLA, and a hard gate before /speckit.implement | `process` | Read+Write | [spec-kit-spec-validate](https://github.com/aeltayeb/spec-kit-spec-validate) |
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
| Superpowers Bridge (WangX0111) | Bridges spec-kit with obra/superpowers (brainstorming, TDD, subagent, code-review) into a unified, resumable workflow with graceful degradation and session progress tracking | `process` | Read+Write | [superspec](https://github.com/WangX0111/superspec) |
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
| Version Guard | Verify tech stack versions against live npm registries before planning and implementation | `process` | Read-only | [spec-kit-version-guard](https://github.com/KevinBrown5280/spec-kit-version-guard) |
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |
| Wireframe Visual Feedback Loop | SVG wireframe generation, review, and sign-off for spec-driven development. Approved wireframes become spec constraints honored by /speckit.plan, /speckit.tasks, and /speckit.implement | `visibility` | Read+Write | [spec-kit-extension-wireframe](https://github.com/TortoiseWolfe/spec-kit-extension-wireframe) |
| Worktree Isolation | Spawn isolated git worktrees for parallel feature development without checkout switching | `process` | Read+Write | [spec-kit-worktree](https://github.com/Quratulain-bilal/spec-kit-worktree) |
| Worktrees | Default-on worktree isolation for parallel agents — sibling or nested layout | `process` | Read+Write | [spec-kit-worktree-parallel](https://github.com/dango85/spec-kit-worktree-parallel) |
@@ -262,57 +278,15 @@ To submit your own extension, see the [Extension Publishing Guide](extensions/EX
## 🎨 Community Presets
> [!NOTE]
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](presets/catalog.community.json):
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes, author voice sample or humanized AI prose. | 21 templates, 17 commands | — | [spec-kit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
To build and publish your own preset, see the [Presets Publishing Guide](presets/PUBLISHING.md).
Community-contributed presets that customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. See the full list on the [Community Presets](https://github.github.io/spec-kit/community/presets.html) page.
## 🚶 Community Walkthroughs
> [!NOTE]
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
- **[Greenfield Spring Boot + React with a custom extension](https://github.com/mnriem/spec-kit-aide-extension-demo)** — Walks through the **AIDE extension**, a community extension that adds an alternative spec-driven workflow to spec-kit with high-level specs (vision) and low-level specs (work items) organized in a 7-step iterative lifecycle: vision → roadmap → progress tracking → work queue → work items → execution → feedback loops. Uses a family trading platform (Spring Boot 4, React 19, PostgreSQL, Docker Compose) as the scenario to illustrate how the extension mechanism lets you plug in a different style of spec-driven development without changing any core tooling — truly utilizing the "Kit" in Spec Kit.
See Spec-Driven Development in action across different scenarios with community-contributed walkthroughs; find the full list on the [Community Walkthroughs](https://github.github.io/spec-kit/community/walkthroughs.html) page.
## 🛠️ Community Friends
> [!NOTE]
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
Community projects that extend, visualize, or build on Spec Kit:
- **[cc-spex](https://github.com/rhuss/cc-spex)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.
Community projects that extend, visualize, or build on Spec Kit. See the full list on the [Community Friends](https://github.github.io/spec-kit/community/friends.html) page.
## 🤖 Supported AI Coding Agent Integrations
@@ -457,7 +431,7 @@ Our research and experimentation focus on:
- **Linux/macOS/Windows**
- [Supported](#-supported-ai-coding-agent-integrations) AI coding agent.
- [uv](https://docs.astral.sh/uv/) for package management
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pypa.github.io/pipx/) for persistent installation
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)

14
docs/community/friends.md Normal file
View File

@@ -0,0 +1,14 @@
# Community Friends
> [!NOTE]
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
Community projects that extend, visualize, or build on Spec Kit:
- **[cc-spex](https://github.com/rhuss/cc-spex)** — A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.
- **[cc-spec-kit](https://github.com/speckit-community/cc-spec-kit)** — Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace.

20
docs/community/presets.md Normal file
View File

@@ -0,0 +1,20 @@
# Community Presets
> [!NOTE]
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/presets/catalog.community.json):
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 22 templates, 27 commands | — | [spec-kit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
To build and publish your own preset, see the [Presets Publishing Guide](https://github.com/github/spec-kit/blob/main/presets/PUBLISHING.md).

View File

@@ -0,0 +1,20 @@
# Community Walkthroughs
> [!NOTE]
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
- **[Greenfield Spring Boot + React with a custom extension](https://github.com/mnriem/spec-kit-aide-extension-demo)** — Walks through the **AIDE extension**, a community extension that adds an alternative spec-driven workflow to spec-kit with high-level specs (vision) and low-level specs (work items) organized in a 7-step iterative lifecycle: vision → roadmap → progress tracking → work queue → work items → execution → feedback loops. Uses a family trading platform (Spring Boot 4, React 19, PostgreSQL, Docker Compose) as the scenario to illustrate how the extension mechanism lets you plug in a different style of spec-driven development without changing any core tooling — truly utilizing the "Kit" in Spec Kit.

View File

@@ -4,7 +4,9 @@
{
"files": [
"*.md",
"toc.yml"
"toc.yml",
"community/*.md",
"reference/*.md"
]
},
{

View File

@@ -4,7 +4,7 @@
- **Linux/macOS** (or Windows; PowerShell scripts now supported without WSL)
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
- [uv](https://docs.astral.sh/uv/) for package management
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pypa.github.io/pipx/) for persistent installation
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
@@ -24,6 +24,13 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJE
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
```
> [!NOTE]
> For a persistent installation, `pipx` works equally well:
> ```bash
> pipx install git+https://github.com/github/spec-kit.git@vX.Y.Z
> ```
> The project uses a standard `hatchling` build backend and has no uv-specific dependencies.
Or initialize in the current directory:
```bash

View File

@@ -22,6 +22,17 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
uvx --from git+https://github.com/github/spec-kit.git specify init .
```
> [!NOTE]
> You can also install the CLI persistently with `pipx`:
> ```bash
> pipx install git+https://github.com/github/spec-kit.git
> ```
> After installing with `pipx`, run `specify` directly instead of `uvx --from ... specify`, for example:
> ```bash
> specify init <PROJECT_NAME>
> specify init .
> ```
Pick script type explicitly (optional):
```bash

View File

@@ -33,3 +33,13 @@
items:
- name: Local Development
href: local-development.md
# Community
- name: Community
items:
- name: Presets
href: community/presets.md
- name: Walkthroughs
href: community/walkthroughs.md
- name: Friends
href: community/friends.md

View File

@@ -9,6 +9,7 @@
| What to Upgrade | Command | When to Use |
|----------------|---------|-------------|
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | Get latest CLI features without touching project files |
| **CLI Tool Only (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Reinstall/upgrade a pipx-installed CLI to a specific release |
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
@@ -34,6 +35,14 @@ Specify the desired release tag:
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai copilot
```
### If you installed with `pipx`
Upgrade to a specific release:
```bash
pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z
```
### Verify the upgrade
```bash
@@ -53,8 +62,8 @@ When Spec Kit releases new features (like new slash commands or updated template
Running `specify init --here --force` will update:
-**Slash command files** (`.claude/commands/`, `.github/prompts/`, etc.)
-**Script files** (`.specify/scripts/`)
-**Template files** (`.specify/templates/`)
-**Script files** (`.specify/scripts/`)**only with `--force`**; without it, only missing files are added
-**Template files** (`.specify/templates/`)**only with `--force`**; without it, only missing files are added
-**Shared memory files** (`.specify/memory/`) - **⚠️ See warnings below**
### What stays safe?
@@ -94,7 +103,9 @@ Template files will be merged with existing content and may overwrite existing f
Proceed? [y/N]
```
With `--force`, it skips the confirmation and proceeds immediately.
With `--force`, it skips the confirmation and proceeds immediately. It also **overwrites shared infrastructure files** (`.specify/scripts/` and `.specify/templates/`) with the latest versions from the installed Spec Kit release.
Without `--force`, shared infrastructure files that already exist are skipped — the CLI will print a warning listing the skipped files so you know which ones were not updated.
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
@@ -126,13 +137,14 @@ Or use git to restore it:
git restore .specify/memory/constitution.md
```
### 2. Custom template modifications
### 2. Custom script or template modifications
If you customized any templates in `.specify/templates/`, the upgrade will overwrite them. Back them up first:
If you customized files in `.specify/scripts/` or `.specify/templates/`, the `--force` flag will overwrite them. Back them up first:
```bash
# Back up custom templates
# Back up custom templates and scripts
cp -r .specify/templates .specify/templates-backup
cp -r .specify/scripts .specify/scripts-backup
# After upgrade, merge your changes back manually
```

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-16T18:00:00Z",
"updated_at": "2026-04-23T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"aide": {
@@ -170,6 +170,38 @@
"created_at": "2026-03-03T00:00:00Z",
"updated_at": "2026-03-03T00:00:00Z"
},
"blueprint": {
"name": "Blueprint",
"id": "blueprint",
"description": "Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs",
"author": "chordpli",
"version": "1.0.0",
"download_url": "https://github.com/chordpli/spec-kit-blueprint/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/chordpli/spec-kit-blueprint",
"homepage": "https://github.com/chordpli/spec-kit-blueprint",
"documentation": "https://github.com/chordpli/spec-kit-blueprint/blob/main/README.md",
"changelog": "https://github.com/chordpli/spec-kit-blueprint/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": [
"blueprint",
"pre-implementation",
"review",
"scaffolding",
"code-literacy"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-17T00:00:00Z",
"updated_at": "2026-04-17T00:00:00Z"
},
"branch-convention": {
"name": "Branch Convention",
"id": "branch-convention",
@@ -1135,6 +1167,69 @@
"created_at": "2026-03-26T00:00:00Z",
"updated_at": "2026-03-26T00:00:00Z"
},
"memory-loader": {
"name": "Memory Loader",
"id": "memory-loader",
"description": "Loads .specify/memory/ files before spec-kit lifecycle commands so LLM agents have project governance context",
"author": "KevinBrown5280",
"version": "1.0.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-memory-loader/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/KevinBrown5280/spec-kit-memory-loader",
"homepage": "https://github.com/KevinBrown5280/spec-kit-memory-loader",
"documentation": "https://github.com/KevinBrown5280/spec-kit-memory-loader/blob/main/README.md",
"changelog": "https://github.com/KevinBrown5280/spec-kit-memory-loader/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 1,
"hooks": 7
},
"tags": [
"context",
"memory",
"governance",
"hooks"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
},
"memory-md": {
"name": "Memory MD",
"id": "memory-md",
"description": "Repository-native durable memory for Spec Kit projects",
"author": "DyanGalih",
"version": "0.6.2",
"download_url": "https://github.com/DyanGalih/spec-kit-memory-hub/archive/refs/tags/v0.6.2.zip",
"repository": "https://github.com/DyanGalih/spec-kit-memory-hub",
"homepage": "https://github.com/DyanGalih/spec-kit-memory-hub",
"documentation": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/README.md",
"changelog": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/docs/memory-workflow-v0.6.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 5,
"hooks": 0
},
"tags": [
"memory",
"workflow",
"docs",
"copilot",
"markdown"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-23T00:00:00Z",
"updated_at": "2026-04-23T00:00:00Z"
},
"memorylint": {
"name": "MemoryLint",
"id": "memorylint",
@@ -1460,6 +1555,38 @@
"created_at": "2026-03-14T00:00:00Z",
"updated_at": "2026-03-14T00:00:00Z"
},
"red-team": {
"name": "Red Team",
"id": "red-team",
"description": "Adversarial review of functional specs before /speckit.plan. Parallel adversarial lens agents catch hostile actors, silent failures, and regulatory blind spots that clarify/analyze cannot.",
"author": "Ash Brener",
"version": "1.0.2",
"download_url": "https://github.com/ashbrener/spec-kit-red-team/releases/download/v1.0.2/red-team-v1.0.2.zip",
"repository": "https://github.com/ashbrener/spec-kit-red-team",
"homepage": "https://github.com/ashbrener/spec-kit-red-team",
"documentation": "https://github.com/ashbrener/spec-kit-red-team/blob/main/README.md",
"changelog": "https://github.com/ashbrener/spec-kit-red-team/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": [
"adversarial-review",
"quality-gate",
"spec-hardening",
"pre-plan",
"audit"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"refine": {
"name": "Spec Refine",
"id": "refine",
@@ -1625,6 +1752,71 @@
"created_at": "2026-03-06T00:00:00Z",
"updated_at": "2026-04-09T00:00:00Z"
},
"ripple": {
"name": "Ripple",
"id": "ripple",
"description": "Detect side effects that tests can't catch after implementation — delta-anchored analysis across 9 domain-agnostic categories with fix-induced side effect detection",
"author": "chordpli",
"version": "1.0.0",
"download_url": "https://github.com/chordpli/spec-kit-ripple/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/chordpli/spec-kit-ripple",
"homepage": "https://github.com/chordpli/spec-kit-ripple",
"documentation": "https://github.com/chordpli/spec-kit-ripple/blob/main/README.md",
"changelog": "https://github.com/chordpli/spec-kit-ripple/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"side-effects",
"post-implementation",
"analysis",
"quality",
"risk-detection"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
},
"scope": {
"name": "Spec Scope",
"id": "scope",
"description": "Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-scope-/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-scope-",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-scope-",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-scope-/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-scope-/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 4,
"hooks": 1
},
"tags": [
"estimation",
"scope",
"effort",
"planning",
"project-management",
"tracking"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-17T02:00:00Z",
"updated_at": "2026-04-17T02:00:00Z"
},
"security-review": {
"name": "Security Review",
"id": "security-review",
@@ -1731,6 +1923,69 @@
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-01T00:00:00Z"
},
"spec-reference-loader": {
"name": "Spec Reference Loader",
"id": "spec-reference-loader",
"description": "Reads the ## References section from the current feature spec and loads the listed files into context",
"author": "KevinBrown5280",
"version": "1.0.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader",
"homepage": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader",
"documentation": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader/blob/main/README.md",
"changelog": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 1,
"hooks": 6
},
"tags": [
"context",
"references",
"docs",
"hooks"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
},
"spec-validate": {
"name": "Spec Validate",
"id": "spec-validate",
"description": "Comprehension validation, review gating, and approval state for spec-kit artifacts — staged-reveal quizzes, peer review SLA, and a hard gate before /speckit.implement.",
"author": "Ahmed Eltayeb",
"version": "1.0.1",
"download_url": "https://github.com/aeltayeb/spec-kit-spec-validate/archive/refs/tags/v1.0.1.zip",
"repository": "https://github.com/aeltayeb/spec-kit-spec-validate",
"homepage": "https://github.com/aeltayeb/spec-kit-spec-validate",
"documentation": "https://github.com/aeltayeb/spec-kit-spec-validate/blob/main/README.md",
"changelog": "https://github.com/aeltayeb/spec-kit-spec-validate/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.5.0"
},
"provides": {
"commands": 6,
"hooks": 3
},
"tags": [
"validation",
"review",
"quality",
"workflow",
"process"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-21T00:00:00Z"
},
"speckit-utils": {
"name": "SDD Utilities",
"id": "speckit-utils",
@@ -1931,6 +2186,39 @@
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-04-16T14:08:23Z"
},
"superpowers-bridge": {
"name": "Superpowers Bridge",
"id": "superpowers-bridge",
"description": "Bridges spec-kit workflows with obra/superpowers capabilities for brainstorming, TDD, code review, and resumable execution.",
"author": "WangX0111",
"version": "1.0.0",
"download_url": "https://github.com/WangX0111/superspec/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/WangX0111/superspec",
"homepage": "https://github.com/WangX0111/superspec",
"documentation": "https://github.com/WangX0111/superspec/blob/main/README.md",
"changelog": "https://github.com/WangX0111/superspec/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 5,
"hooks": 3
},
"tags": [
"superpowers",
"brainstorming",
"tdd",
"code-review",
"subagent",
"workflow"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"sync": {
"name": "Spec Sync",
"id": "sync",
@@ -2090,6 +2378,37 @@
"created_at": "2026-03-16T00:00:00Z",
"updated_at": "2026-03-16T00:00:00Z"
},
"version-guard": {
"name": "Version Guard",
"id": "version-guard",
"description": "Verify tech stack versions against live registries before planning and implementation",
"author": "KevinBrown5280",
"version": "1.2.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-version-guard/archive/refs/tags/v1.2.0.zip",
"repository": "https://github.com/KevinBrown5280/spec-kit-version-guard",
"homepage": "https://github.com/KevinBrown5280/spec-kit-version-guard",
"documentation": "https://github.com/KevinBrown5280/spec-kit-version-guard/blob/main/README.md",
"changelog": "https://github.com/KevinBrown5280/spec-kit-version-guard/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 3,
"hooks": 4
},
"tags": [
"versioning",
"npm",
"validation",
"hooks"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-22T21:10:00Z"
},
"whatif": {
"name": "What-if Analysis",
"id": "whatif",
@@ -2118,6 +2437,41 @@
"created_at": "2026-04-13T00:00:00Z",
"updated_at": "2026-04-13T00:00:00Z"
},
"wireframe": {
"name": "Wireframe Visual Feedback Loop",
"id": "wireframe",
"description": "SVG wireframe generation, review, and sign-off for spec-driven development. Approved wireframes become spec constraints honored by /speckit.plan, /speckit.tasks, and /speckit.implement.",
"author": "TortoiseWolfe",
"version": "0.1.1",
"download_url": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/archive/refs/tags/v0.1.1.zip",
"repository": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe",
"homepage": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe",
"documentation": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/README.md",
"changelog": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 6,
"hooks": 3
},
"tags": [
"wireframe",
"visual",
"design",
"ui",
"mockup",
"svg",
"feedback-loop",
"sign-off"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"worktree": {
"name": "Worktree Isolation",
"id": "worktree",

View File

@@ -36,10 +36,17 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
exit 0
}
# Temporarily relax ErrorActionPreference so git stderr warnings
# (e.g. CRLF notices on Windows) do not become terminating errors.
$savedEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
git rev-parse --is-inside-work-tree 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { throw "not a repo" }
} catch {
$isRepo = $LASTEXITCODE -eq 0
} finally {
$ErrorActionPreference = $savedEAP
}
if (-not $isRepo) {
Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
exit 0
}
@@ -117,9 +124,16 @@ if (-not $enabled) {
}
# Check if there are changes to commit
$diffHead = git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
$diffCached = git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
$untracked = git ls-files --others --exclude-standard 2>$null
# Relax ErrorActionPreference so CRLF warnings on stderr do not terminate.
$savedEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
$untracked = git ls-files --others --exclude-standard 2>$null
} finally {
$ErrorActionPreference = $savedEAP
}
if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
@@ -136,6 +150,10 @@ if (-not $commitMsg) {
}
# Stage and commit
# Relax ErrorActionPreference so CRLF warnings on stderr do not terminate,
# while still allowing redirected error output to be captured for diagnostics.
$savedEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$out = git add . 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
@@ -144,6 +162,8 @@ try {
} catch {
Write-Warning "[specify] Error: $_"
exit 1
} finally {
$ErrorActionPreference = $savedEAP
}
Write-Host "[OK] Changes committed $phase $commandName"

View File

@@ -41,6 +41,24 @@ The resolution is implemented three times to ensure consistency:
- **Bash**: `resolve_template()` in `scripts/bash/common.sh`
- **PowerShell**: `Resolve-Template` in `scripts/powershell/common.ps1`
### Composition Strategies
Templates, commands, and scripts support a `strategy` field that controls how a preset's content is combined with lower-priority content instead of fully replacing it:
| Strategy | Description | Templates | Commands | Scripts |
|----------|-------------|-----------|----------|---------|
| `replace` (default) | Fully replaces lower-priority content | ✓ | ✓ | ✓ |
| `prepend` | Places content before lower-priority content (separated by a blank line) | ✓ | ✓ | — |
| `append` | Places content after lower-priority content (separated by a blank line) | ✓ | ✓ | — |
| `wrap` | Content contains `{CORE_TEMPLATE}` (templates/commands) or `$CORE_SCRIPT` (scripts) placeholder replaced with lower-priority content | ✓ | ✓ | ✓ |
Composition is recursive — multiple composing presets chain. The `PresetResolver.resolve_content()` method walks the full priority stack bottom-up and applies each layer's strategy.
Content resolution functions for composition:
- **Python**: `PresetResolver.resolve_content()` in `src/specify_cli/presets.py` (templates, commands, and scripts)
- **Bash**: `resolve_template_content()` in `scripts/bash/common.sh` (templates only; command/script composition is handled by the Python resolver)
- **PowerShell**: `Resolve-TemplateContent` in `scripts/powershell/common.ps1` (templates only; command/script composition is handled by the Python resolver)
## Command Registration
When a preset is installed with `type: "command"` entries, the `PresetManager` registers them into all detected agent directories using the shared `CommandRegistrar` from `src/specify_cli/agents.py`.

View File

@@ -61,7 +61,37 @@ specify preset add healthcare-compliance --priority 5 # overrides enterprise-sa
specify preset add pm-workflow --priority 1 # overrides everything
```
Presets **override**, they don't merge. If two presets both provide `spec-template`, the one with the lowest priority number wins entirely.
Presets **override by default**, they don't merge. If two presets both provide `spec-template` with the default `replace` strategy, the one with the lowest priority number wins entirely. However, presets can use **composition strategies** to augment rather than replace content.
### Composition Strategies
Presets can declare a `strategy` per template to control how content is combined. The `name` field identifies which template to compose with in the priority stack, while `file` points to the actual content file (which can differ from the convention path `templates/<name>.md`):
```yaml
provides:
templates:
- type: "template"
name: "spec-template"
file: "templates/spec-addendum.md"
strategy: "append" # adds content after the core template
```
| Strategy | Description |
|----------|-------------|
| `replace` (default) | Fully replaces the lower-priority template |
| `prepend` | Places content **before** the resolved lower-priority template, separated by a blank line |
| `append` | Places content **after** the resolved lower-priority template, separated by a blank line |
| `wrap` | Content contains `{CORE_TEMPLATE}` placeholder (or `$CORE_SCRIPT` for scripts) replaced with the lower-priority content |
**Supported combinations:**
| Type | `replace` | `prepend` | `append` | `wrap` |
|------|-----------|-----------|----------|--------|
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |
Multiple composing presets chain recursively. For example, a security preset with `prepend` and a compliance preset with `append` will produce: security header + core content + compliance footer.
## Catalog Management
@@ -108,13 +138,5 @@ See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset
The following enhancements are under consideration for future releases:
- **Composition strategies** — Allow presets to declare a `strategy` per template instead of the default `replace`:
| Type | `replace` | `prepend` | `append` | `wrap` |
|------|-----------|-----------|----------|--------|
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |
For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.
- **Structural merge strategies** — Parsing Markdown sections for per-section granularity (e.g., "replace only ## Security").
- **Conflict detection** — `specify preset lint` / `specify preset doctor` for detecting composition conflicts.

View File

@@ -108,11 +108,11 @@
"fiction-book-writing": {
"name": "Fiction Book Writing",
"id": "fiction-book-writing",
"version": "1.3.0",
"description": "Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc.",
"version": "1.6.0",
"description": "Spec-Driven Development for novel and long-form fiction. 27 AI commands from idea to submission: story bible governance, 9 POV modes, all major plot structure frameworks, scene-by-scene drafting with quality gates, audiobook pipeline (SSML/ElevenLabs), cover design, sensitivity review, pacing and prose statistics, and pandoc-based export to DOCX/EPUB/LaTeX. Two style modes: author voice sample extraction or humanized-AI prose with 5 craft profiles. 12 languages supported.",
"author": "Andreas Daumann",
"repository": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.3.0.zip",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.6.0.zip",
"homepage": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"documentation": "https://github.com/adaumann/speckit-preset-fiction-book-writing/blob/main/fiction-book-writing/README.md",
"license": "MIT",
@@ -120,25 +120,28 @@
"speckit_version": ">=0.5.0"
},
"provides": {
"templates": 21,
"commands": 17,
"templates": 22,
"commands": 27,
"scripts": 1
},
"tags": [
"writing",
"novel",
"book",
"fiction",
"storytelling",
"creative-writing",
"kdp",
"single-pov",
"multi-pov",
"export"
"export",
"book",
"brainstorming",
"roleplay",
"audiobook",
"language-support"
],
"created_at": "2026-04-09T08:00:00Z",
"updated_at": "2026-04-09T08:00:00Z"
},
"updated_at": "2026-04-19T08:00:00Z"
},
"multi-repo-branching": {
"name": "Multi-Repo Branching",
"id": "multi-repo-branching",

View File

@@ -32,6 +32,15 @@ provides:
templates:
# CUSTOMIZE: Define your template overrides
# Templates are document scaffolds (spec-template.md, plan-template.md, etc.)
#
# Strategy options (optional, defaults to "replace"):
# replace - Fully replaces the lower-priority template (default)
# prepend - Places this content BEFORE the lower-priority template
# append - Places this content AFTER the lower-priority template
# wrap - Uses {CORE_TEMPLATE} placeholder (templates/commands) or
# $CORE_SCRIPT placeholder (scripts), replaced with lower-priority content
#
# Note: Scripts only support "replace" and "wrap" strategies.
- type: "template"
name: "spec-template"
file: "templates/spec-template.md"
@@ -45,6 +54,26 @@ provides:
# description: "Custom plan template"
# replaces: "plan-template"
# COMPOSITION EXAMPLES:
# The `file` field points to the content file (can differ from the
# convention path `templates/<name>.md`). The `name` field identifies
# which template to compose with in the priority stack.
#
# Append additional sections to an existing template:
# - type: "template"
# name: "spec-template"
# file: "templates/spec-addendum.md"
# description: "Add compliance section to spec template"
# strategy: "append"
#
# Wrap a command with preamble/sign-off:
# - type: "command"
# name: "speckit.specify"
# file: "commands/specify-wrapper.md"
# description: "Wrap specify command with compliance checks"
# strategy: "wrap"
# # In the wrapper file, use {CORE_TEMPLATE} where the original content goes
# OVERRIDE EXTENSION TEMPLATES:
# Presets sit above extensions in the resolution stack, so you can
# override templates provided by any installed extension.

View File

@@ -0,0 +1,14 @@
---
description: "Self-test wrap command — pre/post around core"
strategy: wrap
---
## Preset Pre-Logic
preset:self-test wrap-pre
{CORE_TEMPLATE}
## Preset Post-Logic
preset:self-test wrap-post

View File

@@ -56,6 +56,11 @@ provides:
description: "Self-test override of the specify command"
replaces: "speckit.specify"
- type: "command"
name: "speckit.wrap-test"
file: "commands/speckit.wrap-test.md"
description: "Self-test wrap strategy command"
tags:
- "testing"
- "self-test"

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.7.2.dev0"
version = "0.8.0"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
@@ -28,7 +28,6 @@ packages = ["src/specify_cli"]
[tool.hatch.build.targets.wheel.force-include]
# Bundle core assets so `specify init` works without network access (air-gapped / enterprise)
# Page templates (exclude commands/ — bundled separately below to avoid duplication)
"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md"
"templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md"
"templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-template.md"
"templates/plan-template.md" = "specify_cli/core_pack/templates/plan-template.md"

View File

@@ -320,8 +320,9 @@ try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
print(pid)
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
@@ -373,3 +374,225 @@ except Exception:
return 1
}
# Resolve a template name to composed content using composition strategies.
# Reads strategy metadata from preset manifests and composes content
# from multiple layers using prepend, append, or wrap strategies.
#
# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
# Returns composed content string on stdout; exit code 1 if not found.
resolve_template_content() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"
# Collect all layers (highest priority first)
local -a layer_paths=()
local -a layer_strategies=()
# Priority 1: Project overrides (always "replace")
local override="$base/overrides/${template_name}.md"
if [ -f "$override" ]; then
layer_paths+=("$override")
layer_strategies+=("replace")
fi
# Priority 2: Installed presets (sorted by priority from .registry)
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
local sorted_presets=""
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
local yaml_warned=false
while IFS= read -r preset_id; do
# Read strategy and file path from preset manifest
local strategy="replace"
local manifest_file=""
local manifest="$presets_dir/$preset_id/preset.yml"
if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then
# Requires PyYAML; falls back to replace/convention if unavailable
local result
local py_stderr
py_stderr=$(mktemp)
result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" python3 -c "
import sys, os
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(os.environ['SPECKIT_MANIFEST']) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
" 2>"$py_stderr")
local parse_status=$?
if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
IFS=$'\t' read -r strategy manifest_file <<< "$result"
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
fi
if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
yaml_warned=true
fi
rm -f "$py_stderr"
fi
# Try manifest file path first, then convention path
local candidate=""
if [ -n "$manifest_file" ]; then
# Reject absolute paths and parent traversal
case "$manifest_file" in
/*|*../*|../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
local mf="$presets_dir/$preset_id/$manifest_file"
[ -f "$mf" ] && candidate="$mf"
fi
if [ -z "$candidate" ]; then
local cf="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$cf" ] && candidate="$cf"
fi
if [ -n "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("$strategy")
fi
done <<< "$sorted_presets"
fi
else
# python3 failed — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
else
# No python3 or registry — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
fi
# Priority 3: Extension-provided templates (always "replace")
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
case "$(basename "$ext")" in .*) continue;; esac
local candidate="$ext/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
# Priority 4: Core templates (always "replace")
local core="$base/${template_name}.md"
if [ -f "$core" ]; then
layer_paths+=("$core")
layer_strategies+=("replace")
fi
local count=${#layer_paths[@]}
[ "$count" -eq 0 ] && return 1
# Check if any layer uses a non-replace strategy
local has_composition=false
for s in "${layer_strategies[@]}"; do
[ "$s" != "replace" ] && has_composition=true && break
done
# If the top (highest-priority) layer is replace, it wins entirely —
# lower layers are irrelevant regardless of their strategies.
if [ "${layer_strategies[0]}" = "replace" ]; then
cat "${layer_paths[0]}"
return 0
fi
if [ "$has_composition" = false ]; then
cat "${layer_paths[0]}"
return 0
fi
# Find the effective base: scan from highest priority (index 0) downward
# to find the nearest replace layer. Only compose layers above that base.
local base_idx=-1
local i
for (( i=0; i<count; i++ )); do
if [ "${layer_strategies[$i]}" = "replace" ]; then
base_idx=$i
break
fi
done
if [ $base_idx -lt 0 ]; then
return 1 # no base layer found
fi
# Read the base content; compose layers above the base (higher priority)
local content
content=$(cat "${layer_paths[$base_idx]}"; printf x)
content="${content%x}"
for (( i=base_idx-1; i>=0; i-- )); do
local path="${layer_paths[$i]}"
local strat="${layer_strategies[$i]}"
local layer_content
# Preserve trailing newlines
layer_content=$(cat "$path"; printf x)
layer_content="${layer_content%x}"
case "$strat" in
replace) content="$layer_content" ;;
prepend) content="$(printf '%s\n\n%s' "$layer_content" "$content")" ;;
append) content="$(printf '%s\n\n%s' "$content" "$layer_content")" ;;
wrap)
case "$layer_content" in
*'{CORE_TEMPLATE}'*) ;;
*) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;;
esac
while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
local before="${layer_content%%\{CORE_TEMPLATE\}*}"
local after="${layer_content#*\{CORE_TEMPLATE\}}"
layer_content="${before}${content}${after}"
done
content="$layer_content"
;;
*) echo "Error: unknown strategy '$strat'" >&2; return 1 ;;
esac
done
printf '%s' "$content"
return 0
}

View File

@@ -1,857 +0,0 @@
#!/usr/bin/env bash
# Update agent context files with information from plan.md
#
# This script maintains AI agent context files by parsing feature specifications
# and updating agent-specific configuration files with project information.
#
# MAIN FUNCTIONS:
# 1. Environment Validation
# - Verifies git repository structure and branch information
# - Checks for required plan.md files and templates
# - Validates file permissions and accessibility
#
# 2. Plan Data Extraction
# - Parses plan.md files to extract project metadata
# - Identifies language/version, frameworks, databases, and project types
# - Handles missing or incomplete specification data gracefully
#
# 3. Agent File Management
# - Creates new agent context files from templates when needed
# - Updates existing agent files with new project information
# - Preserves manual additions and custom configurations
# - Supports multiple AI agent formats and directory structures
#
# 4. Content Generation
# - Generates language-specific build/test commands
# - Creates appropriate project directory structures
# - Updates technology stacks and recent changes sections
# - Maintains consistent formatting and timestamps
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Forge, Goose, Antigravity or Generic
# - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic
# Leave empty to update all existing agent files
set -e
# Enable strict error handling
set -u
set -o pipefail
#==============================================================================
# Configuration and Global Variables
#==============================================================================
# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
AGENT_TYPE="${1:-}"
# Agent-specific file paths
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md"
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
# Amp, Kiro CLI, IBM Bob, Pi, Forge, and Goose all share AGENTS.md — use AGENTS_FILE to avoid
# updating the same file multiple times.
AMP_FILE="$AGENTS_FILE"
SHAI_FILE="$REPO_ROOT/SHAI.md"
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
KIRO_FILE="$AGENTS_FILE"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$AGENTS_FILE"
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
KIMI_FILE="$REPO_ROOT/KIMI.md"
TRAE_FILE="$REPO_ROOT/.trae/rules/project_rules.md"
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
FORGE_FILE="$AGENTS_FILE"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
# Global variables for parsed plan data
NEW_LANG=""
NEW_FRAMEWORK=""
NEW_DB=""
NEW_PROJECT_TYPE=""
#==============================================================================
# Utility Functions
#==============================================================================
log_info() {
echo "INFO: $1"
}
log_success() {
echo "$1"
}
log_error() {
echo "ERROR: $1" >&2
}
log_warning() {
echo "WARNING: $1" >&2
}
# Track temporary files for cleanup on interrupt
_CLEANUP_FILES=()
# Cleanup function for temporary files
cleanup() {
local exit_code=$?
# Disarm traps to prevent re-entrant loop
trap - EXIT INT TERM
if [ ${#_CLEANUP_FILES[@]} -gt 0 ]; then
for f in "${_CLEANUP_FILES[@]}"; do
rm -f "$f" "$f.bak" "$f.tmp"
done
fi
exit $exit_code
}
# Set up cleanup trap
trap cleanup EXIT INT TERM
#==============================================================================
# Validation Functions
#==============================================================================
validate_environment() {
# Check if we have a current branch/feature (git or non-git)
if [[ -z "$CURRENT_BRANCH" ]]; then
log_error "Unable to determine current feature"
if [[ "$HAS_GIT" == "true" ]]; then
log_info "Make sure you're on a feature branch"
else
log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
fi
exit 1
fi
# Check if plan.md exists
if [[ ! -f "$NEW_PLAN" ]]; then
log_error "No plan.md found at $NEW_PLAN"
log_info "Make sure you're working on a feature with a corresponding spec directory"
if [[ "$HAS_GIT" != "true" ]]; then
log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
fi
exit 1
fi
# Check if template exists (needed for new files)
if [[ ! -f "$TEMPLATE_FILE" ]]; then
log_warning "Template file not found at $TEMPLATE_FILE"
log_warning "Creating new agent files will fail"
fi
}
#==============================================================================
# Plan Parsing Functions
#==============================================================================
extract_plan_field() {
local field_pattern="$1"
local plan_file="$2"
grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \
head -1 | \
sed "s|^\*\*${field_pattern}\*\*: ||" | \
sed 's/^[ \t]*//;s/[ \t]*$//' | \
grep -v "NEEDS CLARIFICATION" | \
grep -v "^N/A$" || echo ""
}
parse_plan_data() {
local plan_file="$1"
if [[ ! -f "$plan_file" ]]; then
log_error "Plan file not found: $plan_file"
return 1
fi
if [[ ! -r "$plan_file" ]]; then
log_error "Plan file is not readable: $plan_file"
return 1
fi
log_info "Parsing plan data from $plan_file"
NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
NEW_DB=$(extract_plan_field "Storage" "$plan_file")
NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
# Log what we found
if [[ -n "$NEW_LANG" ]]; then
log_info "Found language: $NEW_LANG"
else
log_warning "No language information found in plan"
fi
if [[ -n "$NEW_FRAMEWORK" ]]; then
log_info "Found framework: $NEW_FRAMEWORK"
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
log_info "Found database: $NEW_DB"
fi
if [[ -n "$NEW_PROJECT_TYPE" ]]; then
log_info "Found project type: $NEW_PROJECT_TYPE"
fi
}
format_technology_stack() {
local lang="$1"
local framework="$2"
local parts=()
# Add non-empty parts
[[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
[[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")
# Join with proper formatting
if [[ ${#parts[@]} -eq 0 ]]; then
echo ""
elif [[ ${#parts[@]} -eq 1 ]]; then
echo "${parts[0]}"
else
# Join multiple parts with " + "
local result="${parts[0]}"
for ((i=1; i<${#parts[@]}; i++)); do
result="$result + ${parts[i]}"
done
echo "$result"
fi
}
#==============================================================================
# Template and Content Generation Functions
#==============================================================================
get_project_structure() {
local project_type="$1"
if [[ "$project_type" == *"web"* ]]; then
echo "backend/\\nfrontend/\\ntests/"
else
echo "src/\\ntests/"
fi
}
get_commands_for_language() {
local lang="$1"
case "$lang" in
*"Python"*)
echo "cd src && pytest && ruff check ."
;;
*"Rust"*)
echo "cargo test && cargo clippy"
;;
*"JavaScript"*|*"TypeScript"*)
echo "npm test && npm run lint"
;;
*)
echo "# Add commands for $lang"
;;
esac
}
get_language_conventions() {
local lang="$1"
echo "$lang: Follow standard conventions"
}
# Escape sed replacement-side specials for | delimiter.
# & and \ are replacement-side specials; | is our sed delimiter.
_esc_sed() { printf '%s\n' "$1" | sed 's/[\\&|]/\\&/g'; }
create_new_agent_file() {
local target_file="$1"
local temp_file="$2"
local project_name
project_name=$(_esc_sed "$3")
local current_date="$4"
if [[ ! -f "$TEMPLATE_FILE" ]]; then
log_error "Template not found at $TEMPLATE_FILE"
return 1
fi
if [[ ! -r "$TEMPLATE_FILE" ]]; then
log_error "Template file is not readable: $TEMPLATE_FILE"
return 1
fi
log_info "Creating new agent context file from template..."
if ! cp "$TEMPLATE_FILE" "$temp_file"; then
log_error "Failed to copy template file"
return 1
fi
# Replace template placeholders
local project_structure
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
project_structure=$(_esc_sed "$project_structure")
local commands
commands=$(get_commands_for_language "$NEW_LANG")
local language_conventions
language_conventions=$(get_language_conventions "$NEW_LANG")
local escaped_lang=$(_esc_sed "$NEW_LANG")
local escaped_framework=$(_esc_sed "$NEW_FRAMEWORK")
commands=$(_esc_sed "$commands")
language_conventions=$(_esc_sed "$language_conventions")
local escaped_branch=$(_esc_sed "$CURRENT_BRANCH")
# Build technology stack and recent change strings conditionally
local tech_stack
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
elif [[ -n "$escaped_lang" ]]; then
tech_stack="- $escaped_lang ($escaped_branch)"
elif [[ -n "$escaped_framework" ]]; then
tech_stack="- $escaped_framework ($escaped_branch)"
else
tech_stack="- ($escaped_branch)"
fi
local recent_change
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
elif [[ -n "$escaped_lang" ]]; then
recent_change="- $escaped_branch: Added $escaped_lang"
elif [[ -n "$escaped_framework" ]]; then
recent_change="- $escaped_branch: Added $escaped_framework"
else
recent_change="- $escaped_branch: Added"
fi
local substitutions=(
"s|\[PROJECT NAME\]|$project_name|"
"s|\[DATE\]|$current_date|"
"s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
"s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
"s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
"s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
"s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
)
for substitution in "${substitutions[@]}"; do
if ! sed -i.bak -e "$substitution" "$temp_file"; then
log_error "Failed to perform substitution: $substitution"
rm -f "$temp_file" "$temp_file.bak"
return 1
fi
done
# Convert literal \n sequences to actual newlines (portable — works on BSD + GNU)
awk '{gsub(/\\n/,"\n")}1' "$temp_file" > "$temp_file.tmp"
mv "$temp_file.tmp" "$temp_file"
# Clean up backup files from sed -i.bak
rm -f "$temp_file.bak"
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if [[ "$target_file" == *.mdc ]]; then
local frontmatter_file
frontmatter_file=$(mktemp) || return 1
_CLEANUP_FILES+=("$frontmatter_file")
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
return 0
}
update_existing_agent_file() {
local target_file="$1"
local current_date="$2"
log_info "Updating existing agent context file..."
# Use a single temporary file for atomic update
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
_CLEANUP_FILES+=("$temp_file")
# Process the file in one pass
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
local new_tech_entries=()
local new_change_entry=""
# Prepare new technology entries
if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then
new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
fi
# Prepare new change entry
if [[ -n "$tech_stack" ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
fi
# Check if sections exist in the file
local has_active_technologies=0
local has_recent_changes=0
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
has_active_technologies=1
fi
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
has_recent_changes=1
fi
# Process file line by line
local in_tech_section=false
local in_changes_section=false
local tech_entries_added=false
local changes_entries_added=false
local existing_changes_count=0
local file_ended=false
while IFS= read -r line || [[ -n "$line" ]]; do
# Handle Active Technologies section
if [[ "$line" == "## Active Technologies" ]]; then
echo "$line" >> "$temp_file"
in_tech_section=true
continue
elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
# Add new tech entries before closing the section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
echo "$line" >> "$temp_file"
in_tech_section=false
continue
elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then
# Add new tech entries before empty line in tech section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
echo "$line" >> "$temp_file"
continue
fi
# Handle Recent Changes section
if [[ "$line" == "## Recent Changes" ]]; then
echo "$line" >> "$temp_file"
# Add new change entry right after the heading
if [[ -n "$new_change_entry" ]]; then
echo "$new_change_entry" >> "$temp_file"
fi
in_changes_section=true
changes_entries_added=true
continue
elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
echo "$line" >> "$temp_file"
in_changes_section=false
continue
elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then
# Keep only first 2 existing changes
if [[ $existing_changes_count -lt 2 ]]; then
echo "$line" >> "$temp_file"
((existing_changes_count++))
fi
continue
fi
# Update timestamp
if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
else
echo "$line" >> "$temp_file"
fi
done < "$target_file"
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
# If sections don't exist, add them at the end of the file
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
echo "" >> "$temp_file"
echo "## Active Technologies" >> "$temp_file"
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
echo "" >> "$temp_file"
echo "## Recent Changes" >> "$temp_file"
echo "$new_change_entry" >> "$temp_file"
changes_entries_added=true
fi
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if [[ "$target_file" == *.mdc ]]; then
if ! head -1 "$temp_file" | grep -q '^---'; then
local frontmatter_file
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
_CLEANUP_FILES+=("$frontmatter_file")
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
cat "$temp_file" >> "$frontmatter_file"
mv "$frontmatter_file" "$temp_file"
fi
fi
# Move temp file to target atomically
if ! mv "$temp_file" "$target_file"; then
log_error "Failed to update target file"
rm -f "$temp_file"
return 1
fi
return 0
}
#==============================================================================
# Main Agent File Update Function
#==============================================================================
update_agent_file() {
local target_file="$1"
local agent_name="$2"
if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then
log_error "update_agent_file requires target_file and agent_name parameters"
return 1
fi
log_info "Updating $agent_name context file: $target_file"
local project_name
project_name=$(basename "$REPO_ROOT")
local current_date
current_date=$(date +%Y-%m-%d)
# Create directory if it doesn't exist
local target_dir
target_dir=$(dirname "$target_file")
if [[ ! -d "$target_dir" ]]; then
if ! mkdir -p "$target_dir"; then
log_error "Failed to create directory: $target_dir"
return 1
fi
fi
if [[ ! -f "$target_file" ]]; then
# Create new file from template
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
_CLEANUP_FILES+=("$temp_file")
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
if mv "$temp_file" "$target_file"; then
log_success "Created new $agent_name context file"
else
log_error "Failed to move temporary file to $target_file"
rm -f "$temp_file"
return 1
fi
else
log_error "Failed to create new agent file"
rm -f "$temp_file"
return 1
fi
else
# Update existing file
if [[ ! -r "$target_file" ]]; then
log_error "Cannot read existing file: $target_file"
return 1
fi
if [[ ! -w "$target_file" ]]; then
log_error "Cannot write to existing file: $target_file"
return 1
fi
if update_existing_agent_file "$target_file" "$current_date"; then
log_success "Updated existing $agent_name context file"
else
log_error "Failed to update existing agent file"
return 1
fi
fi
return 0
}
#==============================================================================
# Agent Selection and Processing
#==============================================================================
update_specific_agent() {
local agent_type="$1"
case "$agent_type" in
claude)
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
;;
gemini)
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
;;
copilot)
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
;;
cursor-agent)
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
;;
qwen)
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
;;
opencode)
update_agent_file "$AGENTS_FILE" "opencode" || return 1
;;
codex)
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
;;
windsurf)
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
;;
junie)
update_agent_file "$JUNIE_FILE" "Junie" || return 1
;;
kilocode)
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
;;
auggie)
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
;;
roo)
update_agent_file "$ROO_FILE" "Roo Code" || return 1
;;
codebuddy)
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
;;
qodercli)
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
;;
amp)
update_agent_file "$AMP_FILE" "Amp" || return 1
;;
shai)
update_agent_file "$SHAI_FILE" "SHAI" || return 1
;;
tabnine)
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
;;
kiro-cli)
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
;;
agy)
update_agent_file "$AGY_FILE" "Antigravity" || return 1
;;
bob)
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
;;
vibe)
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
;;
kimi)
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
;;
trae)
update_agent_file "$TRAE_FILE" "Trae" || return 1
;;
pi)
update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1
;;
iflow)
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
;;
forge)
update_agent_file "$AGENTS_FILE" "Forge" || return 1
;;
goose)
update_agent_file "$AGENTS_FILE" "Goose" || return 1
;;
generic)
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
;;
*)
log_error "Unknown agent type '$agent_type'"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic"
exit 1
;;
esac
}
# Helper: skip non-existent files and files already updated (dedup by
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
# Uses a linear array instead of associative array for bash 3.2 compatibility.
# Note: defined at top level because bash 3.2 does not support true
# nested/local functions. _updated_paths, _found_agent, and _all_ok are
# initialised exclusively inside update_all_existing_agents so that
# sourcing this script has no side effects on the caller's environment.
_update_if_new() {
local file="$1" name="$2"
[[ -f "$file" ]] || return 0
local real_path
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
local p
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
for p in "${_updated_paths[@]}"; do
[[ "$p" == "$real_path" ]] && return 0
done
fi
# Record the file as seen before attempting the update so that:
# (a) aliases pointing to the same path are not retried on failure
# (b) _found_agent reflects file existence, not update success
_updated_paths+=("$real_path")
_found_agent=true
update_agent_file "$file" "$name"
}
update_all_existing_agents() {
_found_agent=false
_updated_paths=()
local _all_ok=true
_update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false
_update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
_update_if_new "$AGENTS_FILE" "Codex/opencode/Amp/Kiro/Bob/Pi/Forge/Goose" || _all_ok=false
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
_update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false
_update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false
_update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false
_update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false
_update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false
_update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false
_update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false
_update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false
_update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false
_update_if_new "$TRAE_FILE" "Trae" || _all_ok=false
_update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false
# If no agent files exist, create a default Claude file
if [[ "$_found_agent" == false ]]; then
log_info "No existing agent files found, creating default Claude file..."
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
fi
[[ "$_all_ok" == true ]]
}
print_summary() {
echo
log_info "Summary of changes:"
if [[ -n "$NEW_LANG" ]]; then
echo " - Added language: $NEW_LANG"
fi
if [[ -n "$NEW_FRAMEWORK" ]]; then
echo " - Added framework: $NEW_FRAMEWORK"
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
echo " - Added database: $NEW_DB"
fi
echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic]"
}
#==============================================================================
# Main Execution
#==============================================================================
main() {
# Validate environment before proceeding
validate_environment
log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
# Parse the plan file to extract project information
if ! parse_plan_data "$NEW_PLAN"; then
log_error "Failed to parse plan data"
exit 1
fi
# Process based on agent type argument
local success=true
if [[ -z "$AGENT_TYPE" ]]; then
# No specific agent provided - update all existing agent files
log_info "No agent specified, updating all existing agent files..."
if ! update_all_existing_agents; then
success=false
fi
else
# Specific agent provided - update only that agent
log_info "Updating specific agent: $AGENT_TYPE"
if ! update_specific_agent "$AGENT_TYPE"; then
success=false
fi
fi
# Print summary
print_summary
if [[ "$success" == true ]]; then
log_success "Agent context update completed successfully"
exit 0
else
log_error "Agent context update completed with errors"
exit 1
fi
}
# Execute main function if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -287,6 +287,21 @@ function Test-DirHasFiles {
}
}
# Find a usable Python 3 executable (python3, python, or py -3).
# Returns the command/arguments as an array, or $null if none found.
function Get-Python3Command {
if (Get-Command python3 -ErrorAction SilentlyContinue) { return @('python3') }
if (Get-Command python -ErrorAction SilentlyContinue) {
$ver = & python --version 2>&1
if ($ver -match 'Python 3') { return @('python') }
}
if (Get-Command py -ErrorAction SilentlyContinue) {
$ver = & py -3 --version 2>&1
if ($ver -match 'Python 3') { return @('py', '-3') }
}
return $null
}
# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
@@ -315,6 +330,7 @@ function Resolve-Template {
$presets = $registryData.presets
if ($presets) {
$sortedPresets = $presets.PSObject.Properties |
Where-Object { $null -eq $_.Value.enabled -or $_.Value.enabled -ne $false } |
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
ForEach-Object { $_.Name }
}
@@ -354,3 +370,206 @@ function Resolve-Template {
return $null
}
# Resolve a template name to composed content using composition strategies.
# Reads strategy metadata from preset manifests and composes content
# from multiple layers using prepend, append, or wrap strategies.
function Resolve-TemplateContent {
param(
[Parameter(Mandatory=$true)][string]$TemplateName,
[Parameter(Mandatory=$true)][string]$RepoRoot
)
$base = Join-Path $RepoRoot '.specify/templates'
# Collect all layers (highest priority first)
$layerPaths = @()
$layerStrategies = @()
# Priority 1: Project overrides (always "replace")
$override = Join-Path $base "overrides/$TemplateName.md"
if (Test-Path $override) {
$layerPaths += $override
$layerStrategies += 'replace'
}
# Priority 2: Installed presets (sorted by priority from .registry)
$presetsDir = Join-Path $RepoRoot '.specify/presets'
if (Test-Path $presetsDir) {
$registryFile = Join-Path $presetsDir '.registry'
$sortedPresets = @()
if (Test-Path $registryFile) {
try {
$registryData = Get-Content $registryFile -Raw | ConvertFrom-Json
$presets = $registryData.presets
if ($presets) {
$sortedPresets = $presets.PSObject.Properties |
Where-Object { $null -eq $_.Value.enabled -or $_.Value.enabled -ne $false } |
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
ForEach-Object { $_.Name }
}
} catch {
$sortedPresets = @()
}
}
if ($sortedPresets.Count -gt 0) {
$pyCmd = Get-Python3Command
if (-not $pyCmd) {
# Check if any preset has strategy fields that would be ignored
foreach ($pid in $sortedPresets) {
$mf = Join-Path $presetsDir "$pid/preset.yml"
if ((Test-Path $mf) -and (Select-String -Path $mf -Pattern 'strategy:' -Quiet -ErrorAction SilentlyContinue)) {
Write-Warning "No Python 3 found; preset composition strategies will be ignored"
break
}
}
}
$yamlWarned = $false
foreach ($presetId in $sortedPresets) {
# Read strategy and file path from preset manifest
$strategy = 'replace'
$manifestFilePath = ''
$manifest = Join-Path $presetsDir "$presetId/preset.yml"
if ((Test-Path $manifest) -and $pyCmd) {
try {
# Use Python to parse YAML manifest for strategy and file path
$pyArgs = if ($pyCmd.Count -gt 1) { $pyCmd[1..($pyCmd.Count-1)] } else { @() }
$pyStderrFile = [System.IO.Path]::GetTempFileName()
$stratResult = & $pyCmd[0] @pyArgs -c @"
import sys
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(sys.argv[1]) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == sys.argv[2] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
"@ $manifest $TemplateName 2>$pyStderrFile
if ($stratResult) {
$parts = $stratResult.Trim() -split "`t", 2
$strategy = $parts[0].ToLowerInvariant()
if ($parts.Count -gt 1 -and $parts[1]) { $manifestFilePath = $parts[1] }
}
if (-not $yamlWarned -and (Test-Path $pyStderrFile) -and (Get-Content $pyStderrFile -Raw -ErrorAction SilentlyContinue) -match 'yaml_missing') {
Write-Warning "PyYAML not available; composition strategies may be ignored"
$yamlWarned = $true
}
Remove-Item $pyStderrFile -Force -ErrorAction SilentlyContinue
} catch {
$strategy = 'replace'
if ($pyStderrFile) { Remove-Item $pyStderrFile -Force -ErrorAction SilentlyContinue }
}
}
# Try manifest file path first, then convention path
$candidate = $null
if ($manifestFilePath) {
# Reject absolute paths and parent traversal
if ([System.IO.Path]::IsPathRooted($manifestFilePath) -or $manifestFilePath -match '\.\.[\\/]') {
$manifestFilePath = ''
}
}
if ($manifestFilePath) {
$mf = Join-Path $presetsDir "$presetId/$manifestFilePath"
if (Test-Path $mf) { $candidate = $mf }
}
if (-not $candidate) {
$cf = Join-Path $presetsDir "$presetId/templates/$TemplateName.md"
if (Test-Path $cf) { $candidate = $cf }
}
if ($candidate) {
$layerPaths += $candidate
$layerStrategies += $strategy
}
}
} else {
# Fallback: alphabetical directory order (no registry or parse failure)
foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) {
$candidate = Join-Path $preset.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) {
$layerPaths += $candidate
$layerStrategies += 'replace'
}
}
}
}
# Priority 3: Extension-provided templates (always "replace")
$extDir = Join-Path $RepoRoot '.specify/extensions'
if (Test-Path $extDir) {
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) {
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) {
$layerPaths += $candidate
$layerStrategies += 'replace'
}
}
}
# Priority 4: Core templates (always "replace")
$core = Join-Path $base "$TemplateName.md"
if (Test-Path $core) {
$layerPaths += $core
$layerStrategies += 'replace'
}
if ($layerPaths.Count -eq 0) { return $null }
# If the top (highest-priority) layer is replace, it wins entirely —
# lower layers are irrelevant regardless of their strategies.
if ($layerStrategies[0] -eq 'replace') {
return (Get-Content $layerPaths[0] -Raw)
}
# Check if any layer uses a non-replace strategy
$hasComposition = $false
foreach ($s in $layerStrategies) {
if ($s -ne 'replace') { $hasComposition = $true; break }
}
if (-not $hasComposition) {
return (Get-Content $layerPaths[0] -Raw)
}
# Find the effective base: scan from highest priority (index 0) downward
# to find the nearest replace layer. Only compose layers above that base.
$baseIdx = -1
for ($i = 0; $i -lt $layerPaths.Count; $i++) {
if ($layerStrategies[$i] -eq 'replace') {
$baseIdx = $i
break
}
}
if ($baseIdx -lt 0) { return $null }
$content = Get-Content $layerPaths[$baseIdx] -Raw
for ($i = $baseIdx - 1; $i -ge 0; $i--) {
$path = $layerPaths[$i]
$strat = $layerStrategies[$i]
$layerContent = Get-Content $path -Raw
switch ($strat) {
'replace' { $content = $layerContent }
'prepend' { $content = "$layerContent`n`n$content" }
'append' { $content = "$content`n`n$layerContent" }
'wrap' {
if (-not $layerContent.Contains('{CORE_TEMPLATE}')) {
throw "Wrap strategy missing {CORE_TEMPLATE} placeholder"
}
$content = $layerContent.Replace('{CORE_TEMPLATE}', $content)
}
default { throw "Unknown strategy: $strat" }
}
}
return $content
}

View File

@@ -1,515 +0,0 @@
#!/usr/bin/env pwsh
<#!
.SYNOPSIS
Update agent context files with information from plan.md (PowerShell version)
.DESCRIPTION
Mirrors the behavior of scripts/bash/update-agent-context.sh:
1. Environment Validation
2. Plan Data Extraction
3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, forge, goose, generic)
.PARAMETER AgentType
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
.EXAMPLE
./update-agent-context.ps1 -AgentType claude
.EXAMPLE
./update-agent-context.ps1 # Updates all existing agent files
.NOTES
Relies on common helper functions in common.ps1
#>
param(
[Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','vibe','qodercli','kimi','trae','pi','iflow','forge','goose','generic')]
[string]$AgentType
)
$ErrorActionPreference = 'Stop'
# Import common helpers
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDir 'common.ps1')
# Acquire environment paths
$envData = Get-FeaturePathsEnv
$REPO_ROOT = $envData.REPO_ROOT
$CURRENT_BRANCH = $envData.CURRENT_BRANCH
$HAS_GIT = $envData.HAS_GIT
$IMPL_PLAN = $envData.IMPL_PLAN
$NEW_PLAN = $IMPL_PLAN
# Agent file paths
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md'
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md'
$JUNIE_FILE = Join-Path $REPO_ROOT '.junie/AGENTS.md'
$KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md'
$AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md'
$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md'
$CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
$TABNINE_FILE = Join-Path $REPO_ROOT 'TABNINE.md'
$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/project_rules.md'
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
$FORGE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$GOOSE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
# Parsed plan data placeholders
$script:NEW_LANG = ''
$script:NEW_FRAMEWORK = ''
$script:NEW_DB = ''
$script:NEW_PROJECT_TYPE = ''
function Write-Info {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "INFO: $Message"
}
function Write-Success {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "$([char]0x2713) $Message"
}
function Write-WarningMsg {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Warning $Message
}
function Write-Err {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "ERROR: $Message" -ForegroundColor Red
}
function Validate-Environment {
if (-not $CURRENT_BRANCH) {
Write-Err 'Unable to determine current feature'
if ($HAS_GIT) { Write-Info "Make sure you're on a feature branch" } else { Write-Info 'Set SPECIFY_FEATURE environment variable or create a feature first' }
exit 1
}
if (-not (Test-Path $NEW_PLAN)) {
Write-Err "No plan.md found at $NEW_PLAN"
Write-Info 'Ensure you are working on a feature with a corresponding spec directory'
if (-not $HAS_GIT) { Write-Info 'Use: $env:SPECIFY_FEATURE=your-feature-name or create a new feature first' }
exit 1
}
if (-not (Test-Path $TEMPLATE_FILE)) {
Write-Err "Template file not found at $TEMPLATE_FILE"
Write-Info 'Run specify init to scaffold .specify/templates, or add agent-file-template.md there.'
exit 1
}
}
function Extract-PlanField {
param(
[Parameter(Mandatory=$true)]
[string]$FieldPattern,
[Parameter(Mandatory=$true)]
[string]$PlanFile
)
if (-not (Test-Path $PlanFile)) { return '' }
# Lines like **Language/Version**: Python 3.12
$regex = "^\*\*$([Regex]::Escape($FieldPattern))\*\*: (.+)$"
Get-Content -LiteralPath $PlanFile -Encoding utf8 | ForEach-Object {
if ($_ -match $regex) {
$val = $Matches[1].Trim()
if ($val -notin @('NEEDS CLARIFICATION','N/A')) { return $val }
}
} | Select-Object -First 1
}
function Parse-PlanData {
param(
[Parameter(Mandatory=$true)]
[string]$PlanFile
)
if (-not (Test-Path $PlanFile)) { Write-Err "Plan file not found: $PlanFile"; return $false }
Write-Info "Parsing plan data from $PlanFile"
$script:NEW_LANG = Extract-PlanField -FieldPattern 'Language/Version' -PlanFile $PlanFile
$script:NEW_FRAMEWORK = Extract-PlanField -FieldPattern 'Primary Dependencies' -PlanFile $PlanFile
$script:NEW_DB = Extract-PlanField -FieldPattern 'Storage' -PlanFile $PlanFile
$script:NEW_PROJECT_TYPE = Extract-PlanField -FieldPattern 'Project Type' -PlanFile $PlanFile
if ($NEW_LANG) { Write-Info "Found language: $NEW_LANG" } else { Write-WarningMsg 'No language information found in plan' }
if ($NEW_FRAMEWORK) { Write-Info "Found framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Info "Found database: $NEW_DB" }
if ($NEW_PROJECT_TYPE) { Write-Info "Found project type: $NEW_PROJECT_TYPE" }
return $true
}
function Format-TechnologyStack {
param(
[Parameter(Mandatory=$false)]
[string]$Lang,
[Parameter(Mandatory=$false)]
[string]$Framework
)
$parts = @()
if ($Lang -and $Lang -ne 'NEEDS CLARIFICATION') { $parts += $Lang }
if ($Framework -and $Framework -notin @('NEEDS CLARIFICATION','N/A')) { $parts += $Framework }
if (-not $parts) { return '' }
return ($parts -join ' + ')
}
function Get-ProjectStructure {
param(
[Parameter(Mandatory=$false)]
[string]$ProjectType
)
if ($ProjectType -match 'web') { return "backend/`nfrontend/`ntests/" } else { return "src/`ntests/" }
}
function Get-CommandsForLanguage {
param(
[Parameter(Mandatory=$false)]
[string]$Lang
)
switch -Regex ($Lang) {
'Python' { return "cd src; pytest; ruff check ." }
'Rust' { return "cargo test; cargo clippy" }
'JavaScript|TypeScript' { return "npm test; npm run lint" }
default { return "# Add commands for $Lang" }
}
}
function Get-LanguageConventions {
param(
[Parameter(Mandatory=$false)]
[string]$Lang
)
if ($Lang) { "${Lang}: Follow standard conventions" } else { 'General: Follow standard conventions' }
}
function New-AgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[string]$ProjectName,
[Parameter(Mandatory=$true)]
[datetime]$Date
)
if (-not (Test-Path $TEMPLATE_FILE)) { Write-Err "Template not found at $TEMPLATE_FILE"; return $false }
$temp = New-TemporaryFile
Copy-Item -LiteralPath $TEMPLATE_FILE -Destination $temp -Force
$projectStructure = Get-ProjectStructure -ProjectType $NEW_PROJECT_TYPE
$commands = Get-CommandsForLanguage -Lang $NEW_LANG
$languageConventions = Get-LanguageConventions -Lang $NEW_LANG
$escaped_lang = $NEW_LANG
$escaped_framework = $NEW_FRAMEWORK
$escaped_branch = $CURRENT_BRANCH
$content = Get-Content -LiteralPath $temp -Raw -Encoding utf8
$content = $content -replace '\[PROJECT NAME\]',$ProjectName
$content = $content -replace '\[DATE\]',$Date.ToString('yyyy-MM-dd')
# Build the technology stack string safely
$techStackForTemplate = ""
if ($escaped_lang -and $escaped_framework) {
$techStackForTemplate = "- $escaped_lang + $escaped_framework ($escaped_branch)"
} elseif ($escaped_lang) {
$techStackForTemplate = "- $escaped_lang ($escaped_branch)"
} elseif ($escaped_framework) {
$techStackForTemplate = "- $escaped_framework ($escaped_branch)"
}
$content = $content -replace '\[EXTRACTED FROM ALL PLAN.MD FILES\]',$techStackForTemplate
# For project structure we manually embed (keep newlines)
$escapedStructure = [Regex]::Escape($projectStructure)
$content = $content -replace '\[ACTUAL STRUCTURE FROM PLANS\]',$escapedStructure
# Replace escaped newlines placeholder after all replacements
$content = $content -replace '\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]',$commands
$content = $content -replace '\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]',$languageConventions
# Build the recent changes string safely
$recentChangesForTemplate = ""
if ($escaped_lang -and $escaped_framework) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang} + ${escaped_framework}"
} elseif ($escaped_lang) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang}"
} elseif ($escaped_framework) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_framework}"
}
$content = $content -replace '\[LAST 3 FEATURES AND WHAT THEY ADDED\]',$recentChangesForTemplate
# Convert literal \n sequences introduced by Escape to real newlines
$content = $content -replace '\\n',[Environment]::NewLine
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
if ($TargetFile -match '\.mdc$') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine
$content = $frontmatter + $content
}
$parent = Split-Path -Parent $TargetFile
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
Remove-Item $temp -Force
return $true
}
function Update-ExistingAgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[datetime]$Date
)
if (-not (Test-Path $TargetFile)) { return (New-AgentFile -TargetFile $TargetFile -ProjectName (Split-Path $REPO_ROOT -Leaf) -Date $Date) }
$techStack = Format-TechnologyStack -Lang $NEW_LANG -Framework $NEW_FRAMEWORK
$newTechEntries = @()
if ($techStack) {
$escapedTechStack = [Regex]::Escape($techStack)
if (-not (Select-String -Pattern $escapedTechStack -Path $TargetFile -Quiet)) {
$newTechEntries += "- $techStack ($CURRENT_BRANCH)"
}
}
if ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) {
$escapedDB = [Regex]::Escape($NEW_DB)
if (-not (Select-String -Pattern $escapedDB -Path $TargetFile -Quiet)) {
$newTechEntries += "- $NEW_DB ($CURRENT_BRANCH)"
}
}
$newChangeEntry = ''
if ($techStack) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${techStack}" }
elseif ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${NEW_DB}" }
$lines = Get-Content -LiteralPath $TargetFile -Encoding utf8
$output = New-Object System.Collections.Generic.List[string]
$inTech = $false; $inChanges = $false; $techAdded = $false; $changeAdded = $false; $existingChanges = 0
for ($i=0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -eq '## Active Technologies') {
$output.Add($line)
$inTech = $true
continue
}
if ($inTech -and $line -match '^##\s') {
if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true }
$output.Add($line); $inTech = $false; continue
}
if ($inTech -and [string]::IsNullOrWhiteSpace($line)) {
if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true }
$output.Add($line); continue
}
if ($line -eq '## Recent Changes') {
$output.Add($line)
if ($newChangeEntry) { $output.Add($newChangeEntry); $changeAdded = $true }
$inChanges = $true
continue
}
if ($inChanges -and $line -match '^##\s') { $output.Add($line); $inChanges = $false; continue }
if ($inChanges -and $line -match '^- ') {
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
continue
}
if ($line -match '(\*\*)?Last updated(\*\*)?: .*\d{4}-\d{2}-\d{2}') {
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
continue
}
$output.Add($line)
}
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
if ($inTech -and -not $techAdded -and $newTechEntries.Count -gt 0) {
$newTechEntries | ForEach-Object { $output.Add($_) }
}
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') {
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','')
$output.InsertRange(0, $frontmatter)
}
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
return $true
}
function Update-AgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[string]$AgentName
)
if (-not $TargetFile -or -not $AgentName) { Write-Err 'Update-AgentFile requires TargetFile and AgentName'; return $false }
Write-Info "Updating $AgentName context file: $TargetFile"
$projectName = Split-Path $REPO_ROOT -Leaf
$date = Get-Date
$dir = Split-Path -Parent $TargetFile
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
if (-not (Test-Path $TargetFile)) {
if (New-AgentFile -TargetFile $TargetFile -ProjectName $projectName -Date $date) { Write-Success "Created new $AgentName context file" } else { Write-Err 'Failed to create new agent file'; return $false }
} else {
try {
if (Update-ExistingAgentFile -TargetFile $TargetFile -Date $date) { Write-Success "Updated existing $AgentName context file" } else { Write-Err 'Failed to update agent file'; return $false }
} catch {
Write-Err "Cannot access or update existing file: $TargetFile. $_"
return $false
}
}
return $true
}
function Update-SpecificAgent {
param(
[Parameter(Mandatory=$true)]
[string]$Type
)
switch ($Type) {
'claude' { Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code' }
'gemini' { Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI' }
'copilot' { Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot' }
'cursor-agent' { Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE' }
'qwen' { Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code' }
'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' }
'codex' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex CLI' }
'windsurf' { Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf' }
'junie' { Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie' }
'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' }
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
'tabnine' { Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI' }
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' }
'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' }
'forge' { Update-AgentFile -TargetFile $FORGE_FILE -AgentName 'Forge' }
'goose' { Update-AgentFile -TargetFile $GOOSE_FILE -AgentName 'Goose' }
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic'; return $false }
}
}
function Update-AllExistingAgents {
$found = $false
$ok = $true
$updatedPaths = @()
# Helper function to update only if file exists and hasn't been updated yet
function Update-IfNew {
param(
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$true)]
[string]$AgentName
)
if (-not (Test-Path $FilePath)) { return $true }
# Get the real path to detect duplicates (e.g., AMP_FILE, KIRO_FILE, BOB_FILE all point to AGENTS.md)
$realPath = (Get-Item -LiteralPath $FilePath).FullName
# Check if we've already updated this file
if ($updatedPaths -contains $realPath) {
return $true
}
# Record the file as seen before attempting the update
# Use parent scope (1) to modify Update-AllExistingAgents' local variables
Set-Variable -Name updatedPaths -Value ($updatedPaths + $realPath) -Scope 1
Set-Variable -Name found -Value $true -Scope 1
# Perform the update
return (Update-AgentFile -TargetFile $FilePath -AgentName $AgentName)
}
if (-not (Update-IfNew -FilePath $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }
if (-not (Update-IfNew -FilePath $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }
if (-not (Update-IfNew -FilePath $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AGENTS_FILE -AgentName 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge/Goose')) { $ok = $false }
if (-not (Update-IfNew -FilePath $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }
if (-not (Update-IfNew -FilePath $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }
if (-not (Update-IfNew -FilePath $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }
if (-not (Update-IfNew -FilePath $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }
if (-not (Update-IfNew -FilePath $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }
if (-not (Update-IfNew -FilePath $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }
if (-not (Update-IfNew -FilePath $TRAE_FILE -AgentName 'Trae')) { $ok = $false }
if (-not (Update-IfNew -FilePath $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }
if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
}
return $ok
}
function Print-Summary {
Write-Host ''
Write-Info 'Summary of changes:'
if ($NEW_LANG) { Write-Host " - Added language: $NEW_LANG" }
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
Write-Host ''
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic]'
}
function Main {
Validate-Environment
Write-Info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
if (-not (Parse-PlanData -PlanFile $NEW_PLAN)) { Write-Err 'Failed to parse plan data'; exit 1 }
$success = $true
if ($AgentType) {
Write-Info "Updating specific agent: $AgentType"
if (-not (Update-SpecificAgent -Type $AgentType)) { $success = $false }
}
else {
Write-Info 'No agent specified, updating all existing agent files...'
if (-not (Update-AllExistingAgents)) { $success = $false }
}
Print-Summary
if ($success) { Write-Success 'Agent context update completed successfully'; exit 0 } else { Write-Err 'Agent context update completed with errors'; exit 1 }
}
Main

View File

@@ -7,6 +7,8 @@
# "platformdirs",
# "readchar",
# "json5",
# "pyyaml",
# "packaging",
# ]
# ///
"""
@@ -34,8 +36,12 @@ import json
import json5
import stat
import shlex
import urllib.error
import urllib.request
import yaml
from pathlib import Path
from packaging.version import InvalidVersion, Version
from typing import Any, Optional
import typer
@@ -51,6 +57,8 @@ from typer.core import TyperGroup
# For cross-platform keyboard input
import readchar
GITHUB_API_LATEST = "https://api.github.com/repos/github/spec-kit/releases/latest"
def _build_agent_config() -> dict[str, dict[str, Any]]:
"""Derive AGENT_CONFIG from INTEGRATION_REGISTRY."""
from .integrations import INTEGRATION_REGISTRY
@@ -318,7 +326,7 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def
return selected_key
console = Console()
console = Console(highlight=False)
class BannerGroup(TyperGroup):
"""Custom group that shows banner before help."""
@@ -714,12 +722,18 @@ def _install_shared_infra(
project_path: Path,
script_type: str,
tracker: StepTracker | None = None,
force: bool = False,
) -> bool:
"""Install shared infrastructure files into *project_path*.
Copies ``.specify/scripts/`` and ``.specify/templates/`` from the
bundled core_pack or source checkout. Tracks all installed files
in ``speckit.manifest.json``.
When *force* is ``True``, existing files are overwritten with the
latest bundled versions. When ``False`` (default), only missing
files are added and existing ones are skipped.
Returns ``True`` on success.
"""
from .integrations.manifest import IntegrationManifest
@@ -744,12 +758,11 @@ def _install_shared_infra(
if variant_src.is_dir():
dest_variant = dest_scripts / variant_dir
dest_variant.mkdir(parents=True, exist_ok=True)
# Merge without overwriting — only add files that don't exist yet
for src_path in variant_src.rglob("*"):
if src_path.is_file():
rel_path = src_path.relative_to(variant_src)
dst_path = dest_variant / rel_path
if dst_path.exists():
if dst_path.exists() and not force:
skipped_files.append(str(dst_path.relative_to(project_path)))
else:
dst_path.parent.mkdir(parents=True, exist_ok=True)
@@ -770,7 +783,7 @@ def _install_shared_infra(
for f in templates_src.iterdir():
if f.is_file() and f.name != "vscode-settings.json" and not f.name.startswith("."):
dst = dest_templates / f.name
if dst.exists():
if dst.exists() and not force:
skipped_files.append(str(dst.relative_to(project_path)))
else:
shutil.copy2(f, dst)
@@ -778,10 +791,15 @@ def _install_shared_infra(
manifest.record_existing(rel)
if skipped_files:
import logging
logging.getLogger(__name__).warning(
"The following shared files already exist and were not overwritten:\n%s",
"\n".join(f" {f}" for f in skipped_files),
console.print(
f"[yellow]⚠[/yellow] {len(skipped_files)} shared infrastructure file(s) already exist and were not updated:"
)
for f in skipped_files:
console.print(f" {f}")
console.print(
"To refresh shared infrastructure, run "
"[cyan]specify init --here --force[/cyan] or "
"[cyan]specify integration upgrade --force[/cyan]."
)
manifest.save()
@@ -920,7 +938,6 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
# Constants kept for backward compatibility with presets and extensions.
DEFAULT_SKILLS_DIR = ".agents/skills"
NATIVE_SKILLS_AGENTS = {"codex", "kimi"}
SKILL_DESCRIPTIONS = {
"specify": "Create or update feature specifications from natural language descriptions.",
"plan": "Generate technical implementation plans from feature specifications.",
@@ -1115,7 +1132,7 @@ def init(
console.print(f"[cyan]--force supplied: merging into existing directory '[cyan]{project_name}[/cyan]'[/cyan]")
else:
error_panel = Panel(
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
f"Directory already exists: '[cyan]{project_name}[/cyan]'\n"
"Please choose a different project name or remove the existing directory.\n"
"Use [bold]--force[/bold] to merge into the existing directory.",
title="[red]Directory Conflict[/red]",
@@ -1251,6 +1268,12 @@ def init(
integration_parsed_options["commands_dir"] = ai_commands_dir
if ai_skills:
integration_parsed_options["skills"] = True
# Parse --integration-options and merge into parsed_options so
# flags like --skills reach the integration's setup().
if integration_options:
extra = _parse_integration_options(resolved_integration, integration_options)
if extra:
integration_parsed_options.update(extra)
resolved_integration.setup(
project_path, manifest,
@@ -1261,22 +1284,18 @@ def init(
manifest.save()
# Write .specify/integration.json
script_ext = "sh" if selected_script == "sh" else "ps1"
integration_json = project_path / ".specify" / "integration.json"
integration_json.parent.mkdir(parents=True, exist_ok=True)
integration_json.write_text(json.dumps({
"integration": resolved_integration.key,
"version": get_speckit_version(),
"scripts": {
"update-context": f".specify/integrations/{resolved_integration.key}/scripts/update-context.{script_ext}",
},
}, indent=2) + "\n", encoding="utf-8")
tracker.complete("integration", resolved_integration.config.get("name", resolved_integration.key))
# Install shared infrastructure (scripts, templates)
tracker.start("shared-infra")
_install_shared_infra(project_path, selected_script, tracker=tracker)
_install_shared_infra(project_path, selected_script, tracker=tracker, force=force)
tracker.complete("shared-infra", f"scripts ({selected_script}) + templates")
ensure_constitution_from_template(project_path, tracker=tracker)
@@ -1373,15 +1392,17 @@ def init(
"ai": selected_ai,
"integration": resolved_integration.key,
"branch_numbering": branch_numbering or "sequential",
"context_file": resolved_integration.context_file,
"here": here,
"preset": preset,
"script": selected_script,
"speckit_version": get_speckit_version(),
}
# Ensure ai_skills is set for SkillsIntegration so downstream
# tools (extensions, presets) emit SKILL.md overrides correctly.
# Also set for integrations running in skills mode (e.g. Copilot
# with --skills).
from .integrations.base import SkillsIntegration as _SkillsPersist
if isinstance(resolved_integration, _SkillsPersist):
if isinstance(resolved_integration, _SkillsPersist) or getattr(resolved_integration, "_skills_mode", False):
init_opts["ai_skills"] = True
save_init_options(project_path, init_opts)
@@ -1493,7 +1514,7 @@ def init(
# Determine skill display mode for the next-steps panel.
# Skills integrations (codex, kimi, agy, trae, cursor-agent) should show skill invocation syntax.
from .integrations.base import SkillsIntegration as _SkillsInt
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
_is_skills_integration = isinstance(resolved_integration, _SkillsInt) or getattr(resolved_integration, "_skills_mode", False)
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
claude_skill_mode = selected_ai == "claude" and (ai_skills or _is_skills_integration)
@@ -1501,7 +1522,8 @@ def init(
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
trae_skill_mode = selected_ai == "trae"
cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration)
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode
copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode
if codex_skill_mode and not ai_skills:
# Integration path installed skills; show the helpful notice
@@ -1522,7 +1544,7 @@ def init(
return f"/speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit-{name}"
if cursor_agent_skill_mode:
if cursor_agent_skill_mode or copilot_skill_mode:
return f"/speckit-{name}"
return f"/speckit.{name}"
@@ -1603,25 +1625,10 @@ def check():
def version():
"""Display version and system information."""
import platform
import importlib.metadata
show_banner()
# Get CLI version from package metadata
cli_version = "unknown"
try:
cli_version = importlib.metadata.version("specify-cli")
except Exception:
# Fallback: try reading from pyproject.toml if running from source
try:
import tomllib
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
if pyproject_path.exists():
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
cli_version = data.get("project", {}).get("version", "unknown")
except Exception:
pass
cli_version = get_speckit_version()
info_table = Table(show_header=False, box=None, padding=(0, 2))
info_table.add_column("Key", style="cyan", justify="right")
@@ -1644,6 +1651,163 @@ def version():
console.print(panel)
console.print()
def _get_installed_version() -> str:
"""Return the installed specify-cli distribution version or 'unknown'.
Uses importlib.metadata so the value reflects what was actually installed
by pip/uv/pipx — not a value read from pyproject.toml. This is
intentional for `specify self check`, which should reason about the
installed distribution rather than a source-tree fallback. Callers must
treat the sentinel string 'unknown' as an indeterminate value (see FR-020).
"""
import importlib.metadata
metadata_errors = [importlib.metadata.PackageNotFoundError]
invalid_metadata_error = getattr(importlib.metadata, "InvalidMetadataError", None)
if invalid_metadata_error is not None:
metadata_errors.append(invalid_metadata_error)
try:
return importlib.metadata.version("specify-cli")
except tuple(metadata_errors):
return "unknown"
def _normalize_tag(tag: str) -> str:
"""Strip exactly one leading 'v' from a release tag.
Returns the rest of the string unchanged. This handles the common
'vX.Y.Z' tag convention in this repo; it MUST NOT strip more
aggressively (e.g., two leading 'v's keeps one).
"""
return tag[1:] if tag.startswith("v") else tag
def _is_newer(latest: str, current: str) -> bool:
"""Return True iff `latest` is strictly greater than `current` under PEP 440.
Returns False whenever either side is 'unknown' or fails to parse; this
keeps the comparison indeterminate (rather than crashing or falsely
recommending a downgrade) on edge inputs.
"""
if latest == "unknown" or current == "unknown":
return False
try:
return Version(latest) > Version(current)
except InvalidVersion:
return False
def _fetch_latest_release_tag() -> tuple[str | None, str | None]:
"""Return (tag, failure_category). Exactly one outbound call, 5 s timeout.
On success: (tag_name, None).
On a documented network/HTTP failure (added in T029/T030): (None, category).
On anything else — including a malformed response body — the exception
propagates; there is no catch-all (research D-006).
"""
req = urllib.request.Request(
GITHUB_API_LATEST,
headers={"Accept": "application/vnd.github+json"},
)
token = None
for env_var in ("GH_TOKEN", "GITHUB_TOKEN"):
candidate = os.environ.get(env_var)
if candidate is not None:
candidate = candidate.strip()
if candidate:
token = candidate
break
if token:
req.add_header("Authorization", f"Bearer {token}")
try:
with urllib.request.urlopen(req, timeout=5) as resp:
payload = json.loads(resp.read().decode("utf-8"))
tag = payload.get("tag_name")
if not isinstance(tag, str) or not tag:
raise ValueError("GitHub API response missing valid tag_name")
return tag, None
except urllib.error.HTTPError as e:
# Order matters: HTTPError is a subclass of URLError.
if e.code == 403:
return None, "rate limited (try setting GH_TOKEN or GITHUB_TOKEN)"
return None, f"HTTP {e.code}"
except (urllib.error.URLError, OSError):
return None, "offline or timeout"
# ===== Self Commands =====
self_app = typer.Typer(
name="self",
help="Manage the specify CLI itself (read-only check and reserved upgrade command).",
add_completion=False,
)
app.add_typer(self_app, name="self")
@self_app.command("check")
def self_check() -> None:
"""Check whether a newer specify-cli release is available. Read-only.
This command only checks for updates; it does not modify your installation.
The reserved (and currently non-destructive) `specify self upgrade` command
is the name that a future release will use for actual self-upgrade — its
behavior is not implemented in this release and is intentionally out of
scope here. See `specify self upgrade --help` for its current status.
"""
installed = _get_installed_version()
tag, failure_reason = _fetch_latest_release_tag()
if tag is None:
# Graceful-failure path (FR-008). `failure_reason` is one of the
# enumerated strings produced by _fetch_latest_release_tag() — it
# never contains a URL, headers, response body, or traceback.
assert failure_reason is not None
console.print(f"Installed: {installed}")
console.print(f"[yellow]Could not check latest release:[/yellow] {failure_reason}")
return
latest_normalized = _normalize_tag(tag)
if installed == "unknown":
# FR-020: surface the latest release and the recovery action even
# when the local distribution metadata is unavailable.
console.print("Current version could not be determined.")
console.print(f"Latest release: {latest_normalized}")
console.print("\nTo reinstall:")
console.print(" uv tool install specify-cli --force \\")
console.print(f" --from git+https://github.com/github/spec-kit.git@{tag}")
return
if _is_newer(latest_normalized, installed):
console.print(f"[green]Update available:[/green] {installed}{latest_normalized}")
console.print("\nTo upgrade:")
console.print(" uv tool install specify-cli --force \\")
console.print(f" --from git+https://github.com/github/spec-kit.git@{tag}")
return
# Installed is parseable AND is >= latest → "up to date" (FR-006).
# Also reached when the tag is unparseable (InvalidVersion) → _is_newer
# returns False, and the up-to-date branch is the safer default per
# FR-004 / test T016.
console.print(f"[green]Up to date:[/green] {installed}")
@self_app.command("upgrade")
def self_upgrade() -> None:
"""Reserved command surface for self-upgrade; not implemented in this release.
This command is a documented non-destructive stub in this release: it
performs no outbound network request, no install-method detection, and
invokes no installer. It prints a three-line guidance message and exits 0.
Actual self-upgrade is planned as follow-up work.
Use `specify self check` today to see whether a newer release is available
and to get a copy-pasteable reinstall command.
"""
console.print("specify self upgrade is not implemented yet.")
console.print("Run 'specify self check' to see whether a newer release is available.")
console.print("Actual self-upgrade is planned as follow-up work.")
# ===== Extension Commands =====
@@ -1737,18 +1901,13 @@ def _read_integration_json(project_root: Path) -> dict[str, Any]:
def _write_integration_json(
project_root: Path,
integration_key: str,
script_type: str,
) -> None:
"""Write ``.specify/integration.json`` for *integration_key*."""
script_ext = "sh" if script_type == "sh" else "ps1"
dest = project_root / INTEGRATION_JSON
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(json.dumps({
"integration": integration_key,
"version": get_speckit_version(),
"scripts": {
"update-context": f".specify/integrations/{integration_key}/scripts/update-context.{script_ext}",
},
}, indent=2) + "\n", encoding="utf-8")
@@ -1936,7 +2095,7 @@ def integration_install(
raw_options=integration_options,
)
manifest.save()
_write_integration_json(project_root, integration.key, selected_script)
_write_integration_json(project_root, integration.key)
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
except Exception as e:
@@ -2013,9 +2172,10 @@ def _update_init_options_for_integration(
opts = load_init_options(project_root)
opts["integration"] = integration.key
opts["ai"] = integration.key
opts["context_file"] = integration.context_file
if script_type:
opts["script"] = script_type
if isinstance(integration, SkillsIntegration):
if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False):
opts["ai_skills"] = True
else:
opts.pop("ai_skills", None)
@@ -2064,6 +2224,7 @@ def integration_uninstall(
opts.pop("integration", None)
opts.pop("ai", None)
opts.pop("ai_skills", None)
opts.pop("context_file", None)
save_init_options(project_root, opts)
raise typer.Exit(0)
@@ -2082,6 +2243,10 @@ def integration_uninstall(
removed, skipped = manifest.uninstall(project_root, force=force)
# Remove managed context section from the agent context file
if integration:
integration.remove_context_section(project_root)
_remove_integration_json(project_root)
# Update init-options.json to clear the integration
@@ -2090,6 +2255,7 @@ def integration_uninstall(
opts.pop("integration", None)
opts.pop("ai", None)
opts.pop("ai_skills", None)
opts.pop("context_file", None)
save_init_options(project_root, opts)
name = (integration.config or {}).get("name", key) if integration else key
@@ -2156,6 +2322,7 @@ def integration_switch(
)
raise typer.Exit(1)
removed, skipped = old_manifest.uninstall(project_root, force=force)
current_integration.remove_context_section(project_root)
if removed:
console.print(f" Removed {len(removed)} file(s)")
if skipped:
@@ -2186,6 +2353,7 @@ def integration_switch(
opts.pop("integration", None)
opts.pop("ai", None)
opts.pop("ai_skills", None)
opts.pop("context_file", None)
save_init_options(project_root, opts)
# Ensure shared infrastructure is present (safe to run unconditionally;
@@ -2212,7 +2380,7 @@ def integration_switch(
raw_options=integration_options,
)
manifest.save()
_write_integration_json(project_root, target_integration.key, selected_script)
_write_integration_json(project_root, target_integration.key)
_update_init_options_for_integration(project_root, target_integration, script_type=selected_script)
except Exception as e:
@@ -2297,9 +2465,8 @@ def integration_upgrade(
selected_script = _resolve_script_type(project_root, script)
# Ensure shared infrastructure is present (safe to run unconditionally;
# _install_shared_infra merges missing files without overwriting).
_install_shared_infra(project_root, selected_script)
# Ensure shared infrastructure is up to date; --force overwrites existing files.
_install_shared_infra(project_root, selected_script, force=force)
if os.name != "nt":
ensure_executable_scripts(project_root)
@@ -2320,7 +2487,7 @@ def integration_upgrade(
raw_options=integration_options,
)
new_manifest.save()
_write_integration_json(project_root, key, selected_script)
_write_integration_json(project_root, key)
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
except Exception as exc:
# Don't teardown — setup overwrites in-place, so teardown would
@@ -2595,14 +2762,58 @@ def preset_resolve(
raise typer.Exit(1)
resolver = PresetResolver(project_root)
result = resolver.resolve_with_source(template_name)
layers = resolver.collect_all_layers(template_name)
if result:
console.print(f" [bold]{template_name}[/bold]: {result['path']}")
console.print(f" [dim](from: {result['source']})[/dim]")
if layers:
# Use the highest-priority layer for display because the final output
# may be composed and may not map to resolve_with_source()'s single path.
display_layer = layers[0]
console.print(f" [bold]{template_name}[/bold]: {display_layer['path']}")
console.print(f" [dim](top layer from: {display_layer['source']})[/dim]")
has_composition = (
layers[0]["strategy"] != "replace"
and any(layer["strategy"] != "replace" for layer in layers)
)
if has_composition:
# Verify composition is actually possible
try:
composed = resolver.resolve_content(template_name)
except Exception as exc:
composed = None
console.print(f" [yellow]Warning: composition error: {exc}[/yellow]")
if composed is None:
console.print(" [yellow]Warning: composition cannot produce output (no base layer with 'replace' strategy)[/yellow]")
else:
console.print(" [dim]Final output is composed from multiple preset layers; the path above is the highest-priority contributing layer.[/dim]")
console.print("\n [bold]Composition chain:[/bold]")
# Compute the effective base: first replace layer scanning from
# highest priority (matching resolve_content top-down logic).
# Only show layers from the base upward (lower layers are ignored).
effective_base_idx = None
for idx, lyr in enumerate(layers):
if lyr["strategy"] == "replace":
effective_base_idx = idx
break
# Show only contributing layers (base and above)
if effective_base_idx is not None:
contributing = layers[:effective_base_idx + 1]
else:
contributing = layers
for i, layer in enumerate(reversed(contributing)):
strategy_label = layer["strategy"]
if strategy_label == "replace" and i == 0:
strategy_label = "base"
console.print(f" {i + 1}. [{strategy_label}] {layer['source']}{layer['path']}")
else:
console.print(f" [yellow]{template_name}[/yellow]: not found")
console.print(" [dim]No template with this name exists in the resolution stack[/dim]")
# No layers found — fall back to resolve_with_source for non-composition cases
result = resolver.resolve_with_source(template_name)
if result:
console.print(f" [bold]{template_name}[/bold]: {result['path']}")
console.print(f" [dim](from: {result['source']})[/dim]")
else:
console.print(f" [yellow]{template_name}[/yellow]: not found")
console.print(" [dim]No template with this name exists in the resolution stack[/dim]")
@preset_app.command("info")

View File

@@ -6,8 +6,9 @@ Used by both the extension system and the preset system to write
command files into agent-specific directories in the correct format.
"""
import os
from pathlib import Path
from typing import Dict, List, Any
from typing import Dict, List, Any, Optional
import platform
import re
@@ -110,9 +111,9 @@ class CommandRegistrar:
"""Normalize script paths in frontmatter to generated project locations.
Rewrites known repo-relative and top-level script paths under the
`scripts` and `agent_scripts` keys (for example `../../scripts/`,
`../../templates/`, `../../memory/`, `scripts/`, `templates/`, and
`memory/`) to the `.specify/...` paths used in generated projects.
``scripts`` key (for example ``../../scripts/``,
``../../templates/``, ``../../memory/``, ``scripts/``, ``templates/``, and
``memory/``) to the ``.specify/...`` paths used in generated projects.
Args:
frontmatter: Frontmatter dictionary
@@ -122,11 +123,8 @@ class CommandRegistrar:
"""
frontmatter = deepcopy(frontmatter)
for script_key in ("scripts", "agent_scripts"):
scripts = frontmatter.get(script_key)
if not isinstance(scripts, dict):
continue
scripts = frontmatter.get("scripts")
if isinstance(scripts, dict):
for key, script_path in scripts.items():
if isinstance(script_path, str):
scripts[key] = self.rewrite_project_relative_paths(script_path)
@@ -284,7 +282,8 @@ class CommandRegistrar:
if not isinstance(frontmatter, dict):
frontmatter = {}
if agent_name in {"codex", "kimi"}:
agent_config = self.AGENT_CONFIGS.get(agent_name, {})
if agent_config.get("extension") == "/SKILL.md":
body = self.resolve_skill_placeholders(
agent_name, frontmatter, body, project_root
)
@@ -333,11 +332,8 @@ class CommandRegistrar:
frontmatter = {}
scripts = frontmatter.get("scripts", {}) or {}
agent_scripts = frontmatter.get("agent_scripts", {}) or {}
if not isinstance(scripts, dict):
scripts = {}
if not isinstance(agent_scripts, dict):
agent_scripts = {}
init_opts = load_init_options(project_root)
if not isinstance(init_opts, dict):
@@ -351,17 +347,14 @@ class CommandRegistrar:
)
secondary_variant = "sh" if default_variant == "ps" else "ps"
if default_variant in scripts or default_variant in agent_scripts:
if default_variant in scripts:
fallback_order.append(default_variant)
if secondary_variant in scripts or secondary_variant in agent_scripts:
if secondary_variant in scripts:
fallback_order.append(secondary_variant)
for key in scripts:
if key not in fallback_order:
fallback_order.append(key)
for key in agent_scripts:
if key not in fallback_order:
fallback_order.append(key)
script_variant = fallback_order[0] if fallback_order else None
@@ -370,14 +363,12 @@ class CommandRegistrar:
script_command = script_command.replace("{ARGS}", "$ARGUMENTS")
body = body.replace("{SCRIPT}", script_command)
agent_script_command = (
agent_scripts.get(script_variant) if script_variant else None
)
if agent_script_command:
agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS")
body = body.replace("{AGENT_SCRIPT}", agent_script_command)
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
# Resolve __CONTEXT_FILE__ from init-options
context_file = init_opts.get("context_file") or ""
body = body.replace("__CONTEXT_FILE__", context_file)
return CommandRegistrar.rewrite_project_relative_paths(body)
def _convert_argument_placeholder(
@@ -410,6 +401,28 @@ class CommandRegistrar:
return f"speckit-{short_name}"
@staticmethod
def _ensure_inside(candidate: Path, base: Path) -> None:
"""Validate that a write target stays within the expected base directory.
Uses lexical normalization so traversal via ``..`` or absolute paths is
rejected while intentionally symlinked sub-directories remain
supported.
Args:
candidate: Path that will be written.
base: Directory the write must remain within.
Raises:
ValueError: If the normalized candidate path escapes ``base``.
"""
normalized = Path(os.path.normpath(candidate))
base_normalized = Path(os.path.normpath(base))
if not normalized.is_relative_to(base_normalized):
raise ValueError(
f"Output path {candidate!r} escapes directory {base!r}"
)
def register_commands(
self,
agent_name: str,
@@ -456,6 +469,15 @@ class CommandRegistrar:
content = source_file.read_text(encoding="utf-8")
frontmatter, body = self.parse_frontmatter(content)
if frontmatter.get("strategy") == "wrap":
from .presets import _substitute_core_template
body, core_frontmatter = _substitute_core_template(body, cmd_name, project_root, self)
frontmatter = dict(frontmatter)
for key in ("scripts", "agent_scripts"):
if key not in frontmatter and key in core_frontmatter:
frontmatter[key] = core_frontmatter[key]
frontmatter.pop("strategy", None)
frontmatter = self._adjust_script_paths(frontmatter)
for key in agent_config.get("strip_frontmatter_keys", []):
@@ -483,10 +505,12 @@ class CommandRegistrar:
project_root,
)
elif agent_config["format"] == "markdown":
output = self.render_markdown_command(
frontmatter, body, source_id, context_note
)
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
elif agent_config["format"] == "toml":
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_toml_command(frontmatter, body, source_id)
elif agent_config["format"] == "yaml":
output = self.render_yaml_command(
@@ -496,6 +520,7 @@ class CommandRegistrar:
raise ValueError(f"Unsupported format: {agent_config['format']}")
dest_file = commands_dir / f"{output_name}{agent_config['extension']}"
self._ensure_inside(dest_file, commands_dir)
dest_file.parent.mkdir(parents=True, exist_ok=True)
dest_file.write_text(output, encoding="utf-8")
@@ -561,12 +586,7 @@ class CommandRegistrar:
alias_file = (
commands_dir / f"{alias_output_name}{agent_config['extension']}"
)
try:
alias_file.resolve().relative_to(commands_dir.resolve())
except ValueError:
raise ValueError(
f"Alias output path escapes commands directory: {alias_file!r}"
)
self._ensure_inside(alias_file, commands_dir)
alias_file.parent.mkdir(parents=True, exist_ok=True)
alias_file.write_text(alias_output, encoding="utf-8")
if agent_name == "copilot":
@@ -586,6 +606,7 @@ class CommandRegistrar:
prompts_dir = project_root / ".github" / "prompts"
prompts_dir.mkdir(parents=True, exist_ok=True)
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
CommandRegistrar._ensure_inside(prompt_file, prompts_dir)
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
def register_commands_for_all_agents(
@@ -631,6 +652,49 @@ class CommandRegistrar:
return results
def register_commands_for_non_skill_agents(
self,
commands: List[Dict[str, Any]],
source_id: str,
source_dir: Path,
project_root: Path,
context_note: Optional[str] = None,
) -> Dict[str, List[str]]:
"""Register commands for all non-skill agents in the project.
Like register_commands_for_all_agents but skips skill-based agents
(those with extension '/SKILL.md'). Used by reconciliation to avoid
overwriting properly formatted SKILL.md files.
Args:
commands: List of command info dicts
source_id: Identifier of the source
source_dir: Directory containing command source files
project_root: Path to project root
context_note: Custom context comment for markdown output
Returns:
Dictionary mapping agent names to list of registered commands
"""
results = {}
self._ensure_configs()
for agent_name, agent_config in self.AGENT_CONFIGS.items():
if agent_config.get("extension") == "/SKILL.md":
continue
agent_dir = project_root / agent_config["dir"]
if agent_dir.exists():
try:
registered = self.register_commands(
agent_name, commands, source_id,
source_dir, project_root,
context_note=context_note,
)
if registered:
results[agent_name] = registered
except ValueError:
continue
return results
def unregister_commands(
self, registered_commands: Dict[str, List[str]], project_root: Path
) -> None:

View File

@@ -140,11 +140,16 @@ class ExtensionManifest:
"""Load YAML file safely."""
try:
with open(path, 'r') as f:
return yaml.safe_load(f) or {}
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValidationError(f"Invalid YAML in {path}: {e}")
except FileNotFoundError:
raise ValidationError(f"Manifest not found: {path}")
if not isinstance(data, dict):
raise ValidationError(
f"Manifest must be a YAML mapping, got {type(data).__name__}: {path}"
)
return data
def _validate(self):
"""Validate manifest structure and required fields."""

View File

@@ -1,13 +1,18 @@
"""Antigravity (agy) integration — skills-based agent.
Antigravity uses ``.agent/skills/speckit-<name>/SKILL.md`` layout.
Explicit command support was deprecated in version 1.20.5;
``--skills`` defaults to ``True``.
Antigravity uses ``.agents/skills/speckit-<name>/SKILL.md`` layout (enforced since v1.20.5).
"""
from __future__ import annotations
from ..base import IntegrationOption, SkillsIntegration
from pathlib import Path
from typing import TYPE_CHECKING, Any
from ..base import SkillsIntegration
if TYPE_CHECKING:
from ..manifest import IntegrationManifest
class AgyIntegration(SkillsIntegration):
@@ -16,26 +21,32 @@ class AgyIntegration(SkillsIntegration):
key = "agy"
config = {
"name": "Antigravity",
"folder": ".agent/",
"folder": ".agents/",
"commands_subdir": "skills",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".agent/skills",
"dir": ".agents/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Antigravity since v1.20.5)",
),
]
def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
import click
click.secho(
"Warning: The .agents/ layout requires Antigravity v1.20.5 or newer. "
"Please ensure your agy installation is up to date.",
fg="yellow",
err=True,
)
return super().setup(project_root, manifest, parsed_options=parsed_options, **opts)

View File

@@ -1,17 +0,0 @@
# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Amp integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Amp integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie

View File

@@ -84,6 +84,11 @@ class IntegrationBase(ABC):
context_file: str | None = None
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
# -- Markers for managed context section ------------------------------
CONTEXT_MARKER_START = "<!-- SPECKIT START -->"
CONTEXT_MARKER_END = "<!-- SPECKIT END -->"
# -- Public API -------------------------------------------------------
@classmethod
@@ -380,22 +385,235 @@ class IntegrationBase(ABC):
return created
# -- Agent context file management ------------------------------------
@staticmethod
def _ensure_mdc_frontmatter(content: str) -> str:
"""Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``.
If frontmatter is missing, prepend it. If frontmatter exists but
``alwaysApply`` is absent or not ``true``, inject/fix it.
Uses string/regex manipulation to preserve comments and formatting
in existing frontmatter.
"""
import re as _re
leading_ws = len(content) - len(content.lstrip())
leading = content[:leading_ws]
stripped = content[leading_ws:]
if not stripped.startswith("---"):
return "---\nalwaysApply: true\n---\n\n" + content
# Match frontmatter block: ---\n...\n---
match = _re.match(
r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)",
stripped,
_re.DOTALL,
)
if not match:
return "---\nalwaysApply: true\n---\n\n" + content
opening, fm_text, closing, sep, rest = match.groups()
newline = "\r\n" if "\r\n" in opening else "\n"
# Already correct?
if _re.search(
r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text
):
return content
# alwaysApply exists but wrong value — fix in place while preserving
# indentation and any trailing inline comment.
if _re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text):
fm_text = _re.sub(
r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$",
r"\1alwaysApply: true\2",
fm_text,
count=1,
)
elif fm_text.strip():
fm_text = fm_text + newline + "alwaysApply: true"
else:
fm_text = "alwaysApply: true"
return f"{leading}{opening}{fm_text}{closing}{sep}{rest}"
@staticmethod
def _build_context_section(plan_path: str = "") -> str:
"""Build the content for the managed section between markers.
*plan_path* is the project-relative path to the current plan
(e.g. ``"specs/<feature>/plan.md"``). When empty, the section
contains only the generic directive without a concrete path.
"""
lines = [
"For additional context about technologies to be used, project structure,",
"shell commands, and other important information, read the current plan",
]
if plan_path:
lines.append(f"at {plan_path}")
return "\n".join(lines)
def upsert_context_section(
self,
project_root: Path,
plan_path: str = "",
) -> Path | None:
"""Create or update the managed section in the agent context file.
If the context file does not exist it is created with just the
managed section. If it exists, the content between
``<!-- SPECKIT START -->`` and ``<!-- SPECKIT END -->`` markers
is replaced (or appended when no markers are found).
Returns the path to the context file, or ``None`` when
``context_file`` is not set.
"""
if not self.context_file:
return None
ctx_path = project_root / self.context_file
section = (
f"{self.CONTEXT_MARKER_START}\n"
f"{self._build_context_section(plan_path)}\n"
f"{self.CONTEXT_MARKER_END}\n"
)
if ctx_path.exists():
content = ctx_path.read_text(encoding="utf-8-sig")
start_idx = content.find(self.CONTEXT_MARKER_START)
end_idx = content.find(
self.CONTEXT_MARKER_END,
start_idx if start_idx != -1 else 0,
)
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
# Replace existing section (include the end marker + newline)
end_of_marker = end_idx + len(self.CONTEXT_MARKER_END)
# Consume trailing line ending (CRLF or LF)
if end_of_marker < len(content) and content[end_of_marker] == "\r":
end_of_marker += 1
if end_of_marker < len(content) and content[end_of_marker] == "\n":
end_of_marker += 1
new_content = content[:start_idx] + section + content[end_of_marker:]
elif start_idx != -1:
# Corrupted: start marker without end — replace from start through EOF
new_content = content[:start_idx] + section
elif end_idx != -1:
# Corrupted: end marker without start — replace BOF through end marker
end_of_marker = end_idx + len(self.CONTEXT_MARKER_END)
if end_of_marker < len(content) and content[end_of_marker] == "\r":
end_of_marker += 1
if end_of_marker < len(content) and content[end_of_marker] == "\n":
end_of_marker += 1
new_content = section + content[end_of_marker:]
else:
# No markers found — append
if content:
if not content.endswith("\n"):
content += "\n"
new_content = content + "\n" + section
else:
new_content = section
# Ensure .mdc files have required YAML frontmatter
if ctx_path.suffix == ".mdc":
new_content = self._ensure_mdc_frontmatter(new_content)
else:
ctx_path.parent.mkdir(parents=True, exist_ok=True)
# Cursor .mdc files require YAML frontmatter to be loaded
if ctx_path.suffix == ".mdc":
new_content = self._ensure_mdc_frontmatter(section)
else:
new_content = section
normalized = new_content.replace("\r\n", "\n").replace("\r", "\n")
ctx_path.write_bytes(normalized.encode("utf-8"))
return ctx_path
def remove_context_section(self, project_root: Path) -> bool:
"""Remove the managed section from the agent context file.
Returns ``True`` if the section was found and removed. If the
file becomes empty (or whitespace-only) after removal it is
deleted.
"""
if not self.context_file:
return False
ctx_path = project_root / self.context_file
if not ctx_path.exists():
return False
content = ctx_path.read_text(encoding="utf-8-sig")
start_idx = content.find(self.CONTEXT_MARKER_START)
end_idx = content.find(
self.CONTEXT_MARKER_END,
start_idx if start_idx != -1 else 0,
)
# Only remove a complete, well-ordered managed section. If either
# marker is missing, leave the file unchanged to avoid deleting
# unrelated user-authored content.
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
return False
removal_start = start_idx
removal_end = end_idx + len(self.CONTEXT_MARKER_END)
# Consume trailing line ending (CRLF or LF)
if removal_end < len(content) and content[removal_end] == "\r":
removal_end += 1
if removal_end < len(content) and content[removal_end] == "\n":
removal_end += 1
# Also strip a blank line before the section if present
if removal_start > 0 and content[removal_start - 1] == "\n":
if removal_start > 1 and content[removal_start - 2] == "\n":
removal_start -= 1
new_content = content[:removal_start] + content[removal_end:]
# Normalize line endings before comparisons
normalized = new_content.replace("\r\n", "\n").replace("\r", "\n")
# For .mdc files, treat Speckit-generated frontmatter-only content as empty
if ctx_path.suffix == ".mdc":
import re
# Delete the file if only YAML frontmatter remains (no body content)
frontmatter_only = re.match(
r"^---\n.*?\n---\s*$", normalized, re.DOTALL
)
if not normalized.strip() or frontmatter_only:
ctx_path.unlink()
return True
if not normalized.strip():
ctx_path.unlink()
else:
ctx_path.write_bytes(normalized.encode("utf-8"))
return True
@staticmethod
def process_template(
content: str,
agent_name: str,
script_type: str,
arg_placeholder: str = "$ARGUMENTS",
context_file: str = "",
) -> str:
"""Process a raw command template into agent-ready content.
Performs the same transformations as the release script:
1. Extract ``scripts.<script_type>`` value from YAML frontmatter
2. Replace ``{SCRIPT}`` with the extracted script command
3. Extract ``agent_scripts.<script_type>`` and replace ``{AGENT_SCRIPT}``
4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter
5. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
6. Replace ``__AGENT__`` with *agent_name*
3. Strip ``scripts:`` section from frontmatter
4. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
5. Replace ``__AGENT__`` with *agent_name*
6. Replace ``__CONTEXT_FILE__`` with *context_file*
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
"""
# 1. Extract script command from frontmatter
@@ -421,25 +639,7 @@ class IntegrationBase(ABC):
if script_command:
content = content.replace("{SCRIPT}", script_command)
# 3. Extract agent_script command
agent_script_command = ""
in_agent_scripts = False
for line in content.splitlines():
if line.strip() == "agent_scripts:":
in_agent_scripts = True
continue
if in_agent_scripts and line and not line[0].isspace():
in_agent_scripts = False
if in_agent_scripts:
m = script_pattern.match(line)
if m:
agent_script_command = m.group(1).strip()
break
if agent_script_command:
content = content.replace("{AGENT_SCRIPT}", agent_script_command)
# 4. Strip scripts: and agent_scripts: sections from frontmatter
# 3. Strip scripts: section from frontmatter
lines = content.splitlines(keepends=True)
output_lines: list[str] = []
in_frontmatter = False
@@ -457,23 +657,26 @@ class IntegrationBase(ABC):
output_lines.append(line)
continue
if in_frontmatter:
if stripped in ("scripts:", "agent_scripts:"):
if stripped == "scripts:":
skip_section = True
continue
if skip_section:
if line[0:1].isspace():
continue # skip indented content under scripts/agent_scripts
continue # skip indented content under scripts
skip_section = False
output_lines.append(line)
content = "".join(output_lines)
# 5. Replace {ARGS} and $ARGUMENTS
# 4. Replace {ARGS} and $ARGUMENTS
content = content.replace("{ARGS}", arg_placeholder)
content = content.replace("$ARGUMENTS", arg_placeholder)
# 6. Replace __AGENT__
# 5. Replace __AGENT__
content = content.replace("__AGENT__", agent_name)
# 6. Replace __CONTEXT_FILE__
content = content.replace("__CONTEXT_FILE__", context_file)
# 7. Rewrite paths — delegate to the shared implementation in
# CommandRegistrar so extension-local paths are preserved and
# boundary rules stay consistent across the codebase.
@@ -526,6 +729,9 @@ class IntegrationBase(ABC):
self.record_file_in_manifest(dst_file, project_root, manifest)
created.append(dst_file)
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created
def teardown(
@@ -539,9 +745,11 @@ class IntegrationBase(ABC):
Delegates to ``manifest.uninstall()`` which only removes files
whose hash still matches the recorded value (unless *force*).
Also removes the managed context section from the agent file.
Returns ``(removed, skipped)`` file lists.
"""
self.remove_context_section(project_root)
return manifest.uninstall(project_root, force=force)
# -- Convenience helpers for subclasses -------------------------------
@@ -579,8 +787,8 @@ class MarkdownIntegration(IntegrationBase):
(and optionally ``context_file``). Everything else is inherited.
``setup()`` processes command templates (replacing ``{SCRIPT}``,
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
integration-specific scripts (``update-context.sh`` / ``.ps1``).
``{ARGS}``, ``__AGENT__``, rewriting paths) and upserts the
managed context section into the agent context file.
"""
def build_exec_args(
@@ -638,7 +846,8 @@ class MarkdownIntegration(IntegrationBase):
for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(
raw, self.key, script_type, arg_placeholder
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
@@ -646,7 +855,9 @@ class MarkdownIntegration(IntegrationBase):
)
created.append(dst_file)
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created
@@ -841,7 +1052,8 @@ class TomlIntegration(IntegrationBase):
raw = src_file.read_text(encoding="utf-8")
description = self._extract_description(raw)
processed = self.process_template(
raw, self.key, script_type, arg_placeholder
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
_, body = self._split_frontmatter(processed)
toml_content = self._render_toml(description, body)
@@ -851,7 +1063,9 @@ class TomlIntegration(IntegrationBase):
)
created.append(dst_file)
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created
@@ -1021,7 +1235,8 @@ class YamlIntegration(IntegrationBase):
title = self._human_title(src_file.stem)
processed = self.process_template(
raw, self.key, script_type, arg_placeholder
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
_, body = self._split_frontmatter(processed)
yaml_content = self._render_yaml(
@@ -1033,7 +1248,9 @@ class YamlIntegration(IntegrationBase):
)
created.append(dst_file)
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created
@@ -1176,7 +1393,8 @@ class SkillsIntegration(IntegrationBase):
# Process body through the standard template pipeline
processed_body = self.process_template(
raw, self.key, script_type, arg_placeholder
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
# Strip the processed frontmatter — we rebuild it for skills.
# Preserve leading whitespace in the body to match release ZIP
@@ -1220,5 +1438,7 @@ class SkillsIntegration(IntegrationBase):
)
created.append(dst)
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — IBM Bob integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — IBM Bob integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Claude Code integration: create/update CLAUDE.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Claude Code integration: create/update CLAUDE.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — CodeBuddy integration: create/update CODEBUDDY.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codebuddy

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — CodeBuddy integration: create/update CODEBUDDY.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codebuddy

View File

@@ -1,17 +0,0 @@
# update-context.ps1 — Codex CLI integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codex

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Codex CLI integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codex

View File

@@ -5,25 +5,83 @@ Copilot has several unique behaviors compared to standard markdown agents:
- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
- Installs ``.vscode/settings.json`` with prompt file recommendations
- Context file lives at ``.github/copilot-instructions.md``
When ``--skills`` is passed via ``--integration-options``, Copilot scaffolds
commands as ``speckit-<name>/SKILL.md`` directories under ``.github/skills/``
instead. The two modes are mutually exclusive.
"""
from __future__ import annotations
import json
import os
import shutil
import warnings
from pathlib import Path
from typing import Any
from ..base import IntegrationBase
from ..base import IntegrationBase, IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest
def _allow_all() -> bool:
"""Return True if the Copilot CLI should run with full permissions.
Checks ``SPECKIT_COPILOT_ALLOW_ALL_TOOLS`` first (new canonical name).
Falls back to the deprecated ``SPECKIT_ALLOW_ALL_TOOLS`` if set,
emitting a deprecation warning. Default when neither is set: enabled.
"""
new_var = os.environ.get("SPECKIT_COPILOT_ALLOW_ALL_TOOLS")
if new_var is not None:
return new_var != "0"
old_var = os.environ.get("SPECKIT_ALLOW_ALL_TOOLS")
if old_var is not None:
warnings.warn(
"SPECKIT_ALLOW_ALL_TOOLS is deprecated; "
"use SPECKIT_COPILOT_ALLOW_ALL_TOOLS instead.",
UserWarning,
stacklevel=2,
)
return old_var != "0"
return True
class _CopilotSkillsHelper(SkillsIntegration):
"""Internal helper used when Copilot is scaffolded in skills mode.
Not registered in the integration registry — only used as a delegate
by ``CopilotIntegration`` when ``--skills`` is passed.
"""
key = "copilot"
config = {
"name": "GitHub Copilot",
"folder": ".github/",
"commands_subdir": "skills",
"install_url": "https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli",
"requires_cli": False,
}
registrar_config = {
"dir": ".github/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = ".github/copilot-instructions.md"
class CopilotIntegration(IntegrationBase):
"""Integration for GitHub Copilot (VS Code IDE + CLI).
The IDE integration (``requires_cli: False``) installs ``.agent.md``
command files. Workflow dispatch additionally requires the
``copilot`` CLI to be installed separately.
When ``--skills`` is passed via ``--integration-options``, commands
are scaffolded as ``speckit-<name>/SKILL.md`` under ``.github/skills/``
instead of the default ``.agent.md`` + ``.prompt.md`` layout.
"""
key = "copilot"
@@ -42,6 +100,20 @@ class CopilotIntegration(IntegrationBase):
}
context_file = ".github/copilot-instructions.md"
# Mutable flag set by setup() — indicates the active scaffolding mode.
_skills_mode: bool = False
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=False,
help="Scaffold commands as agent skills (speckit-<name>/SKILL.md) instead of .agent.md files",
),
]
def build_exec_args(
self,
prompt: str,
@@ -50,13 +122,15 @@ class CopilotIntegration(IntegrationBase):
output_json: bool = True,
) -> list[str] | None:
# GitHub Copilot CLI uses ``copilot -p "prompt"`` for
# non-interactive mode. --allow-all-tools is required for the
# agent to perform file edits and shell commands. Controlled
# by SPECKIT_ALLOW_ALL_TOOLS env var (default: enabled).
import os
# non-interactive mode. --yolo enables all permissions
# (tools, paths, and URLs) so the agent can perform file
# edits and shell commands without interactive prompts.
# Controlled by SPECKIT_COPILOT_ALLOW_ALL_TOOLS env var
# (default: enabled). The deprecated SPECKIT_ALLOW_ALL_TOOLS
# is also honoured as a fallback.
args = ["copilot", "-p", prompt]
if os.environ.get("SPECKIT_ALLOW_ALL_TOOLS", "1") != "0":
args.append("--allow-all-tools")
if _allow_all():
args.append("--yolo")
if model:
args.extend(["--model", model])
if output_json:
@@ -64,7 +138,19 @@ class CopilotIntegration(IntegrationBase):
return args
def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Copilot agents are not slash-commands — just return the args as prompt."""
"""Build the native invocation for a Copilot command.
Default mode: agents are not slash-commands — return args as prompt.
Skills mode: ``/speckit-<stem>`` slash-command dispatch.
"""
if self._skills_mode:
stem = command_name
if "." in stem:
stem = stem.rsplit(".", 1)[-1]
invocation = f"/speckit-{stem}"
if args:
invocation = f"{invocation} {args}"
return invocation
return args or ""
def dispatch_command(
@@ -82,22 +168,39 @@ class CopilotIntegration(IntegrationBase):
Copilot ``.agent.md`` files are agents, not skills. The CLI
selects them with ``--agent <name>`` and the prompt is just
the user's arguments.
In skills mode, the prompt includes the skill invocation
(``/speckit-<stem>``).
"""
import subprocess
stem = command_name
if "." in stem:
stem = stem.rsplit(".", 1)[-1]
agent_name = f"speckit.{stem}"
prompt = args or ""
import os
cli_args = [
"copilot", "-p", prompt,
"--agent", agent_name,
]
if os.environ.get("SPECKIT_ALLOW_ALL_TOOLS", "1") != "0":
cli_args.append("--allow-all-tools")
# Detect skills mode from project layout when not set via setup()
skills_mode = self._skills_mode
if not skills_mode and project_root:
skills_dir = project_root / ".github" / "skills"
if skills_dir.is_dir():
skills_mode = any(
d.is_dir() and (d / "SKILL.md").is_file()
for d in skills_dir.glob("speckit-*")
)
if skills_mode:
prompt = f"/speckit-{stem}"
if args:
prompt = f"{prompt} {args}"
else:
agent_name = f"speckit.{stem}"
prompt = args or ""
cli_args = ["copilot", "-p", prompt]
if not skills_mode:
cli_args.extend(["--agent", agent_name])
if _allow_all():
cli_args.append("--yolo")
if model:
cli_args.extend(["--model", model])
if not stream:
@@ -141,6 +244,59 @@ class CopilotIntegration(IntegrationBase):
"""Copilot commands use ``.agent.md`` extension."""
return f"speckit.{template_name}.agent.md"
def post_process_skill_content(self, content: str) -> str:
"""Inject Copilot-specific ``mode:`` field into SKILL.md frontmatter.
Inserts ``mode: speckit.<stem>`` before the closing ``---`` so
Copilot can associate the skill with its agent mode.
"""
lines = content.splitlines(keepends=True)
# Extract skill name from frontmatter to derive the mode value
dash_count = 0
skill_name = ""
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1:
if stripped.startswith("mode:"):
return content # already present
if stripped.startswith("name:"):
# Parse: name: "speckit-plan" → speckit.plan
val = stripped.split(":", 1)[1].strip().strip('"').strip("'")
# Convert speckit-plan → speckit.plan
if val.startswith("speckit-"):
skill_name = "speckit." + val[len("speckit-"):]
else:
skill_name = val
if not skill_name:
return content
# Inject mode: before the closing --- of frontmatter
out: list[str] = []
dash_count = 0
injected = False
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2 and not injected:
if line.endswith("\r\n"):
eol = "\r\n"
elif line.endswith("\n"):
eol = "\n"
else:
eol = ""
out.append(f"mode: {skill_name}{eol}")
injected = True
out.append(line)
return "".join(out)
def setup(
self,
project_root: Path,
@@ -150,10 +306,24 @@ class CopilotIntegration(IntegrationBase):
) -> list[Path]:
"""Install copilot commands, companion prompts, and VS Code settings.
Uses base class primitives to: read templates, process them
(replace placeholders, strip script blocks, rewrite paths),
write as ``.agent.md``, then add companion prompts and VS Code settings.
When ``parsed_options["skills"]`` is truthy, delegates to skills
scaffolding (``speckit-<name>/SKILL.md`` under ``.github/skills/``).
Otherwise uses the default ``.agent.md`` + ``.prompt.md`` layout.
"""
parsed_options = parsed_options or {}
self._skills_mode = bool(parsed_options.get("skills"))
if self._skills_mode:
return self._setup_skills(project_root, manifest, parsed_options, **opts)
return self._setup_default(project_root, manifest, parsed_options, **opts)
def _setup_default(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Default mode: .agent.md + .prompt.md + VS Code settings merge."""
project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
@@ -183,7 +353,10 @@ class CopilotIntegration(IntegrationBase):
# 1. Process and write command files as .agent.md
for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
processed = self.process_template(
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
processed, dest / dst_name, project_root, manifest
@@ -217,8 +390,39 @@ class CopilotIntegration(IntegrationBase):
self.record_file_in_manifest(dst_settings, project_root, manifest)
created.append(dst_settings)
# 4. Install integration-specific update-context scripts
created.extend(self.install_scripts(project_root, manifest))
# 4. Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created
def _setup_skills(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Skills mode: delegate to ``_CopilotSkillsHelper`` then post-process."""
helper = _CopilotSkillsHelper()
created = SkillsIntegration.setup(
helper, project_root, manifest, parsed_options, **opts
)
# Post-process generated skill files with Copilot-specific frontmatter
skills_dir = helper.skills_dest(project_root).resolve()
for path in created:
try:
path.resolve().relative_to(skills_dir)
except ValueError:
continue
if path.name != "SKILL.md":
continue
content = path.read_text(encoding="utf-8")
updated = self.post_process_skill_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)
return created

View File

@@ -1,32 +0,0 @@
# update-context.ps1 — Copilot integration: create/update .github/copilot-instructions.md
#
# This is the copilot-specific implementation that produces the GitHub
# Copilot instructions file. The shared dispatcher reads
# .specify/integration.json and calls this script.
#
# NOTE: This script is not yet active. It will be activated in Stage 7
# when the shared update-agent-context.ps1 replaces its switch statement
# with integration.json-based dispatch. The shared script must also be
# refactored to support SPECKIT_SOURCE_ONLY (guard the Main call) before
# dot-sourcing will work.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
# Invoke shared update-agent-context script as a separate process.
# Dot-sourcing is unsafe until that script guards its Main call.
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType copilot

View File

@@ -1,37 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Copilot integration: create/update .github/copilot-instructions.md
#
# This is the copilot-specific implementation that produces the GitHub
# Copilot instructions file. The shared dispatcher reads
# .specify/integration.json and calls this script.
#
# NOTE: This script is not yet active. It will be activated in Stage 7
# when the shared update-agent-context.sh replaces its case statement
# with integration.json-based dispatch. The shared script must also be
# refactored to support SPECKIT_SOURCE_ONLY (guard the main logic)
# before sourcing will work.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
# Invoke shared update-agent-context script as a separate process.
# Sourcing is unsafe until that script guards its main logic.
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" copilot

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Cursor integration: create/update .cursor/rules/specify-rules.mdc
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType cursor-agent

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Cursor integration: create/update .cursor/rules/specify-rules.mdc
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" cursor-agent

View File

@@ -130,7 +130,10 @@ class ForgeIntegration(MarkdownIntegration):
for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
# Process template with standard MarkdownIntegration logic
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
processed = self.process_template(
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
# FORGE-SPECIFIC: Ensure any remaining $ARGUMENTS placeholders are
# converted to {{parameters}}
@@ -145,8 +148,8 @@ class ForgeIntegration(MarkdownIntegration):
)
created.append(dst_file)
# Install integration-specific update-context scripts
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created

View File

@@ -1,33 +0,0 @@
# update-context.ps1 — Forge integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if (-not (Test-Path $sharedScript)) {
Write-Error "Error: shared agent context updater not found: $sharedScript"
Write-Error "Forge integration requires support in scripts/powershell/update-agent-context.ps1."
exit 1
}
& $sharedScript -AgentType forge
exit $LASTEXITCODE

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Forge integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if [ ! -x "$shared_script" ]; then
echo "Error: shared agent context updater not found or not executable:" >&2
echo " $shared_script" >&2
echo "Forge integration requires support in scripts/bash/update-agent-context.sh." >&2
exit 1
fi
exec "$shared_script" forge

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Gemini CLI integration: create/update GEMINI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType gemini

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Gemini CLI integration: create/update GEMINI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" gemini

View File

@@ -31,7 +31,7 @@ class GenericIntegration(MarkdownIntegration):
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = None
context_file = "AGENTS.md"
@classmethod
def options(cls) -> list[IntegrationOption]:
@@ -122,12 +122,17 @@ class GenericIntegration(MarkdownIntegration):
for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
processed = self.process_template(
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
processed, dest / dst_name, project_root, manifest
)
created.append(dst_file)
created.extend(self.install_scripts(project_root, manifest))
# Upsert managed context section into the agent context file
self.upsert_context_section(project_root)
return created

View File

@@ -1,17 +0,0 @@
# update-context.ps1 — Generic integration: create/update context file
#
# Thin wrapper that delegates to the shared update-agent-context script.
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType generic

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Generic integration: create/update context file
#
# Thin wrapper that delegates to the shared update-agent-context script.
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" generic

View File

@@ -1,33 +0,0 @@
# update-context.ps1 — Goose integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if (-not (Test-Path $sharedScript)) {
Write-Error "Error: shared agent context updater not found: $sharedScript"
Write-Error "Goose integration requires support in scripts/powershell/update-agent-context.ps1."
exit 1
}
& $sharedScript -AgentType goose
exit $LASTEXITCODE

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Goose integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh"
# Always delegate to the shared updater; fail clearly if it is unavailable.
if [ ! -x "$shared_script" ]; then
echo "Error: shared agent context updater not found or not executable:" >&2
echo " $shared_script" >&2
echo "Goose integration requires support in scripts/bash/update-agent-context.sh." >&2
exit 1
fi
exec "$shared_script" goose

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — iFlow CLI integration: create/update IFLOW.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType iflow

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — iFlow CLI integration: create/update IFLOW.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" iflow

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Junie integration: create/update .junie/AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType junie

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Junie integration: create/update .junie/AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" junie

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Kilo Code integration: create/update .kilocode/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kilocode

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Kilo Code integration: create/update .kilocode/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kilocode

View File

@@ -1,17 +0,0 @@
# update-context.ps1 — Kimi Code integration: create/update KIMI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kimi

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Kimi Code integration: create/update KIMI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kimi

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Kiro CLI integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kiro-cli

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Kiro CLI integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kiro-cli

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — opencode integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType opencode

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — opencode integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" opencode

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Pi Coding Agent integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType pi

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Pi Coding Agent integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" pi

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Qoder CLI integration: create/update QODER.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qodercli

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Qoder CLI integration: create/update QODER.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qodercli

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Qwen Code integration: create/update QWEN.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qwen

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Qwen Code integration: create/update QWEN.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qwen

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Roo Code integration: create/update .roo/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType roo

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Roo Code integration: create/update .roo/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" roo

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — SHAI integration: create/update SHAI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType shai

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — SHAI integration: create/update SHAI.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" shai

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Tabnine CLI integration: create/update TABNINE.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType tabnine

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Tabnine CLI integration: create/update TABNINE.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" tabnine

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Trae integration: create/update .trae/rules/project_rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType trae

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Trae integration: create/update .trae/rules/project_rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" trae

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType vibe

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" vibe

View File

@@ -1,23 +0,0 @@
# update-context.ps1 — Windsurf integration: create/update .windsurf/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
$ErrorActionPreference = 'Stop'
# Derive repo root from script location (walks up to find .specify/)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
# If git did not return a repo root, or the git root does not contain .specify,
# fall back to walking up from the script directory to find the initialized project root.
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType windsurf

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# update-context.sh — Windsurf integration: create/update .windsurf/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.
set -euo pipefail
# Derive repo root from script location (walks up to find .specify/)
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" windsurf

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
# [PROJECT NAME] Development Guidelines
Auto-generated from all feature plans. Last updated: [DATE]
## Active Technologies
[EXTRACTED FROM ALL PLAN.MD FILES]
## Project Structure
```text
[ACTUAL STRUCTURE FROM PLANS]
```
## Commands
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
## Code Style
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
## Recent Changes
[LAST 3 FEATURES AND WHAT THEY ADDED]
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@@ -11,9 +11,6 @@ handoffs:
scripts:
sh: scripts/bash/setup-plan.sh --json
ps: scripts/powershell/setup-plan.ps1 -Json
agent_scripts:
sh: scripts/bash/update-agent-context.sh __AGENT__
ps: scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__
---
## User Input
@@ -145,15 +142,11 @@ You **MUST** consider the user input before proceeding (if not empty).
- Skip if project is purely internal (build scripts, one-off tools, etc.)
3. **Agent context update**:
- Run `{AGENT_SCRIPT}`
- These scripts detect which AI agent is in use
- Update the appropriate agent-specific context file
- Add only new technology from current plan
- Preserve manual additions between markers
- Update the plan reference between the `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` markers in `__CONTEXT_FILE__` to point to the plan file created in step 1 (the IMPL_PLAN path)
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file
## Key rules
- Use absolute paths
- Use absolute paths for filesystem operations; use project-relative paths for references in documentation and agent context files
- ERROR on gate failures or unresolved clarifications

View File

@@ -14,6 +14,7 @@ import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
@@ -585,6 +586,156 @@ class TestAutoCommitPowerShell:
assert "\u2713" not in result.stdout, "Must not use Unicode checkmark"
# ── auto-commit.ps1 CRLF warning tests (issue #2253) ────────────────────────
@pytest.mark.skipif(not HAS_PWSH, reason="pwsh not available")
class TestAutoCommitPowerShellCRLF:
"""Tests for CRLF warning handling in auto-commit.ps1 (issue #2253).
On Windows, git emits CRLF warnings to stderr when core.autocrlf=true
and files use LF line endings. PowerShell's $ErrorActionPreference='Stop'
converts stderr output into terminating errors, crashing the script.
These tests use core.autocrlf=true + explicit LF-ending files. On Windows
the CRLF warnings fire and exercise the fix; on other platforms the tests
still run (they just won't produce stderr warnings, so they pass trivially).
"""
# -- positive tests (fix works) ----------------------------------------
def test_commit_succeeds_with_autocrlf(self, tmp_path: Path):
"""auto-commit.ps1 creates a commit when core.autocrlf=true (CRLF
warnings on stderr must not crash the script)."""
project = _setup_project(tmp_path)
_write_config(project, (
"auto_commit:\n"
" default: false\n"
" after_specify:\n"
" enabled: true\n"
' message: "crlf commit"\n'
))
# Create and commit a tracked LF-ending file first so the script's
# `git diff --quiet HEAD` checks inspect a tracked modification.
tracked = project / "crlf-test.txt"
tracked.write_bytes(b"line one\nline two\nline three\n")
subprocess.run(["git", "add", "crlf-test.txt"], cwd=project, check=True)
subprocess.run(
["git", "commit", "-m", "seed tracked file"],
cwd=project, check=True, env={**os.environ, **_GIT_ENV},
)
subprocess.run(
["git", "config", "core.autocrlf", "true"],
cwd=project, check=True,
)
# Modify the tracked file with explicit LF endings to trigger the
# CRLF warning during diff/status checks on Windows.
tracked.write_bytes(b"line one\nline two changed\nline three\n")
# On Windows, verify the test setup actually produces a CRLF warning.
if sys.platform == "win32":
probe = subprocess.run(
["git", "diff", "--quiet", "HEAD"],
cwd=project, capture_output=True, text=True,
)
assert "LF will be replaced by CRLF" in probe.stderr, (
"Expected CRLF warning from git on Windows; test setup may be wrong"
)
result = _run_pwsh("auto-commit.ps1", project, "after_specify")
assert result.returncode == 0, (
f"Script crashed (likely CRLF stderr); stderr:\n{result.stderr}"
)
assert "[OK] Changes committed" in result.stdout
log = subprocess.run(
["git", "log", "--oneline", "-1"],
cwd=project, capture_output=True, text=True,
)
assert "crlf commit" in log.stdout
def test_custom_message_not_corrupted_by_crlf(self, tmp_path: Path):
"""Commit message is the configured value, not a CRLF warning."""
project = _setup_project(tmp_path)
_write_config(project, (
"auto_commit:\n"
" default: false\n"
" after_plan:\n"
" enabled: true\n"
' message: "[Project] Plan done"\n'
))
subprocess.run(
["git", "config", "core.autocrlf", "true"],
cwd=project, check=True,
)
(project / "plan.txt").write_bytes(b"plan\ncontent\n")
result = _run_pwsh("auto-commit.ps1", project, "after_plan")
assert result.returncode == 0
log = subprocess.run(
["git", "log", "--format=%s", "-1"],
cwd=project, capture_output=True, text=True,
)
assert "[Project] Plan done" in log.stdout.strip()
def test_no_changes_still_skips_with_autocrlf(self, tmp_path: Path):
"""Script correctly detects 'no changes' even with core.autocrlf=true."""
project = _setup_project(tmp_path)
_write_config(project, (
"auto_commit:\n"
" default: false\n"
" after_specify:\n"
" enabled: true\n"
))
subprocess.run(
["git", "config", "core.autocrlf", "true"],
cwd=project, check=True,
)
# Stage and commit everything so the working tree is clean.
subprocess.run(["git", "add", "."], cwd=project, check=True,
env={**os.environ, **_GIT_ENV})
subprocess.run(["git", "commit", "-m", "setup", "-q"], cwd=project,
check=True, env={**os.environ, **_GIT_ENV})
result = _run_pwsh("auto-commit.ps1", project, "after_specify")
assert result.returncode == 0
assert "[OK]" not in result.stdout, "Should not have committed anything"
# -- negative tests (real errors still surface) ------------------------
def test_not_a_repo_still_detected_with_autocrlf(self, tmp_path: Path):
"""Script still exits gracefully when not in a git repo, even though
ErrorActionPreference is relaxed around the rev-parse call."""
project = _setup_project(tmp_path, git=False)
_write_config(project, "auto_commit:\n default: true\n")
result = _run_pwsh("auto-commit.ps1", project, "after_specify")
assert result.returncode == 0
combined = result.stdout + result.stderr
assert "not a git repository" in combined.lower() or "warning" in combined.lower()
def test_missing_config_still_exits_cleanly_with_autocrlf(self, tmp_path: Path):
"""Script exits 0 when git-config.yml is absent (no over-suppression)."""
project = _setup_project(tmp_path)
subprocess.run(
["git", "config", "core.autocrlf", "true"],
cwd=project, check=True,
)
config = project / ".specify" / "extensions" / "git" / "git-config.yml"
config.unlink(missing_ok=True)
result = _run_pwsh("auto-commit.ps1", project, "after_specify")
assert result.returncode == 0
# Should not have committed anything — config file missing means disabled.
log = subprocess.run(
["git", "log", "--oneline"],
cwd=project, capture_output=True, text=True,
)
assert log.stdout.strip().count("\n") == 0 # only the seed commit
# ── git-common.sh Tests ──────────────────────────────────────────────────────

View File

@@ -56,14 +56,19 @@ class TestInitIntegrationFlag:
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
assert data["integration"] == "copilot"
assert "scripts" in data
assert "update-context" in data["scripts"]
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
assert opts["integration"] == "copilot"
assert opts["context_file"] == ".github/copilot-instructions.md"
assert (project / ".specify" / "integrations" / "copilot.manifest.json").exists()
assert (project / ".specify" / "integrations" / "copilot" / "scripts" / "update-context.sh").exists()
# Context section should be upserted into the copilot instructions file
ctx_file = project / ".github" / "copilot-instructions.md"
assert ctx_file.exists()
ctx_content = ctx_file.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in ctx_content
assert "<!-- SPECKIT END -->" in ctx_content
shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json"
assert shared_manifest.exists()
@@ -168,13 +173,13 @@ class TestInitIntegrationFlag:
assert "speckit-specify" in command_file.read_text(encoding="utf-8")
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
def test_shared_infra_skips_existing_files(self, tmp_path):
"""Pre-existing shared files are not overwritten by _install_shared_infra."""
from typer.testing import CliRunner
from specify_cli import app
def test_shared_infra_skips_existing_files_without_force(self, tmp_path):
"""Pre-existing shared files are not overwritten without --force."""
from specify_cli import _install_shared_infra
project = tmp_path / "skip-test"
project.mkdir()
(project / ".specify").mkdir()
# Pre-create a shared script with custom content
scripts_dir = project / ".specify" / "scripts" / "bash"
@@ -188,6 +193,97 @@ class TestInitIntegrationFlag:
custom_template = "# user-modified spec-template\n"
(templates_dir / "spec-template.md").write_text(custom_template, encoding="utf-8")
_install_shared_infra(project, "sh", force=False)
# User's files should be preserved (not overwritten)
assert (scripts_dir / "common.sh").read_text(encoding="utf-8") == custom_content
assert (templates_dir / "spec-template.md").read_text(encoding="utf-8") == custom_template
# Other shared files should still be installed
assert (scripts_dir / "setup-plan.sh").exists()
assert (templates_dir / "plan-template.md").exists()
def test_shared_infra_overwrites_existing_files_with_force(self, tmp_path):
"""Pre-existing shared files ARE overwritten when force=True."""
from specify_cli import _install_shared_infra
project = tmp_path / "force-test"
project.mkdir()
(project / ".specify").mkdir()
# Pre-create a shared script with custom content
scripts_dir = project / ".specify" / "scripts" / "bash"
scripts_dir.mkdir(parents=True)
custom_content = "# user-modified common.sh\n"
(scripts_dir / "common.sh").write_text(custom_content, encoding="utf-8")
# Pre-create a shared template with custom content
templates_dir = project / ".specify" / "templates"
templates_dir.mkdir(parents=True)
custom_template = "# user-modified spec-template\n"
(templates_dir / "spec-template.md").write_text(custom_template, encoding="utf-8")
_install_shared_infra(project, "sh", force=True)
# Files should be overwritten with bundled versions
assert (scripts_dir / "common.sh").read_text(encoding="utf-8") != custom_content
assert (templates_dir / "spec-template.md").read_text(encoding="utf-8") != custom_template
# Other shared files should also be installed
assert (scripts_dir / "setup-plan.sh").exists()
assert (templates_dir / "plan-template.md").exists()
def test_shared_infra_skip_warning_displayed(self, tmp_path, capsys):
"""Console warning is displayed when files are skipped."""
from specify_cli import _install_shared_infra
project = tmp_path / "warn-test"
project.mkdir()
(project / ".specify").mkdir()
scripts_dir = project / ".specify" / "scripts" / "bash"
scripts_dir.mkdir(parents=True)
(scripts_dir / "common.sh").write_text("# custom\n", encoding="utf-8")
_install_shared_infra(project, "sh", force=False)
captured = capsys.readouterr()
assert "already exist and were not updated" in captured.out
assert "specify init --here --force" in captured.out
# Rich may wrap long lines; normalize whitespace for the second command
normalized = " ".join(captured.out.split())
assert "specify integration upgrade --force" in normalized
def test_shared_infra_no_warning_when_forced(self, tmp_path, capsys):
"""No skip warning when force=True (all files overwritten)."""
from specify_cli import _install_shared_infra
project = tmp_path / "no-warn-test"
project.mkdir()
(project / ".specify").mkdir()
scripts_dir = project / ".specify" / "scripts" / "bash"
scripts_dir.mkdir(parents=True)
(scripts_dir / "common.sh").write_text("# custom\n", encoding="utf-8")
_install_shared_infra(project, "sh", force=True)
captured = capsys.readouterr()
assert "already exist and were not updated" not in captured.out
def test_init_here_force_overwrites_shared_infra(self, tmp_path):
"""E2E: specify init --here --force overwrites shared infra files."""
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / "e2e-force"
project.mkdir()
scripts_dir = project / ".specify" / "scripts" / "bash"
scripts_dir.mkdir(parents=True)
custom_content = "# user-modified common.sh\n"
(scripts_dir / "common.sh").write_text(custom_content, encoding="utf-8")
old_cwd = os.getcwd()
try:
os.chdir(project)
@@ -202,14 +298,40 @@ class TestInitIntegrationFlag:
os.chdir(old_cwd)
assert result.exit_code == 0
# --force should overwrite the custom file
assert (scripts_dir / "common.sh").read_text(encoding="utf-8") != custom_content
# User's files should be preserved
def test_init_here_without_force_preserves_shared_infra(self, tmp_path):
"""E2E: specify init --here (no --force) preserves existing shared infra files."""
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / "e2e-no-force"
project.mkdir()
scripts_dir = project / ".specify" / "scripts" / "bash"
scripts_dir.mkdir(parents=True)
custom_content = "# user-modified common.sh\n"
(scripts_dir / "common.sh").write_text(custom_content, encoding="utf-8")
old_cwd = os.getcwd()
try:
os.chdir(project)
runner = CliRunner()
result = runner.invoke(app, [
"init", "--here",
"--integration", "copilot",
"--script", "sh",
"--no-git",
], input="y\n", catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
# Without --force, custom file should be preserved
assert (scripts_dir / "common.sh").read_text(encoding="utf-8") == custom_content
assert (templates_dir / "spec-template.md").read_text(encoding="utf-8") == custom_template
# Other shared files should still be installed
assert (scripts_dir / "setup-plan.sh").exists()
assert (templates_dir / "plan-template.md").exists()
# Warning about skipped files should appear
assert "not updated" in result.output
class TestForceExistingDirectory:
@@ -256,7 +378,7 @@ class TestForceExistingDirectory:
], catch_exceptions=False)
assert result.exit_code == 1
assert "already exists" in result.output
assert "already exists" in _normalize_cli_output(result.output)
class TestGitExtensionAutoInstall:

View File

@@ -5,12 +5,17 @@ from .test_integration_base_skills import SkillsIntegrationTests
class TestAgyIntegration(SkillsIntegrationTests):
KEY = "agy"
FOLDER = ".agent/"
FOLDER = ".agents/"
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".agent/skills"
REGISTRAR_DIR = ".agents/skills"
CONTEXT_FILE = "AGENTS.md"
def test_options_include_skills_flag(self):
"""Override inherited test: AgyIntegration should not expose a --skills flag because .agents/ is its only layout."""
from specify_cli.integrations import get_integration
i = get_integration(self.KEY)
skills_opts = [o for o in i.options() if o.name == "--skills"]
assert len(skills_opts) == 0
class TestAgyAutoPromote:
"""--ai agy auto-promotes to integration path."""
@@ -24,4 +29,17 @@ class TestAgyAutoPromote:
result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh"])
assert result.exit_code == 0, f"init --ai agy failed: {result.output}"
assert (target / ".agent" / "skills" / "speckit-plan" / "SKILL.md").exists()
assert (target / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists()
def test_agy_setup_warning(self, tmp_path):
"""Agy integration should print a warning about v1.20.5 requirement during setup."""
from typer.testing import CliRunner
from specify_cli import app
# Click >= 8.2 separates stdout and stderr natively, mix_stderr is removed
runner = CliRunner()
target = tmp_path / "test-proj2"
result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh"])
assert result.exit_code == 0
assert "Warning: The .agents/ layout requires Antigravity v1.20.5 or newer" in result.stderr

View File

@@ -99,7 +99,23 @@ class MarkdownIntegrationTests:
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
assert "\nscripts:\n" not in content, f"{f.name} has unstripped scripts: block"
assert "\nagent_scripts:\n" not in content, f"{f.name} has unstripped agent_scripts: block"
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan command must reference this integration's context file."""
i = get_integration(self.KEY)
if not i.context_file:
return
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
plan_file = i.commands_dest(tmp_path) / i.command_filename("plan")
assert plan_file.exists(), f"Plan file {plan_file} not created"
content = plan_file.read_text(encoding="utf-8")
assert i.context_file in content, (
f"Plan command should reference {i.context_file!r} but it was not found in {plan_file.name}"
)
assert "__CONTEXT_FILE__" not in content, (
f"Plan command has unprocessed __CONTEXT_FILE__ placeholder in {plan_file.name}"
)
def test_all_files_tracked_in_manifest(self, tmp_path):
i = get_integration(self.KEY)
@@ -132,30 +148,35 @@ class MarkdownIntegrationTests:
assert modified_file.exists()
assert modified_file in skipped
# -- Scripts ----------------------------------------------------------
# -- Context section ---------------------------------------------------
def test_setup_installs_update_context_scripts(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
created = i.setup(tmp_path, m)
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
assert (scripts_dir / "update-context.sh").exists()
assert (scripts_dir / "update-context.ps1").exists()
def test_scripts_tracked_in_manifest(self, tmp_path):
def test_setup_upserts_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
script_rels = [k for k in m.files if "update-context" in k]
assert len(script_rels) >= 2
if i.context_file:
ctx_path = tmp_path / i.context_file
assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}"
content = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in content
assert "<!-- SPECKIT END -->" in content
assert "read the current plan" in content
def test_sh_script_is_executable(self, tmp_path):
def test_teardown_removes_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
assert os.access(sh, os.X_OK)
m.save()
if i.context_file:
ctx_path = tmp_path / i.context_file
# Add user content around the section
content = ctx_path.read_text(encoding="utf-8")
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
i.teardown(tmp_path, m)
remaining = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" not in remaining
assert "<!-- SPECKIT END -->" not in remaining
assert "# My Rules" in remaining
# -- CLI auto-promote -------------------------------------------------
@@ -203,6 +224,30 @@ class MarkdownIntegrationTests:
commands = sorted(cmd_dir.glob("speckit.*"))
assert len(commands) > 0, f"No command files in {cmd_dir}"
def test_init_options_includes_context_file(self, tmp_path):
"""init-options.json must include context_file for the active integration."""
import json
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / f"opts-{self.KEY}"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
result = CliRunner().invoke(app, [
"init", "--here", "--integration", self.KEY, "--script", "sh",
"--no-git", "--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
opts = json.loads((project / ".specify" / "init-options.json").read_text())
i = get_integration(self.KEY)
assert opts.get("context_file") == i.context_file, (
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
)
# -- Complete file inventory ------------------------------------------
COMMAND_STEMS = [
@@ -220,10 +265,6 @@ class MarkdownIntegrationTests:
for stem in self.COMMAND_STEMS:
files.append(f"{cmd_dir}/speckit.{stem}.md")
# Integration scripts
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1")
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh")
# Framework files
files.append(f".specify/integration.json")
files.append(f".specify/init-options.json")
@@ -232,14 +273,14 @@ class MarkdownIntegrationTests:
if script_variant == "sh":
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh",
"setup-plan.sh", "update-agent-context.sh"]:
"setup-plan.sh"]:
files.append(f".specify/scripts/bash/{name}")
else:
for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1",
"setup-plan.ps1", "update-agent-context.ps1"]:
"setup-plan.ps1"]:
files.append(f".specify/scripts/powershell/{name}")
for name in ["agent-file-template.md", "checklist-template.md",
for name in ["checklist-template.md",
"constitution-template.md", "plan-template.md",
"spec-template.md", "tasks-template.md"]:
files.append(f".specify/templates/{name}")
@@ -248,6 +289,11 @@ class MarkdownIntegrationTests:
# Bundled workflow
files.append(".specify/workflows/speckit/workflow.yml")
files.append(".specify/workflows/workflow-registry.json")
# Agent context file (if set)
if i.context_file:
files.append(i.context_file)
return sorted(files)
def test_complete_file_inventory_sh(self, tmp_path):

View File

@@ -173,6 +173,23 @@ class SkillsIntegrationTests:
body = parts[2].strip() if len(parts) >= 3 else ""
assert len(body) > 0, f"{f} has empty body"
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan skill must reference this integration's context file."""
i = get_integration(self.KEY)
if not i.context_file:
return
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
plan_file = i.skills_dest(tmp_path) / "speckit-plan" / "SKILL.md"
assert plan_file.exists(), f"Plan skill {plan_file} not created"
content = plan_file.read_text(encoding="utf-8")
assert i.context_file in content, (
f"Plan skill should reference {i.context_file!r} but it was not found"
)
assert "__CONTEXT_FILE__" not in content, (
"Plan skill has unprocessed __CONTEXT_FILE__ placeholder"
)
def test_all_files_tracked_in_manifest(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
@@ -217,30 +234,34 @@ class SkillsIntegrationTests:
assert (foreign_dir / "SKILL.md").exists(), "Foreign skill was removed"
# -- Scripts ----------------------------------------------------------
# -- Context section ---------------------------------------------------
def test_setup_installs_update_context_scripts(self, tmp_path):
def test_setup_upserts_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
assert (scripts_dir / "update-context.sh").exists()
assert (scripts_dir / "update-context.ps1").exists()
if i.context_file:
ctx_path = tmp_path / i.context_file
assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}"
content = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in content
assert "<!-- SPECKIT END -->" in content
assert "read the current plan" in content
def test_scripts_tracked_in_manifest(self, tmp_path):
def test_teardown_removes_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
script_rels = [k for k in m.files if "update-context" in k]
assert len(script_rels) >= 2
def test_sh_script_is_executable(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
assert os.access(sh, os.X_OK)
m.save()
if i.context_file:
ctx_path = tmp_path / i.context_file
content = ctx_path.read_text(encoding="utf-8")
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
i.teardown(tmp_path, m)
remaining = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" not in remaining
assert "<!-- SPECKIT END -->" not in remaining
assert "# My Rules" in remaining
# -- CLI auto-promote -------------------------------------------------
@@ -286,6 +307,30 @@ class SkillsIntegrationTests:
skills_dir = i.skills_dest(project)
assert skills_dir.is_dir(), f"Skills directory {skills_dir} not created"
def test_init_options_includes_context_file(self, tmp_path):
"""init-options.json must include context_file for the active integration."""
import json
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / f"opts-{self.KEY}"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
result = CliRunner().invoke(app, [
"init", "--here", "--integration", self.KEY, "--script", "sh",
"--no-git", "--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
opts = json.loads((project / ".specify" / "init-options.json").read_text())
i = get_integration(self.KEY)
assert opts.get("context_file") == i.context_file, (
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
)
# -- IntegrationOption ------------------------------------------------
def test_options_include_skills_flag(self):
@@ -316,8 +361,6 @@ class SkillsIntegrationTests:
".specify/init-options.json",
".specify/integration.json",
f".specify/integrations/{self.KEY}.manifest.json",
f".specify/integrations/{self.KEY}/scripts/update-context.ps1",
f".specify/integrations/{self.KEY}/scripts/update-context.sh",
".specify/integrations/speckit.manifest.json",
".specify/memory/constitution.md",
]
@@ -328,7 +371,6 @@ class SkillsIntegrationTests:
".specify/scripts/bash/common.sh",
".specify/scripts/bash/create-new-feature.sh",
".specify/scripts/bash/setup-plan.sh",
".specify/scripts/bash/update-agent-context.sh",
]
else:
files += [
@@ -336,11 +378,9 @@ class SkillsIntegrationTests:
".specify/scripts/powershell/common.ps1",
".specify/scripts/powershell/create-new-feature.ps1",
".specify/scripts/powershell/setup-plan.ps1",
".specify/scripts/powershell/update-agent-context.ps1",
]
# Templates
files += [
".specify/templates/agent-file-template.md",
".specify/templates/checklist-template.md",
".specify/templates/constitution-template.md",
".specify/templates/plan-template.md",
@@ -352,6 +392,9 @@ class SkillsIntegrationTests:
".specify/workflows/speckit/workflow.yml",
".specify/workflows/workflow-registry.json",
]
# Agent context file (if set)
if i.context_file:
files.append(i.context_file)
return sorted(files)
def test_complete_file_inventory_sh(self, tmp_path):

View File

@@ -310,6 +310,23 @@ class TomlIntegrationTests:
raise AssertionError(f"{f.name} is not valid TOML: {exc}") from exc
assert "prompt" in parsed, f"{f.name} parsed TOML has no 'prompt' key"
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan command must reference this integration's context file."""
i = get_integration(self.KEY)
if not i.context_file:
return
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
plan_file = i.commands_dest(tmp_path) / i.command_filename("plan")
assert plan_file.exists(), f"Plan file {plan_file} not created"
content = plan_file.read_text(encoding="utf-8")
assert i.context_file in content, (
f"Plan command should reference {i.context_file!r} but it was not found in {plan_file.name}"
)
assert "__CONTEXT_FILE__" not in content, (
f"Plan command has unprocessed __CONTEXT_FILE__ placeholder in {plan_file.name}"
)
def test_all_files_tracked_in_manifest(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
@@ -341,37 +358,34 @@ class TomlIntegrationTests:
assert modified_file.exists()
assert modified_file in skipped
# -- Scripts ----------------------------------------------------------
# -- Context section ---------------------------------------------------
def test_setup_installs_update_context_scripts(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
created = i.setup(tmp_path, m)
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
assert (scripts_dir / "update-context.sh").exists()
assert (scripts_dir / "update-context.ps1").exists()
def test_scripts_tracked_in_manifest(self, tmp_path):
def test_setup_upserts_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
script_rels = [k for k in m.files if "update-context" in k]
assert len(script_rels) >= 2
if i.context_file:
ctx_path = tmp_path / i.context_file
assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}"
content = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in content
assert "<!-- SPECKIT END -->" in content
assert "read the current plan" in content
def test_sh_script_is_executable(self, tmp_path):
def test_teardown_removes_context_section(self, tmp_path):
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
i.setup(tmp_path, m)
sh = (
tmp_path
/ ".specify"
/ "integrations"
/ self.KEY
/ "scripts"
/ "update-context.sh"
)
assert os.access(sh, os.X_OK)
m.save()
if i.context_file:
ctx_path = tmp_path / i.context_file
content = ctx_path.read_text(encoding="utf-8")
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
i.teardown(tmp_path, m)
remaining = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" not in remaining
assert "<!-- SPECKIT END -->" not in remaining
assert "# My Rules" in remaining
# -- CLI auto-promote -------------------------------------------------
@@ -441,6 +455,30 @@ class TomlIntegrationTests:
commands = sorted(cmd_dir.glob("speckit.*.toml"))
assert len(commands) > 0, f"No command files in {cmd_dir}"
def test_init_options_includes_context_file(self, tmp_path):
"""init-options.json must include context_file for the active integration."""
import json
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / f"opts-{self.KEY}"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
result = CliRunner().invoke(app, [
"init", "--here", "--integration", self.KEY, "--script", "sh",
"--no-git", "--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
opts = json.loads((project / ".specify" / "init-options.json").read_text())
i = get_integration(self.KEY)
assert opts.get("context_file") == i.context_file, (
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
)
# -- Complete file inventory ------------------------------------------
COMMAND_STEMS = [
@@ -465,10 +503,6 @@ class TomlIntegrationTests:
for stem in self.COMMAND_STEMS:
files.append(f"{cmd_dir}/speckit.{stem}.toml")
# Integration scripts
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1")
files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh")
# Framework files
files.append(".specify/integration.json")
files.append(".specify/init-options.json")
@@ -481,7 +515,6 @@ class TomlIntegrationTests:
"common.sh",
"create-new-feature.sh",
"setup-plan.sh",
"update-agent-context.sh",
]:
files.append(f".specify/scripts/bash/{name}")
else:
@@ -490,12 +523,10 @@ class TomlIntegrationTests:
"common.ps1",
"create-new-feature.ps1",
"setup-plan.ps1",
"update-agent-context.ps1",
]:
files.append(f".specify/scripts/powershell/{name}")
for name in [
"agent-file-template.md",
"checklist-template.md",
"constitution-template.md",
"plan-template.md",
@@ -508,6 +539,11 @@ class TomlIntegrationTests:
# Bundled workflow
files.append(".specify/workflows/speckit/workflow.yml")
files.append(".specify/workflows/workflow-registry.json")
# Agent context file (if set)
if i.context_file:
files.append(i.context_file)
return sorted(files)
def test_complete_file_inventory_sh(self, tmp_path):

Some files were not shown because too many files have changed in this diff Show More