Compare commits

..

34 Commits

Author SHA1 Message Date
github-actions[bot]
5b70de43dc chore: bump version to 0.7.5 2026-04-22 21:22:39 +00: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
107 changed files with 4241 additions and 3656 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

@@ -2,6 +2,54 @@
<!-- insert new changelog below this comment -->
## [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

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

@@ -195,6 +195,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 +223,7 @@ 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) |
| 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 +236,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 +272,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

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

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

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-16T18:00:00Z",
"updated_at": "2026-04-22T17:54: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,37 @@
"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"
},
"memorylint": {
"name": "MemoryLint",
"id": "memorylint",
@@ -1460,6 +1523,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 +1720,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 +1891,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 +2154,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 +2346,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.1.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-version-guard/archive/refs/tags/v1.1.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-22T17:54:00Z"
},
"whatif": {
"name": "What-if Analysis",
"id": "whatif",
@@ -2118,6 +2405,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",
@@ -2183,4 +2505,4 @@
"updated_at": "2026-04-13T00:00:00Z"
}
}
}
}

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

@@ -116,5 +116,5 @@ The following enhancements are under consideration for future releases:
| **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.
For artifacts and commands (which are LLM directives), `wrap` injects preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder (implemented). For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable (not yet implemented).
- **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.

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

@@ -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"
version = "0.7.5"
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

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

@@ -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."""
@@ -920,7 +928,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 +1122,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]",
@@ -1261,15 +1268,11 @@ 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))
@@ -1373,8 +1376,8 @@ 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(),
}
@@ -1603,25 +1606,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 +1632,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 +1882,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 +2076,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,6 +2153,7 @@ 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):
@@ -2064,6 +2205,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 +2224,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 +2236,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 +2303,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 +2334,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 +2361,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:
@@ -2320,7 +2469,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

View File

@@ -6,6 +6,7 @@ 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
@@ -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(

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

@@ -10,7 +10,9 @@ Copilot has several unique behaviors compared to standard markdown agents:
from __future__ import annotations
import json
import os
import shutil
import warnings
from pathlib import Path
from typing import Any
@@ -18,6 +20,30 @@ from ..base import IntegrationBase
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 CopilotIntegration(IntegrationBase):
"""Integration for GitHub Copilot (VS Code IDE + CLI).
@@ -50,13 +76,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:
@@ -91,13 +119,12 @@ class CopilotIntegration(IntegrationBase):
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")
if _allow_all():
cli_args.append("--yolo")
if model:
cli_args.extend(["--model", model])
if not stream:
@@ -183,7 +210,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 +247,8 @@ 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

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

View File

