Skip to content

Agent catalog: self-bootstrapping agent packs with manifest schema, resolution stack, file-tracked teardown, and CLI#1925

Draft
Copilot wants to merge 20 commits intomainfrom
copilot/agent-catalog-self-bootstrapping
Draft

Agent catalog: self-bootstrapping agent packs with manifest schema, resolution stack, file-tracked teardown, and CLI#1925
Copilot wants to merge 20 commits intomainfrom
copilot/agent-catalog-self-bootstrapping

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

Adding a new agent today requires changes to 10+ files across 5 languages. This PR introduces the foundational agent pack infrastructure that replaces the monolithic AGENT_CONFIG + case/switch architecture with self-contained, declarative agent packs — while preserving full backward compatibility during migration.

Agent pack module (src/specify_cli/agent_pack.py)

  • AgentManifest — parsed/validated speckit-agent.yml with schema version enforcement
  • AgentBootstrap — abstract base with setup() → List[Path] / teardown(force, files) / finalize_setup(agent_files, extension_files) lifecycle contract
  • DefaultBootstrap — generic bootstrap that derives AGENT_DIR and COMMANDS_SUBDIR from the manifest's commands_dir field, eliminating the need for per-agent bootstrap.py files when the agent follows the standard setup/teardown pattern
  • 4-level resolution stack: user (~/.specify/agents/) → project (.specify/agents/) → catalog cache → embedded in wheel
  • load_bootstrap() dynamically imports pack's bootstrap module and discovers the AgentBootstrap subclass; falls back to DefaultBootstrap when no bootstrap.py exists
  • Agent ID validation via _validate_agent_id() — rejects IDs containing /, .., or characters outside [a-z0-9-] to prevent path traversal through the resolution stack
  • validate_pack(), export_pack(), list_embedded_agents(), list_all_agents()

setup() owns scaffolding and returns actual installed files

Each bootstrap's setup() calls _scaffold_project(), a base-class helper that invokes scaffold_from_core_pack, snapshots the project directory before and after, and returns every new file created. This means setup() is the single entry point for agent installation — it runs the shared scaffolding and reports what it created.

  • _scaffold_project(project_path, script_type) — calls scaffold_from_core_pack, diffs before/after snapshots, returns List[Path] of all new files
  • collect_installed_files(project_path) — scans the agent's directory tree and returns all files found
  • agent_dir() — guarded against empty commands_dir; raises AgentPackError when the manifest's commands_dir is empty
  • finalize_setup(agent_files, extension_files) — filters agent_files to only track files under the agent's own directory tree (shared .specify/ infrastructure is not tracked per-agent), scans for additional files from later pipeline steps (skills, extensions), and writes .specify/agent-manifest<agent_id>.json

Installed-file tracking with SHA-256 hashes

  • finalize_setup(agent_files, extension_files) accepts categorised file lists from the CLI and records them in .specify/agent-manifest<agent_id>.json with separate agent_files and extension_files sections
  • get_tracked_files() returns an (agent_files, extension_files) tuple so the CLI can retrieve and split the tracked sets
  • check_modified_files() compares current file hashes against the recorded manifest to detect user modifications across both categories
  • remove_tracked_files(force, files) deletes only individual tracked files — directories are never deleted. Accepts an explicit files dict so the CLI can feed tracked file lists directly into teardown. Raises AgentFileModifiedError if any tracked file was modified since installation and force is False
  • _reregister_extension_commands() returns List[Path] of files created by extension registration, which are passed to finalize_setup() as extension_files. Logs caught exceptions at debug level instead of silently swallowing them.

CLI-level modification check before teardown

specify agent switch checks for modified files before calling teardown and prompts the user for confirmation. The CLI retrieves the tracked file lists via get_tracked_files(), checks for modifications, lists any changed files, and only proceeds with teardown after the user confirms (or if --force is passed). The file lists are then fed directly into teardown().

Rollback on failed agent switch

If the new agent's setup() fails after the old agent was torn down, agent_switch attempts to restore the old agent by re-running its setup() and finalize_setup(). If rollback also fails, it records an agent_switch_error key in init-options.json with a specific repair command so the user knows how to recover.

--agent flag on specify init

specify init --agent claude opts into the pack-based init flow. The --agent flag:

  • Resolves the agent through the pack system (same 4-level priority stack)
  • Validates the pack exists and can be loaded
  • Routes scaffolding through setup() — the bootstrap owns the scaffolding process, always using bundled assets
  • After all files are created (skills, presets, extensions), calls finalize_setup(agent_files=setup_files) to record every installed file in .specify/agent-manifest<id>.json
  • Records "agent_pack": true in init-options.json

