mirror of
https://github.com/github/spec-kit.git
synced 2026-07-06 22:32:13 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4f095a49 | ||
|
|
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
|
uses: actions/configure-pages@v6
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: 'docs/_site'
|
path: 'docs/_site'
|
||||||
|
|
||||||
|
|||||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
<!-- insert new changelog below this comment -->
|
<!-- insert new changelog below this comment -->
|
||||||
|
|
||||||
|
## [0.7.3] - 2026-04-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- fix: replace shell-based context updates with marker-based upsert (#2259)
|
||||||
|
- Add Community Friends page to docs site (#2261)
|
||||||
|
- Add Spec Scope extension to community catalog (#2172)
|
||||||
|
- docs: add Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace to README (#2250)
|
||||||
|
- fix: suppress CRLF warnings in auto-commit.ps1 (#2258)
|
||||||
|
- feat: register Blueprint in community catalog (#2252)
|
||||||
|
- preset: Update preset-fiction-book-writing to community catalog -> v1.5.0 (#2256)
|
||||||
|
- chore(deps): bump actions/upload-pages-artifact from 3 to 5 (#2251)
|
||||||
|
- fix: add reference/*.md to docfx content glob (#2248)
|
||||||
|
- chore: release 0.7.2, begin 0.7.3.dev0 development (#2247)
|
||||||
|
|
||||||
|
## [0.7.2] - 2026-04-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- docs: add core commands reference and simplify README CLI section (#2245)
|
||||||
|
- docs: add workflows reference, reorganize into docs/reference/, and add --version flag (#2244)
|
||||||
|
- docs: add presets reference page and rename pack_id to preset_id (#2243)
|
||||||
|
- docs: add extensions reference page and integrations FAQ (#2242)
|
||||||
|
- docs: consolidate integration documentation into docs/integrations.md (#2241)
|
||||||
|
- feat: update memorylint and superpowers-bridge versions to 1.3.0 with new download URLs (#2240)
|
||||||
|
- feat: Integration catalog — discovery, versioning, and community distribution (#2130)
|
||||||
|
- Add Catalog CI extension to community catalog (#2239)
|
||||||
|
- Added issues extension (#2194)
|
||||||
|
- chore: release 0.7.1, begin 0.7.2.dev0 development (#2235)
|
||||||
|
|
||||||
## [0.7.1] - 2026-04-15
|
## [0.7.1] - 2026-04-15
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
15
README.md
15
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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
||||||
@@ -245,6 +246,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| 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 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 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 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 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) |
|
||||||
| 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) |
|
| 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) |
|
| 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) |
|
||||||
@@ -272,7 +274,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
|
|||||||
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations): 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 with 5 prose profiles. Supports interactive elements like brainstorming, interview, roleplay. | 21 templates, 26 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
||||||
@@ -303,16 +305,7 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
|
|
||||||
## 🛠️ Community Friends
|
## 🛠️ Community Friends
|
||||||
|
|
||||||
> [!NOTE]
|
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.
|
||||||
> 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.
|
|
||||||
|
|
||||||
## 🤖 Supported AI Coding Agent Integrations
|
## 🤖 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.
|
||||||
@@ -4,7 +4,9 @@
|
|||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"*.md",
|
"*.md",
|
||||||
"toc.yml"
|
"toc.yml",
|
||||||
|
"community/*.md",
|
||||||
|
"reference/*.md"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,3 +33,9 @@
|
|||||||
items:
|
items:
|
||||||
- name: Local Development
|
- name: Local Development
|
||||||
href: local-development.md
|
href: local-development.md
|
||||||
|
|
||||||
|
# Community
|
||||||
|
- name: Community
|
||||||
|
items:
|
||||||
|
- name: Friends
|
||||||
|
href: community/friends.md
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-04-16T18:00:00Z",
|
"updated_at": "2026-04-17T02:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"aide": {
|
"aide": {
|
||||||
@@ -170,6 +170,38 @@
|
|||||||
"created_at": "2026-03-03T00:00:00Z",
|
"created_at": "2026-03-03T00:00:00Z",
|
||||||
"updated_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": {
|
"branch-convention": {
|
||||||
"name": "Branch Convention",
|
"name": "Branch Convention",
|
||||||
"id": "branch-convention",
|
"id": "branch-convention",
|
||||||
@@ -1625,6 +1657,39 @@
|
|||||||
"created_at": "2026-03-06T00:00:00Z",
|
"created_at": "2026-03-06T00:00:00Z",
|
||||||
"updated_at": "2026-04-09T00:00:00Z"
|
"updated_at": "2026-04-09T00: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": {
|
"security-review": {
|
||||||
"name": "Security Review",
|
"name": "Security Review",
|
||||||
"id": "security-review",
|
"id": "security-review",
|
||||||
|
|||||||
@@ -36,10 +36,17 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|||||||
exit 0
|
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 {
|
try {
|
||||||
git rev-parse --is-inside-work-tree 2>$null | Out-Null
|
git rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||||
if ($LASTEXITCODE -ne 0) { throw "not a repo" }
|
$isRepo = $LASTEXITCODE -eq 0
|
||||||
} catch {
|
} finally {
|
||||||
|
$ErrorActionPreference = $savedEAP
|
||||||
|
}
|
||||||
|
if (-not $isRepo) {
|
||||||
Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
|
Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -117,9 +124,16 @@ if (-not $enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Check if there are changes to commit
|
# Check if there are changes to commit
|
||||||
$diffHead = git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
|
# Relax ErrorActionPreference so CRLF warnings on stderr do not terminate.
|
||||||
$diffCached = git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
|
$savedEAP = $ErrorActionPreference
|
||||||
$untracked = git ls-files --others --exclude-standard 2>$null
|
$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) {
|
if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
|
||||||
Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
|
Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
|
||||||
@@ -136,6 +150,10 @@ if (-not $commitMsg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Stage and commit
|
# 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 {
|
try {
|
||||||
$out = git add . 2>&1 | Out-String
|
$out = git add . 2>&1 | Out-String
|
||||||
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
|
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
|
||||||
@@ -144,6 +162,8 @@ try {
|
|||||||
} catch {
|
} catch {
|
||||||
Write-Warning "[specify] Error: $_"
|
Write-Warning "[specify] Error: $_"
|
||||||
exit 1
|
exit 1
|
||||||
|
} finally {
|
||||||
|
$ErrorActionPreference = $savedEAP
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "[OK] Changes committed $phase $commandName"
|
Write-Host "[OK] Changes committed $phase $commandName"
|
||||||
|
|||||||
@@ -108,11 +108,11 @@
|
|||||||
"fiction-book-writing": {
|
"fiction-book-writing": {
|
||||||
"name": "Fiction Book Writing",
|
"name": "Fiction Book Writing",
|
||||||
"id": "fiction-book-writing",
|
"id": "fiction-book-writing",
|
||||||
"version": "1.3.0",
|
"version": "1.5.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.",
|
"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.",
|
||||||
"author": "Andreas Daumann",
|
"author": "Andreas Daumann",
|
||||||
"repository": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
|
"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.5.0.zip",
|
||||||
"homepage": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
|
"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",
|
"documentation": "https://github.com/adaumann/speckit-preset-fiction-book-writing/blob/main/fiction-book-writing/README.md",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -121,23 +121,24 @@
|
|||||||
},
|
},
|
||||||
"provides": {
|
"provides": {
|
||||||
"templates": 21,
|
"templates": 21,
|
||||||
"commands": 17,
|
"commands": 26
|
||||||
"scripts": 1
|
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"writing",
|
"writing",
|
||||||
"novel",
|
"novel",
|
||||||
"book",
|
|
||||||
"fiction",
|
"fiction",
|
||||||
"storytelling",
|
"storytelling",
|
||||||
"creative-writing",
|
"creative-writing",
|
||||||
"kdp",
|
"kdp",
|
||||||
"single-pov",
|
|
||||||
"multi-pov",
|
"multi-pov",
|
||||||
"export"
|
"export",
|
||||||
|
"book",
|
||||||
|
"brainstorming",
|
||||||
|
"roleplay",
|
||||||
|
"audiobook"
|
||||||
],
|
],
|
||||||
"created_at": "2026-04-09T08:00:00Z",
|
"created_at": "2026-04-09T08:00:00Z",
|
||||||
"updated_at": "2026-04-09T08:00:00Z"
|
"updated_at": "2026-04-16T08:00:00Z"
|
||||||
},
|
},
|
||||||
"multi-repo-branching": {
|
"multi-repo-branching": {
|
||||||
"name": "Multi-Repo Branching",
|
"name": "Multi-Repo Branching",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.7.2.dev0"
|
version = "0.7.3"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -28,7 +28,6 @@ packages = ["src/specify_cli"]
|
|||||||
[tool.hatch.build.targets.wheel.force-include]
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
# Bundle core assets so `specify init` works without network access (air-gapped / enterprise)
|
# Bundle core assets so `specify init` works without network access (air-gapped / enterprise)
|
||||||
# Page templates (exclude commands/ — bundled separately below to avoid duplication)
|
# 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/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md"
|
||||||
"templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-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"
|
"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
|
|
||||||
@@ -1261,15 +1261,11 @@ def init(
|
|||||||
manifest.save()
|
manifest.save()
|
||||||
|
|
||||||
# Write .specify/integration.json
|
# Write .specify/integration.json
|
||||||
script_ext = "sh" if selected_script == "sh" else "ps1"
|
|
||||||
integration_json = project_path / ".specify" / "integration.json"
|
integration_json = project_path / ".specify" / "integration.json"
|
||||||
integration_json.parent.mkdir(parents=True, exist_ok=True)
|
integration_json.parent.mkdir(parents=True, exist_ok=True)
|
||||||
integration_json.write_text(json.dumps({
|
integration_json.write_text(json.dumps({
|
||||||
"integration": resolved_integration.key,
|
"integration": resolved_integration.key,
|
||||||
"version": get_speckit_version(),
|
"version": get_speckit_version(),
|
||||||
"scripts": {
|
|
||||||
"update-context": f".specify/integrations/{resolved_integration.key}/scripts/update-context.{script_ext}",
|
|
||||||
},
|
|
||||||
}, indent=2) + "\n", encoding="utf-8")
|
}, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
tracker.complete("integration", resolved_integration.config.get("name", resolved_integration.key))
|
tracker.complete("integration", resolved_integration.config.get("name", resolved_integration.key))
|
||||||
@@ -1373,6 +1369,7 @@ def init(
|
|||||||
"ai": selected_ai,
|
"ai": selected_ai,
|
||||||
"integration": resolved_integration.key,
|
"integration": resolved_integration.key,
|
||||||
"branch_numbering": branch_numbering or "sequential",
|
"branch_numbering": branch_numbering or "sequential",
|
||||||
|
"context_file": resolved_integration.context_file,
|
||||||
"here": here,
|
"here": here,
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
"script": selected_script,
|
"script": selected_script,
|
||||||
@@ -1737,18 +1734,13 @@ def _read_integration_json(project_root: Path) -> dict[str, Any]:
|
|||||||
def _write_integration_json(
|
def _write_integration_json(
|
||||||
project_root: Path,
|
project_root: Path,
|
||||||
integration_key: str,
|
integration_key: str,
|
||||||
script_type: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Write ``.specify/integration.json`` for *integration_key*."""
|
"""Write ``.specify/integration.json`` for *integration_key*."""
|
||||||
script_ext = "sh" if script_type == "sh" else "ps1"
|
|
||||||
dest = project_root / INTEGRATION_JSON
|
dest = project_root / INTEGRATION_JSON
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
dest.write_text(json.dumps({
|
dest.write_text(json.dumps({
|
||||||
"integration": integration_key,
|
"integration": integration_key,
|
||||||
"version": get_speckit_version(),
|
"version": get_speckit_version(),
|
||||||
"scripts": {
|
|
||||||
"update-context": f".specify/integrations/{integration_key}/scripts/update-context.{script_ext}",
|
|
||||||
},
|
|
||||||
}, indent=2) + "\n", encoding="utf-8")
|
}, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@@ -1936,7 +1928,7 @@ def integration_install(
|
|||||||
raw_options=integration_options,
|
raw_options=integration_options,
|
||||||
)
|
)
|
||||||
manifest.save()
|
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)
|
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2013,6 +2005,7 @@ def _update_init_options_for_integration(
|
|||||||
opts = load_init_options(project_root)
|
opts = load_init_options(project_root)
|
||||||
opts["integration"] = integration.key
|
opts["integration"] = integration.key
|
||||||
opts["ai"] = integration.key
|
opts["ai"] = integration.key
|
||||||
|
opts["context_file"] = integration.context_file
|
||||||
if script_type:
|
if script_type:
|
||||||
opts["script"] = script_type
|
opts["script"] = script_type
|
||||||
if isinstance(integration, SkillsIntegration):
|
if isinstance(integration, SkillsIntegration):
|
||||||
@@ -2064,6 +2057,7 @@ def integration_uninstall(
|
|||||||
opts.pop("integration", None)
|
opts.pop("integration", None)
|
||||||
opts.pop("ai", None)
|
opts.pop("ai", None)
|
||||||
opts.pop("ai_skills", None)
|
opts.pop("ai_skills", None)
|
||||||
|
opts.pop("context_file", None)
|
||||||
save_init_options(project_root, opts)
|
save_init_options(project_root, opts)
|
||||||
raise typer.Exit(0)
|
raise typer.Exit(0)
|
||||||
|
|
||||||
@@ -2082,6 +2076,10 @@ def integration_uninstall(
|
|||||||
|
|
||||||
removed, skipped = manifest.uninstall(project_root, force=force)
|
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)
|
_remove_integration_json(project_root)
|
||||||
|
|
||||||
# Update init-options.json to clear the integration
|
# Update init-options.json to clear the integration
|
||||||
@@ -2090,6 +2088,7 @@ def integration_uninstall(
|
|||||||
opts.pop("integration", None)
|
opts.pop("integration", None)
|
||||||
opts.pop("ai", None)
|
opts.pop("ai", None)
|
||||||
opts.pop("ai_skills", None)
|
opts.pop("ai_skills", None)
|
||||||
|
opts.pop("context_file", None)
|
||||||
save_init_options(project_root, opts)
|
save_init_options(project_root, opts)
|
||||||
|
|
||||||
name = (integration.config or {}).get("name", key) if integration else key
|
name = (integration.config or {}).get("name", key) if integration else key
|
||||||
@@ -2156,6 +2155,7 @@ def integration_switch(
|
|||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
removed, skipped = old_manifest.uninstall(project_root, force=force)
|
removed, skipped = old_manifest.uninstall(project_root, force=force)
|
||||||
|
current_integration.remove_context_section(project_root)
|
||||||
if removed:
|
if removed:
|
||||||
console.print(f" Removed {len(removed)} file(s)")
|
console.print(f" Removed {len(removed)} file(s)")
|
||||||
if skipped:
|
if skipped:
|
||||||
@@ -2186,6 +2186,7 @@ def integration_switch(
|
|||||||
opts.pop("integration", None)
|
opts.pop("integration", None)
|
||||||
opts.pop("ai", None)
|
opts.pop("ai", None)
|
||||||
opts.pop("ai_skills", None)
|
opts.pop("ai_skills", None)
|
||||||
|
opts.pop("context_file", None)
|
||||||
save_init_options(project_root, opts)
|
save_init_options(project_root, opts)
|
||||||
|
|
||||||
# Ensure shared infrastructure is present (safe to run unconditionally;
|
# Ensure shared infrastructure is present (safe to run unconditionally;
|
||||||
@@ -2212,7 +2213,7 @@ def integration_switch(
|
|||||||
raw_options=integration_options,
|
raw_options=integration_options,
|
||||||
)
|
)
|
||||||
manifest.save()
|
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)
|
_update_init_options_for_integration(project_root, target_integration, script_type=selected_script)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2320,7 +2321,7 @@ def integration_upgrade(
|
|||||||
raw_options=integration_options,
|
raw_options=integration_options,
|
||||||
)
|
)
|
||||||
new_manifest.save()
|
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)
|
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Don't teardown — setup overwrites in-place, so teardown would
|
# Don't teardown — setup overwrites in-place, so teardown would
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ class CommandRegistrar:
|
|||||||
"""Normalize script paths in frontmatter to generated project locations.
|
"""Normalize script paths in frontmatter to generated project locations.
|
||||||
|
|
||||||
Rewrites known repo-relative and top-level script paths under the
|
Rewrites known repo-relative and top-level script paths under the
|
||||||
`scripts` and `agent_scripts` keys (for example `../../scripts/`,
|
``scripts`` key (for example ``../../scripts/``,
|
||||||
`../../templates/`, `../../memory/`, `scripts/`, `templates/`, and
|
``../../templates/``, ``../../memory/``, ``scripts/``, ``templates/``, and
|
||||||
`memory/`) to the `.specify/...` paths used in generated projects.
|
``memory/``) to the ``.specify/...`` paths used in generated projects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frontmatter: Frontmatter dictionary
|
frontmatter: Frontmatter dictionary
|
||||||
@@ -122,11 +122,8 @@ class CommandRegistrar:
|
|||||||
"""
|
"""
|
||||||
frontmatter = deepcopy(frontmatter)
|
frontmatter = deepcopy(frontmatter)
|
||||||
|
|
||||||
for script_key in ("scripts", "agent_scripts"):
|
scripts = frontmatter.get("scripts")
|
||||||
scripts = frontmatter.get(script_key)
|
if isinstance(scripts, dict):
|
||||||
if not isinstance(scripts, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for key, script_path in scripts.items():
|
for key, script_path in scripts.items():
|
||||||
if isinstance(script_path, str):
|
if isinstance(script_path, str):
|
||||||
scripts[key] = self.rewrite_project_relative_paths(script_path)
|
scripts[key] = self.rewrite_project_relative_paths(script_path)
|
||||||
@@ -333,11 +330,8 @@ class CommandRegistrar:
|
|||||||
frontmatter = {}
|
frontmatter = {}
|
||||||
|
|
||||||
scripts = frontmatter.get("scripts", {}) or {}
|
scripts = frontmatter.get("scripts", {}) or {}
|
||||||
agent_scripts = frontmatter.get("agent_scripts", {}) or {}
|
|
||||||
if not isinstance(scripts, dict):
|
if not isinstance(scripts, dict):
|
||||||
scripts = {}
|
scripts = {}
|
||||||
if not isinstance(agent_scripts, dict):
|
|
||||||
agent_scripts = {}
|
|
||||||
|
|
||||||
init_opts = load_init_options(project_root)
|
init_opts = load_init_options(project_root)
|
||||||
if not isinstance(init_opts, dict):
|
if not isinstance(init_opts, dict):
|
||||||
@@ -351,17 +345,14 @@ class CommandRegistrar:
|
|||||||
)
|
)
|
||||||
secondary_variant = "sh" if default_variant == "ps" else "ps"
|
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)
|
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)
|
fallback_order.append(secondary_variant)
|
||||||
|
|
||||||
for key in scripts:
|
for key in scripts:
|
||||||
if key not in fallback_order:
|
if key not in fallback_order:
|
||||||
fallback_order.append(key)
|
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
|
script_variant = fallback_order[0] if fallback_order else None
|
||||||
|
|
||||||
@@ -370,14 +361,12 @@ class CommandRegistrar:
|
|||||||
script_command = script_command.replace("{ARGS}", "$ARGUMENTS")
|
script_command = script_command.replace("{ARGS}", "$ARGUMENTS")
|
||||||
body = body.replace("{SCRIPT}", script_command)
|
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)
|
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)
|
return CommandRegistrar.rewrite_project_relative_paths(body)
|
||||||
|
|
||||||
def _convert_argument_placeholder(
|
def _convert_argument_placeholder(
|
||||||
|
|||||||
@@ -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
|
context_file: str | None = None
|
||||||
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
|
"""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 -------------------------------------------------------
|
# -- Public API -------------------------------------------------------
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -380,22 +385,235 @@ class IntegrationBase(ABC):
|
|||||||
|
|
||||||
return created
|
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")
|
||||||
|
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")
|
||||||
|
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
|
@staticmethod
|
||||||
def process_template(
|
def process_template(
|
||||||
content: str,
|
content: str,
|
||||||
agent_name: str,
|
agent_name: str,
|
||||||
script_type: str,
|
script_type: str,
|
||||||
arg_placeholder: str = "$ARGUMENTS",
|
arg_placeholder: str = "$ARGUMENTS",
|
||||||
|
context_file: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Process a raw command template into agent-ready content.
|
"""Process a raw command template into agent-ready content.
|
||||||
|
|
||||||
Performs the same transformations as the release script:
|
Performs the same transformations as the release script:
|
||||||
1. Extract ``scripts.<script_type>`` value from YAML frontmatter
|
1. Extract ``scripts.<script_type>`` value from YAML frontmatter
|
||||||
2. Replace ``{SCRIPT}`` with the extracted script command
|
2. Replace ``{SCRIPT}`` with the extracted script command
|
||||||
3. Extract ``agent_scripts.<script_type>`` and replace ``{AGENT_SCRIPT}``
|
3. Strip ``scripts:`` section from frontmatter
|
||||||
4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter
|
4. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
|
||||||
5. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
|
5. Replace ``__AGENT__`` with *agent_name*
|
||||||
6. Replace ``__AGENT__`` with *agent_name*
|
6. Replace ``__CONTEXT_FILE__`` with *context_file*
|
||||||
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
||||||
"""
|
"""
|
||||||
# 1. Extract script command from frontmatter
|
# 1. Extract script command from frontmatter
|
||||||
@@ -421,25 +639,7 @@ class IntegrationBase(ABC):
|
|||||||
if script_command:
|
if script_command:
|
||||||
content = content.replace("{SCRIPT}", script_command)
|
content = content.replace("{SCRIPT}", script_command)
|
||||||
|
|
||||||
# 3. Extract agent_script command
|
# 3. Strip scripts: section from frontmatter
|
||||||
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
|
|
||||||
lines = content.splitlines(keepends=True)
|
lines = content.splitlines(keepends=True)
|
||||||
output_lines: list[str] = []
|
output_lines: list[str] = []
|
||||||
in_frontmatter = False
|
in_frontmatter = False
|
||||||
@@ -457,23 +657,26 @@ class IntegrationBase(ABC):
|
|||||||
output_lines.append(line)
|
output_lines.append(line)
|
||||||
continue
|
continue
|
||||||
if in_frontmatter:
|
if in_frontmatter:
|
||||||
if stripped in ("scripts:", "agent_scripts:"):
|
if stripped == "scripts:":
|
||||||
skip_section = True
|
skip_section = True
|
||||||
continue
|
continue
|
||||||
if skip_section:
|
if skip_section:
|
||||||
if line[0:1].isspace():
|
if line[0:1].isspace():
|
||||||
continue # skip indented content under scripts/agent_scripts
|
continue # skip indented content under scripts
|
||||||
skip_section = False
|
skip_section = False
|
||||||
output_lines.append(line)
|
output_lines.append(line)
|
||||||
content = "".join(output_lines)
|
content = "".join(output_lines)
|
||||||
|
|
||||||
# 5. Replace {ARGS} and $ARGUMENTS
|
# 4. Replace {ARGS} and $ARGUMENTS
|
||||||
content = content.replace("{ARGS}", arg_placeholder)
|
content = content.replace("{ARGS}", arg_placeholder)
|
||||||
content = content.replace("$ARGUMENTS", arg_placeholder)
|
content = content.replace("$ARGUMENTS", arg_placeholder)
|
||||||
|
|
||||||
# 6. Replace __AGENT__
|
# 5. Replace __AGENT__
|
||||||
content = content.replace("__AGENT__", agent_name)
|
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
|
# 7. Rewrite paths — delegate to the shared implementation in
|
||||||
# CommandRegistrar so extension-local paths are preserved and
|
# CommandRegistrar so extension-local paths are preserved and
|
||||||
# boundary rules stay consistent across the codebase.
|
# boundary rules stay consistent across the codebase.
|
||||||
@@ -526,6 +729,9 @@ class IntegrationBase(ABC):
|
|||||||
self.record_file_in_manifest(dst_file, project_root, manifest)
|
self.record_file_in_manifest(dst_file, project_root, manifest)
|
||||||
created.append(dst_file)
|
created.append(dst_file)
|
||||||
|
|
||||||
|
# Upsert managed context section into the agent context file
|
||||||
|
self.upsert_context_section(project_root)
|
||||||
|
|
||||||
return created
|
return created
|
||||||
|
|
||||||
def teardown(
|
def teardown(
|
||||||
@@ -539,9 +745,11 @@ class IntegrationBase(ABC):
|
|||||||
|
|
||||||
Delegates to ``manifest.uninstall()`` which only removes files
|
Delegates to ``manifest.uninstall()`` which only removes files
|
||||||
whose hash still matches the recorded value (unless *force*).
|
whose hash still matches the recorded value (unless *force*).
|
||||||
|
Also removes the managed context section from the agent file.
|
||||||
|
|
||||||
Returns ``(removed, skipped)`` file lists.
|
Returns ``(removed, skipped)`` file lists.
|
||||||
"""
|
"""
|
||||||
|
self.remove_context_section(project_root)
|
||||||
return manifest.uninstall(project_root, force=force)
|
return manifest.uninstall(project_root, force=force)
|
||||||
|
|
||||||
# -- Convenience helpers for subclasses -------------------------------
|
# -- Convenience helpers for subclasses -------------------------------
|
||||||
@@ -579,8 +787,8 @@ class MarkdownIntegration(IntegrationBase):
|
|||||||
(and optionally ``context_file``). Everything else is inherited.
|
(and optionally ``context_file``). Everything else is inherited.
|
||||||
|
|
||||||
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
||||||
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
|
``{ARGS}``, ``__AGENT__``, rewriting paths) and upserts the
|
||||||
integration-specific scripts (``update-context.sh`` / ``.ps1``).
|
managed context section into the agent context file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def build_exec_args(
|
def build_exec_args(
|
||||||
@@ -638,7 +846,8 @@ class MarkdownIntegration(IntegrationBase):
|
|||||||
for src_file in templates:
|
for src_file in templates:
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
processed = self.process_template(
|
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_name = self.command_filename(src_file.stem)
|
||||||
dst_file = self.write_file_and_record(
|
dst_file = self.write_file_and_record(
|
||||||
@@ -646,7 +855,9 @@ class MarkdownIntegration(IntegrationBase):
|
|||||||
)
|
)
|
||||||
created.append(dst_file)
|
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
|
return created
|
||||||
|
|
||||||
|
|
||||||
@@ -841,7 +1052,8 @@ class TomlIntegration(IntegrationBase):
|
|||||||
raw = src_file.read_text(encoding="utf-8")
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
description = self._extract_description(raw)
|
description = self._extract_description(raw)
|
||||||
processed = self.process_template(
|
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)
|
_, body = self._split_frontmatter(processed)
|
||||||
toml_content = self._render_toml(description, body)
|
toml_content = self._render_toml(description, body)
|
||||||
@@ -851,7 +1063,9 @@ class TomlIntegration(IntegrationBase):
|
|||||||
)
|
)
|
||||||
created.append(dst_file)
|
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
|
return created
|
||||||
|
|
||||||
|
|
||||||
@@ -1021,7 +1235,8 @@ class YamlIntegration(IntegrationBase):
|
|||||||
title = self._human_title(src_file.stem)
|
title = self._human_title(src_file.stem)
|
||||||
|
|
||||||
processed = self.process_template(
|
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)
|
_, body = self._split_frontmatter(processed)
|
||||||
yaml_content = self._render_yaml(
|
yaml_content = self._render_yaml(
|
||||||
@@ -1033,7 +1248,9 @@ class YamlIntegration(IntegrationBase):
|
|||||||
)
|
)
|
||||||
created.append(dst_file)
|
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
|
return created
|
||||||
|
|
||||||
|
|
||||||
@@ -1176,7 +1393,8 @@ class SkillsIntegration(IntegrationBase):
|
|||||||
|
|
||||||
# Process body through the standard template pipeline
|
# Process body through the standard template pipeline
|
||||||
processed_body = self.process_template(
|
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.
|
# Strip the processed frontmatter — we rebuild it for skills.
|
||||||
# Preserve leading whitespace in the body to match release ZIP
|
# Preserve leading whitespace in the body to match release ZIP
|
||||||
@@ -1220,5 +1438,7 @@ class SkillsIntegration(IntegrationBase):
|
|||||||
)
|
)
|
||||||
created.append(dst)
|
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
|
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
|
|
||||||
@@ -183,7 +183,10 @@ class CopilotIntegration(IntegrationBase):
|
|||||||
# 1. Process and write command files as .agent.md
|
# 1. Process and write command files as .agent.md
|
||||||
for src_file in templates:
|
for src_file in templates:
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
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_name = self.command_filename(src_file.stem)
|
||||||
dst_file = self.write_file_and_record(
|
dst_file = self.write_file_and_record(
|
||||||
processed, dest / dst_name, project_root, manifest
|
processed, dest / dst_name, project_root, manifest
|
||||||
@@ -217,8 +220,8 @@ class CopilotIntegration(IntegrationBase):
|
|||||||
self.record_file_in_manifest(dst_settings, project_root, manifest)
|
self.record_file_in_manifest(dst_settings, project_root, manifest)
|
||||||
created.append(dst_settings)
|
created.append(dst_settings)
|
||||||
|
|
||||||
# 4. Install integration-specific update-context scripts
|
# 4. Upsert managed context section into the agent context file
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
self.upsert_context_section(project_root)
|
||||||
|
|
||||||
return created
|
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:
|
for src_file in templates:
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
# Process template with standard MarkdownIntegration logic
|
# 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
|
# FORGE-SPECIFIC: Ensure any remaining $ARGUMENTS placeholders are
|
||||||
# converted to {{parameters}}
|
# converted to {{parameters}}
|
||||||
@@ -145,8 +148,8 @@ class ForgeIntegration(MarkdownIntegration):
|
|||||||
)
|
)
|
||||||
created.append(dst_file)
|
created.append(dst_file)
|
||||||
|
|
||||||
# Install integration-specific update-context scripts
|
# Upsert managed context section into the agent context file
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
self.upsert_context_section(project_root)
|
||||||
|
|
||||||
return created
|
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",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md",
|
"extension": ".md",
|
||||||
}
|
}
|
||||||
context_file = None
|
context_file = "AGENTS.md"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def options(cls) -> list[IntegrationOption]:
|
def options(cls) -> list[IntegrationOption]:
|
||||||
@@ -122,12 +122,17 @@ class GenericIntegration(MarkdownIntegration):
|
|||||||
|
|
||||||
for src_file in templates:
|
for src_file in templates:
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
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_name = self.command_filename(src_file.stem)
|
||||||
dst_file = self.write_file_and_record(
|
dst_file = self.write_file_and_record(
|
||||||
processed, dest / dst_name, project_root, manifest
|
processed, dest / dst_name, project_root, manifest
|
||||||
)
|
)
|
||||||
created.append(dst_file)
|
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
|
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
|
|
||||||
@@ -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:
|
scripts:
|
||||||
sh: scripts/bash/setup-plan.sh --json
|
sh: scripts/bash/setup-plan.sh --json
|
||||||
ps: scripts/powershell/setup-plan.ps1 -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
|
## 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.)
|
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||||
|
|
||||||
3. **Agent context update**:
|
3. **Agent context update**:
|
||||||
- Run `{AGENT_SCRIPT}`
|
- 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)
|
||||||
- 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
|
|
||||||
|
|
||||||
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
|
**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file
|
||||||
|
|
||||||
## Key rules
|
## 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
|
- ERROR on gate failures or unresolved clarifications
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -585,6 +586,156 @@ class TestAutoCommitPowerShell:
|
|||||||
assert "\u2713" not in result.stdout, "Must not use Unicode checkmark"
|
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 ──────────────────────────────────────────────────────
|
# ── git-common.sh Tests ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,14 +56,19 @@ class TestInitIntegrationFlag:
|
|||||||
|
|
||||||
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
|
||||||
assert data["integration"] == "copilot"
|
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"))
|
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
|
||||||
assert opts["integration"] == "copilot"
|
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.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"
|
shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json"
|
||||||
assert shared_manifest.exists()
|
assert shared_manifest.exists()
|
||||||
|
|||||||
@@ -99,7 +99,23 @@ class MarkdownIntegrationTests:
|
|||||||
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
|
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
|
||||||
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
|
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 "\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):
|
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
@@ -132,30 +148,35 @@ class MarkdownIntegrationTests:
|
|||||||
assert modified_file.exists()
|
assert modified_file.exists()
|
||||||
assert modified_file in skipped
|
assert modified_file in skipped
|
||||||
|
|
||||||
# -- 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)
|
|
||||||
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):
|
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
script_rels = [k for k in m.files if "update-context" in k]
|
if i.context_file:
|
||||||
assert len(script_rels) >= 2
|
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)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
|
m.save()
|
||||||
assert os.access(sh, os.X_OK)
|
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 -------------------------------------------------
|
# -- CLI auto-promote -------------------------------------------------
|
||||||
|
|
||||||
@@ -203,6 +224,30 @@ class MarkdownIntegrationTests:
|
|||||||
commands = sorted(cmd_dir.glob("speckit.*"))
|
commands = sorted(cmd_dir.glob("speckit.*"))
|
||||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
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 ------------------------------------------
|
# -- Complete file inventory ------------------------------------------
|
||||||
|
|
||||||
COMMAND_STEMS = [
|
COMMAND_STEMS = [
|
||||||
@@ -220,10 +265,6 @@ class MarkdownIntegrationTests:
|
|||||||
for stem in self.COMMAND_STEMS:
|
for stem in self.COMMAND_STEMS:
|
||||||
files.append(f"{cmd_dir}/speckit.{stem}.md")
|
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
|
# Framework files
|
||||||
files.append(f".specify/integration.json")
|
files.append(f".specify/integration.json")
|
||||||
files.append(f".specify/init-options.json")
|
files.append(f".specify/init-options.json")
|
||||||
@@ -232,14 +273,14 @@ class MarkdownIntegrationTests:
|
|||||||
|
|
||||||
if script_variant == "sh":
|
if script_variant == "sh":
|
||||||
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.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}")
|
files.append(f".specify/scripts/bash/{name}")
|
||||||
else:
|
else:
|
||||||
for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1",
|
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}")
|
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",
|
"constitution-template.md", "plan-template.md",
|
||||||
"spec-template.md", "tasks-template.md"]:
|
"spec-template.md", "tasks-template.md"]:
|
||||||
files.append(f".specify/templates/{name}")
|
files.append(f".specify/templates/{name}")
|
||||||
@@ -248,6 +289,11 @@ class MarkdownIntegrationTests:
|
|||||||
# Bundled workflow
|
# Bundled workflow
|
||||||
files.append(".specify/workflows/speckit/workflow.yml")
|
files.append(".specify/workflows/speckit/workflow.yml")
|
||||||
files.append(".specify/workflows/workflow-registry.json")
|
files.append(".specify/workflows/workflow-registry.json")
|
||||||
|
|
||||||
|
# Agent context file (if set)
|
||||||
|
if i.context_file:
|
||||||
|
files.append(i.context_file)
|
||||||
|
|
||||||
return sorted(files)
|
return sorted(files)
|
||||||
|
|
||||||
def test_complete_file_inventory_sh(self, tmp_path):
|
def test_complete_file_inventory_sh(self, tmp_path):
|
||||||
|
|||||||
@@ -173,6 +173,23 @@ class SkillsIntegrationTests:
|
|||||||
body = parts[2].strip() if len(parts) >= 3 else ""
|
body = parts[2].strip() if len(parts) >= 3 else ""
|
||||||
assert len(body) > 0, f"{f} has empty body"
|
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):
|
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
@@ -217,30 +234,34 @@ class SkillsIntegrationTests:
|
|||||||
|
|
||||||
assert (foreign_dir / "SKILL.md").exists(), "Foreign skill was removed"
|
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)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts"
|
if i.context_file:
|
||||||
assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}"
|
ctx_path = tmp_path / i.context_file
|
||||||
assert (scripts_dir / "update-context.sh").exists()
|
assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}"
|
||||||
assert (scripts_dir / "update-context.ps1").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
|
||||||
|
|
||||||
def test_scripts_tracked_in_manifest(self, tmp_path):
|
def test_teardown_removes_context_section(self, tmp_path):
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
script_rels = [k for k in m.files if "update-context" in k]
|
m.save()
|
||||||
assert len(script_rels) >= 2
|
if i.context_file:
|
||||||
|
ctx_path = tmp_path / i.context_file
|
||||||
def test_sh_script_is_executable(self, tmp_path):
|
content = ctx_path.read_text(encoding="utf-8")
|
||||||
i = get_integration(self.KEY)
|
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
i.teardown(tmp_path, m)
|
||||||
i.setup(tmp_path, m)
|
remaining = ctx_path.read_text(encoding="utf-8")
|
||||||
sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh"
|
assert "<!-- SPECKIT START -->" not in remaining
|
||||||
assert os.access(sh, os.X_OK)
|
assert "<!-- SPECKIT END -->" not in remaining
|
||||||
|
assert "# My Rules" in remaining
|
||||||
|
|
||||||
# -- CLI auto-promote -------------------------------------------------
|
# -- CLI auto-promote -------------------------------------------------
|
||||||
|
|
||||||
@@ -286,6 +307,30 @@ class SkillsIntegrationTests:
|
|||||||
skills_dir = i.skills_dest(project)
|
skills_dir = i.skills_dest(project)
|
||||||
assert skills_dir.is_dir(), f"Skills directory {skills_dir} not created"
|
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 ------------------------------------------------
|
# -- IntegrationOption ------------------------------------------------
|
||||||
|
|
||||||
def test_options_include_skills_flag(self):
|
def test_options_include_skills_flag(self):
|
||||||
@@ -316,8 +361,6 @@ class SkillsIntegrationTests:
|
|||||||
".specify/init-options.json",
|
".specify/init-options.json",
|
||||||
".specify/integration.json",
|
".specify/integration.json",
|
||||||
f".specify/integrations/{self.KEY}.manifest.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/integrations/speckit.manifest.json",
|
||||||
".specify/memory/constitution.md",
|
".specify/memory/constitution.md",
|
||||||
]
|
]
|
||||||
@@ -328,7 +371,6 @@ class SkillsIntegrationTests:
|
|||||||
".specify/scripts/bash/common.sh",
|
".specify/scripts/bash/common.sh",
|
||||||
".specify/scripts/bash/create-new-feature.sh",
|
".specify/scripts/bash/create-new-feature.sh",
|
||||||
".specify/scripts/bash/setup-plan.sh",
|
".specify/scripts/bash/setup-plan.sh",
|
||||||
".specify/scripts/bash/update-agent-context.sh",
|
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
files += [
|
files += [
|
||||||
@@ -336,11 +378,9 @@ class SkillsIntegrationTests:
|
|||||||
".specify/scripts/powershell/common.ps1",
|
".specify/scripts/powershell/common.ps1",
|
||||||
".specify/scripts/powershell/create-new-feature.ps1",
|
".specify/scripts/powershell/create-new-feature.ps1",
|
||||||
".specify/scripts/powershell/setup-plan.ps1",
|
".specify/scripts/powershell/setup-plan.ps1",
|
||||||
".specify/scripts/powershell/update-agent-context.ps1",
|
|
||||||
]
|
]
|
||||||
# Templates
|
# Templates
|
||||||
files += [
|
files += [
|
||||||
".specify/templates/agent-file-template.md",
|
|
||||||
".specify/templates/checklist-template.md",
|
".specify/templates/checklist-template.md",
|
||||||
".specify/templates/constitution-template.md",
|
".specify/templates/constitution-template.md",
|
||||||
".specify/templates/plan-template.md",
|
".specify/templates/plan-template.md",
|
||||||
@@ -352,6 +392,9 @@ class SkillsIntegrationTests:
|
|||||||
".specify/workflows/speckit/workflow.yml",
|
".specify/workflows/speckit/workflow.yml",
|
||||||
".specify/workflows/workflow-registry.json",
|
".specify/workflows/workflow-registry.json",
|
||||||
]
|
]
|
||||||
|
# Agent context file (if set)
|
||||||
|
if i.context_file:
|
||||||
|
files.append(i.context_file)
|
||||||
return sorted(files)
|
return sorted(files)
|
||||||
|
|
||||||
def test_complete_file_inventory_sh(self, tmp_path):
|
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
|
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"
|
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):
|
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
@@ -341,37 +358,34 @@ class TomlIntegrationTests:
|
|||||||
assert modified_file.exists()
|
assert modified_file.exists()
|
||||||
assert modified_file in skipped
|
assert modified_file in skipped
|
||||||
|
|
||||||
# -- 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)
|
|
||||||
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):
|
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
script_rels = [k for k in m.files if "update-context" in k]
|
if i.context_file:
|
||||||
assert len(script_rels) >= 2
|
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)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
sh = (
|
m.save()
|
||||||
tmp_path
|
if i.context_file:
|
||||||
/ ".specify"
|
ctx_path = tmp_path / i.context_file
|
||||||
/ "integrations"
|
content = ctx_path.read_text(encoding="utf-8")
|
||||||
/ self.KEY
|
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
|
||||||
/ "scripts"
|
i.teardown(tmp_path, m)
|
||||||
/ "update-context.sh"
|
remaining = ctx_path.read_text(encoding="utf-8")
|
||||||
)
|
assert "<!-- SPECKIT START -->" not in remaining
|
||||||
assert os.access(sh, os.X_OK)
|
assert "<!-- SPECKIT END -->" not in remaining
|
||||||
|
assert "# My Rules" in remaining
|
||||||
|
|
||||||
# -- CLI auto-promote -------------------------------------------------
|
# -- CLI auto-promote -------------------------------------------------
|
||||||
|
|
||||||
@@ -441,6 +455,30 @@ class TomlIntegrationTests:
|
|||||||
commands = sorted(cmd_dir.glob("speckit.*.toml"))
|
commands = sorted(cmd_dir.glob("speckit.*.toml"))
|
||||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
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 ------------------------------------------
|
# -- Complete file inventory ------------------------------------------
|
||||||
|
|
||||||
COMMAND_STEMS = [
|
COMMAND_STEMS = [
|
||||||
@@ -465,10 +503,6 @@ class TomlIntegrationTests:
|
|||||||
for stem in self.COMMAND_STEMS:
|
for stem in self.COMMAND_STEMS:
|
||||||
files.append(f"{cmd_dir}/speckit.{stem}.toml")
|
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
|
# Framework files
|
||||||
files.append(".specify/integration.json")
|
files.append(".specify/integration.json")
|
||||||
files.append(".specify/init-options.json")
|
files.append(".specify/init-options.json")
|
||||||
@@ -481,7 +515,6 @@ class TomlIntegrationTests:
|
|||||||
"common.sh",
|
"common.sh",
|
||||||
"create-new-feature.sh",
|
"create-new-feature.sh",
|
||||||
"setup-plan.sh",
|
"setup-plan.sh",
|
||||||
"update-agent-context.sh",
|
|
||||||
]:
|
]:
|
||||||
files.append(f".specify/scripts/bash/{name}")
|
files.append(f".specify/scripts/bash/{name}")
|
||||||
else:
|
else:
|
||||||
@@ -490,12 +523,10 @@ class TomlIntegrationTests:
|
|||||||
"common.ps1",
|
"common.ps1",
|
||||||
"create-new-feature.ps1",
|
"create-new-feature.ps1",
|
||||||
"setup-plan.ps1",
|
"setup-plan.ps1",
|
||||||
"update-agent-context.ps1",
|
|
||||||
]:
|
]:
|
||||||
files.append(f".specify/scripts/powershell/{name}")
|
files.append(f".specify/scripts/powershell/{name}")
|
||||||
|
|
||||||
for name in [
|
for name in [
|
||||||
"agent-file-template.md",
|
|
||||||
"checklist-template.md",
|
"checklist-template.md",
|
||||||
"constitution-template.md",
|
"constitution-template.md",
|
||||||
"plan-template.md",
|
"plan-template.md",
|
||||||
@@ -508,6 +539,11 @@ class TomlIntegrationTests:
|
|||||||
# Bundled workflow
|
# Bundled workflow
|
||||||
files.append(".specify/workflows/speckit/workflow.yml")
|
files.append(".specify/workflows/speckit/workflow.yml")
|
||||||
files.append(".specify/workflows/workflow-registry.json")
|
files.append(".specify/workflows/workflow-registry.json")
|
||||||
|
|
||||||
|
# Agent context file (if set)
|
||||||
|
if i.context_file:
|
||||||
|
files.append(i.context_file)
|
||||||
|
|
||||||
return sorted(files)
|
return sorted(files)
|
||||||
|
|
||||||
def test_complete_file_inventory_sh(self, tmp_path):
|
def test_complete_file_inventory_sh(self, tmp_path):
|
||||||
|
|||||||
@@ -189,6 +189,23 @@ class YamlIntegrationTests:
|
|||||||
assert "scripts:" not in parsed["prompt"]
|
assert "scripts:" not in parsed["prompt"]
|
||||||
assert "---" 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):
|
def test_all_files_tracked_in_manifest(self, tmp_path):
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
@@ -220,37 +237,34 @@ class YamlIntegrationTests:
|
|||||||
assert modified_file.exists()
|
assert modified_file.exists()
|
||||||
assert modified_file in skipped
|
assert modified_file in skipped
|
||||||
|
|
||||||
# -- 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)
|
|
||||||
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):
|
|
||||||
i = get_integration(self.KEY)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
script_rels = [k for k in m.files if "update-context" in k]
|
if i.context_file:
|
||||||
assert len(script_rels) >= 2
|
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)
|
i = get_integration(self.KEY)
|
||||||
m = IntegrationManifest(self.KEY, tmp_path)
|
m = IntegrationManifest(self.KEY, tmp_path)
|
||||||
i.setup(tmp_path, m)
|
i.setup(tmp_path, m)
|
||||||
sh = (
|
m.save()
|
||||||
tmp_path
|
if i.context_file:
|
||||||
/ ".specify"
|
ctx_path = tmp_path / i.context_file
|
||||||
/ "integrations"
|
content = ctx_path.read_text(encoding="utf-8")
|
||||||
/ self.KEY
|
ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8")
|
||||||
/ "scripts"
|
i.teardown(tmp_path, m)
|
||||||
/ "update-context.sh"
|
remaining = ctx_path.read_text(encoding="utf-8")
|
||||||
)
|
assert "<!-- SPECKIT START -->" not in remaining
|
||||||
assert os.access(sh, os.X_OK)
|
assert "<!-- SPECKIT END -->" not in remaining
|
||||||
|
assert "# My Rules" in remaining
|
||||||
|
|
||||||
# -- CLI auto-promote -------------------------------------------------
|
# -- CLI auto-promote -------------------------------------------------
|
||||||
|
|
||||||
@@ -320,6 +334,30 @@ class YamlIntegrationTests:
|
|||||||
commands = sorted(cmd_dir.glob("speckit.*.yaml"))
|
commands = sorted(cmd_dir.glob("speckit.*.yaml"))
|
||||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
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 ------------------------------------------
|
# -- Complete file inventory ------------------------------------------
|
||||||
|
|
||||||
COMMAND_STEMS = [
|
COMMAND_STEMS = [
|
||||||
@@ -344,10 +382,6 @@ class YamlIntegrationTests:
|
|||||||
for stem in self.COMMAND_STEMS:
|
for stem in self.COMMAND_STEMS:
|
||||||
files.append(f"{cmd_dir}/speckit.{stem}.yaml")
|
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
|
# Framework files
|
||||||
files.append(".specify/integration.json")
|
files.append(".specify/integration.json")
|
||||||
files.append(".specify/init-options.json")
|
files.append(".specify/init-options.json")
|
||||||
@@ -360,7 +394,6 @@ class YamlIntegrationTests:
|
|||||||
"common.sh",
|
"common.sh",
|
||||||
"create-new-feature.sh",
|
"create-new-feature.sh",
|
||||||
"setup-plan.sh",
|
"setup-plan.sh",
|
||||||
"update-agent-context.sh",
|
|
||||||
]:
|
]:
|
||||||
files.append(f".specify/scripts/bash/{name}")
|
files.append(f".specify/scripts/bash/{name}")
|
||||||
else:
|
else:
|
||||||
@@ -369,12 +402,10 @@ class YamlIntegrationTests:
|
|||||||
"common.ps1",
|
"common.ps1",
|
||||||
"create-new-feature.ps1",
|
"create-new-feature.ps1",
|
||||||
"setup-plan.ps1",
|
"setup-plan.ps1",
|
||||||
"update-agent-context.ps1",
|
|
||||||
]:
|
]:
|
||||||
files.append(f".specify/scripts/powershell/{name}")
|
files.append(f".specify/scripts/powershell/{name}")
|
||||||
|
|
||||||
for name in [
|
for name in [
|
||||||
"agent-file-template.md",
|
|
||||||
"checklist-template.md",
|
"checklist-template.md",
|
||||||
"constitution-template.md",
|
"constitution-template.md",
|
||||||
"plan-template.md",
|
"plan-template.md",
|
||||||
@@ -387,6 +418,11 @@ class YamlIntegrationTests:
|
|||||||
# Bundled workflow
|
# Bundled workflow
|
||||||
files.append(".specify/workflows/speckit/workflow.yml")
|
files.append(".specify/workflows/speckit/workflow.yml")
|
||||||
files.append(".specify/workflows/workflow-registry.json")
|
files.append(".specify/workflows/workflow-registry.json")
|
||||||
|
|
||||||
|
# Agent context file (if set)
|
||||||
|
if i.context_file:
|
||||||
|
files.append(i.context_file)
|
||||||
|
|
||||||
return sorted(files)
|
return sorted(files)
|
||||||
|
|
||||||
def test_complete_file_inventory_sh(self, tmp_path):
|
def test_complete_file_inventory_sh(self, tmp_path):
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ VALID_DESCRIPTOR = {
|
|||||||
"commands": [
|
"commands": [
|
||||||
{"name": "speckit.specify", "file": "templates/speckit.specify.md"},
|
{"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.description == "Integration for My Agent"
|
||||||
assert desc.requires_speckit_version == ">=0.6.0"
|
assert desc.requires_speckit_version == ">=0.6.0"
|
||||||
assert len(desc.commands) == 1
|
assert len(desc.commands) == 1
|
||||||
assert desc.scripts == ["update-context.sh"]
|
assert desc.scripts == []
|
||||||
|
|
||||||
def test_missing_schema_version(self, tmp_path):
|
def test_missing_schema_version(self, tmp_path):
|
||||||
data = {**VALID_DESCRIPTOR}
|
data = {**VALID_DESCRIPTOR}
|
||||||
|
|||||||
@@ -62,19 +62,17 @@ class TestClaudeIntegration:
|
|||||||
assert parsed["disable-model-invocation"] is False
|
assert parsed["disable-model-invocation"] is False
|
||||||
assert parsed["metadata"]["source"] == "templates/commands/plan.md"
|
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")
|
integration = get_integration("claude")
|
||||||
manifest = IntegrationManifest("claude", tmp_path)
|
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"
|
ctx_path = tmp_path / integration.context_file
|
||||||
assert scripts_dir.is_dir()
|
assert ctx_path.exists()
|
||||||
assert (scripts_dir / "update-context.sh").exists()
|
content = ctx_path.read_text(encoding="utf-8")
|
||||||
assert (scripts_dir / "update-context.ps1").exists()
|
assert "<!-- SPECKIT START -->" in content
|
||||||
|
assert "<!-- SPECKIT END -->" in content
|
||||||
tracked = {path.resolve().relative_to(tmp_path.resolve()).as_posix() for path in created}
|
assert "read the current plan" in content
|
||||||
assert ".specify/integrations/claude/scripts/update-context.sh" in tracked
|
|
||||||
assert ".specify/integrations/claude/scripts/update-context.ps1" in tracked
|
|
||||||
|
|
||||||
def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path):
|
def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path):
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|||||||
@@ -143,7 +143,20 @@ class TestCopilotIntegration:
|
|||||||
assert "__AGENT__" not in content, f"{agent_file.name} has unprocessed __AGENT__"
|
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 "{ARGS}" not in content, f"{agent_file.name} has unprocessed {{ARGS}}"
|
||||||
assert "\nscripts:\n" not in content
|
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):
|
def test_complete_file_inventory_sh(self, tmp_path):
|
||||||
"""Every file produced by specify init --integration copilot --script sh."""
|
"""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.tasks.prompt.md",
|
||||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||||
".vscode/settings.json",
|
".vscode/settings.json",
|
||||||
|
".github/copilot-instructions.md",
|
||||||
".specify/integration.json",
|
".specify/integration.json",
|
||||||
".specify/init-options.json",
|
".specify/init-options.json",
|
||||||
".specify/integrations/copilot.manifest.json",
|
".specify/integrations/copilot.manifest.json",
|
||||||
".specify/integrations/speckit.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/check-prerequisites.sh",
|
||||||
".specify/scripts/bash/common.sh",
|
".specify/scripts/bash/common.sh",
|
||||||
".specify/scripts/bash/create-new-feature.sh",
|
".specify/scripts/bash/create-new-feature.sh",
|
||||||
".specify/scripts/bash/setup-plan.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/checklist-template.md",
|
||||||
".specify/templates/constitution-template.md",
|
".specify/templates/constitution-template.md",
|
||||||
".specify/templates/plan-template.md",
|
".specify/templates/plan-template.md",
|
||||||
@@ -243,18 +253,15 @@ class TestCopilotIntegration:
|
|||||||
".github/prompts/speckit.tasks.prompt.md",
|
".github/prompts/speckit.tasks.prompt.md",
|
||||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||||
".vscode/settings.json",
|
".vscode/settings.json",
|
||||||
|
".github/copilot-instructions.md",
|
||||||
".specify/integration.json",
|
".specify/integration.json",
|
||||||
".specify/init-options.json",
|
".specify/init-options.json",
|
||||||
".specify/integrations/copilot.manifest.json",
|
".specify/integrations/copilot.manifest.json",
|
||||||
".specify/integrations/speckit.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/check-prerequisites.ps1",
|
||||||
".specify/scripts/powershell/common.ps1",
|
".specify/scripts/powershell/common.ps1",
|
||||||
".specify/scripts/powershell/create-new-feature.ps1",
|
".specify/scripts/powershell/create-new-feature.ps1",
|
||||||
".specify/scripts/powershell/setup-plan.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/checklist-template.md",
|
||||||
".specify/templates/constitution-template.md",
|
".specify/templates/constitution-template.md",
|
||||||
".specify/templates/plan-template.md",
|
".specify/templates/plan-template.md",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
"""Tests for CursorAgentIntegration."""
|
"""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
|
from .test_integration_base_skills import SkillsIntegrationTests
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +16,81 @@ class TestCursorAgentIntegration(SkillsIntegrationTests):
|
|||||||
CONTEXT_FILE = ".cursor/rules/specify-rules.mdc"
|
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:
|
class TestCursorAgentAutoPromote:
|
||||||
"""--ai cursor-agent auto-promotes to integration path."""
|
"""--ai cursor-agent auto-promotes to integration path."""
|
||||||
|
|
||||||
|
|||||||
@@ -73,19 +73,16 @@ class TestForgeIntegration:
|
|||||||
for f in command_files:
|
for f in command_files:
|
||||||
assert f.name.endswith(".md")
|
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
|
from specify_cli.integrations.forge import ForgeIntegration
|
||||||
forge = ForgeIntegration()
|
forge = ForgeIntegration()
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
m = IntegrationManifest("forge", tmp_path)
|
||||||
created = forge.setup(tmp_path, m)
|
forge.setup(tmp_path, m)
|
||||||
script_files = [f for f in created if "scripts" in f.parts]
|
ctx_path = tmp_path / forge.context_file
|
||||||
assert len(script_files) > 0
|
assert ctx_path.exists()
|
||||||
sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh"
|
content = ctx_path.read_text(encoding="utf-8")
|
||||||
ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1"
|
assert "<!-- SPECKIT START -->" in content
|
||||||
assert sh_script in created
|
assert "<!-- SPECKIT END -->" in content
|
||||||
assert ps_script in created
|
|
||||||
assert sh_script.exists()
|
|
||||||
assert ps_script.exists()
|
|
||||||
|
|
||||||
def test_all_created_files_tracked_in_manifest(self, tmp_path):
|
def test_all_created_files_tracked_in_manifest(self, tmp_path):
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
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"
|
assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS"
|
||||||
# Frontmatter sections should be stripped
|
# Frontmatter sections should be stripped
|
||||||
assert "\nscripts:\n" not in content
|
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):
|
def test_forge_specific_transformations(self, tmp_path):
|
||||||
"""Test Forge-specific processing: name injection and handoffs stripping."""
|
"""Test Forge-specific processing: name injection and handoffs stripping."""
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ class TestGenericIntegration:
|
|||||||
i = get_integration("generic")
|
i = get_integration("generic")
|
||||||
assert i.config["requires_cli"] is False
|
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")
|
i = get_integration("generic")
|
||||||
assert i.context_file is None
|
assert i.context_file == "AGENTS.md"
|
||||||
|
|
||||||
# -- Options ----------------------------------------------------------
|
# -- Options ----------------------------------------------------------
|
||||||
|
|
||||||
@@ -158,30 +158,31 @@ class TestGenericIntegration:
|
|||||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||||
assert len(cmd_files) > 0
|
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")
|
i = get_integration("generic")
|
||||||
m = IntegrationManifest("generic", tmp_path)
|
m = IntegrationManifest("generic", tmp_path)
|
||||||
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
||||||
scripts_dir = tmp_path / ".specify" / "integrations" / "generic" / "scripts"
|
if i.context_file:
|
||||||
assert scripts_dir.is_dir(), "Scripts directory not created for generic"
|
ctx_path = tmp_path / i.context_file
|
||||||
assert (scripts_dir / "update-context.sh").exists()
|
assert ctx_path.exists()
|
||||||
assert (scripts_dir / "update-context.ps1").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")
|
i = get_integration("generic")
|
||||||
m = IntegrationManifest("generic", tmp_path)
|
m = IntegrationManifest("generic", tmp_path)
|
||||||
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
||||||
script_rels = [k for k in m.files if "update-context" in k]
|
plan_file = tmp_path / ".custom" / "cmds" / "speckit.plan.md"
|
||||||
assert len(script_rels) >= 2
|
assert plan_file.exists()
|
||||||
|
content = plan_file.read_text(encoding="utf-8")
|
||||||
def test_sh_script_is_executable(self, tmp_path):
|
assert i.context_file in content, (
|
||||||
i = get_integration("generic")
|
f"Plan command should reference {i.context_file!r}"
|
||||||
m = IntegrationManifest("generic", tmp_path)
|
)
|
||||||
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
assert "__CONTEXT_FILE__" not in content
|
||||||
sh = tmp_path / ".specify" / "integrations" / "generic" / "scripts" / "update-context.sh"
|
|
||||||
assert os.access(sh, os.X_OK)
|
|
||||||
|
|
||||||
# -- CLI --------------------------------------------------------------
|
# -- CLI --------------------------------------------------------------
|
||||||
|
|
||||||
@@ -198,6 +199,28 @@ class TestGenericIntegration:
|
|||||||
# The integration path validates via setup()
|
# The integration path validates via setup()
|
||||||
assert result.exit_code != 0
|
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):
|
def test_complete_file_inventory_sh(self, tmp_path):
|
||||||
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script sh."""
|
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script sh."""
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
@@ -221,6 +244,7 @@ class TestGenericIntegration:
|
|||||||
for p in project.rglob("*") if p.is_file()
|
for p in project.rglob("*") if p.is_file()
|
||||||
)
|
)
|
||||||
expected = sorted([
|
expected = sorted([
|
||||||
|
"AGENTS.md",
|
||||||
".myagent/commands/speckit.analyze.md",
|
".myagent/commands/speckit.analyze.md",
|
||||||
".myagent/commands/speckit.checklist.md",
|
".myagent/commands/speckit.checklist.md",
|
||||||
".myagent/commands/speckit.clarify.md",
|
".myagent/commands/speckit.clarify.md",
|
||||||
@@ -233,16 +257,12 @@ class TestGenericIntegration:
|
|||||||
".specify/init-options.json",
|
".specify/init-options.json",
|
||||||
".specify/integration.json",
|
".specify/integration.json",
|
||||||
".specify/integrations/generic.manifest.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/integrations/speckit.manifest.json",
|
||||||
".specify/memory/constitution.md",
|
".specify/memory/constitution.md",
|
||||||
".specify/scripts/bash/check-prerequisites.sh",
|
".specify/scripts/bash/check-prerequisites.sh",
|
||||||
".specify/scripts/bash/common.sh",
|
".specify/scripts/bash/common.sh",
|
||||||
".specify/scripts/bash/create-new-feature.sh",
|
".specify/scripts/bash/create-new-feature.sh",
|
||||||
".specify/scripts/bash/setup-plan.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/checklist-template.md",
|
||||||
".specify/templates/constitution-template.md",
|
".specify/templates/constitution-template.md",
|
||||||
".specify/templates/plan-template.md",
|
".specify/templates/plan-template.md",
|
||||||
@@ -279,6 +299,7 @@ class TestGenericIntegration:
|
|||||||
for p in project.rglob("*") if p.is_file()
|
for p in project.rglob("*") if p.is_file()
|
||||||
)
|
)
|
||||||
expected = sorted([
|
expected = sorted([
|
||||||
|
"AGENTS.md",
|
||||||
".myagent/commands/speckit.analyze.md",
|
".myagent/commands/speckit.analyze.md",
|
||||||
".myagent/commands/speckit.checklist.md",
|
".myagent/commands/speckit.checklist.md",
|
||||||
".myagent/commands/speckit.clarify.md",
|
".myagent/commands/speckit.clarify.md",
|
||||||
@@ -291,16 +312,12 @@ class TestGenericIntegration:
|
|||||||
".specify/init-options.json",
|
".specify/init-options.json",
|
||||||
".specify/integration.json",
|
".specify/integration.json",
|
||||||
".specify/integrations/generic.manifest.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/integrations/speckit.manifest.json",
|
||||||
".specify/memory/constitution.md",
|
".specify/memory/constitution.md",
|
||||||
".specify/scripts/powershell/check-prerequisites.ps1",
|
".specify/scripts/powershell/check-prerequisites.ps1",
|
||||||
".specify/scripts/powershell/common.ps1",
|
".specify/scripts/powershell/common.ps1",
|
||||||
".specify/scripts/powershell/create-new-feature.ps1",
|
".specify/scripts/powershell/create-new-feature.ps1",
|
||||||
".specify/scripts/powershell/setup-plan.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/checklist-template.md",
|
||||||
".specify/templates/constitution-template.md",
|
".specify/templates/constitution-template.md",
|
||||||
".specify/templates/plan-template.md",
|
".specify/templates/plan-template.md",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Consistency checks for agent configuration across runtime surfaces."""
|
"""Consistency checks for agent configuration across runtime surfaces."""
|
||||||
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP
|
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 "sha256sum -c -" in post_create_text
|
||||||
assert "KIRO_SKIP_KIRO_INSTALLER_VERIFY" not 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 ---
|
# --- Tabnine CLI consistency checks ---
|
||||||
|
|
||||||
def test_runtime_config_includes_tabnine(self):
|
def test_runtime_config_includes_tabnine(self):
|
||||||
@@ -96,20 +81,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert cfg["args"] == "{{args}}"
|
assert cfg["args"] == "{{args}}"
|
||||||
assert cfg["extension"] == ".toml"
|
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):
|
def test_ai_help_includes_tabnine(self):
|
||||||
"""CLI help text for --ai should include tabnine."""
|
"""CLI help text for --ai should include tabnine."""
|
||||||
assert "tabnine" in AI_ASSISTANT_HELP
|
assert "tabnine" in AI_ASSISTANT_HELP
|
||||||
@@ -132,18 +103,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert kimi_cfg["dir"] == ".kimi/skills"
|
assert kimi_cfg["dir"] == ".kimi/skills"
|
||||||
assert kimi_cfg["extension"] == "/SKILL.md"
|
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):
|
def test_ai_help_includes_kimi(self):
|
||||||
"""CLI help text for --ai should include kimi."""
|
"""CLI help text for --ai should include kimi."""
|
||||||
assert "kimi" in AI_ASSISTANT_HELP
|
assert "kimi" in AI_ASSISTANT_HELP
|
||||||
@@ -168,32 +127,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert trae_cfg["args"] == "$ARGUMENTS"
|
assert trae_cfg["args"] == "$ARGUMENTS"
|
||||||
assert trae_cfg["extension"] == "/SKILL.md"
|
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):
|
def test_ai_help_includes_trae(self):
|
||||||
"""CLI help text for --ai should include trae."""
|
"""CLI help text for --ai should include trae."""
|
||||||
assert "trae" in AI_ASSISTANT_HELP
|
assert "trae" in AI_ASSISTANT_HELP
|
||||||
@@ -219,32 +152,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert pi_cfg["args"] == "$ARGUMENTS"
|
assert pi_cfg["args"] == "$ARGUMENTS"
|
||||||
assert pi_cfg["extension"] == ".md"
|
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):
|
def test_ai_help_includes_pi(self):
|
||||||
"""CLI help text for --ai should include pi."""
|
"""CLI help text for --ai should include pi."""
|
||||||
assert "pi" in AI_ASSISTANT_HELP
|
assert "pi" in AI_ASSISTANT_HELP
|
||||||
@@ -267,20 +174,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert cfg["iflow"]["format"] == "markdown"
|
assert cfg["iflow"]["format"] == "markdown"
|
||||||
assert cfg["iflow"]["args"] == "$ARGUMENTS"
|
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):
|
def test_ai_help_includes_iflow(self):
|
||||||
"""CLI help text for --ai should include iflow."""
|
"""CLI help text for --ai should include iflow."""
|
||||||
assert "iflow" in AI_ASSISTANT_HELP
|
assert "iflow" in AI_ASSISTANT_HELP
|
||||||
@@ -303,18 +196,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert cfg["goose"]["format"] == "yaml"
|
assert cfg["goose"]["format"] == "yaml"
|
||||||
assert cfg["goose"]["args"] == "{{args}}"
|
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):
|
def test_ai_help_includes_goose(self):
|
||||||
"""CLI help text for --ai should include goose."""
|
"""CLI help text for --ai should include goose."""
|
||||||
assert "goose" in AI_ASSISTANT_HELP
|
assert "goose" in AI_ASSISTANT_HELP
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for Cursor .mdc frontmatter generation (issue #669).
|
|
||||||
|
|
||||||
Verifies that update-agent-context.sh properly prepends YAML frontmatter
|
|
||||||
to .mdc files so that Cursor IDE auto-includes the rules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from tests.conftest import requires_bash
|
|
||||||
|
|
||||||
SCRIPT_PATH = os.path.join(
|
|
||||||
os.path.dirname(__file__),
|
|
||||||
os.pardir,
|
|
||||||
"scripts",
|
|
||||||
"bash",
|
|
||||||
"update-agent-context.sh",
|
|
||||||
)
|
|
||||||
|
|
||||||
EXPECTED_FRONTMATTER_LINES = [
|
|
||||||
"---",
|
|
||||||
"description: Project Development Guidelines",
|
|
||||||
'globs: ["**/*"]',
|
|
||||||
"alwaysApply: true",
|
|
||||||
"---",
|
|
||||||
]
|
|
||||||
|
|
||||||
requires_git = pytest.mark.skipif(
|
|
||||||
shutil.which("git") is None,
|
|
||||||
reason="git is not installed",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestScriptFrontmatterPattern:
|
|
||||||
"""Static analysis — no git required."""
|
|
||||||
|
|
||||||
def test_create_new_has_mdc_frontmatter_logic(self):
|
|
||||||
"""create_new_agent_file() must contain .mdc frontmatter logic."""
|
|
||||||
with open(SCRIPT_PATH, encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
assert 'if [[ "$target_file" == *.mdc ]]' in content
|
|
||||||
assert "alwaysApply: true" in content
|
|
||||||
|
|
||||||
def test_update_existing_has_mdc_frontmatter_logic(self):
|
|
||||||
"""update_existing_agent_file() must also handle .mdc frontmatter."""
|
|
||||||
with open(SCRIPT_PATH, encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
# There should be two occurrences of the .mdc check — one per function
|
|
||||||
occurrences = content.count('if [[ "$target_file" == *.mdc ]]')
|
|
||||||
assert occurrences >= 2, (
|
|
||||||
f"Expected at least 2 .mdc frontmatter checks, found {occurrences}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_powershell_script_has_mdc_frontmatter_logic(self):
|
|
||||||
"""PowerShell script must also handle .mdc frontmatter."""
|
|
||||||
ps_path = os.path.join(
|
|
||||||
os.path.dirname(__file__),
|
|
||||||
os.pardir,
|
|
||||||
"scripts",
|
|
||||||
"powershell",
|
|
||||||
"update-agent-context.ps1",
|
|
||||||
)
|
|
||||||
with open(ps_path, encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
assert "alwaysApply: true" in content
|
|
||||||
occurrences = content.count(r"\.mdc$")
|
|
||||||
assert occurrences >= 2, (
|
|
||||||
f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@requires_git
|
|
||||||
@requires_bash
|
|
||||||
class TestCursorFrontmatterIntegration:
|
|
||||||
"""Integration tests using a real git repo."""
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def git_repo(self, tmp_path):
|
|
||||||
"""Create a minimal git repo with the spec-kit structure."""
|
|
||||||
repo = tmp_path / "repo"
|
|
||||||
repo.mkdir()
|
|
||||||
|
|
||||||
# Init git repo
|
|
||||||
subprocess.run(
|
|
||||||
["git", "init"], cwd=str(repo), capture_output=True, check=True
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "user.email", "test@test.com"],
|
|
||||||
cwd=str(repo),
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "user.name", "Test"],
|
|
||||||
cwd=str(repo),
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create .specify dir with config
|
|
||||||
specify_dir = repo / ".specify"
|
|
||||||
specify_dir.mkdir()
|
|
||||||
(specify_dir / "config.yaml").write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
project_type: webapp
|
|
||||||
language: python
|
|
||||||
framework: fastapi
|
|
||||||
database: N/A
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create template
|
|
||||||
templates_dir = specify_dir / "templates"
|
|
||||||
templates_dir.mkdir()
|
|
||||||
(templates_dir / "agent-file-template.md").write_text(
|
|
||||||
"# [PROJECT NAME] Development Guidelines\n\n"
|
|
||||||
"Auto-generated from all feature plans. Last updated: [DATE]\n\n"
|
|
||||||
"## Active Technologies\n\n"
|
|
||||||
"[EXTRACTED FROM ALL PLAN.MD FILES]\n\n"
|
|
||||||
"## Project Structure\n\n"
|
|
||||||
"[ACTUAL STRUCTURE FROM PLANS]\n\n"
|
|
||||||
"## Development Commands\n\n"
|
|
||||||
"[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n"
|
|
||||||
"## Coding Conventions\n\n"
|
|
||||||
"[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n"
|
|
||||||
"## Recent Changes\n\n"
|
|
||||||
"[LAST 3 FEATURES AND WHAT THEY ADDED]\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create initial commit
|
|
||||||
subprocess.run(
|
|
||||||
["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "commit", "-m", "init"],
|
|
||||||
cwd=str(repo),
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a feature branch so CURRENT_BRANCH detection works
|
|
||||||
subprocess.run(
|
|
||||||
["git", "checkout", "-b", "001-test-feature"],
|
|
||||||
cwd=str(repo),
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a spec so the script detects the feature
|
|
||||||
spec_dir = repo / "specs" / "001-test-feature"
|
|
||||||
spec_dir.mkdir(parents=True)
|
|
||||||
(spec_dir / "plan.md").write_text(
|
|
||||||
"# Test Feature Plan\n\n"
|
|
||||||
"## Technology Stack\n\n"
|
|
||||||
"- Language: Python\n"
|
|
||||||
"- Framework: FastAPI\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
return repo
|
|
||||||
|
|
||||||
def _run_update(self, repo, agent_type="cursor-agent"):
|
|
||||||
"""Run update-agent-context.sh for a specific agent type."""
|
|
||||||
script = os.path.abspath(SCRIPT_PATH)
|
|
||||||
result = subprocess.run(
|
|
||||||
["bash", script, agent_type],
|
|
||||||
cwd=str(repo),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def test_new_mdc_file_has_frontmatter(self, git_repo):
|
|
||||||
"""Creating a new .mdc file must include YAML frontmatter."""
|
|
||||||
result = self._run_update(git_repo)
|
|
||||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
||||||
|
|
||||||
mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc"
|
|
||||||
assert mdc_file.exists(), "Cursor .mdc file was not created"
|
|
||||||
|
|
||||||
content = mdc_file.read_text()
|
|
||||||
lines = content.splitlines()
|
|
||||||
|
|
||||||
# First line must be the opening ---
|
|
||||||
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
|
|
||||||
|
|
||||||
# Check all frontmatter lines are present
|
|
||||||
for expected in EXPECTED_FRONTMATTER_LINES:
|
|
||||||
assert expected in content, f"Missing frontmatter line: {expected}"
|
|
||||||
|
|
||||||
# Content after frontmatter should be the template content
|
|
||||||
assert "Development Guidelines" in content
|
|
||||||
|
|
||||||
def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo):
|
|
||||||
"""Updating an existing .mdc file that lacks frontmatter must add it."""
|
|
||||||
# First, create the file WITHOUT frontmatter (simulating pre-fix state)
|
|
||||||
cursor_dir = git_repo / ".cursor" / "rules"
|
|
||||||
cursor_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
mdc_file = cursor_dir / "specify-rules.mdc"
|
|
||||||
mdc_file.write_text(
|
|
||||||
"# repo Development Guidelines\n\n"
|
|
||||||
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
|
|
||||||
"## Active Technologies\n\n"
|
|
||||||
"- Python + FastAPI (main)\n\n"
|
|
||||||
"## Recent Changes\n\n"
|
|
||||||
"- main: Added Python + FastAPI\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self._run_update(git_repo)
|
|
||||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
||||||
|
|
||||||
content = mdc_file.read_text()
|
|
||||||
lines = content.splitlines()
|
|
||||||
|
|
||||||
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
|
|
||||||
for expected in EXPECTED_FRONTMATTER_LINES:
|
|
||||||
assert expected in content, f"Missing frontmatter line: {expected}"
|
|
||||||
|
|
||||||
def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo):
|
|
||||||
"""Updating an .mdc file that already has frontmatter must not duplicate it."""
|
|
||||||
cursor_dir = git_repo / ".cursor" / "rules"
|
|
||||||
cursor_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
mdc_file = cursor_dir / "specify-rules.mdc"
|
|
||||||
|
|
||||||
frontmatter = (
|
|
||||||
"---\n"
|
|
||||||
"description: Project Development Guidelines\n"
|
|
||||||
'globs: ["**/*"]\n'
|
|
||||||
"alwaysApply: true\n"
|
|
||||||
"---\n\n"
|
|
||||||
)
|
|
||||||
body = (
|
|
||||||
"# repo Development Guidelines\n\n"
|
|
||||||
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
|
|
||||||
"## Active Technologies\n\n"
|
|
||||||
"- Python + FastAPI (main)\n\n"
|
|
||||||
"## Recent Changes\n\n"
|
|
||||||
"- main: Added Python + FastAPI\n"
|
|
||||||
)
|
|
||||||
mdc_file.write_text(frontmatter + body)
|
|
||||||
|
|
||||||
result = self._run_update(git_repo)
|
|
||||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
||||||
|
|
||||||
content = mdc_file.read_text()
|
|
||||||
# Count occurrences of the frontmatter delimiter
|
|
||||||
assert content.count("alwaysApply: true") == 1, (
|
|
||||||
"Frontmatter was duplicated"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_non_mdc_file_has_no_frontmatter(self, git_repo):
|
|
||||||
"""Non-.mdc agent files (e.g., Claude) must NOT get frontmatter."""
|
|
||||||
result = self._run_update(git_repo, agent_type="claude")
|
|
||||||
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
||||||
|
|
||||||
claude_file = git_repo / ".claude" / "CLAUDE.md"
|
|
||||||
if claude_file.exists():
|
|
||||||
content = claude_file.read_text()
|
|
||||||
assert not content.startswith("---"), (
|
|
||||||
"Non-mdc file should not have frontmatter"
|
|
||||||
)
|
|
||||||
@@ -396,11 +396,8 @@ class TestExtensionSkillRegistration:
|
|||||||
"description: Scripted plan command\n"
|
"description: Scripted plan command\n"
|
||||||
"scripts:\n"
|
"scripts:\n"
|
||||||
" sh: ../../scripts/bash/setup-plan.sh --json \"{ARGS}\"\n"
|
" sh: ../../scripts/bash/setup-plan.sh --json \"{ARGS}\"\n"
|
||||||
"agent_scripts:\n"
|
|
||||||
" sh: ../../scripts/bash/update-agent-context.sh __AGENT__\n"
|
|
||||||
"---\n\n"
|
"---\n\n"
|
||||||
"Run {SCRIPT}\n"
|
"Run {SCRIPT}\n"
|
||||||
"Then {AGENT_SCRIPT}\n"
|
|
||||||
"Review templates/checklist.md and memory/constitution.md for __AGENT__.\n"
|
"Review templates/checklist.md and memory/constitution.md for __AGENT__.\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -409,11 +406,9 @@ class TestExtensionSkillRegistration:
|
|||||||
|
|
||||||
content = (skills_dir / "speckit-scripted-ext-plan" / "SKILL.md").read_text()
|
content = (skills_dir / "speckit-scripted-ext-plan" / "SKILL.md").read_text()
|
||||||
assert "{SCRIPT}" not in content
|
assert "{SCRIPT}" not in content
|
||||||
assert "{AGENT_SCRIPT}" not in content
|
|
||||||
assert "{ARGS}" not in content
|
assert "{ARGS}" not in content
|
||||||
assert "__AGENT__" not in content
|
assert "__AGENT__" not in content
|
||||||
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
||||||
assert ".specify/scripts/bash/update-agent-context.sh claude" in content
|
|
||||||
assert ".specify/templates/checklist.md" in content
|
assert ".specify/templates/checklist.md" in content
|
||||||
assert ".specify/memory/constitution.md" in content
|
assert ".specify/memory/constitution.md" in content
|
||||||
|
|
||||||
|
|||||||
@@ -1334,13 +1334,9 @@ description: "Scripted command"
|
|||||||
scripts:
|
scripts:
|
||||||
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
||||||
ps: ../../scripts/powershell/setup-plan.ps1 -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__
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Run {SCRIPT}
|
Run {SCRIPT}
|
||||||
Then {AGENT_SCRIPT}
|
|
||||||
Agent __AGENT__
|
Agent __AGENT__
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -1361,11 +1357,9 @@ Agent __AGENT__
|
|||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
assert "{SCRIPT}" not in content
|
assert "{SCRIPT}" not in content
|
||||||
assert "{AGENT_SCRIPT}" not in content
|
|
||||||
assert "__AGENT__" not in content
|
assert "__AGENT__" not in content
|
||||||
assert "{ARGS}" not in content
|
assert "{ARGS}" not in content
|
||||||
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
||||||
assert ".specify/scripts/bash/update-agent-context.sh codex" in content
|
|
||||||
|
|
||||||
def test_codex_skill_alias_frontmatter_matches_alias_name(self, project_dir, temp_dir):
|
def test_codex_skill_alias_frontmatter_matches_alias_name(self, project_dir, temp_dir):
|
||||||
"""Codex alias skills should render their own matching `name:` frontmatter."""
|
"""Codex alias skills should render their own matching `name:` frontmatter."""
|
||||||
@@ -1451,13 +1445,9 @@ description: "Fallback scripted command"
|
|||||||
scripts:
|
scripts:
|
||||||
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
||||||
ps: ../../scripts/powershell/setup-plan.ps1 -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 __AGENT__
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Run {SCRIPT}
|
Run {SCRIPT}
|
||||||
Then {AGENT_SCRIPT}
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1474,13 +1464,10 @@ Then {AGENT_SCRIPT}
|
|||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
assert "{SCRIPT}" not in content
|
assert "{SCRIPT}" not in content
|
||||||
assert "{AGENT_SCRIPT}" not in content
|
|
||||||
if platform.system().lower().startswith("win"):
|
if platform.system().lower().startswith("win"):
|
||||||
assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content
|
assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content
|
||||||
assert ".specify/scripts/powershell/update-agent-context.ps1 codex" in content
|
|
||||||
else:
|
else:
|
||||||
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
|
||||||
assert ".specify/scripts/bash/update-agent-context.sh codex" in content
|
|
||||||
|
|
||||||
def test_codex_skill_registration_handles_non_dict_init_options(
|
def test_codex_skill_registration_handles_non_dict_init_options(
|
||||||
self, project_dir, temp_dir
|
self, project_dir, temp_dir
|
||||||
@@ -1577,13 +1564,9 @@ description: "Windows fallback scripted command"
|
|||||||
scripts:
|
scripts:
|
||||||
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}"
|
||||||
ps: ../../scripts/powershell/setup-plan.ps1 -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__
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Run {SCRIPT}
|
Run {SCRIPT}
|
||||||
Then {AGENT_SCRIPT}
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1599,7 +1582,6 @@ Then {AGENT_SCRIPT}
|
|||||||
|
|
||||||
content = skill_file.read_text()
|
content = skill_file.read_text()
|
||||||
assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content
|
assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content
|
||||||
assert ".specify/scripts/powershell/update-agent-context.ps1 -AgentType codex" in content
|
|
||||||
assert ".specify/scripts/bash/setup-plan.sh" not in content
|
assert ".specify/scripts/bash/setup-plan.sh" not in content
|
||||||
|
|
||||||
def test_register_commands_for_copilot(self, extension_dir, project_dir):
|
def test_register_commands_for_copilot(self, extension_dir, project_dir):
|
||||||
|
|||||||
@@ -1648,7 +1648,6 @@ CORE_TEMPLATE_NAMES = [
|
|||||||
"tasks-template",
|
"tasks-template",
|
||||||
"checklist-template",
|
"checklist-template",
|
||||||
"constitution-template",
|
"constitution-template",
|
||||||
"agent-file-template",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -2911,7 +2910,7 @@ class TestLeanPreset:
|
|||||||
assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}"
|
assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}"
|
||||||
|
|
||||||
def test_lean_commands_have_no_scripts(self):
|
def test_lean_commands_have_no_scripts(self):
|
||||||
"""Verify lean commands have no scripts or agent_scripts in frontmatter."""
|
"""Verify lean commands have no scripts in frontmatter."""
|
||||||
from specify_cli.agents import CommandRegistrar
|
from specify_cli.agents import CommandRegistrar
|
||||||
|
|
||||||
for name in LEAN_COMMAND_NAMES:
|
for name in LEAN_COMMAND_NAMES:
|
||||||
@@ -2919,7 +2918,6 @@ class TestLeanPreset:
|
|||||||
content = cmd_path.read_text()
|
content = cmd_path.read_text()
|
||||||
frontmatter, _ = CommandRegistrar.parse_frontmatter(content)
|
frontmatter, _ = CommandRegistrar.parse_frontmatter(content)
|
||||||
assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter"
|
assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter"
|
||||||
assert "agent_scripts" not in frontmatter, f"{name} should not have agent_scripts in frontmatter"
|
|
||||||
|
|
||||||
def test_lean_commands_have_no_hooks(self):
|
def test_lean_commands_have_no_hooks(self):
|
||||||
"""Verify lean commands do not contain extension hook boilerplate."""
|
"""Verify lean commands do not contain extension hook boilerplate."""
|
||||||
|
|||||||
Reference in New Issue
Block a user