@@ -16,7 +16,10 @@ import zipfile
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Dict, List, Any
from typing import TYPE_CHECKING, Optional, Dict, List, Any
if TYPE_CHECKING:
from .agents import CommandRegistrar
from datetime import datetime, timezone
import re
@@ -27,6 +30,59 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier
from .extensions import ExtensionRegistry, normalize_priority
def _substitute_core_template(
body: str,
cmd_name: str,
project_root: "Path",
registrar: "CommandRegistrar",
) -> "tuple[str, dict]":
"""Substitute {CORE_TEMPLATE} with the body of the installed core command template.
Args:
body: Preset command body (may contain {CORE_TEMPLATE} placeholder).
cmd_name: Full command name (e.g. "speckit.git.feature" or "speckit.specify").
project_root: Project root path.
registrar: CommandRegistrar instance for parse_frontmatter.
Returns:
A tuple of (body, core_frontmatter) where body has {CORE_TEMPLATE} replaced
by the core template body and core_frontmatter holds the core template's parsed
frontmatter (so callers can inherit scripts/agent_scripts from it). Both are
unchanged / empty when the placeholder is absent or the core template file does
not exist.
"""
if "{CORE_TEMPLATE}" not in body:
return body, {}
# Derive the short name (strip "speckit." prefix) used by core command templates.
short_name = cmd_name
if short_name.startswith("speckit."):
short_name = short_name[len("speckit."):]
resolver = PresetResolver(project_root)
# Resolution order for the core template:
# 1. resolve_core(cmd_name) — covers tier-1 project overrides and tier-3/4
# name-based lookup (file named <cmd_name>.md). Checked first so that a
# local override always wins, even for extension commands.
# 2. resolve_extension_command_via_manifest(cmd_name) — manifest-based tier-3
# fallback for extension commands whose file is named differently from the
# command name (e.g. speckit.selftest.extension → commands/selftest.md).
# 3. resolve_core(short_name) — core template fallback using the unprefixed
# name (e.g. specify → templates/commands/specify.md).
# resolve_core() skips installed presets (tier 2) to prevent accidental nesting
# where another preset's wrap output is mistaken for the real core.
core_file = (
resolver.resolve_core(cmd_name, "command")
or resolver.resolve_extension_command_via_manifest(cmd_name)
or resolver.resolve_core(short_name, "command")
)
if core_file is None:
return body, {}
core_frontmatter, core_body = registrar.parse_frontmatter(core_file.read_text(encoding="utf-8"))
return body.replace("{CORE_TEMPLATE}", core_body), core_frontmatter
@dataclass
class PresetCatalogEntry:
"""Represents a single entry in the preset catalog stack."""
@@ -555,6 +611,232 @@ class PresetManager:
registrar = CommandRegistrar()
registrar.unregister_commands(registered_commands, self.project_root)
def _replay_wraps_for_command(self, cmd_name: str) -> None:
"""Recompose and rewrite agent files for a wrap-strategy command.
Collects all installed presets that declare cmd_name in their
wrap_commands registry field, sorts them so the highest-precedence
preset (lowest priority number) wraps outermost, then writes the
fully composed output to every agent directory.
Called after every install and remove to keep agent files correct
regardless of installation order.
Args:
cmd_name: Full command name (e.g. "speckit.specify")
"""
try:
from .agents import CommandRegistrar
except ImportError:
return
# Collect enabled presets that wrap this command, sorted ascending
# (lowest priority number = highest precedence = outermost).
wrap_presets = []
for pack_id, metadata in self.registry.list_by_priority(include_disabled=False):
if cmd_name not in metadata.get("wrap_commands", []):
continue
pack_dir = self.presets_dir / pack_id
if not pack_dir.is_dir():
continue # corrupted state — skip
wrap_presets.append((pack_id, pack_dir))
if not wrap_presets:
return
# Derive short name for core resolution fallback.
short_name = cmd_name
if short_name.startswith("speckit."):
short_name = short_name[len("speckit."):]
resolver = PresetResolver(self.project_root)
core_file = (
resolver.resolve_core(cmd_name, "command")
or resolver.resolve_extension_command_via_manifest(cmd_name)
or (
resolver.resolve_extension_command_via_manifest(short_name)
if short_name != cmd_name
else None
)
or resolver.resolve_core(short_name, "command")
)
if core_file is None:
return
registrar = CommandRegistrar()
core_frontmatter, core_body = registrar.parse_frontmatter(
core_file.read_text(encoding="utf-8")
)
replay_aliases: List[str] = []
seen_aliases: set[str] = set()
# Apply wraps innermost-first (reverse of ascending list).
accumulated_body = core_body
outermost_frontmatter = {}
outermost_pack_id = wrap_presets[0][0] # fallback; updated per contributing preset
for pack_id, pack_dir in reversed(wrap_presets):
manifest_path = pack_dir / "preset.yml"
cmd_file: Optional[Path] = None
if manifest_path.exists():
try:
manifest = PresetManifest(manifest_path)
except (PresetValidationError, KeyError, TypeError, ValueError):
manifest = None
if manifest is not None:
for template in manifest.templates:
if template.get("type") != "command" or template.get("name") != cmd_name:
continue
file_rel = template.get("file")
if isinstance(file_rel, str):
rel_path = Path(file_rel)
if not rel_path.is_absolute():
try:
preset_root = pack_dir.resolve()
candidate = (preset_root / rel_path).resolve()
candidate.relative_to(preset_root)
except (OSError, ValueError):
candidate = None
if candidate is not None:
cmd_file = candidate
aliases = template.get("aliases", [])
if not isinstance(aliases, list):
aliases = []
for alias in aliases:
if isinstance(alias, str) and alias not in seen_aliases:
replay_aliases.append(alias)
seen_aliases.add(alias)
break
if cmd_file is None:
cmd_file = pack_dir / "commands" / f"{cmd_name}.md"
if not cmd_file.exists():
continue
wrap_fm, wrap_body = registrar.parse_frontmatter(
cmd_file.read_text(encoding="utf-8")
)
accumulated_body = wrap_body.replace("{CORE_TEMPLATE}", accumulated_body)
outermost_frontmatter = wrap_fm # last iteration = outermost preset
outermost_pack_id = pack_id
# Build final frontmatter: outermost preset wins; fall back to core for
# scripts/agent_scripts if the outermost preset does not define them.
final_frontmatter = dict(outermost_frontmatter)
final_frontmatter.pop("strategy", None)
for key in ("scripts", "agent_scripts"):
if key not in final_frontmatter and key in core_frontmatter:
final_frontmatter[key] = core_frontmatter[key]
composed_content = (
registrar.render_frontmatter(final_frontmatter) + "\n" + accumulated_body
)
self._replay_skill_override(cmd_name, composed_content, outermost_pack_id)
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
cmd_dir = tmp_path / "commands"
cmd_dir.mkdir()
(cmd_dir / f"{cmd_name}.md").write_text(composed_content, encoding="utf-8")
registrar._ensure_configs()
for agent_name, agent_config in registrar.AGENT_CONFIGS.items():
if agent_config.get("extension") == "/SKILL.md":
continue
agent_dir = self.project_root / agent_config["dir"]
if not agent_dir.exists():
continue
try:
registrar.register_commands(
agent_name,
[{
"name": cmd_name,
"file": f"commands/{cmd_name}.md",
"aliases": replay_aliases,
}],
f"preset:{outermost_pack_id}",
tmp_path,
self.project_root,
)
except ValueError:
continue
def _replay_skill_override(
self,
cmd_name: str,
composed_content: str,
outermost_pack_id: str,
) -> None:
"""Rewrite any active SKILL.md override for a replayed wrap command."""
skills_dir = self._get_skills_dir()
if not skills_dir:
return
from . import SKILL_DESCRIPTIONS, load_init_options
from .agents import CommandRegistrar
from .integrations import get_integration
init_opts = load_init_options(self.project_root)
if not isinstance(init_opts, dict):
init_opts = {}
selected_ai = init_opts.get("ai")
if not isinstance(selected_ai, str):
return
registrar = CommandRegistrar()
integration = get_integration(selected_ai)
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
create_missing_skills = bool(init_opts.get("ai_skills")) and agent_config.get("extension") != "/SKILL.md"
skill_name, legacy_skill_name = self._skill_names_for_command(cmd_name)
target_skill_names: List[str] = []
if (skills_dir / skill_name).is_dir():
target_skill_names.append(skill_name)
if legacy_skill_name != skill_name and (skills_dir / legacy_skill_name).is_dir():
target_skill_names.append(legacy_skill_name)
if not target_skill_names and create_missing_skills:
missing_skill_dir = skills_dir / skill_name
if not missing_skill_dir.exists():
target_skill_names.append(skill_name)
if not target_skill_names:
return
raw_short_name = cmd_name
if raw_short_name.startswith("speckit."):
raw_short_name = raw_short_name[len("speckit."):]
short_name = raw_short_name.replace(".", "-")
skill_title = self._skill_title_from_command(cmd_name)
frontmatter, body = registrar.parse_frontmatter(composed_content)
original_desc = frontmatter.get("description", "")
enhanced_desc = SKILL_DESCRIPTIONS.get(
short_name,
original_desc or f"Spec-kit workflow command: {short_name}",
)
body = registrar.resolve_skill_placeholders(
selected_ai, dict(frontmatter), body, self.project_root
)
for target_skill_name in target_skill_names:
skill_subdir = skills_dir / target_skill_name
if skill_subdir.exists() and not skill_subdir.is_dir():
continue
skill_subdir.mkdir(parents=True, exist_ok=True)
frontmatter_data = registrar.build_skill_frontmatter(
selected_ai,
target_skill_name,
enhanced_desc,
f"preset:{outermost_pack_id}",
)
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
skill_content = (
f"---\n"
f"{frontmatter_text}\n"
f"---\n\n"
f"# Speckit {skill_title} Skill\n\n"
f"{body}\n"
)
if integration is not None and hasattr(integration, "post_process_skill_content"):
skill_content = integration.post_process_skill_content(skill_content)
(skill_subdir / "SKILL.md").write_text(skill_content, encoding="utf-8")
def _get_skills_dir(self) -> Optional[Path]:
"""Return the active skills directory for preset skill overrides.
@@ -624,7 +906,7 @@ class PresetManager:
try:
manifest = ExtensionManifest(manifest_path)
except ValidationError:
except (ValidationError, TypeError, AttributeError):
continue
ext_root = ext_dir.resolve()
@@ -761,6 +1043,13 @@ class PresetManager:
content = source_file.read_text(encoding="utf-8")
frontmatter, body = registrar.parse_frontmatter(content)
if frontmatter.get("strategy") == "wrap":
body, core_frontmatter = _substitute_core_template(body, cmd_name, self.project_root, registrar)
frontmatter = dict(frontmatter)
for key in ("scripts", "agent_scripts"):
if key not in frontmatter and key in core_frontmatter:
frontmatter[key] = core_frontmatter[key]
original_desc = frontmatter.get("description", "")
enhanced_desc = SKILL_DESCRIPTIONS.get(
short_name,
@@ -974,6 +1263,24 @@ class PresetManager:
# Update corresponding skills when --ai-skills was previously used
registered_skills = self._register_skills(manifest, dest_dir)
# Detect wrap commands before registry.add() so a read failure doesn't
# leave a partially-committed registry entry.
wrap_commands = []
try:
from .agents import CommandRegistrar as _CR
_registrar = _CR()
for cmd_tmpl in manifest.templates:
if cmd_tmpl.get("type") != "command":
continue
cmd_file = dest_dir / cmd_tmpl["file"]
if not cmd_file.exists():
continue
cmd_fm, _ = _registrar.parse_frontmatter(cmd_file.read_text(encoding="utf-8"))
if cmd_fm.get("strategy") == "wrap":
wrap_commands.append(cmd_tmpl["name"])
except ImportError:
pass
self.registry.add(manifest.id, {
"version": manifest.version,
"source": "local",
@@ -982,8 +1289,12 @@ class PresetManager:
"priority": priority,
"registered_commands": registered_commands,
"registered_skills": registered_skills,
"wrap_commands": wrap_commands,
})
for cmd_name in wrap_commands:
self._replay_wraps_for_command(cmd_name)
return manifest
def install_from_zip(
@@ -1058,9 +1369,16 @@ class PresetManager:
# Restore original skills when preset is removed
registered_skills = metadata.get("registered_skills", []) if metadata else []
registered_commands = metadata.get("registered_commands", {}) if metadata else {}
wrap_commands = metadata.get("wrap_commands", []) if metadata else []
pack_dir = self.presets_dir / pack_id
# _unregister_skills must run before directory deletion (reads preset files)
if registered_skills:
self._unregister_skills(registered_skills, pack_dir)
# When _unregister_skills has already handled skill-agent files, strip
# those entries from registered_commands to avoid double-deletion.
# (When registered_skills is empty, skill-agent entries in
# registered_commands are the only deletion path for those files.)
try:
from .agents import CommandRegistrar
except ImportError:
@@ -1072,14 +1390,44 @@ class PresetManager:
if CommandRegistrar.AGENT_CONFIGS.get(agent_name, {}).get("extension") != "/SKILL.md"
}
# Unregister non-skill command files from AI agents.
if registered_commands:
self._unregister_commands(registered_commands)
# Delete the preset directory before mutating the registry so a
# filesystem failure cannot leave files on disk without a registry entry.
if pack_dir.exists():
shutil.rmtree(pack_dir)
# Remove from registry before replaying so _replay_wraps_for_command sees
# the post-removal registry state.
self.registry.remove(pack_id)
# Separate wrap commands from non-wrap commands in registered_commands.
non_wrap_commands = {
agent_name: [c for c in cmd_names if c not in wrap_commands]
for agent_name, cmd_names in registered_commands.items()
}
non_wrap_commands = {k: v for k, v in non_wrap_commands.items() if v}
# Unregister non-wrap command files from AI agents.
if non_wrap_commands:
self._unregister_commands(non_wrap_commands)
# For each wrapped command, either re-compose remaining wraps or delete.
for cmd_name in wrap_commands:
remaining = [
pid for pid, meta in self.registry.list().items()
if cmd_name in meta.get("wrap_commands", [])
]
if remaining:
self._replay_wraps_for_command(cmd_name)
else:
# No wrap presets remain — delete the agent file entirely.
wrap_agent_commands = {
agent_name: [c for c in cmd_names if c == cmd_name]
for agent_name, cmd_names in registered_commands.items()
}
wrap_agent_commands = {k: v for k, v in wrap_agent_commands.items() if v}
if wrap_agent_commands:
self._unregister_commands(wrap_agent_commands)
return True
def list_installed(self) -> List[Dict[str, Any]]:
@@ -1735,6 +2083,7 @@ class PresetResolver:
self,
template_name: str,
template_type: str = "template",
skip_presets: bool = False,
) -> Optional[Path]:
"""Resolve a template name to its file path.
@@ -1743,6 +2092,8 @@ class PresetResolver:
Args:
template_name: Template name (e.g., "spec-template")
template_type: Template type ("template", "command", or "script")
skip_presets: When True, skip tier 2 (installed presets). Use
resolve_core() as the preferred caller-facing API for this.
Returns:
Path to the resolved template file, or None if not found
@@ -1771,7 +2122,7 @@ class PresetResolver:
return override
# Priority 2: Installed presets (sorted by priority — lower number wins)
if self.presets_dir.exists():
if not skip_presets and self.presets_dir.exists():
registry = PresetRegistry(self.presets_dir)
for pack_id, _metadata in registry.list_by_priority():
pack_dir = self.presets_dir / pack_id
@@ -1810,6 +2161,99 @@ class PresetResolver:
if core.exists():
return core
# Priority 5: Bundled core_pack (wheel install) or repo-root templates
# (source-checkout / editable install). This is the canonical home for
# speckit's built-in command/template files and must always be checked
# so that strategy:wrap presets can locate {CORE_TEMPLATE}.
from specify_cli import _locate_core_pack # local import to avoid cycles
_core_pack = _locate_core_pack()
if _core_pack is not None:
# Wheel install path
if template_type == "template":
candidate = _core_pack / "templates" / f"{template_name}.md"
elif template_type == "command":
candidate = _core_pack / "commands" / f"{template_name}.md"
elif template_type == "script":
candidate = _core_pack / "scripts" / f"{template_name}{ext}"
else:
candidate = _core_pack / f"{template_name}.md"
if candidate.exists():
return candidate
else:
# Source-checkout / editable install: templates live at repo root
repo_root = Path(__file__).parent.parent.parent
if template_type == "template":
candidate = repo_root / "templates" / f"{template_name}.md"
elif template_type == "command":
candidate = repo_root / "templates" / "commands" / f"{template_name}.md"
elif template_type == "script":
candidate = repo_root / "scripts" / f"{template_name}{ext}"
else:
candidate = repo_root / f"{template_name}.md"
if candidate.exists():
return candidate
return None
def resolve_core(
self,
template_name: str,
template_type: str = "template",
) -> Optional[Path]:
"""Resolve while skipping installed presets (tier 2).
Searches tiers 1, 3, 4, and 5 (bundled core_pack / repo-root fallback).
Use when resolving {CORE_TEMPLATE} to guarantee the result is actual
base content, never another preset's wrap output.
"""
return self.resolve(template_name, template_type, skip_presets=True)
def resolve_extension_command_via_manifest(self, cmd_name: str) -> Optional[Path]:
"""Resolve an extension command by consulting installed extension manifests.
Walks installed extension directories in priority order, loads each
extension.yml via ExtensionManifest, and looks up the command by its
declared name to find the actual file path. This is necessary because
the manifest's ``provides.commands[].file`` field is authoritative and
may differ from the command name
(e.g. ``speckit.selftest.extension`` → ``commands/selftest.md``).
Returns None if no manifest maps the given command name, so the caller
can fall back to the name-based lookup.
"""
if not self.extensions_dir.exists():
return None
from .extensions import ExtensionManifest, ValidationError
for _priority, ext_id, _metadata in self._get_all_extensions_by_priority():
ext_dir = self.extensions_dir / ext_id
manifest_path = ext_dir / "extension.yml"
if not manifest_path.is_file():
continue
try:
manifest = ExtensionManifest(manifest_path)
except (ValidationError, OSError, TypeError, AttributeError):
continue
for cmd_info in manifest.commands:
if cmd_info.get("name") != cmd_name:
continue
file_rel = cmd_info.get("file")
if not file_rel:
continue
# Mirror the containment check in ExtensionManager to guard against
# path traversal via a malformed manifest (e.g. file: ../../AGENTS.md).
cmd_path = Path(file_rel)
if cmd_path.is_absolute():
continue
try:
ext_root = ext_dir.resolve()
candidate = (ext_root / cmd_path).resolve()
candidate.relative_to(ext_root) # raises ValueError if outside
except (OSError, ValueError):
continue
if candidate.is_file():
return candidate
return None
def resolve_with_source(

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()
@@ -256,7 +261,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):