--agent and --ai are mutually exclusive. --agent accepts the same agent IDs as --ai but additionally enables file tracking for clean teardown when switching agents.

specify init my-project --agent claude
specify init --here --agent gemini --ai-skills

25 embedded agent packs (src/specify_cli/core_pack/agents/<id>/)

Each pack contains speckit-agent.yml. Per-agent bootstrap.py files have been replaced by the DefaultBootstrap class in agent_pack.py, which derives its directory layout from the manifest's commands_dir field. Manifests carry command registration metadata (commands_dir, format, arg_placeholder, file_extension) sufficient for CommandRegistrar to render extension commands without importing a bootstrap module.

Bundled in the wheel via pyproject.toml force-include.

specify agent CLI subcommands

list, info, validate, export, switch, search, add, remove — following the same typer.Typer() + app.add_typer() pattern as specify extension and specify preset.

list --installed filters to only agents that have an install manifest (.specify/agent-manifest-<id>.json) in the current project, rather than showing all embedded agents.

switch checks for modified files and prompts before teardown, tears down the old agent (file-by-file using the tracked file lists), sets up the new one (with rollback on failure), re-registers extension commands, then calls finalize_setup() with both agent and extension files to record the new install manifest. Updates init-options.json accordingly.

remove now accepts --force and prompts for confirmation before deleting user-level overrides, since those affect all projects globally.

$ specify agent info claude
  Agent: Claude Code (claude)
  Source: embedded (bundled in specify-cli wheel)
  Commands Dir: .claude/commands
  Format: markdown

$ specify agent search --tag skills
  codex   Codex CLI   cli, openai, codex, skills   embedded
  kimi    Kimi Code   cli, moonshot, kimi, skills   embedded

Trust boundary

Agent packs can include a bootstrap.py module that is dynamically imported and executed. Pack authors can run arbitrary code through this mechanism. The 4-level resolution stack means that placing a pack in any of the resolution directories causes its code to run when the agent is loaded. Only install packs from trusted sources. This is documented in the load_bootstrap() docstring and in AGENTS.md.

Backward compatibility

AGENT_CONFIG and all existing code paths are untouched. The pack system is purely additive — specify init --ai <agent> continues to resolve through AGENT_CONFIG. Pack-based resolution is available via specify init --agent <agent> and specify agent commands and will become the default after validation.

Documentation

AGENTS.md updated with a new "Agent Pack System" section covering:

  • --agent flag on specify init
  • All specify agent subcommands with descriptions
  • Pack resolution order (user → project → catalog → embedded)
  • Trust boundary for dynamically loaded bootstrap.py modules
  • Installed-file tracking and modification detection

Tests

471 tests in test_agent_pack.py covering:

  • Manifest validation — schema errors, optional defaults, field parsing
  • Bootstrap contract — setup returns files, teardown with explicit files, dynamic loading, DefaultBootstrap fallback
  • DefaultBootstrap — derives AGENT_DIR/COMMANDS_SUBDIR from manifest, handles empty commands_dir
  • agent_dir() guard — raises AgentPackError on empty commands_dir
  • Agent ID validation — rejects path traversal (../, /), uppercase, special characters; accepts valid [a-z0-9-] IDs
  • Resolution order — embedded, project-level override, user-level override, catalog override
  • Discovery and export — list_embedded_agents, list_all_agents, export_pack
  • Consistency — embedded packs match both AGENT_CONFIG and CommandRegistrar.AGENT_CONFIGS
  • File tracking — record/check unmodified, detect modification, refuse modified without force, allow modified with force, directory preservation, deleted-file handling, SHA-256 consistency, categorised manifest structure, get_tracked_files split, explicit-file teardown, extension file modification detection
  • setup() returns actual files (TestSetupReturnsFiles, 100 tests) — every agent's setup() returns non-empty list of existing absolute paths, including files under the agent's directory tree
  • End-to-end init parity (TestInitFlowParity, 300 tests) — uses typer.testing.CliRunner to invoke the real specify init command for both --ai and --agent flows across all 25 agents, comparing file sets, file contents, directory structure, tracking manifests, commands_dir alignment, and CommandRegistrar metadata byte-for-byte
  • Full lifecycle — setup → scaffold → finalize → teardown for all 25 agents verifies clean removal of every tracked file

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…n, resolution, and CLI commands

- Create src/specify_cli/agent_pack.py with AgentBootstrap base class,
  AgentManifest schema/validation, pack resolution (user > project > catalog > embedded)
