fips-agents-cli 0.7.0__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. fips_agents_cli-0.8.2/.github/CODEOWNERS +2 -0
  2. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.gitignore +4 -0
  3. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/CLAUDE.md +16 -5
  4. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/PKG-INFO +78 -6
  5. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/README.md +77 -5
  6. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/pyproject.toml +1 -1
  7. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/scripts/release.sh +27 -6
  8. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/cli.py +2 -0
  9. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/create.py +25 -1
  10. fips_agents_cli-0.8.2/src/fips_agents_cli/commands/vendor.py +154 -0
  11. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/git.py +10 -1
  12. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/project.py +130 -1
  13. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/version.py +1 -1
  14. fips_agents_cli-0.8.2/tests/fixtures/middleware_template/component.py.j2 +39 -0
  15. fips_agents_cli-0.8.2/tests/fixtures/middleware_template/test.py.j2 +40 -0
  16. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_create.py +4 -4
  17. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_generate.py +117 -0
  18. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.claude/commands/create-release.md +0 -0
  19. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.claude/docs-state.json +0 -0
  20. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.github/agents/README.md +0 -0
  21. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.github/agents/create-release.agent.md +0 -0
  22. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.github/workflows/test.yml +0 -0
  23. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/.github/workflows/workflow.yaml +0 -0
  24. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/LICENSE +0 -0
  25. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/RELEASE_CHECKLIST.md +0 -0
  26. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/docs/PUBLISHING.md +0 -0
  27. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/docs/QUICK_START_PUBLISHING.md +0 -0
  28. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/docs/README.md +0 -0
  29. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/llms.txt +0 -0
  30. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/AGENT_FRAMEWORK_PLAN.md +0 -0
  31. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/GENERATOR_IMPLEMENTATION_PLAN.md +0 -0
  32. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/IMPLEMENTATION_SUMMARY.md +0 -0
  33. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/MVP-PLAN.md +0 -0
  34. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/PLAN.md +0 -0
  35. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/PROMPT_ISSUE.md +0 -0
  36. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/agent-registry-roadmap.md +0 -0
  37. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/agent-template-gaps.md +0 -0
  38. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/planning/composable-agent-capabilities.md +0 -0
  39. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/research/BAML_RESEARCH_REPORT.md +0 -0
  40. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/research/Ignite-CLI-Architecture-Analysis.md +0 -0
  41. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/retrospectives/2026-04-06_issue-triage-v0.3.0/RETRO.md +0 -0
  42. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/retrospectives/2026-04-10_full-stack-integration/RETRO.md +0 -0
  43. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/scripts/README.md +0 -0
  44. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/__init__.py +0 -0
  45. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/__main__.py +0 -0
  46. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/__init__.py +0 -0
  47. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/add.py +0 -0
  48. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/generate.py +0 -0
  49. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/model_car.py +0 -0
  50. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/commands/patch.py +0 -0
  51. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/__init__.py +0 -0
  52. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/filesystem.py +0 -0
  53. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/generators.py +0 -0
  54. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/github.py +0 -0
  55. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/patching.py +0 -0
  56. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/src/fips_agents_cli/tools/validation.py +0 -0
  57. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/__init__.py +0 -0
  58. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/conftest.py +0 -0
  59. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_filesystem.py +0 -0
  60. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_generators.py +0 -0
  61. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_github.py +0 -0
  62. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_model_car.py +0 -0
  63. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_project.py +0 -0
  64. {fips_agents_cli-0.7.0 → fips_agents_cli-0.8.2}/tests/test_validation.py +0 -0
@@ -0,0 +1,2 @@
1
+ # Code Owners - all PRs require approval from @rdwj
2
+ * @rdwj
@@ -25,6 +25,7 @@ MANIFEST
25
25
 
26
26
  # Virtual environments
27
27
  venv/
28
+ .venv/
28
29
  env/
29
30
  ENV/
30
31
  env.bak/
@@ -88,3 +89,6 @@ tokens.txt
88
89
 
89
90
  # Project specific
90
91
  setup_structure.sh
92
+
93
+ # Session handoff notes (local-only — content lives in MEMORY.md)
94
+ NEXT_SESSION.md
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  **fips-agents-cli** is a Python-based CLI tool for scaffolding FIPS-compliant AI agent projects. It scaffolds MCP (Model Context Protocol) servers and AI agent projects from production-ready templates, customizes them for new projects, and prepares them for immediate development use.
8
8
 
9
- **Current Status:** `create mcp-server` and `create agent` commands implemented.
9
+ **Current Status:** Scaffolding commands implemented: `create mcp-server`, `create agent`, `create gateway`, `create ui`, `create sandbox`, `create model-car`. Post-scaffolding commands: `generate` (tool, resource, prompt, middleware), `patch` (check, generators, core, docs, build, all), `add` (code-executor), `vendor`. Note: `create workflow` exists in code but is not yet working.
10
10
 
11
11
  ## Development Commands
12
12
 
@@ -91,18 +91,25 @@ This command will:
91
91
 
92
92
  ```bash
93
93
  # 1. Update version in both files manually or use the script:
94
- ./scripts/release.sh <version> "<commit-message>"
94
+ ./scripts/release.sh <version> [<summary>]
95
95
 
96
- # Example:
96
+ # Examples:
97
+ ./scripts/release.sh 0.1.2
97
98
  ./scripts/release.sh 0.1.2 "Add new generator features"
98
99
 
99
100
  # The script handles:
100
101
  # - Updating version.py and pyproject.toml
102
+ # - Constructing the release commit message from the project convention
101
103
  # - Committing changes (including README.md changelog)
102
104
  # - Creating and pushing tag
103
105
  # - Triggering GitHub Actions
104
106
  ```
105
107
 
108
+ **Release commit message convention**: `chore: Release fips-agents-cli vX.Y.Z`,
109
+ optionally followed by ` — <summary>`. The script always constructs the message
110
+ from the version argument; callers only supply the summary, never the full
111
+ message. This keeps `git log --grep "Release fips-agents-cli"` reliable.
112
+
106
113
  **Note**: Always update the changelog in README.md before running the script.
107
114
 
108
115
  See `RELEASE_CHECKLIST.md` for detailed release procedures and troubleshooting.
@@ -141,7 +148,7 @@ Two cloning strategies exist:
141
148
  1. Clone monorepo, extract `templates/agent-loop/` subdirectory