View File

@@ -189,6 +189,23 @@ class YamlIntegrationTests:
assert "scripts:" not in parsed["prompt"]
assert "---" not in parsed["prompt"]
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)
@@ -220,37 +237,34 @@ class YamlIntegrationTests:
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 -------------------------------------------------
@@ -320,6 +334,30 @@ class YamlIntegrationTests:
commands = sorted(cmd_dir.glob("speckit.*.yaml"))
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 = [
@@ -344,10 +382,6 @@ class YamlIntegrationTests:
for stem in self.COMMAND_STEMS:
files.append(f"{cmd_dir}/speckit.{stem}.yaml")
# 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")
@@ -360,7 +394,6 @@ class YamlIntegrationTests:
"common.sh",
"create-new-feature.sh",
"setup-plan.sh",
"update-agent-context.sh",
]:
files.append(f".specify/scripts/bash/{name}")
else:
@@ -369,12 +402,10 @@ class YamlIntegrationTests:
"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",
@@ -387,6 +418,11 @@ class YamlIntegrationTests:
# 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

@@ -285,7 +285,7 @@ VALID_DESCRIPTOR = {
"commands": [
{"name": "speckit.specify", "file": "templates/speckit.specify.md"},
],
"scripts": ["update-context.sh"],
"scripts": [],
},
}
@@ -305,7 +305,7 @@ class TestIntegrationDescriptor:
assert desc.description == "Integration for My Agent"
assert desc.requires_speckit_version == ">=0.6.0"
assert len(desc.commands) == 1
assert desc.scripts == ["update-context.sh"]
assert desc.scripts == []
def test_missing_schema_version(self, tmp_path):
data = {**VALID_DESCRIPTOR}