- Generate all 25 official agent packs under src/specify_cli/core_pack/agents/
  with speckit-agent.yml manifests and bootstrap.py modules
- Add 'specify agent' CLI subcommands: list, info, validate, export,
  switch, search, add, remove
- Update pyproject.toml to bundle agent packs in the wheel
- Add comprehensive tests (39 tests): manifest validation, bootstrap API,
  resolution order, discovery, consistency with AGENT_CONFIG

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/ef8b4682-7f1a-4b04-a112-df0878236b6b
…est assertions

- Copilot: only remove .github/agents/ (preserves workflows, templates)
- Tabnine: only remove .tabnine/agent/ (preserves other config)
- Amp/Codex: only remove respective subdirs (commands/skills)
  to avoid deleting each other's files in shared .agents/ dir
- Tests: use flexible assertions instead of hardcoded >= 25 counts

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/ef8b4682-7f1a-4b04-a112-df0878236b6b
Copilot AI changed the title [WIP] Update agent catalog for self-bootstrapping agent packs Agent catalog: self-bootstrapping agent packs with manifest schema, resolution stack, and CLI Mar 20, 2026
Copilot AI requested a review from mnriem March 20, 2026 21:06
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The setup should store a list of installed files with their hash in the .specify directory so we can track what was installed by the agent. At teardown time that file needs to be used to determine if ANY of the installed files was updated and if so the remove should require a --force. Note you will NOT delete any directories but rather just the individual files.

Setup records installed files and their SHA-256 hashes in
.specify/agent-manifest-<agent_id>.json. Teardown uses the manifest
to remove only individual files (never directories). If any tracked
file was modified since installation, teardown requires --force.

- Add record_installed_files(), check_modified_files(), remove_tracked_files()
  and AgentFileModifiedError to agent_pack.py
- Update all 25 bootstrap modules to use file-tracked setup/teardown
- Add --force flag to 'specify agent switch'
- Add 11 new tests for file tracking (record, check, remove, force,
  directory preservation, deleted-file handling, manifest structure)

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/779eabf6-21d5-428b-9f01-dd363df4c84a
…writes files

Address code review: setup() now only creates directories, while
finalize_setup() (on base class) scans the agent's commands_dir
for all files and records them. This ensures files are tracked
after the full init pipeline has written them, not before.

- Add AgentBootstrap.finalize_setup() that scans commands_dir
- Remove premature record_installed_files() from all 25 setup() methods
- agent_switch calls finalize_setup() after setup() completes
- Update test helper to match new pattern

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/779eabf6-21d5-428b-9f01-dd363df4c84a
Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

@copilot The setup should store a list of installed files with their hash in the .specify directory so we can track what was installed by the agent. At teardown time that file needs to be used to dete...

Implemented in b5a5e3f (file tracking) and a63c248 (finalize_setup fix):

  • setup() creates directories; finalize_setup() (base class method) scans the agent's commands_dir and writes .specify/agent-manifest-<agent_id>.json with SHA-256 hashes of all installed files
  • teardown(force=False) reads the manifest and removes only individual tracked files — directories are never deleted
  • If any tracked file was modified since installation, teardown raises AgentFileModifiedError unless --force is passed
  • specify agent switch now accepts --force to pass through to teardown
  • 11 new tests cover: record/check unmodified, detect modification, refuse without force, allow with force, directory preservation, deleted-file handling, manifest JSON structure

Copilot AI changed the title Agent catalog: self-bootstrapping agent packs with manifest schema, resolution stack, and CLI Agent catalog: self-bootstrapping agent packs with manifest schema, resolution stack, file-tracked teardown, and CLI Mar 20, 2026
Copilot AI requested a review from mnriem March 20, 2026 21:21
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot the agent setup should report the files it installed as the cli needs to record both what was installed by the agent for the core commands / templates as well as what installed by any extensions. It should the record that. And then BEFORE agent teardown is called determine if any file was changed and ask for confirmation. And then feed those lists into the teardown of the agent and/or the installed extensions.

…wn, categorised manifest

- setup() returns List[Path] of installed files so CLI can record them
- finalize_setup() accepts agent_files + extension_files for combined tracking
- Install manifest categorises files: agent_files and extension_files
- get_tracked_files() returns (agent_files, extension_files) split
- remove_tracked_files() accepts explicit files dict for CLI-driven teardown
- agent_switch checks for modifications BEFORE teardown and prompts user
- _reregister_extension_commands() returns List[Path] of created files
- teardown() accepts files parameter to receive explicit file lists
- All 25 bootstraps updated with new signatures
- 5 new tests: categorised manifest, get_tracked_files, explicit file teardown,
  extension file modification detection

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/32e470fc-6bf5-453c-bf6c-79a8521efa56
Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

