Skip to content

Commit 9b580a5

Browse files
Copilotmnriem
andauthored
feat: setup() owns scaffolding and returns actual installed files
- AgentBootstrap._scaffold_project() calls scaffold_from_core_pack, snapshots before/after, returns all new files - finalize_setup() filters agent_files to only track files under the agent's own directory tree (shared .specify/ files not tracked) - All 25 bootstrap setup() methods call _scaffold_project() and return the actual file list instead of [] - --agent init flow routes through setup() for scaffolding instead of calling scaffold_from_core_pack directly - 100 new tests (TestSetupReturnsFiles): verify every agent's setup() returns non-empty, existing, absolute paths including agent-dir files - Parity tests use CliRunner to invoke the real init command - finalize_setup bug fix: skills-migrated agents (agy) now have their skills directory scanned correctly - 1262 tests pass (452 in test_agent_pack.py alone) Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/spec-kit/sessions/054690bb-c048-41e0-b553-377d5cb36b78
1 parent d6016ab commit 9b580a5

File tree

28 files changed

+592
-121
lines changed

28 files changed

+592
-121
lines changed

src/specify_cli/__init__.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,7 +1984,10 @@ def init(
19841984
"This will become the default in v0.6.0."
19851985
)
19861986

1987-
if use_github:
1987+
if use_agent_pack:
1988+
# Pack-based flow: setup() owns scaffolding, always uses bundled assets.
1989+
tracker.add("scaffold", "Apply bundled assets")
1990+
elif use_github:
19881991
for key, label in [
19891992
("fetch", "Fetch latest release"),
19901993
("download", "Download template"),
@@ -2019,7 +2022,26 @@ def init(
20192022
verify = not skip_tls
20202023
local_ssl_context = ssl_context if verify else False
20212024

2022-
if use_github:
2025+
# -- scaffolding ------------------------------------------------
2026+
# Pack-based flow (--agent): setup() owns scaffolding and
2027+
# returns every file it created. Legacy flow (--ai): scaffold
2028+
# directly or download from GitHub.
2029+
agent_setup_files: list[Path] = []
2030+
2031+
if use_agent_pack and agent_bootstrap is not None:
2032+
tracker.start("scaffold")
2033+
try:
2034+
agent_setup_files = agent_bootstrap.setup(
2035+
project_path, selected_script, {"here": here})
2036+
tracker.complete(
2037+
"scaffold",
2038+
f"{selected_ai} ({len(agent_setup_files)} files)")
2039+
except Exception as exc:
2040+
tracker.error("scaffold", str(exc))
2041+
if not here and project_path.exists():
2042+
shutil.rmtree(project_path)
2043+
raise typer.Exit(1)
2044+
elif use_github:
20232045
with httpx.Client(verify=local_ssl_context) as local_client:
20242046
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
20252047
else:
@@ -2162,11 +2184,14 @@ def init(
21622184
tracker.skip("cleanup", "not needed (no download)")
21632185

21642186
# When --agent is used, record all installed agent files for
2165-
# tracked teardown. This runs AFTER the full init pipeline has
2166-
# finished creating files (scaffolding, skills, presets,
2167-
# extensions) so finalize_setup captures everything.
2187+
# tracked teardown. setup() already returned the files it
2188+
# created; pass them to finalize_setup so the manifest is
2189+
# accurate. finalize_setup also scans the agent directory
2190+
# to catch any additional files created by later pipeline
2191+
# steps (skills, extensions, presets).
21682192
if use_agent_pack and agent_bootstrap is not None:
2169-
agent_bootstrap.finalize_setup(project_path)
2193+
agent_bootstrap.finalize_setup(
2194+
project_path, agent_files=agent_setup_files)
21702195

21712196
tracker.complete("final", "project ready")
21722197
except (typer.Exit, SystemExit):

src/specify_cli/agent_pack.py

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,57 @@ def agent_dir(self, project_path: Path) -> Path:
245245
"""Return the agent's top-level directory inside the project."""
246246
return project_path / self.manifest.commands_dir.split("/")[0]
247247

248+
def collect_installed_files(self, project_path: Path) -> List[Path]:
249+
"""Return every file under the agent's directory tree.
250+
251+
Subclasses should call this at the end of :meth:`setup` to build
252+
the return list. Any files present in the agent directory at
253+
that point — whether created by ``setup()`` itself, by the
254+
scaffold pipeline, or by a preceding step — are reported.
255+
"""
256+
root = self.agent_dir(project_path)
257+
if not root.is_dir():
258+
return []
259+
return sorted(p for p in root.rglob("*") if p.is_file())
260+
261+
def _scaffold_project(
262+
self,
263+
project_path: Path,
264+
script_type: str,
265+
is_current_dir: bool = False,
266+
) -> List[Path]:
267+
"""Run the shared scaffolding pipeline and return new files.
268+
269+
Calls ``scaffold_from_core_pack`` for this agent and then
270+
collects every file that was created. Subclasses should call
271+
this from :meth:`setup` when they want to use the shared
272+
scaffolding rather than creating files manually.
273+
274+
Returns:
275+
List of absolute paths of **all** files created by the
276+
scaffold (agent-specific commands, shared scripts,
277+
templates, etc.).
278+
"""
279+
# Lazy import to avoid circular dependency (agent_pack is
280+
# imported by specify_cli.__init__).
281+
from specify_cli import scaffold_from_core_pack
282+
283+
# Snapshot existing files
284+
before: set[Path] = set()
285+
if project_path.exists():
286+
before = {p for p in project_path.rglob("*") if p.is_file()}
287+
288+
ok = scaffold_from_core_pack(
289+
project_path, self.manifest.id, script_type, is_current_dir,
290+
)
291+
if not ok:
292+
raise AgentPackError(
293+
f"Scaffolding failed for agent '{self.manifest.id}'")
294+
295+
# Collect every new file
296+
after = {p for p in project_path.rglob("*") if p.is_file()}
297+
return sorted(after - before)
298+
248299
def finalize_setup(
249300
self,
250301
project_path: Path,
@@ -257,25 +308,51 @@ def finalize_setup(
257308
writing files (commands, context files, extensions) into the
258309
project. It combines the files reported by :meth:`setup` with
259310
any extra files (e.g. from extension registration), scans the
260-
agent's ``commands_dir`` for anything additional, and writes the
311+
agent's directory tree for anything additional, and writes the
261312
install manifest.
262313
314+
``setup()`` may return *all* files created by the shared
315+
scaffolding (including shared project files in ``.specify/``).
316+
Only files under the agent's own directory tree are recorded as
317+
``agent_files`` — shared project infrastructure is not tracked
318+
per-agent and will not be removed during teardown.
319+
263320
Args:
264321
agent_files: Files reported by :meth:`setup`.
265322
extension_files: Files created by extension registration.
266323
"""
267-
all_agent = list(agent_files or [])
268324
all_extension = list(extension_files or [])
269325

270-
# Also scan the commands directory for files created by the
271-
# init pipeline that setup() did not report directly.
326+
# Filter agent_files: only keep files under the agent's directory
327+
# tree. setup() may return shared project files (e.g. .specify/)
328+
# which must not be tracked per-agent.
329+
agent_root = self.agent_dir(project_path)
330+
agent_root_resolved = agent_root.resolve()
331+
all_agent: List[Path] = []
332+
for p in (agent_files or []):
333+
try:
334+
p.resolve().relative_to(agent_root_resolved)
335+
all_agent.append(p)
336+
except ValueError:
337+
pass # shared file — not tracked per-agent
338+
339+
# Scan the agent's directory tree for files created by the init
340+
# pipeline that setup() did not report directly. We scan the
341+
# entire agent directory (the parent of commands_dir) because
342+
# skills-migrated agents replace the commands directory with a
343+
# sibling skills directory during init.
272344
if self.manifest.commands_dir:
273345
commands_dir = project_path / self.manifest.commands_dir
274-
if commands_dir.is_dir():
275-
agent_set = {p.resolve() for p in all_agent}
276-
for p in commands_dir.rglob("*"):
277-
if p.is_file() and p.resolve() not in agent_set:
278-
all_agent.append(p)
346+
# Scan the agent root (e.g. .claude/) so we catch both
347+
# commands and skills directories.
348+
agent_root = commands_dir.parent
349+
agent_set = {p.resolve() for p in all_agent}
350+
for scan_dir in (commands_dir, agent_root):
351+
if scan_dir.is_dir():
352+
for p in scan_dir.rglob("*"):
353+
if p.is_file() and p.resolve() not in agent_set:
354+
all_agent.append(p)
355+
agent_set.add(p.resolve())
279356

280357
record_installed_files(
281358
project_path,

src/specify_cli/core_pack/agents/agy/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install Antigravity agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove Antigravity agent files from the project.

src/specify_cli/core_pack/agents/amp/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install Amp agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove Amp agent files from the project.

src/specify_cli/core_pack/agents/auggie/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install Auggie CLI agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove Auggie CLI agent files from the project.

src/specify_cli/core_pack/agents/bob/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install IBM Bob agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove IBM Bob agent files from the project.

src/specify_cli/core_pack/agents/claude/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install Claude Code agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove Claude Code agent files from the project.

src/specify_cli/core_pack/agents/codebuddy/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install CodeBuddy agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove CodeBuddy agent files from the project.

src/specify_cli/core_pack/agents/codex/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install Codex CLI agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove Codex CLI agent files from the project.

src/specify_cli/core_pack/agents/copilot/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -
1616
"""Install GitHub Copilot agent files into the project."""
1717
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
1818
commands_dir.mkdir(parents=True, exist_ok=True)
19-
return [] # directories only — actual files are created by the init pipeline
19+
return self._scaffold_project(project_path, script_type)
2020

2121
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
2222
"""Remove GitHub Copilot agent files from the project.

0 commit comments

Comments
 (0)