View File

@@ -1,5 +1,6 @@
"""Tests for ClaudeIntegration."""
import codecs
import json
import os
from unittest.mock import patch
@@ -62,19 +63,57 @@ class TestClaudeIntegration:
assert parsed["disable-model-invocation"] is False
assert parsed["metadata"]["source"] == "templates/commands/plan.md"
def test_setup_installs_update_context_scripts(self, tmp_path):
def test_setup_upserts_context_section(self, tmp_path):
integration = get_integration("claude")
manifest = IntegrationManifest("claude", tmp_path)
created = integration.setup(tmp_path, manifest, script_type="sh")
integration.setup(tmp_path, manifest, script_type="sh")
scripts_dir = tmp_path / ".specify" / "integrations" / "claude" / "scripts"
assert scripts_dir.is_dir()
assert (scripts_dir / "update-context.sh").exists()
assert (scripts_dir / "update-context.ps1").exists()
ctx_path = tmp_path / integration.context_file
assert ctx_path.exists()
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
tracked = {path.resolve().relative_to(tmp_path.resolve()).as_posix() for path in created}
assert ".specify/integrations/claude/scripts/update-context.sh" in tracked
assert ".specify/integrations/claude/scripts/update-context.ps1" in tracked
def test_upsert_context_section_strips_bom(self, tmp_path):
"""Existing context file with UTF-8 BOM must be cleaned up on upsert."""
integration = get_integration("claude")
ctx_path = tmp_path / integration.context_file
# Write a file that starts with a UTF-8 BOM (as the old PowerShell script did)
bom = codecs.BOM_UTF8
ctx_path.write_bytes(bom + b"# CLAUDE.md\n\nSome existing content.\n")
integration.upsert_context_section(tmp_path)
result = ctx_path.read_bytes()
assert not result.startswith(bom), "BOM must be stripped after upsert"
content = result.decode("utf-8")
assert "<!-- SPECKIT START -->" in content
assert "Some existing content." in content
def test_remove_context_section_strips_bom(self, tmp_path):
"""remove_context_section must clean BOM from context file on Windows-authored files."""
integration = get_integration("claude")
ctx_path = tmp_path / integration.context_file
marker_content = (
"# CLAUDE.md\n\n"
"<!-- SPECKIT START -->\n"
"For additional context about technologies to be used, project structure,\n"
"shell commands, and other important information, read the current plan\n"
"<!-- SPECKIT END -->\n"
)
ctx_path.write_bytes(codecs.BOM_UTF8 + marker_content.encode("utf-8"))
result = integration.remove_context_section(tmp_path)
assert result is True
assert ctx_path.exists(), "File should exist (non-empty content remains)"
remaining = ctx_path.read_bytes()
assert not remaining.startswith(codecs.BOM_UTF8), "BOM must be stripped after remove"
assert b"<!-- SPECKIT" not in remaining
assert b"# CLAUDE.md" in remaining
def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path):
from typer.testing import CliRunner