@copilot the agent setup should report the files it installed as the cli needs to record both what was installed by the agent for the core commands / templates as well as what installed by any extensi...

Implemented in e190116:

  • setup() now returns List[Path] of installed files
  • finalize_setup(agent_files=, extension_files=) records both agent and extension files in .specify/agent-manifest-<id>.json with separate agent_files / extension_files categories
  • _reregister_extension_commands() returns List[Path] of files it created, which get passed to finalize_setup() as extension_files
  • Before teardown: CLI calls check_modified_files(), lists modified files, prompts for confirmation — teardown is only called after the user confirms
  • get_tracked_files() returns (agent_files, extension_files) tuple so the CLI can retrieve both sets
  • teardown(files=...) accepts explicit file dict — the CLI feeds the tracked file lists directly into teardown
  • 5 new tests cover: categorised manifest structure, get_tracked_files split, explicit-file teardown, extension file modification detection

Copilot AI requested a review from mnriem March 20, 2026 21:38
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Fixt the code quality issues as per comments

Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot Fixt the code quality issues as per comments

Fixed in 795f1e7 — added explanatory comments to all 5 empty except clauses flagged by code quality:

  • agent_pack.py:338ValueError when path is outside agent root (filtering logic)
  • agent_pack.py:667AgentPackError when embedded manifest is unreadable (override info)
  • test_agent_pack.py:969Exception in mock tracker (safe to ignore)
  • test_agent_pack.py:985ImportError/AttributeError for optional AGENT_SKILLS_MIGRATIONS
  • test_agent_pack.py:1153 — same AGENT_SKILLS_MIGRATIONS guard in parity test

ruff check passes clean, all 452 tests pass.

Copilot AI requested a review from mnriem March 23, 2026 14:03
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Address the following:

  • Add agent ID validation in resolve_agent_pack() — reject IDs containing /, .., or characters outside [a-z0-9-] to prevent path traversal
  • Add rollback logic to agent_switch: if the new agent's setup() fails after the old agent was torn down, restore the old agent's files or set a clear error state in init-options.json
  • Fix cursor-agent/speckit-agent.yml: it sets requires_cli: false but AGENT_CONFIG has requires_cli: True — align the manifest with the config
  • Guard agent_dir() against empty commands_dir — currently split("/")[0] returns "" when commands_dir is empty, making agent_dir() return project_path itself
  • Replace the 25 near-identical bootstrap.py files with a single DefaultBootstrap class in agent_pack.py that reads AGENT_DIR and COMMANDS_SUBDIR from the manifest's commands_dir field
  • Log or warn (at debug level) when _reregister_extension_commands() catches an exception instead of silently swallowing it with bare except Exception: continue
  • Fix the --installed flag on specify agent list — it should filter to only agents that have files present in the current project, not just add project-level resolution while still showing all embedded agents
  • Add a confirmation prompt (or --force flag) to specify agent remove before deleting user-level overrides, since those affect all projects globally
  • Update AGENTS.md to document the new --agent flag on specify init and the specify agent subcommands
  • Add a note about the trust boundary for dynamically loaded bootstrap.py modules — pack authors can execute arbitrary code via the resolution stack

Copilot AI and others added 2 commits March 23, 2026 14:32
…efaultBootstrap, logging, CLI fixes, docs

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/40d5aec5-d8e9-4e3f-ae60-6cf67ff491f3
Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot Address the following:

  • Add agent ID validation in resolve_agent_pack() — reject IDs containing /, .., or characters outside [a-z0-9-] to prevent path traversal...

All 10 items addressed in 00117c5 and ab8c58f:

  1. Agent ID validation_validate_agent_id() rejects IDs with /, .., or chars outside [a-z0-9-] via regex, called at the top of resolve_agent_pack()
  2. Rollback logicagent_switch attempts to restore the old agent if setup fails; on rollback failure, records agent_switch_error with a repair command in init-options.json
  3. cursor-agent requires_cli — verified both AGENT_CONFIG and manifest already agree (requires_cli: False); no change needed
  4. agent_dir() guard — raises AgentPackError when commands_dir is empty
  5. DefaultBootstrap — single class in agent_pack.py replaces all 25 identical bootstrap.py files; load_bootstrap() returns it when no bootstrap.py exists
  6. Extension logging_reregister_extension_commands now logs at debug level when catching exceptions
  7. --installed fix — filters by presence of .specify/agent-manifest-<id>.json instead of project-level resolution
  8. agent remove confirmation--force flag added; user-level overrides prompt before deletion
  9. AGENTS.md — new "Agent Pack System" section documents --agent, all subcommands, resolution order, file tracking, and trust boundary
  10. Trust boundary — documented in load_bootstrap() docstring and AGENTS.md

