Follow-up to #48. After the manifest loader lands, the failure mode for projects scaffolded from a template that does not (yet) ship a `.fips-template.yaml` is a Python traceback rather than a clean error.
Reproduction
After #48 merges, scaffold a gateway / ui / sandbox project with a CLI version whose template repo ships no manifest yet:
```
$ fips-agents patch check
...
ValueError: Patching is not supported for project type 'gateway'.
Supported types: mcp-server, agent, workflow.
```
Same outcome if the manifest is present but missing required fields — `_resolve_categories` warns about the malformed manifest and then falls through to `get_categories_for_type(project_type)`, which raises for any type outside the built-in three.
Why this happens
`tools/patching.py:_resolve_categories`:
```python
def _resolve_categories(template_root, template_info):
manifest = _load_template_manifest(template_root)
if manifest is not None:
resolved = _categories_from_manifest(manifest)
if resolved is not None:
return resolved
console.print("[yellow]⚠[/yellow] manifest malformed; falling back...")
project_type = get_project_type(template_info)
return get_categories_for_type(project_type) # raises for gateway / ui / sandbox
```
The `ValueError` from the constants lookup bubbles up through `check_for_updates` and `patch_category` as an unhandled exception.
Severity
Pre-existing behavior is the same (the old code raised the same `ValueError` from `patch_category` even before manifest support), so #48 doesn't regress anyone. But once manifests start landing in the template repos, the user expectation shifts — "my gateway project should support patch." A user who upgrades the CLI before the gateway-template manifest merge ships will hit this.
Proposed fix
Define a sentinel exception in `tools/patching.py`:
```python
class PatchUnsupportedForProjectType(Exception):
"""Raised when patch is invoked on a project whose template type
has no built-in category set and ships no usable .fips-template.yaml."""
```
Wrap the fallback in `_resolve_categories`:
```python
project_type = get_project_type(template_info)
try:
return get_categories_for_type(project_type)
except ValueError as e:
raise PatchUnsupportedForProjectType(
f"This template type ({project_type}) requires a .fips-template.yaml "
f"manifest to support patching, and none was found in the cloned "
f"template. See https://github.com/fips-agents/fips-agents-cli/issues/45.\"
) from e
```
Catch in the two call sites:
- `check_for_updates` returns `{}` after printing a clean error so `patch check` exits 0 with "no patchable categories" rather than a traceback.
- `patch_category` returns `(False, str(exc))` so the existing red ✗ rendering kicks in.
Test plan
- Unit test: `_resolve_categories` raises `PatchUnsupportedForProjectType` for project_type `gateway` when no manifest is present.
- Unit test: same when manifest is present but malformed (i.e., `_categories_from_manifest` returns `None`).
- Integration test: `patch check` against a fake gateway project (no manifest) exits 0 with a friendly message, no traceback.
- Integration test: `patch chart` against the same project exits 1 with the friendly message.
Out of scope
Follow-up to #48. After the manifest loader lands, the failure mode for projects scaffolded from a template that does not (yet) ship a `.fips-template.yaml` is a Python traceback rather than a clean error.
Reproduction
After #48 merges, scaffold a gateway / ui / sandbox project with a CLI version whose template repo ships no manifest yet:
```
$ fips-agents patch check
...
ValueError: Patching is not supported for project type 'gateway'.
Supported types: mcp-server, agent, workflow.
```
Same outcome if the manifest is present but missing required fields — `_resolve_categories` warns about the malformed manifest and then falls through to `get_categories_for_type(project_type)`, which raises for any type outside the built-in three.
Why this happens
`tools/patching.py:_resolve_categories`:
```python
def _resolve_categories(template_root, template_info):
manifest = _load_template_manifest(template_root)
if manifest is not None:
resolved = _categories_from_manifest(manifest)
if resolved is not None:
return resolved
console.print("[yellow]⚠[/yellow] manifest malformed; falling back...")
project_type = get_project_type(template_info)
return get_categories_for_type(project_type) # raises for gateway / ui / sandbox
```
The `ValueError` from the constants lookup bubbles up through `check_for_updates` and `patch_category` as an unhandled exception.
Severity
Pre-existing behavior is the same (the old code raised the same `ValueError` from `patch_category` even before manifest support), so #48 doesn't regress anyone. But once manifests start landing in the template repos, the user expectation shifts — "my gateway project should support patch." A user who upgrades the CLI before the gateway-template manifest merge ships will hit this.
Proposed fix
Define a sentinel exception in `tools/patching.py`:
```python
class PatchUnsupportedForProjectType(Exception):
"""Raised when patch is invoked on a project whose template type
has no built-in category set and ships no usable .fips-template.yaml."""
```
Wrap the fallback in `_resolve_categories`:
```python
project_type = get_project_type(template_info)
try:
return get_categories_for_type(project_type)
except ValueError as e:
raise PatchUnsupportedForProjectType(
f"This template type ({project_type}) requires a .fips-template.yaml "
f"manifest to support patching, and none was found in the cloned "
f"template. See https://github.com/fips-agents/fips-agents-cli/issues/45.\"
) from e
```
Catch in the two call sites:
Test plan
Out of scope