View File

@@ -143,7 +143,20 @@ class TestCopilotIntegration:
assert "__AGENT__" not in content, f"{agent_file.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{agent_file.name} has unprocessed {{ARGS}}"
assert "\nscripts:\n" not in content
assert "\nagent_scripts:\n" not in content
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan command must reference copilot's context file."""
from specify_cli.integrations.copilot import CopilotIntegration
copilot = CopilotIntegration()
m = IntegrationManifest("copilot", tmp_path)
copilot.setup(tmp_path, m)
plan_file = tmp_path / ".github" / "agents" / "speckit.plan.agent.md"
assert plan_file.exists()
content = plan_file.read_text(encoding="utf-8")
assert copilot.context_file in content, (
f"Plan command should reference {copilot.context_file!r}"
)
assert "__CONTEXT_FILE__" not in content
def test_complete_file_inventory_sh(self, tmp_path):
"""Every file produced by specify init --integration copilot --script sh."""
@@ -181,18 +194,15 @@ class TestCopilotIntegration:
".github/prompts/speckit.tasks.prompt.md",
".github/prompts/speckit.taskstoissues.prompt.md",
".vscode/settings.json",
".github/copilot-instructions.md",
".specify/integration.json",
".specify/init-options.json",
".specify/integrations/copilot.manifest.json",
".specify/integrations/speckit.manifest.json",
".specify/integrations/copilot/scripts/update-context.ps1",
".specify/integrations/copilot/scripts/update-context.sh",
".specify/scripts/bash/check-prerequisites.sh",
".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",
".specify/templates/agent-file-template.md",
".specify/templates/checklist-template.md",
".specify/templates/constitution-template.md",
".specify/templates/plan-template.md",
@@ -243,18 +253,15 @@ class TestCopilotIntegration:
".github/prompts/speckit.tasks.prompt.md",
".github/prompts/speckit.taskstoissues.prompt.md",
".vscode/settings.json",
".github/copilot-instructions.md",
".specify/integration.json",
".specify/init-options.json",
".specify/integrations/copilot.manifest.json",
".specify/integrations/speckit.manifest.json",
".specify/integrations/copilot/scripts/update-context.ps1",
".specify/integrations/copilot/scripts/update-context.sh",
".specify/scripts/powershell/check-prerequisites.ps1",
".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",
".specify/templates/agent-file-template.md",
".specify/templates/checklist-template.md",
".specify/templates/constitution-template.md",
".specify/templates/plan-template.md",