471 tests pass, ruff check clean, CodeQL clean.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new “agent pack” system for specify-cli, moving agent definitions toward self-contained packs (manifest + optional bootstrap) with resolution (user/project/catalog/embedded), installed-file tracking for safe teardown, and a new specify agent CLI surface—while keeping the legacy --ai flow intact.

Changes:

  • Added src/specify_cli/agent_pack.py implementing manifest parsing/validation, pack resolution, bootstrap loading (with DefaultBootstrap fallback), and SHA-256 file tracking for teardown.
  • Extended specify init with --agent (pack-based flow + tracking) and added specify agent subcommands (list/info/validate/export/switch/search/add/remove).
  • Embedded agent pack manifests under src/specify_cli/core_pack/agents/<id>/ and updated wheel packaging; added extensive test coverage in tests/test_agent_pack.py.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/specify_cli/agent_pack.py New agent-pack core: manifest schema, resolution stack, bootstrap loading, file tracking + teardown helpers
src/specify_cli/__init__.py Adds --agent init flow + new specify agent command group (including switch/rollback)
tests/test_agent_pack.py New comprehensive tests for manifest validation, resolution, lifecycle, tracking, and init parity
pyproject.toml Includes embedded agent packs in the wheel via force-include
AGENTS.md Documents the new agent pack system, commands, and resolution order
src/specify_cli/core_pack/agents/agy/speckit-agent.yml Adds embedded manifest for agy
src/specify_cli/core_pack/agents/amp/speckit-agent.yml Adds embedded manifest for amp
src/specify_cli/core_pack/agents/auggie/speckit-agent.yml Adds embedded manifest for auggie
src/specify_cli/core_pack/agents/bob/speckit-agent.yml Adds embedded manifest for bob
src/specify_cli/core_pack/agents/claude/speckit-agent.yml Adds embedded manifest for claude
src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml Adds embedded manifest for codebuddy
src/specify_cli/core_pack/agents/codex/speckit-agent.yml Adds embedded manifest for codex
src/specify_cli/core_pack/agents/copilot/speckit-agent.yml Adds embedded manifest for copilot
src/specify_cli/core_pack/agents/cursor-agent/speckit-agent.yml Adds embedded manifest for cursor-agent
src/specify_cli/core_pack/agents/gemini/speckit-agent.yml Adds embedded manifest for gemini
src/specify_cli/core_pack/agents/iflow/speckit-agent.yml Adds embedded manifest for iflow
src/specify_cli/core_pack/agents/junie/speckit-agent.yml Adds embedded manifest for junie
src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml Adds embedded manifest for kilocode
src/specify_cli/core_pack/agents/kimi/speckit-agent.yml Adds embedded manifest for kimi
src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml Adds embedded manifest for kiro-cli
src/specify_cli/core_pack/agents/opencode/speckit-agent.yml Adds embedded manifest for opencode
src/specify_cli/core_pack/agents/pi/speckit-agent.yml Adds embedded manifest for pi
src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml Adds embedded manifest for qodercli
src/specify_cli/core_pack/agents/qwen/speckit-agent.yml Adds embedded manifest for qwen
src/specify_cli/core_pack/agents/roo/speckit-agent.yml Adds embedded manifest for roo
src/specify_cli/core_pack/agents/shai/speckit-agent.yml Adds embedded manifest for shai
src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml Adds embedded manifest for tabnine
src/specify_cli/core_pack/agents/trae/speckit-agent.yml Adds embedded manifest for trae
src/specify_cli/core_pack/agents/vibe/speckit-agent.yml Adds embedded manifest for vibe
src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml Adds embedded manifest for windsurf
Comments suppressed due to low confidence (4)

src/specify_cli/init.py:2724

  • Rollback path restores the previous agent by rerunning setup()+finalize_setup(), but it doesn’t re-register extension commands (unlike the success path which calls _reregister_extension_commands). Since teardown likely removed tracked extension-generated command files, a rollback can leave the project missing extension commands even though extensions are still installed. Consider calling _reregister_extension_commands(project_path, current_agent) during rollback and passing those created files into finalize_setup().
                rollback_resolved = resolve_agent_pack(current_agent, project_path=project_path)
                rollback_bs = load_bootstrap(rollback_resolved.path, rollback_resolved.manifest)
                rollback_files = rollback_bs.setup(project_path, script_type, options)
                rollback_bs.finalize_setup(
                    project_path,
                    agent_files=rollback_files,
                    extension_files=list(
                        (project_path / p).resolve()
                        for p in old_tracked_ext
                        if (project_path / p).is_file()
                    ),
                )
                console.print(f"  [green]✓[/green] {current_agent} restored")