142
149
  2. Update `pyproject.toml` name field via tomlkit
143
150
  3. String-replace `agent-template` → new name in Chart.yaml, values.yaml, AGENTS.md, Containerfile, Makefile
144
- 4. Source directories are NOT renamed — `base_agent` is a framework component
151
+ 4. Source directories are NOT renamed — the agent-loop template ships with `src/agent.py` directly, not a project-named module
145
152
  5. Initialize fresh git repository with initial commit
146
153
 
147
154
  **Rich Console Output**: All user-facing output uses Rich library for:
@@ -272,7 +279,7 @@ def test_something(temp_dir):
272
279
 
273
280
  **Agent Template** (`fips-agents/agent-template` — monorepo, `templates/agent-loop/`):
274
281
  - `pyproject.toml` with project name (no entry point scripts)
275
- - `src/base_agent/` framework (NOT renamed during customization)
282
+ - `src/agent.py` user agent subclass (lives at this path directly — no project-named module wrapper)
276
283
  - `chart/` Helm chart for OpenShift deployment
277
284
  - `agent.yaml` configuration (customized via `/plan-agent`, not during scaffolding)
278
285
  - `.claude/commands/` with agent development slash commands
@@ -325,6 +332,10 @@ Common causes:
325
332
  2. Dependencies not installed: `pip install -e .[dev]`
326
333
  3. Git not configured globally: `git config --global user.email "test@example.com"`
327
334
 
335
+ ## Release Process Notes
336
+
337
+ When creating a new release, always update the changelog in README.md *before* running the release script. The changelog lives in the README under the `## Changelog` heading. Each version should have its own section with the version number and date, listing new features, fixes, and breaking changes. Forgetting the changelog entry leads to patch releases (like v0.8.1) that exist solely to backfill documentation — avoid that by treating the changelog update as a required step in every release.
338
+
328
339
  ## Repository-Specific Notes
329
340
 