View File

@@ -1,5 +1,10 @@
"""Tests for CursorAgentIntegration."""
from pathlib import Path
from specify_cli.integrations import get_integration
from specify_cli.integrations.manifest import IntegrationManifest
from .test_integration_base_skills import SkillsIntegrationTests
@@ -11,6 +16,81 @@ class TestCursorAgentIntegration(SkillsIntegrationTests):
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
class TestCursorMdcFrontmatter:
"""Verify .mdc frontmatter handling in upsert/remove context section."""
def _setup(self, tmp_path: Path):
i = get_integration("cursor-agent")
m = IntegrationManifest("cursor-agent", tmp_path)
return i, m
def test_new_mdc_gets_frontmatter(self, tmp_path):
"""A freshly created .mdc file includes alwaysApply: true."""
i, m = self._setup(tmp_path)
i.setup(tmp_path, m)
ctx = (tmp_path / i.context_file).read_text(encoding="utf-8")
assert ctx.startswith("---\n")
assert "alwaysApply: true" in ctx
def test_existing_mdc_without_frontmatter_gets_it(self, tmp_path):
"""An existing .mdc without frontmatter gets it added."""
i, m = self._setup(tmp_path)
ctx_path = tmp_path / i.context_file
ctx_path.parent.mkdir(parents=True, exist_ok=True)
ctx_path.write_text("# User rules\n", encoding="utf-8")
i.upsert_context_section(tmp_path)
content = ctx_path.read_text(encoding="utf-8")
assert content.lstrip().startswith("---")
assert "alwaysApply: true" in content
assert "# User rules" in content
def test_existing_mdc_with_frontmatter_preserves_it(self, tmp_path):
"""An existing .mdc with custom frontmatter is preserved."""
i, m = self._setup(tmp_path)
ctx_path = tmp_path / i.context_file
ctx_path.parent.mkdir(parents=True, exist_ok=True)
ctx_path.write_text(
"---\nalwaysApply: true\ncustomKey: hello\n---\n\n# Rules\n",
encoding="utf-8",
)
i.upsert_context_section(tmp_path)
content = ctx_path.read_text(encoding="utf-8")
assert "alwaysApply: true" in content
assert "customKey: hello" in content
assert "<!-- SPECKIT START -->" in content
def test_existing_mdc_wrong_alwaysapply_fixed(self, tmp_path):
"""An .mdc with alwaysApply: false gets corrected."""
i, m = self._setup(tmp_path)
ctx_path = tmp_path / i.context_file
ctx_path.parent.mkdir(parents=True, exist_ok=True)
ctx_path.write_text(
"---\nalwaysApply: false\n---\n\n# Rules\n",
encoding="utf-8",
)
i.upsert_context_section(tmp_path)
content = ctx_path.read_text(encoding="utf-8")
assert "alwaysApply: true" in content
assert "alwaysApply: false" not in content
def test_upsert_idempotent_no_duplicate_frontmatter(self, tmp_path):
"""Repeated upserts don't duplicate frontmatter."""
i, m = self._setup(tmp_path)
i.upsert_context_section(tmp_path)
i.upsert_context_section(tmp_path)
content = (tmp_path / i.context_file).read_text(encoding="utf-8")
assert content.count("alwaysApply") == 1
def test_remove_deletes_mdc_with_only_frontmatter(self, tmp_path):
"""Removing the section from a Speckit-only .mdc deletes the file."""
i, m = self._setup(tmp_path)
i.upsert_context_section(tmp_path)
ctx_path = tmp_path / i.context_file
assert ctx_path.exists()
i.remove_context_section(tmp_path)
assert not ctx_path.exists()
class TestCursorAgentAutoPromote:
"""--ai cursor-agent auto-promotes to integration path."""