src/specify_cli/agent_pack.py:474

  • record_installed_files() stores tracked paths using str(Path.relative_to(...)), which will emit OS-specific separators (e.g. backslashes on Windows). Since this JSON manifest is a project artifact and other code/tests treat paths as POSIX-like, consider normalizing to .as_posix() when writing keys (and likewise when reporting modified paths) for cross-platform stability.
    entries: Dict[str, str] = {}
    for file_path in files:
        abs_path = project_path / file_path if not file_path.is_absolute() else file_path
        if abs_path.is_file():
            rel = str(abs_path.relative_to(project_path))
            entries[rel] = _sha256(abs_path)
    return entries

src/specify_cli/init.py:2746

  • After a successful switch, init-options.json is updated with the new "ai" but the "agent_pack" flag is not set/updated. Since specify init --agent persists "agent_pack": true and other tooling may rely on it, consider setting options["agent_pack"] = True here as well.
    # Update init options
    options["ai"] = agent_id
    options.pop("agent_switch_error", None)  # clear any previous error
    init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8")

src/specify_cli/agent_pack.py:759

  • resolve_agent_pack() loads whatever speckit-agent.yml it finds under /<agent_id>/ but never verifies that manifest.id matches the requested agent_id. A mismatched manifest can lead to confusing behavior (scaffolding/teardown keyed off the manifest id) and makes it easier for a malicious override to masquerade under a different directory name. Validate manifest.id == agent_id (and raise PackResolutionError if not).
    for source, pack_dir in candidates:
        manifest_file = pack_dir / MANIFEST_FILENAME
        if manifest_file.is_file():
            manifest = AgentManifest.from_yaml(manifest_file)
            if source == "embedded":
                embedded_manifest = manifest

            overrides = None
            if source != "embedded" and embedded_manifest is None:
                # Try loading embedded to record what it overrides
                emb_file = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME
                if emb_file.is_file():
                    try:
                        emb = AgentManifest.from_yaml(emb_file)
                        overrides = f"embedded v{emb.version}"
                    except AgentPackError:
                        pass  # Embedded manifest unreadable — skip override info

            return ResolvedPack(
                manifest=manifest,
                source=source,
                path=pack_dir,
                overrides=overrides,
            )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…aths, 3-segment commands_dir, full file tracking

- Legacy --ai teardown: detect empty tracked files and fall back to
  AGENT_CONFIG-based directory removal during agent switch
- --agent generic: falls through to legacy flow (no embedded pack)
- User/catalog dirs: use ~/.specify/ instead of platformdirs for
  consistency with extensions/presets
- DefaultBootstrap: join all path segments after first for COMMANDS_SUBDIR
  (fixes 3+-segment commands_dir like .tabnine/agent/commands)
- agent_add --from: validate manifest.id matches provided agent_id
- finalize_setup: track all files from setup(), not just agent-root files
- setup() docstring: reference --agent not --ai
- AGENTS.md: document generic agent fallback behavior
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

src/specify_cli/agent_pack.py:408

  • DefaultBootstrap.setup() returns _scaffold_project(...), which yields all newly created files across the whole project (not just agent-owned files). When those paths are passed into finalize_setup(), shared scaffolding outputs (e.g. .specify/scripts, templates) can become tracked and later removed during teardown/switch. Consider returning only files under the agent root (e.g. by collecting files under agent_dir() after scaffolding) so tracking/teardown stays scoped to the agent’s directory.
        if self.AGENT_DIR and self.COMMANDS_SUBDIR:
            commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
            commands_dir.mkdir(parents=True, exist_ok=True)
        return self._scaffold_project(project_path, script_type)


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2696 to +2700
agent_folder = agent_config.get("folder")
if agent_folder:
agent_dir = project_path / agent_folder.rstrip("/")
if agent_dir.is_dir():
shutil.rmtree(agent_dir)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the legacy fallback path (no install manifest), agent_switch deletes the entire agent directory with shutil.rmtree(...) without any modification check or confirmation (unless the user happened to have tracked files). This can silently delete user-edited legacy agent commands/prompts. Consider prompting before deleting the legacy agent directory when --force is not set (or at least require --force to proceed).

Copilot uses AI. Check for mistakes.
raise typer.Exit(1)

