mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b70de43dc | ||
|
|
ecb3b94b43 | ||
|
|
c5c20134df | ||
|
|
58f7a43ec3 | ||
|
|
efb04e26eb | ||
|
|
c52ea23ba2 | ||
|
|
d402a392c3 | ||
|
|
deb80956f3 | ||
|
|
4dcf2921d1 | ||
|
|
dd9c0b0500 | ||
|
|
22e76995c7 | ||
|
|
569d18a59d | ||
|
|
f10fd07481 | ||
|
|
370b5b4890 | ||
|
|
52afbea47a | ||
|
|
85e00f63d6 | ||
|
|
fc1cec9748 | ||
|
|
ad46da552c | ||
|
|
919f09283d | ||
|
|
8d99f030ce | ||
|
|
b4c4e86cbc | ||
|
|
dc057a2314 | ||
|
|
2568422085 | ||
|
|
c118c1c30f | ||
|
|
fc3d1244c0 | ||
|
|
518dc9ddad | ||
|
|
13b614e9d5 | ||
|
|
3b82e0bcdd | ||
|
|
ba9a8b8e59 | ||
|
|
dedcae7cd8 | ||
|
|
2c11525be5 | ||
|
|
ca382992f7 | ||
|
|
669e253809 | ||
|
|
26fab003ee |
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -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
29
.zenodo.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -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
31
CITATION.cff
Normal 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
|
||||
58
README.md
58
README.md
@@ -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
14
docs/community/friends.md
Normal 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
20
docs/community/presets.md
Normal 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).
|
||||
20
docs/community/walkthroughs.md
Normal file
20
docs/community/walkthroughs.md
Normal 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.
|
||||
@@ -4,7 +4,9 @@
|
||||
{
|
||||
"files": [
|
||||
"*.md",
|
||||
"toc.yml"
|
||||
"toc.yml",
|
||||
"community/*.md",
|
||||
"reference/*.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
10
docs/toc.yml
10
docs/toc.yml
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
presets/self-test/commands/speckit.wrap-test.md
Normal file
14
presets/self-test/commands/speckit.wrap-test.md
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user