View File

@@ -73,19 +73,16 @@ class TestForgeIntegration:
for f in command_files:
assert f.name.endswith(".md")
def test_setup_installs_update_scripts(self, tmp_path):
def test_setup_upserts_context_section(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
created = forge.setup(tmp_path, m)
script_files = [f for f in created if "scripts" in f.parts]
assert len(script_files) > 0
sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh"
ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1"
assert sh_script in created
assert ps_script in created
assert sh_script.exists()
assert ps_script.exists()
forge.setup(tmp_path, m)
ctx_path = tmp_path / forge.context_file
assert ctx_path.exists()
content = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in content
assert "<!-- SPECKIT END -->" in content
def test_all_created_files_tracked_in_manifest(self, tmp_path):
from specify_cli.integrations.forge import ForgeIntegration
@@ -159,7 +156,20 @@ class TestForgeIntegration:
assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS"
# Frontmatter sections should be stripped
assert "\nscripts:\n" not in content
assert "\nagent_scripts:\n" not in content
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan command must reference forge's context file."""
from specify_cli.integrations.forge import ForgeIntegration
forge = ForgeIntegration()
m = IntegrationManifest("forge", tmp_path)
forge.setup(tmp_path, m)
plan_file = tmp_path / ".forge" / "commands" / "speckit.plan.md"
assert plan_file.exists()
content = plan_file.read_text(encoding="utf-8")
assert forge.context_file in content, (
f"Plan command should reference {forge.context_file!r}"
)
assert "__CONTEXT_FILE__" not in content
def test_forge_specific_transformations(self, tmp_path):
"""Test Forge-specific processing: name injection and handoffs stripping."""

View File

@@ -31,9 +31,9 @@ class TestGenericIntegration:
i = get_integration("generic")
assert i.config["requires_cli"] is False
def test_context_file_is_none(self):
def test_context_file_is_agents_md(self):
i = get_integration("generic")
assert i.context_file is None
assert i.context_file == "AGENTS.md"
# -- Options ----------------------------------------------------------
@@ -158,30 +158,31 @@ class TestGenericIntegration:
cmd_files = [f for f in created if "scripts" not in f.parts]
assert len(cmd_files) > 0
# -- 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("generic")
m = IntegrationManifest("generic", tmp_path)
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
scripts_dir = tmp_path / ".specify" / "integrations" / "generic" / "scripts"
assert scripts_dir.is_dir(), "Scripts directory not created for generic"
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()
content = ctx_path.read_text(encoding="utf-8")
assert "<!-- SPECKIT START -->" in content
assert "<!-- SPECKIT END -->" in content
def test_scripts_tracked_in_manifest(self, tmp_path):
def test_plan_references_correct_context_file(self, tmp_path):
"""The generated plan command must reference generic's context file."""
i = get_integration("generic")
m = IntegrationManifest("generic", tmp_path)
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
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("generic")
m = IntegrationManifest("generic", tmp_path)
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
sh = tmp_path / ".specify" / "integrations" / "generic" / "scripts" / "update-context.sh"
assert os.access(sh, os.X_OK)
plan_file = tmp_path / ".custom" / "cmds" / "speckit.plan.md"
assert plan_file.exists()
content = plan_file.read_text(encoding="utf-8")
assert i.context_file in content, (
f"Plan command should reference {i.context_file!r}"
)
assert "__CONTEXT_FILE__" not in content
# -- CLI --------------------------------------------------------------
@@ -198,6 +199,28 @@ class TestGenericIntegration:
# The integration path validates via setup()
assert result.exit_code != 0
def test_init_options_includes_context_file(self, tmp_path):
"""init-options.json must include context_file for the generic integration."""
import json
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / "opts-generic"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
result = CliRunner().invoke(app, [
"init", "--here", "--integration", "generic",
"--ai-commands-dir", ".myagent/commands",
"--script", "sh", "--no-git",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
opts = json.loads((project / ".specify" / "init-options.json").read_text())
assert opts.get("context_file") == "AGENTS.md"
def test_complete_file_inventory_sh(self, tmp_path):
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script sh."""
from typer.testing import CliRunner
@@ -221,6 +244,7 @@ class TestGenericIntegration:
for p in project.rglob("*") if p.is_file()
)
expected = sorted([
"AGENTS.md",
".myagent/commands/speckit.analyze.md",
".myagent/commands/speckit.checklist.md",
".myagent/commands/speckit.clarify.md",
@@ -233,16 +257,12 @@ class TestGenericIntegration:
".specify/init-options.json",
".specify/integration.json",
".specify/integrations/generic.manifest.json",
".specify/integrations/generic/scripts/update-context.ps1",
".specify/integrations/generic/scripts/update-context.sh",
".specify/integrations/speckit.manifest.json",
".specify/memory/constitution.md",
".specify/scripts/bash/check-prerequisites.sh",
".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",
".specify/templates/agent-file-template.md",
".specify/templates/checklist-template.md",
".specify/templates/constitution-template.md",
".specify/templates/plan-template.md",
@@ -279,6 +299,7 @@ class TestGenericIntegration:
for p in project.rglob("*") if p.is_file()
)
expected = sorted([
"AGENTS.md",
".myagent/commands/speckit.analyze.md",
".myagent/commands/speckit.checklist.md",
".myagent/commands/speckit.clarify.md",
@@ -291,16 +312,12 @@ class TestGenericIntegration:
".specify/init-options.json",
".specify/integration.json",
".specify/integrations/generic.manifest.json",
".specify/integrations/generic/scripts/update-context.ps1",
".specify/integrations/generic/scripts/update-context.sh",
".specify/integrations/speckit.manifest.json",
".specify/memory/constitution.md",
".specify/scripts/powershell/check-prerequisites.ps1",
".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",
".specify/templates/agent-file-template.md",
".specify/templates/checklist-template.md",
".specify/templates/constitution-template.md",
".specify/templates/plan-template.md",

View File

@@ -1,6 +1,5 @@
"""Consistency checks for agent configuration across runtime surfaces."""
import re
from pathlib import Path
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP
@@ -61,20 +60,6 @@ class TestAgentConfigConsistency:
assert "sha256sum -c -" in post_create_text
assert "KIRO_SKIP_KIRO_INSTALLER_VERIFY" not in post_create_text
def test_agent_context_scripts_use_kiro_cli(self):
"""Agent context scripts should advertise kiro-cli and not legacy q agent key."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "kiro-cli" in bash_text
assert "kiro-cli" in pwsh_text
assert "Amazon Q Developer CLI" not in bash_text
assert "Amazon Q Developer CLI" not in pwsh_text
# --- Tabnine CLI consistency checks ---
def test_runtime_config_includes_tabnine(self):
@@ -96,20 +81,6 @@ class TestAgentConfigConsistency:
assert cfg["args"] == "{{args}}"
assert cfg["extension"] == ".toml"
def test_agent_context_scripts_include_tabnine(self):
"""Agent context scripts should support tabnine agent type."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "tabnine" in bash_text
assert "TABNINE_FILE" in bash_text
assert "tabnine" in pwsh_text
assert "TABNINE_FILE" in pwsh_text
def test_ai_help_includes_tabnine(self):
"""CLI help text for --ai should include tabnine."""
assert "tabnine" in AI_ASSISTANT_HELP
@@ -132,18 +103,6 @@ class TestAgentConfigConsistency:
assert kimi_cfg["dir"] == ".kimi/skills"
assert kimi_cfg["extension"] == "/SKILL.md"
def test_kimi_in_powershell_validate_set(self):
"""PowerShell update-agent-context script should include 'kimi' in ValidateSet."""
ps_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
assert validate_set_match is not None
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
assert "kimi" in validate_set_values
def test_ai_help_includes_kimi(self):
"""CLI help text for --ai should include kimi."""
assert "kimi" in AI_ASSISTANT_HELP
@@ -168,32 +127,6 @@ class TestAgentConfigConsistency:
assert trae_cfg["args"] == "$ARGUMENTS"
assert trae_cfg["extension"] == "/SKILL.md"
def test_trae_in_agent_context_scripts(self):
"""Agent context scripts should support trae agent type."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "trae" in bash_text
assert "TRAE_FILE" in bash_text
assert "trae" in pwsh_text
assert "TRAE_FILE" in pwsh_text
def test_trae_in_powershell_validate_set(self):
"""PowerShell update-agent-context script should include 'trae' in ValidateSet."""
ps_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
assert validate_set_match is not None
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
assert "trae" in validate_set_values
def test_ai_help_includes_trae(self):
"""CLI help text for --ai should include trae."""
assert "trae" in AI_ASSISTANT_HELP
@@ -219,32 +152,6 @@ class TestAgentConfigConsistency:
assert pi_cfg["args"] == "$ARGUMENTS"
assert pi_cfg["extension"] == ".md"
def test_pi_in_powershell_validate_set(self):
"""PowerShell update-agent-context script should include 'pi' in ValidateSet."""
ps_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
assert validate_set_match is not None
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
assert "pi" in validate_set_values
def test_agent_context_scripts_include_pi(self):
"""Agent context scripts should support pi agent type."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "pi" in bash_text
assert "Pi Coding Agent" in bash_text
assert "pi" in pwsh_text
assert "Pi Coding Agent" in pwsh_text
def test_ai_help_includes_pi(self):
"""CLI help text for --ai should include pi."""
assert "pi" in AI_ASSISTANT_HELP
@@ -267,20 +174,6 @@ class TestAgentConfigConsistency:
assert cfg["iflow"]["format"] == "markdown"
assert cfg["iflow"]["args"] == "$ARGUMENTS"
def test_iflow_in_agent_context_scripts(self):
"""Agent context scripts should support iflow agent type."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "iflow" in bash_text
assert "IFLOW_FILE" in bash_text
assert "iflow" in pwsh_text
assert "IFLOW_FILE" in pwsh_text
def test_ai_help_includes_iflow(self):
"""CLI help text for --ai should include iflow."""
assert "iflow" in AI_ASSISTANT_HELP
@@ -303,18 +196,6 @@ class TestAgentConfigConsistency:
assert cfg["goose"]["format"] == "yaml"
assert cfg["goose"]["args"] == "{{args}}"
def test_goose_in_agent_context_scripts(self):
"""Agent context scripts should support goose agent type."""
bash_text = (
REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh"
).read_text(encoding="utf-8")
pwsh_text = (
REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1"
).read_text(encoding="utf-8")
assert "goose" in bash_text
assert "goose" in pwsh_text
def test_ai_help_includes_goose(self):
"""CLI help text for --ai should include goose."""
assert "goose" in AI_ASSISTANT_HELP

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