# Update init options
options["ai"] = agent_id
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a successful switch, init-options.json is updated with the new ai value but the agent_pack flag is not updated. If a project started in legacy --ai mode and then switches to a pack-based agent, agent_pack will remain false and the project metadata won’t reflect the actual management mode. Update options["agent_pack"] accordingly during switch (and clear/adjust any legacy-only keys if needed).

Suggested change
options["ai"] = agent_id
options["ai"] = agent_id
options["agent_pack"] = True

Copilot uses AI. Check for mistakes.
Comment on lines +2796 to +2800
try:
from .agents import CommandRegistrar
registrar = CommandRegistrar()
except ImportError:
return created_files
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_reregister_extension_commands() passes agent_id directly into CommandRegistrar.register_commands(), but CommandRegistrar.AGENT_CONFIGS keys don’t match all AGENT_CONFIG/pack IDs (e.g. cursor-agent vs cursor). For those agents, register_commands() raises ValueError("Unsupported agent"), which is caught and only logged at debug, so extension commands won’t be re-registered after a switch. Consider adding missing agent IDs to CommandRegistrar.AGENT_CONFIGS (aliasing where appropriate) or mapping pack IDs to registrar IDs before calling register_commands() so switch preserves extension commands.

Copilot uses AI. Check for mistakes.
mnriem added 2 commits March 23, 2026 10:58
…ck flag, registrar alias, manifest ID validation

- Legacy rmtree: prompt user before deleting agent directory in legacy
  fallback path (both no-manifest and AgentPackError cases), respects --force
- Set options['agent_pack'] = True during agent_switch so projects
  originally created with --ai reflect pack-based management after switch
- Add cursor-agent alias in CommandRegistrar.AGENT_CONFIGS so extension
  re-registration works when switching to/from cursor-agent
- Validate manifest.id matches agent_id in resolve_agent_pack() to
  prevent malicious override packs from injecting different IDs
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

src/specify_cli/agent_pack.py:415

  • DefaultBootstrap.setup() returns self._scaffold_project(...), which runs the full bundled scaffold and will overwrite existing project files via shutil.copy2 (see scaffold_from_core_pack), without any confirmation during specify agent switch. For switching agents, this risks clobbering user edits in shared files (specs/plans/templates/etc.) even though the command claims to preserve them. A safer pattern is to scaffold into a temp dir and copy only the agent-specific subtree (or only create/update the commands/skills dir) when switching.
    def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
        """Install agent files into the project using the standard scaffold."""
        if self.AGENT_DIR and self.COMMANDS_SUBDIR:
            commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
            commands_dir.mkdir(parents=True, exist_ok=True)
        return self._scaffold_project(project_path, script_type)

src/specify_cli/agent_pack.py:465

  • _hash_file_list() derives rel using abs_path.relative_to(project_path) without verifying that the resulting path stays within project_path after normalization. If a caller passes a relative path containing .. segments, you can end up recording entries like ../../somewhere in the manifest, which later makes remove_tracked_files() operate outside the project root. Consider resolving (abs_path.resolve()) and rejecting anything not under project_path.resolve(), and normalizing stored paths to POSIX with no .. components.
def _hash_file_list(
    project_path: Path,
    files: List[Path],
) -> Dict[str, str]:
    """Build a {relative_path: sha256} dict from a list of file paths."""
    entries: Dict[str, str] = {}
    for file_path in files:
        abs_path = project_path / file_path if not file_path.is_absolute() else file_path
        if abs_path.is_file():
            rel = str(abs_path.relative_to(project_path))
            entries[rel] = _sha256(abs_path)
    return entries

src/specify_cli/agent_pack.py:580

  • check_modified_files() trusts manifest keys as relative paths and does project_path / rel_path. If the manifest is corrupted or user-edited to include absolute paths or .. segments, this can read/hash files outside the project. It would be safer to validate each rel_path (must be a relative, normalized path under project_path) before hashing, and skip/flag invalid entries.
    modified: List[str] = []
    for rel_path, original_hash in entries.items():
        abs_path = project_path / rel_path
        if abs_path.is_file():
            if _sha256(abs_path) != original_hash:
                modified.append(rel_path)

src/specify_cli/agent_pack.py:648

  • remove_tracked_files() deletes project_path / rel_path for each manifest entry without validating that rel_path is a safe project-relative path. If .specify/agent-manifest-<id>.json is tampered with (or _hash_file_list records .. segments), this can delete files outside the project directory. Add a safety check (e.g., resolved = (project_path / rel_path).resolve() and ensure it is within project_path.resolve(), and ignore/raise on invalid paths) before unlinking.
    removed: List[str] = []
    for rel_path in entries:
        abs_path = project_path / rel_path
        if abs_path.is_file():
            abs_path.unlink()
            removed.append(rel_path)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…update help text