330
341
  - The main branch is `main` (not `master`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fips-agents-cli
3
- Version: 0.7.0
3
+ Version: 0.8.2
4
4
  Summary: CLI tool for creating and managing FIPS-compliant AI agent projects
5
5
  Project-URL: Homepage, https://github.com/fips-agents/fips-agents-cli
6
6
  Project-URL: Repository, https://github.com/fips-agents/fips-agents-cli
@@ -145,6 +145,7 @@ fips-agents add --help
145
145
  fips-agents create --help
146
146
  fips-agents generate --help
147
147
  fips-agents patch --help
148
+ fips-agents vendor --help
148
149
  ```
149
150
 
150
151
  ---
@@ -216,6 +217,12 @@ Creates an AI agent project from the [agent-template](https://github.com/fips-ag
216
217
 
217
218
  **Options:** Same as `create mcp-server` (see shared options table above).
218
219
 
220
+ **Additional option:**
221
+
222
+ | Option | Description |
223
+ |--------|-------------|
224
+ | `--vendored` | Copy fipsagents source into the project instead of using PyPI dependency |
225
+
219
226
  **Examples:**
220
227
 
221
228
  ```bash
@@ -227,6 +234,9 @@ fips-agents create agent my-agent --github --org fips-agents
227
234
 
228
235
  # Non-interactive mode
229
236
  fips-agents create agent my-agent --yes --local
237
+
238
+ # Create with vendored fipsagents source
239
+ fips-agents create agent my-agent --vendored --local
230
240
  ```
231
241
 
232
242
  #### `create gateway`
@@ -635,6 +645,45 @@ cd my-research-agent
635
645
  fips-agents add code-executor
636
646
  ```
637
647
 
648
+ ---
649
+
650
+ ### Vendor Commands
651
+
652
+ The `vendor` command copies the fipsagents source into your agent project, replacing the PyPI dependency. This gives you full control over the fipsagents code.
653
+
654
+ #### `vendor`
655
+
656
+ ```bash
657
+ fips-agents vendor [OPTIONS]
658
+ ```
659
+
660
+ Copies fipsagents source into `src/fipsagents/` and rewrites `pyproject.toml` to use individual dependencies instead of the fipsagents package.
661
+
662
+ **Options:**
663
+
664
+ | Option | Description |
665
+ |--------|-------------|
666
+ | `--update` | Update an already-vendored project with the latest upstream source |
667
+ | `--version TEXT` | Vendor a specific version tag (e.g., `fipsagents-v0.7.0`). Default: latest main |
668
+
669
+ **Examples:**
670
+
671
+ ```bash
672
+ # Vendor into current project
673
+ fips-agents vendor
674
+
675
+ # Vendor a specific version
676
+ fips-agents vendor --version fipsagents-v0.7.0
677
+
678
+ # Update vendored source from upstream
679
+ fips-agents vendor --update
680
+ ```
681
+
682
+ **When to use vendored vs. PyPI:**
683
+
684
+ - **PyPI dependency** (default): Best for teams running multiple agents that share the same fipsagents version. Centralized updates.
685
+ - **Vendored source**: Best for agents that need custom BaseAgent modifications, environments with no PyPI access, or when you want to read and debug the fipsagents source locally.
686
+
638
687
  ## Project Name Requirements
639
688
 
640
689
  Project names must follow these rules:
@@ -663,10 +712,9 @@ pytest
663
712
 
664
713
  ```bash
665
714
  cd my-research-agent
666
- python -m venv venv
667
- source venv/bin/activate
668
- pip install -e .[dev]
669
- pytest
715
+ make install # Create venv, install dependencies
716
+ make run-local # Start HTTP server on port 8080
717
+ make test # Run tests
670
718
  # See AGENTS.md for the /plan-agent slash command workflow
671
719
  ```
672
720
 
@@ -778,7 +826,8 @@ fips-agents-cli/
778
826
  │ │ ├── create.py # create mcp-server, agent, gateway, ui
779
827
  │ │ ├── generate.py # generate tool/resource/prompt/middleware
780
828
  │ │ ├── model_car.py # create model-car
781
- │ │ └── patch.py # patch command
829
+ │ │ ├── patch.py # patch command
830
+ │ │ └── vendor.py # vendor fipsagents source
782
831
  │ └── tools/ # Utility modules
783
832
  │ ├── filesystem.py
784
833
  │ ├── git.py
@@ -897,6 +946,29 @@ MIT License - see LICENSE file for details
897
946
 
898
947
  ## Changelog
899
948
 
949
+ ### Version 0.8.2
950
+
951
+ - Test: New `TestGenerateMiddlewareRealTemplate` integration test renders the real v3.x middleware Jinja2 template against each `--hook-type` and the no-flag case, with templates committed under `tests/fixtures/middleware_template/` so the suite runs offline (#3)
952
+ - Chore: `scripts/release.sh` now constructs the release commit message from the version argument using the project convention (`chore: Release fips-agents-cli vX.Y.Z` with optional ` — <summary>`); convention documented in CLAUDE.md (#4)
953
+ - Docs: Drop broken `See Also` links from `.fips-agents-cli/README.md` in scaffolded MCP projects (#5, fixed in `fips-agents/mcp-server-template`)
954
+ - Docs: Updated CLAUDE.md release section to reflect the new release-script signature
955
+
956
+ ### Version 0.8.1
957
+
958
+ - Docs: Updated CLAUDE.md current-status line and added release-process notes
959
+ - Chore: Repository hardening — added CODEOWNERS and enabled main branch protection
960
+ - Chore: Black formatting drift cleanup; removed remaining "framework" language from docs in favor of "toolkit"
961
+ - Chore: `.venv/` and `NEXT_SESSION.md` gitignored
962
+
963
+ ### Version 0.8.0
964
+
965
+ - Feature: New `--vendored` flag on `create agent` copies fipsagents source instead of PyPI dependency
966
+ - Feature: New `fips-agents vendor` command for post-scaffold vendoring of existing projects
967
+ - Feature: `fips-agents vendor --update` refreshes vendored source from upstream
968
+ - Feature: `fips-agents vendor --version` pins to a specific fipsagents release tag
969
+ - Fix: `customize_agent_project` now removes monorepo Makefile install line (matching workflow template behavior)
970
+ - Fix: Added `redeploy.sh` to agent project customization file list
971
+
900
972
  ### Version 0.7.0
901
973
 
902
974
  - Feature: New `add` command group for composable agent capabilities
@@ -112,6 +112,7 @@ fips-agents add --help
112
112
  fips-agents create --help
113
113
  fips-agents generate --help
114
114
  fips-agents patch --help
115
+ fips-agents vendor --help
115
116
  ```
116
117
 
117
118
  ---
@@ -183,6 +184,12 @@ Creates an AI agent project from the [agent-template](https://github.com/fips-ag
183
184
 
184
185
  **Options:** Same as `create mcp-server` (see shared options table above).
185
186
 
187
+ **Additional option:**
188
+
189
+ | Option | Description |
190
+ |--------|-------------|
191
+ | `--vendored` | Copy fipsagents source into the project instead of using PyPI dependency |
192
+
186
193
  **Examples:**
187
194
 
188
195
  ```bash
@@ -194,6 +201,9 @@ fips-agents create agent my-agent --github --org fips-agents
194
201
 
195
202
  # Non-interactive mode
196
203
  fips-agents create agent my-agent --yes --local
204
+
205
+ # Create with vendored fipsagents source
206
+ fips-agents create agent my-agent --vendored --local
197
207
  ```
198
208
 
199
209
  #### `create gateway`
@@ -602,6 +612,45 @@ cd my-research-agent
602
612
  fips-agents add code-executor
603
613
  ```
604
614
 
615
+ ---
616
+
617
+ ### Vendor Commands
618
+
619
+ The `vendor` command copies the fipsagents source into your agent project, replacing the PyPI dependency. This gives you full control over the fipsagents code.
620
+
621
+ #### `vendor`
622
+
623
+ ```bash
624
+ fips-agents vendor [OPTIONS]
625
+ ```
626
+
627
+ Copies fipsagents source into `src/fipsagents/` and rewrites `pyproject.toml` to use individual dependencies instead of the fipsagents package.
628
+
629
+ **Options:**
630
+
631
+ | Option | Description |
632
+ |--------|-------------|
633
+ | `--update` | Update an already-vendored project with the latest upstream source |
634
+ | `--version TEXT` | Vendor a specific version tag (e.g., `fipsagents-v0.7.0`). Default: latest main |
635
+
636
+ **Examples:**
637
+
638
+ ```bash
639
+ # Vendor into current project
640
+ fips-agents vendor
641
+
642
+ # Vendor a specific version
643
+ fips-agents vendor --version fipsagents-v0.7.0
644
+
645
+ # Update vendored source from upstream
646
+ fips-agents vendor --update
647
+ ```
648
+
649
+ **When to use vendored vs. PyPI:**
650
+
651
+ - **PyPI dependency** (default): Best for teams running multiple agents that share the same fipsagents version. Centralized updates.
652
+ - **Vendored source**: Best for agents that need custom BaseAgent modifications, environments with no PyPI access, or when you want to read and debug the fipsagents source locally.
653
+
605
654
  ## Project Name Requirements
606
655
 
607
656
  Project names must follow these rules:
@@ -630,10 +679,9 @@ pytest
630
679
 
631
680
  ```bash
632
681
  cd my-research-agent
633
- python -m venv venv
634
- source venv/bin/activate
635
- pip install -e .[dev]
636
- pytest
682
+ make install # Create venv, install dependencies
683
+ make run-local # Start HTTP server on port 8080
684
+ make test # Run tests
637
685
  # See AGENTS.md for the /plan-agent slash command workflow
638
686
  ```
639
687
 
@@ -745,7 +793,8 @@ fips-agents-cli/
745
793
  │ │ ├── create.py # create mcp-server, agent, gateway, ui
746
794
  │ │ ├── generate.py # generate tool/resource/prompt/middleware
747
795
  │ │ ├── model_car.py # create model-car
748
- │ │ └── patch.py # patch command
796
+ │ │ ├── patch.py # patch command
797
+ │ │ └── vendor.py # vendor fipsagents source
749
798
  │ └── tools/ # Utility modules
750
799
  │ ├── filesystem.py
751
800
  │ ├── git.py
@@ -864,6 +913,29 @@ MIT License - see LICENSE file for details
864
913
 
865
914
  ## Changelog
866
915
 
916
+ ### Version 0.8.2
917
+
918
+ - Test: New `TestGenerateMiddlewareRealTemplate` integration test renders the real v3.x middleware Jinja2 template against each `--hook-type` and the no-flag case, with templates committed under `tests/fixtures/middleware_template/` so the suite runs offline (#3)
919
+ - Chore: `scripts/release.sh` now constructs the release commit message from the version argument using the project convention (`chore: Release fips-agents-cli vX.Y.Z` with optional ` — <summary>`); convention documented in CLAUDE.md (#4)
920
+ - Docs: Drop broken `See Also` links from `.fips-agents-cli/README.md` in scaffolded MCP projects (#5, fixed in `fips-agents/mcp-server-template`)
921
+ - Docs: Updated CLAUDE.md release section to reflect the new release-script signature
922
+
923
+ ### Version 0.8.1
924
+
925
+ - Docs: Updated CLAUDE.md current-status line and added release-process notes
926
+ - Chore: Repository hardening — added CODEOWNERS and enabled main branch protection
927
+ - Chore: Black formatting drift cleanup; removed remaining "framework" language from docs in favor of "toolkit"
928
+ - Chore: `.venv/` and `NEXT_SESSION.md` gitignored
929
+
930
+ ### Version 0.8.0
931
+
932
+ - Feature: New `--vendored` flag on `create agent` copies fipsagents source instead of PyPI dependency
933
+ - Feature: New `fips-agents vendor` command for post-scaffold vendoring of existing projects
934
+ - Feature: `fips-agents vendor --update` refreshes vendored source from upstream
935
+ - Feature: `fips-agents vendor --version` pins to a specific fipsagents release tag
936
+ - Fix: `customize_agent_project` now removes monorepo Makefile install line (matching workflow template behavior)
937
+ - Fix: Added `redeploy.sh` to agent project customization file list
938
+
867
939
  ### Version 0.7.0
868
940
 
869
941
  - Feature: New `add` command group for composable agent capabilities
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fips-agents-cli"
7
- version = "0.7.0"
7
+ version = "0.8.2"
8
8
  description = "CLI tool for creating and managing FIPS-compliant AI agent projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,9 +2,20 @@
2
2
  #
3
3
  # release.sh - Automated release script for fips-agents-cli
4
4
  #
5
- # Usage: ./scripts/release.sh <version> <commit-message>
5
+ # Usage: ./scripts/release.sh <version> [<summary>]
6
6
  #
7
- # Example: ./scripts/release.sh 0.1.2 "Add new feature for X"
7
+ # The release commit message is always constructed from the version using
8
+ # the project convention:
9
+ #
10
+ # chore: Release fips-agents-cli vX.Y.Z
11
+ #
12
+ # If a summary is provided, it is appended after an em-dash:
13
+ #
14
+ # chore: Release fips-agents-cli vX.Y.Z — <summary>
15
+ #
16
+ # Examples:
17
+ # ./scripts/release.sh 0.1.2
18
+ # ./scripts/release.sh 0.1.2 "Add new generator features"
8
19
  #
9
20
 
10
21
  set -e # Exit on error
@@ -29,14 +40,16 @@ print_info() {
29
40
  }
30
41
 
31
42
  # Check arguments
32
- if [ $# -ne 2 ]; then
33
- print_error "Usage: $0 <version> <commit-message>"
34
- echo "Example: $0 0.1.2 \"Add new feature for X\""
43
+ if [ $# -lt 1 ] || [ $# -gt 2 ]; then
44
+ print_error "Usage: $0 <version> [<summary>]"
45
+ echo "Examples:"
46
+ echo " $0 0.1.2"
47
+ echo " $0 0.1.2 \"Add new generator features\""
35
48
  exit 1
36
49
  fi
37
50
 
38
51
  VERSION=$1
39
- COMMIT_MSG=$2
52
+ SUMMARY=${2:-}
40
53
 
41
54
  # Validate version format (x.y.z)
42
55
  if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
@@ -45,6 +58,14 @@ if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
45
58
  exit 1
46
59
  fi
47
60
 
61
+ # Construct the conventional release commit message.
62
+ # Convention: "chore: Release fips-agents-cli vX.Y.Z" with optional " — <summary>".
63
+ if [ -n "$SUMMARY" ]; then
64
+ COMMIT_MSG="chore: Release fips-agents-cli v${VERSION} — ${SUMMARY}"
65
+ else
66
+ COMMIT_MSG="chore: Release fips-agents-cli v${VERSION}"
67
+ fi
68
+
48
69
  print_info "Preparing release v$VERSION"
49
70
  echo
50
71
 
@@ -7,6 +7,7 @@ from fips_agents_cli.commands.add import add
7
7
  from fips_agents_cli.commands.create import create
8
8
  from fips_agents_cli.commands.generate import generate
9
9
  from fips_agents_cli.commands.patch import patch
10
+ from fips_agents_cli.commands.vendor import vendor
10
11
  from fips_agents_cli.version import __version__
11
12
 
12
13
  console = Console()
@@ -29,6 +30,7 @@ cli.add_command(add)
29
30
  cli.add_command(create)
30
31
  cli.add_command(generate)
31
32
  cli.add_command(patch)
33
+ cli.add_command(vendor)
32
34
 
33
35
 
34
36
  def main():
@@ -379,6 +379,12 @@ def mcp_server(
379
379
  default=False,
380
380
  help="Create GitHub repo only, don't clone locally",
381
381
  )
382
+ @click.option(
383
+ "--vendored",
384
+ is_flag=True,
385
+ default=False,
386
+ help="Copy fipsagents source into the project instead of using PyPI dependency",
387
+ )
382
388
  def agent(
383
389
  project_name: str,
384
390
  target_dir: str | None,
@@ -390,6 +396,7 @@ def agent(
390
396
  org: str | None,
391
397
  repo_description: str | None,
392
398
  remote_only: bool,
399
+ vendored: bool,
393
400
  ):
394
401
  """
395
402
  Create a new AI agent project from template.
@@ -410,6 +417,8 @@ def agent(
410
417
  fips-agents create agent my-research-agent --local
411
418
 
412
419
  fips-agents create agent my-research-agent --yes
420
+
421
+ fips-agents create agent my-research-agent --vendored --local
413
422
  """
414
423
  try:
415
424
  # Step 1: Validate options
@@ -495,8 +504,17 @@ def agent(
495
504
  ) as progress:
496
505
  progress.add_task(description="Cloning agent template...", total=None)
497
506
  try:
507
+ post_clone = None
508
+ if vendored:
509
+ from fips_agents_cli.tools.project import vendor_fipsagents_from_clone
510
+
511
+ post_clone = vendor_fipsagents_from_clone
512
+
498
513
  template_commit = clone_template_subdir(
499
- AGENT_TEMPLATE_URL, target_path, AGENT_TEMPLATE_SUBDIR
514
+ AGENT_TEMPLATE_URL,
515
+ target_path,
516
+ AGENT_TEMPLATE_SUBDIR,
517
+ post_clone_fn=post_clone,
500
518
  )
501
519
  except Exception as e:
502
520
  console.print("\n[red]✗[/red] Failed to clone agent template")
@@ -517,6 +535,10 @@ def agent(
517
535
  try:
518
536
  customize_agent_project(target_path, project_name, github_repo=github_repo)
519
537
  cleanup_template_files(target_path)
538
+ if vendored:
539
+ from fips_agents_cli.tools.project import rewrite_pyproject_for_vendored
540
+
541
+ rewrite_pyproject_for_vendored(target_path)
520
542
  if template_commit:
521
543
  write_template_info(
522
544
  target_path,
@@ -571,6 +593,8 @@ def agent(
571
593
  github_url=github_url,
572
594
  github_repo=github_repo,
573
595
  )
596
+ if vendored:
597
+ console.print("[cyan] Framework source vendored in src/fipsagents/[/cyan]")
574
598
 
575
599
  except KeyboardInterrupt:
576
600
  console.print("\n[yellow]⚠[/yellow] Operation cancelled by user")
@@ -0,0 +1,154 @@
1
+ """Vendor command for copying fipsagents source into an agent project."""
2
+
3
+ import sys
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+
11
+ console = Console()
12
+
13
+ AGENT_TEMPLATE_URL = "https://github.com/fips-agents/agent-template"
14
+
15
+
16
+ @click.command("vendor")
17
+ @click.option(
18
+ "--update",
19
+ is_flag=True,
20
+ default=False,
21
+ help="Update an already-vendored project with the latest fipsagents source",
22
+ )
23
+ @click.option(
24
+ "--version",
25
+ "tag",
26
+ default=None,
27
+ help="Vendor a specific version (e.g., fipsagents-v0.7.0). Default: latest main.",
28
+ )
29
+ def vendor(update: bool, tag: str | None):
30
+ """
31
+ Vendor fipsagents source into the current project.
32
+
33
+ Copies the fipsagents framework source code into src/fipsagents/ so you
34
+ have full control over the framework. Replaces the PyPI dependency with
35
+ the individual packages that fipsagents depends on.
36
+
37
+ Use --update to refresh an already-vendored project with the latest
38
+ upstream source. Commit your changes first — update overwrites the
39
+ vendored source.
40
+
41
+ Examples:
42
+
43
+ fips-agents vendor
44
+
45
+ fips-agents vendor --version fipsagents-v0.7.0
46
+
47
+ fips-agents vendor --update
48
+ """
49
+ try:
50
+ project_path = Path.cwd()
51
+
52
+ # Verify we're in a project directory
53
+ pyproject_path = project_path / "pyproject.toml"
54
+ if not pyproject_path.exists():
55
+ console.print("[red]✗[/red] No pyproject.toml found. Run this from a project root.")
56
+ sys.exit(1)
57
+
58
+ vendored_marker = project_path / "src" / "fipsagents" / "VENDORED"
59
+ is_already_vendored = vendored_marker.exists()
60
+
61
+ if update and not is_already_vendored:
62
+ console.print(
63
+ "[red]✗[/red] Project is not vendored yet. "
64
+ "Run 'fips-agents vendor' (without --update) first."
65
+ )
66
+ sys.exit(1)
67
+
68
+ if is_already_vendored and not update:
69
+ # Show current version and ask if they want to update
70
+ console.print("[yellow]⚠[/yellow] fipsagents is already vendored in this project.")
71
+ console.print(f" Marker: {vendored_marker}")
72
+ console.print(" Use --update to refresh from upstream.")
73
+ sys.exit(0)
74
+
75
+ # Warn about overwriting local changes
76
+ if update:
77
+ console.print(
78
+ "\n[yellow]⚠[/yellow] This will overwrite src/fipsagents/ with " "upstream source."
79
+ )
80
+ console.print(
81
+ " Commit any local modifications first to preserve them in git history.\n"
82
+ )
83
+
84
+ console.print("[bold cyan]Vendoring fipsagents[/bold cyan]\n")
85
+
86
+ # Clone the agent-template repo
87
+ import git as gitmodule
88
+
89
+ with tempfile.TemporaryDirectory() as tmp:
90
+ tmp_path = Path(tmp) / "repo"
91
+
92
+ with Progress(
93
+ SpinnerColumn(),
94
+ TextColumn("[progress.description]{task.description}"),
95
+ console=console,
96
+ ) as progress:
97
+ progress.add_task(description="Cloning agent-template repo...", total=None)
98
+ try:
99
+ repo = gitmodule.Repo.clone_from(
100
+ AGENT_TEMPLATE_URL,
101
+ str(tmp_path),
102
+ branch="main",
103
+ depth=1,
104
+ single_branch=True,
105
+ )
106
+
107
+ # Checkout specific tag if requested
108
+ if tag:
109
+ try:
110
+ repo.git.fetch("origin", f"refs/tags/{tag}:refs/tags/{tag}")
111
+ repo.git.checkout(tag)
112
+ console.print(f"[green]✓[/green] Checked out tag: {tag}")
113
+ except gitmodule.GitCommandError:
114
+ console.print(
115
+ f"[red]✗[/red] Tag '{tag}' not found. "
116
+ f"Available tags: fipsagents-v0.7.0, etc."
117
+ )
118
+ sys.exit(1)
119
+
120
+ except gitmodule.GitCommandError as e:
121
+ console.print(f"[red]✗[/red] Failed to clone: {e}")
122
+ sys.exit(1)
123
+
124
+ # Copy the fipsagents source
125
+ from fips_agents_cli.tools.project import (
126
+ rewrite_pyproject_for_vendored,
127
+ vendor_fipsagents_from_clone,
128
+ )
129
+
130
+ vendor_fipsagents_from_clone(tmp_path, project_path)
131
+
132
+ # Only rewrite pyproject.toml if not already vendored
133
+ # (on --update, deps are already rewritten)
134
+ if not is_already_vendored:
135
+ rewrite_pyproject_for_vendored(project_path)
136
+
137
+ # Success
138
+ console.print("\n[bold green]Vendoring complete![/bold green]\n")
139
+ console.print("Next steps:")
140
+ console.print(" 1. pip install -e . # Reinstall with vendored source")
141
+ console.print(" 2. make test # Verify everything works")
142
+ console.print(" 3. git add src/fipsagents/ # Commit the vendored source")
143
+ console.print("")
144
+ if update:
145
+ console.print(
146
+ "[dim]Tip: Use 'git diff src/fipsagents/' to review upstream changes.[/dim]"
147
+ )
148
+
149
+ except KeyboardInterrupt:
150
+ console.print("\n[yellow]Cancelled[/yellow]")
151
+ sys.exit(130)
152
+ except Exception as e:
153
+ console.print(f"\n[red]✗[/red] Unexpected error: {e}")
154
+ sys.exit(1)
@@ -1,5 +1,7 @@
1
1
  """Git operations for cloning and initializing repositories."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import shutil
4
6
  import tempfile
5
7
  from pathlib import Path
@@ -60,7 +62,12 @@ def clone_template(repo_url: str, target_path: Path, branch: str = "main") -> st
60
62
 
61
63
 
62
64
  def clone_template_subdir(
63
- repo_url: str, target_path: Path, subdir: str, branch: str = "main"
65
+ repo_url: str,
66
+ target_path: Path,
67
+ subdir: str,
68
+ branch: str = "main",
69
+ *,
70
+ post_clone_fn: callable | None = None,
64
71
  ) -> str:
65
72
  """
66
73
  Clone a monorepo and extract a subdirectory as the project root.
@@ -89,6 +96,8 @@ def clone_template_subdir(
89
96
  if not src.is_dir():
90
97
  raise FileNotFoundError(f"Subdirectory '{subdir}' not found in template repo")
91
98
  shutil.copytree(src, target_path, dirs_exist_ok=True)
99
+ if post_clone_fn is not None:
100
+ post_clone_fn(tmp_path, target_path)
92
101
  return commit_hash
93
102
 
94
103
 
@@ -206,6 +206,7 @@ def customize_agent_project(
206
206
  project_path / "Containerfile",
207
207
  project_path / "Makefile",
208
208
  project_path / "deploy.sh",
209
+ project_path / "redeploy.sh",
209
210
  ]
210
211
 
211
212
  for file_path in files_to_update:
@@ -213,7 +214,31 @@ def customize_agent_project(
213
214
 
214
215
  console.print("[green]✓[/green] Updated configuration files")
215
216
 
216
- # 3. Replace OWNER/REPO placeholder in Containerfile image source label
217
+ # 3. Remove monorepo-specific base-agent install line from Makefile
218
+ # Cloned projects get fipsagents from PyPI, not a local path
219
+ makefile_path = project_path / "Makefile"
220
+ if makefile_path.exists():
221
+ text = makefile_path.read_text()
222
+ # Remove the monorepo install line and its comments
223
+ lines = text.splitlines(keepends=True)
224
+ filtered = []
225
+ skip_block = False
226
+ for line in lines:
227
+ if "In the monorepo" in line:
228
+ skip_block = True
229
+ continue
230
+ if skip_block and ("fips-agents" in line or "scaffolding step" in line):
231
+ continue
232
+ if skip_block and ("$(PIP) install -e" in line and "packages/" in line):
233
+ skip_block = False
234
+ continue
235
+ skip_block = False
236
+ filtered.append(line)
237
+ makefile_path.write_text("".join(filtered))
238
+
239
+ console.print("[green]✓[/green] Cleaned up Makefile for standalone use")
240
+
241
+ # 4. Replace OWNER/REPO placeholder in Containerfile image source label
217
242
  repo_value = github_repo if github_repo else f"OWNER/{new_name}"
218
243
  _replace_in_file(project_path / "Containerfile", "OWNER/REPO", repo_value)
219
244
 
@@ -541,3 +566,107 @@ def write_template_info(
541
566
  except Exception as e:
542
567
  # Don't fail the entire operation if this fails
543
568
  console.print(f"[yellow]⚠[/yellow] Could not write template info: {e}")
569
+
570
+
571
+ def vendor_fipsagents_from_clone(
572
+ clone_path: Path,
573
+ project_path: Path,
574
+ ) -> None:
575
+ """
576
+ Copy fipsagents package source from a monorepo clone into the project.
577
+
578
+ Called as a post_clone_fn callback during clone_template_subdir() when
579
+ --vendored is used, or directly by 'fips-agents vendor' for post-scaffold
580
+ vendoring.
581
+
582
+ Args:
583
+ clone_path: Root of the cloned agent-template monorepo
584
+ project_path: Root of the target project
585
+ """
586
+ pkg_src = clone_path / "packages" / "fipsagents" / "src" / "fipsagents"
587
+ if not pkg_src.is_dir():
588
+ console.print("[red]✗[/red] fipsagents package not found in template repo")
589
+ raise FileNotFoundError(f"Expected fipsagents source at {pkg_src}")
590
+
591
+ dest = project_path / "src" / "fipsagents"
592
+
593
+ # Remove existing vendored source if present (for --update)
594
+ if dest.exists():
595
+ shutil.rmtree(dest)
596
+
597
+ shutil.copytree(pkg_src, dest)
598
+ console.print("[green]✓[/green] Copied fipsagents source to src/fipsagents/")
599
+
600
+ # Copy upstream pyproject.toml as provenance record
601
+ upstream_toml = clone_path / "packages" / "fipsagents" / "pyproject.toml"
602
+ if upstream_toml.exists():
603
+ shutil.copy2(upstream_toml, dest / "UPSTREAM.toml")
604
+
605
+ # Read version from the vendored package
606
+ version = "unknown"
607
+ init_file = dest / "baseagent" / "__init__.py"
608
+ if init_file.exists():
609
+ for line in init_file.read_text().splitlines():
610
+ if line.startswith("__version__"):
611
+ version = line.split("=")[1].strip().strip('"').strip("'")
612
+ break
613
+
614
+ # Write VENDORED marker
615
+ marker = dest / "VENDORED"
616
+ marker.write_text(
617
+ f"version: {version}\n"
618
+ f"vendored: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n"
619
+ f"source: https://github.com/fips-agents/agent-template\n"
620
+ f"\n"
621
+ f"This fipsagents source is vendored into the project.\n"
622
+ f"To update: fips-agents vendor --update\n"
623
+ )
624
+ console.print(f"[green]✓[/green] Wrote VENDORED marker (version {version})")
625
+
626
+
627
+ def rewrite_pyproject_for_vendored(project_path: Path) -> None:
628
+ """
629
+ Rewrite pyproject.toml to replace fipsagents pip dependency with
630
+ the individual dependencies that fipsagents itself declares.
631
+
632
+ Args:
633
+ project_path: Root of the target project
634
+ """
635
+ pyproject_path = project_path / "pyproject.toml"
636
+ if not pyproject_path.exists():
637
+ return
638
+
639
+ with open(pyproject_path) as f:
640
+ pyproject = tomlkit.parse(f.read())
641
+
642
+ # Replace fipsagents[server] with its individual dependencies
643
+ vendored_deps = [
644
+ "litellm>=1.83.0",
645
+ "fastmcp>=3.0.0",
646
+ "pydantic>=2.0",
647
+ "pyyaml",
648
+ "httpx",
649
+ "python-frontmatter",
650
+ "fastapi>=0.110",
651
+ "uvicorn[standard]>=0.27",
652
+ ]
653
+
654
+ if "project" in pyproject and "dependencies" in pyproject["project"]:
655
+ old_deps = list(pyproject["project"]["dependencies"])
656
+ new_deps = [d for d in old_deps if "fipsagents" not in d.lower()]
657
+ new_deps.extend(vendored_deps)
658
+ pyproject["project"]["dependencies"] = new_deps
659
+
660
+ # Fix optional memory dependency too
661
+ if "project" in pyproject:
662
+ opt_deps = pyproject["project"].get("optional-dependencies", {})
663
+ if "memory" in opt_deps:
664
+ old_memory = list(opt_deps["memory"])
665
+ new_memory = [d for d in old_memory if "fipsagents" not in d.lower()]
666
+ new_memory.append("memoryhub")
667
+ opt_deps["memory"] = new_memory
668
+
669
+ with open(pyproject_path, "w") as f:
670
+ f.write(tomlkit.dumps(pyproject))
671
+
672
+ console.print("[green]✓[/green] Updated pyproject.toml (vendored dependencies)")
@@ -1,3 +1,3 @@
1
1
  """Version information for fips-agents-cli."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.2"
@@ -0,0 +1,39 @@
1
+ """{{ description }}"""
2
+
3
+ import mcp.types as mt
4
+ from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
5
+ from fastmcp.tools.tool import ToolResult
6
+
7
+ from src.core.logging import get_logger
8
+
9
+ log = get_logger("middleware.{{ component_name }}")
10
+
11
+
12
+ class {{ component_name | replace('_', ' ') | title | replace(' ', '') }}Middleware(Middleware):
13
+ """{{ description }}
14
+
15
+ To activate, add an instance to the middleware=[] list in create_server()
16
+ (see src/core/server.py).
17
+ """
18
+
19
+ async def on_call_tool(
20
+ self,
21
+ context: MiddlewareContext[mt.CallToolRequestParams],
22
+ call_next: CallNext[mt.CallToolRequestParams, ToolResult],
23
+ ) -> ToolResult:
24
+ """Wrap tool execution with {{ component_name }} logic."""
25
+ tool_name = context.message.name
26
+
27
+ log.debug(f"{{ component_name }} middleware: before {tool_name}")
28
+
29
+ # TODO: Add pre-execution logic here
30
+
31
+ try:
32
+ result = await call_next(context)
33
+ except Exception as e:
34
+ log.error(f"{{ component_name }} middleware: error in {tool_name}: {e}")
35
+ raise
36
+
37
+ # TODO: Add post-execution logic here
38
+
39
+ return result
@@ -0,0 +1,40 @@
1
+ """Tests for {{ component_name }} middleware."""
2
+
3
+ import pytest
4
+ from unittest.mock import AsyncMock, MagicMock
5
+ import mcp.types as mt
6
+
7
+ from src.middleware.{{ module_path }} import {{ component_name | replace('_', ' ') | title | replace(' ', '') }}Middleware
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ async def test_{{ component_name }}_success():
12
+ """Test middleware handles successful execution."""
13
+ middleware = {{ component_name | replace('_', ' ') | title | replace(' ', '') }}Middleware()
14
+
15
+ # Create mock context
16
+ context = MagicMock(spec=mt.CallToolRequestParams)
17
+ context.message.name = "test_tool"
18
+
19
+ # Create mock call_next
20
+ call_next = AsyncMock(return_value="success_result")
21
+
22
+ # Execute middleware
23
+ result = await middleware.on_call_tool(context, call_next)
24
+
25
+ assert result == "success_result"
26
+ call_next.assert_called_once_with(context)
27
+
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_{{ component_name }}_error():
31
+ """Test middleware handles errors."""
32
+ middleware = {{ component_name | replace('_', ' ') | title | replace(' ', '') }}Middleware()
33
+
34
+ context = MagicMock(spec=mt.CallToolRequestParams)
35
+ context.message.name = "failing_tool"
36
+
37
+ call_next = AsyncMock(side_effect=ValueError("Test error"))
38
+
39
+ with pytest.raises(ValueError, match="Test error"):
40
+ await middleware.on_call_tool(context, call_next)
@@ -661,7 +661,7 @@ class TestCreateAgent:
661
661
  mock_is_git_installed.return_value = True
662
662
  mock_gh_installed.return_value = False
663
663
 
664
- def create_minimal_structure(url, target_path, subdir, branch=None):
664
+ def create_minimal_structure(url, target_path, subdir, branch=None, **kwargs):
665
665
  target_path.mkdir(parents=True, exist_ok=True)
666
666
  (target_path / "pyproject.toml").write_text('[project]\nname = "agent-template"')
667
667
 
@@ -698,7 +698,7 @@ class TestCreateAgent:
698
698
  mock_is_git_installed.return_value = True
699
699
  mock_gh_installed.return_value = False
700
700
 
701
- def create_minimal_structure(url, target_path, subdir, branch=None):
701
+ def create_minimal_structure(url, target_path, subdir, branch=None, **kwargs):
702
702
  target_path.mkdir(parents=True, exist_ok=True)
703
703
  (target_path / "pyproject.toml").write_text('[project]\nname = "agent-template"')
704
704
 
@@ -763,7 +763,7 @@ class TestCreateAgentGitHub:
763
763
  mock_get_username.return_value = "testuser"
764
764
  mock_push.return_value = True
765
765
 
766
- def create_minimal_structure(url, target_path, subdir, branch=None):
766
+ def create_minimal_structure(url, target_path, subdir, branch=None, **kwargs):
767
767
  target_path.mkdir(parents=True, exist_ok=True)
768
768
  (target_path / "pyproject.toml").write_text('[project]\nname = "agent-template"')
769
769
 
@@ -839,7 +839,7 @@ class TestCreateAgentGitHub:
839
839
  mock_get_username.return_value = "testuser"
840
840
  mock_push.return_value = True
841
841
 
842
- def create_minimal_structure(url, target_path, subdir, branch=None):
842
+ def create_minimal_structure(url, target_path, subdir, branch=None, **kwargs):
843
843
  target_path.mkdir(parents=True, exist_ok=True)
844
844
  (target_path / "pyproject.toml").write_text('[project]\nname = "agent-template"')
845
845
 
@@ -398,6 +398,123 @@ class TestGenerateMiddlewareCommand:
398
398
  os.chdir(original_cwd)
399
399
 
400
400
 
401
+ @pytest.fixture
402
+ def mock_mcp_project_with_real_middleware_template(tmp_path):
403
+ """Mock MCP project that uses the real middleware Jinja2 templates as fixtures.
404
+
405
+ Templates are committed under tests/fixtures/middleware_template/ so the
406
+ test runs offline and is not coupled to mcp-server-template's git state.
407
+ Refresh the fixtures when the upstream template changes meaningfully.
408
+ """
409
+ import shutil
410
+ from pathlib import Path
411
+
412
+ pyproject_content = """
413
+ [project]
414
+ name = "test-mcp-server"
415
+ version = "0.1.0"
416
+ dependencies = [
417
+ "fastmcp>=3.0.0",
418
+ ]
419
+ """
420
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
421
+
422
+ for component_dir in ["middleware"]:
423
+ (tmp_path / "src" / component_dir).mkdir(parents=True)
424
+ (tmp_path / "tests" / component_dir).mkdir(parents=True)
425
+
426
+ fixture_dir = Path(__file__).parent / "fixtures" / "middleware_template"
427
+ generators_dir = tmp_path / ".fips-agents-cli" / "generators" / "middleware"
428
+ generators_dir.mkdir(parents=True)
429
+ shutil.copy(fixture_dir / "component.py.j2", generators_dir / "component.py.j2")
430
+ shutil.copy(fixture_dir / "test.py.j2", generators_dir / "test.py.j2")
431
+
432
+ return tmp_path
433
+
434
+
435
+ class TestGenerateMiddlewareRealTemplate:
436
+ """Integration tests that render the real v3.x middleware template.
437
+
438
+ These guard against regressions that would slip past the synthetic
439
+ `mock_mcp_project` fixture, which uses a stripped-down template that
440
+ doesn't exercise fastmcp imports, class structure, or the real
441
+ Jinja2 conditionals.
442
+ """
443
+
444
+ @pytest.mark.parametrize("hook_type", ["before_tool", "after_tool", "on_error"])
445
+ def test_real_template_renders_for_each_hook_type(
446
+ self, runner, mock_mcp_project_with_real_middleware_template, hook_type
447
+ ):
448
+ """Each --hook-type renders valid Python against the real v3.x template."""
449
+ import ast
450
+ import os
451
+
452
+ project = mock_mcp_project_with_real_middleware_template
453
+ original_cwd = os.getcwd()
454
+ try:
455
+ os.chdir(project)
456
+ result = runner.invoke(
457
+ generate,
458
+ [
459
+ "middleware",
460
+ f"{hook_type}_mw",
461
+ "--description",
462
+ f"{hook_type} hook middleware",
463
+ "--hook-type",
464
+ hook_type,
465
+ ],
466
+ catch_exceptions=False,
467
+ )
468
+ assert result.exit_code == 0, result.output
469
+
470
+ rendered = project / "src" / "middleware" / f"{hook_type}_mw.py"
471
+ assert rendered.exists()
472
+
473
+ source = rendered.read_text()
474
+ ast.parse(source) # raises if invalid Python
475
+ assert "from fastmcp.server.middleware import" in source
476
+ assert "class " in source and "Middleware(Middleware):" in source
477
+ assert "async def on_call_tool" in source
478
+
479
+ test_file = project / "tests" / "middleware" / f"test_{hook_type}_mw.py"
480
+ assert test_file.exists()
481
+ ast.parse(test_file.read_text())
482
+ finally:
483
+ os.chdir(original_cwd)
484
+
485
+ def test_real_template_renders_without_hook_type(
486
+ self, runner, mock_mcp_project_with_real_middleware_template
487
+ ):
488
+ """Omitting --hook-type still produces a valid generic wrapper (backward compat)."""
489
+ import ast
490
+ import os
491
+
492
+ project = mock_mcp_project_with_real_middleware_template
493
+ original_cwd = os.getcwd()
494
+ try:
495
+ os.chdir(project)
496
+ result = runner.invoke(
497
+ generate,
498
+ [
499
+ "middleware",
500
+ "generic_mw",
501
+ "--description",
502
+ "generic middleware",
503
+ ],
504
+ catch_exceptions=False,
505
+ )
506
+ assert result.exit_code == 0, result.output
507
+
508
+ rendered = project / "src" / "middleware" / "generic_mw.py"
509
+ assert rendered.exists()
510
+ source = rendered.read_text()
511
+ ast.parse(source)
512
+ assert "class GenericMwMiddleware(Middleware):" in source
513
+ assert "async def on_call_tool" in source
514
+ finally:
515
+ os.chdir(original_cwd)
516
+
517
+
401
518
  class TestGenerateErrorCases:
402
519
  """Tests for error handling."""
403
520
 
File without changes