- remove_tracked_files: always compare SHA-256 hash before deleting,
  even when called with explicit files dict; skip modified files unless
  --force is set (was unconditionally deleting all tracked files)
- finalize_setup: track ALL files from setup() (no agent-root filter);
  safe because removal now checks hashes
- list_all_agents: track embedded versions in separate dict so overrides
  always reference the correct embedded version, not a catalog/project
  pack that overwrote the seen dict
- --ai-skills help text: updated to say 'requires --ai or --agent'
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +359 to +382
all_extension = list(extension_files or [])
all_agent: List[Path] = list(agent_files or [])

# Scan the agent's directory tree for files created by later
# init pipeline steps (skills, presets, extensions) that
# setup() did not report. We scan the agent root directory
# (e.g. .claude/) so we catch both commands and skills
# directories (skills-migrated agents replace the commands
# directory with a sibling skills directory during init).
if self.manifest.commands_dir:
agent_root = self.agent_dir(project_path)
if agent_root.is_dir():
agent_set = {p.resolve() for p in all_agent}
for p in agent_root.rglob("*"):
if p.is_file() and p.resolve() not in agent_set:
all_agent.append(p)
agent_set.add(p.resolve())

record_installed_files(
project_path,
self.manifest.id,
agent_files=all_agent,
extension_files=all_extension,
)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finalize_setup() currently records all paths passed via agent_files (plus the full agent_root scan) without filtering them to the agent’s own directory tree. In the specify init --agent flow, setup() returns every scaffolded file in the project, so this will cause the tracking manifest to include shared project infrastructure (e.g., .specify/, templates), and specify agent switch will then delete those files during teardown. Filter agent_files down to paths under the agent root (or another well-defined agent-owned subtree) before calling record_installed_files() so teardown cannot remove non-agent project files.

Copilot uses AI. Check for mistakes.
Comment on lines +640 to +648
for rel_path, original_hash in entries.items():
abs_path = project_path / rel_path
if abs_path.is_file():
if original_hash and _sha256(abs_path) != original_hash:
# File was modified since installation — skip unless forced
if not force:
continue
abs_path.unlink()
removed.append(rel_path)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove_tracked_files() trusts rel_path values loaded from .specify/agent-manifest-<id>.json (or the caller-provided files mapping) and does project_path / rel_path before unlinking. If the manifest is corrupted or edited to include ../... paths, this can delete files outside the project directory. Before hashing/unlinking, resolve the candidate path and verify it is within project_path (e.g., resolved.is_relative_to(project_root)), and skip/raise on any entry that escapes the project root.

Copilot uses AI. Check for mistakes.
Comment on lines +2743 to +2750
rollback_bs.finalize_setup(
project_path,
agent_files=rollback_files,
extension_files=list(
(project_path / p).resolve()
for p in old_tracked_ext
if (project_path / p).is_file()
),
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the rollback path (when new agent setup fails), the code restores the previous agent by re-running setup() and finalize_setup(), but it does not re-register extension commands (unlike the success path, which calls _reregister_extension_commands). Since teardown removed extension_files, a successful rollback can leave extension command files missing while reporting the agent as restored. Consider calling _reregister_extension_commands(project_path, current_agent) during rollback and passing those returned paths to finalize_setup() (instead of relying on old_tracked_ext entries that likely no longer exist).

Suggested change
rollback_bs.finalize_setup(
project_path,
agent_files=rollback_files,
extension_files=list(
(project_path / p).resolve()
for p in old_tracked_ext
if (project_path / p).is_file()
),
# Re-register extension commands for the restored agent, just like in the success path.
rollback_extension_files = _reregister_extension_commands(project_path, current_agent)
rollback_bs.finalize_setup(
project_path,
agent_files=rollback_files,
extension_files=rollback_extension_files,

Copilot uses AI. Check for mistakes.
…cle docs

- remove_tracked_files: validate resolved path stays within project_path
  before unlinking; reject entries with '../' that escape the project root
- Rollback: call _reregister_extension_commands() during rollback (same
  as success path) so extension files are properly restored
- AgentBootstrap: comprehensive lifecycle flow docstring documenting the
  setup → finalize_setup → get_tracked_files → check_modified → teardown
  chain and explaining why tracking all files is safe (hash check)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent Catalog -- Self-Bootstrapping Agent Packs with Catalog Override

3 participants