mirror of
https://github.com/github/spec-kit.git
synced 2026-03-24 14:23:09 +00:00
fix: force flag passthrough, cross-platform hashes, manifest retention, docstring accuracy
- agent_switch: pass force=force (user's actual flag) instead of force=True so hash-check protection is preserved for unconfirmed files - _hash_file_list: use as_posix() for POSIX-stable manifest keys; guard relative_to with try/except to skip files outside project root - remove_tracked_files: updated docstring to accurately describe hash comparison behavior (values ARE used, not ignored); manifest is only deleted when all tracked files were removed (preserves tracking of skipped modified files)
This commit is contained in:
@@ -2685,7 +2685,7 @@ def agent_switch(
|
|||||||
console.print(f" [dim]Tearing down {current_agent}...[/dim]")
|
console.print(f" [dim]Tearing down {current_agent}...[/dim]")
|
||||||
current_bootstrap.teardown(
|
current_bootstrap.teardown(
|
||||||
project_path,
|
project_path,
|
||||||
force=True, # already confirmed above
|
force=force,
|
||||||
files=all_files,
|
files=all_files,
|
||||||
)
|
)
|
||||||
console.print(f" [green]✓[/green] {current_agent} removed")
|
console.print(f" [green]✓[/green] {current_agent} removed")
|
||||||
|
|||||||
@@ -469,13 +469,23 @@ def _hash_file_list(
|
|||||||
project_path: Path,
|
project_path: Path,
|
||||||
files: List[Path],
|
files: List[Path],
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
"""Build a {relative_path: sha256} dict from a list of file paths."""
|
"""Build a {relative_path: sha256} dict from a list of file paths.
|
||||||
|
|
||||||
|
Uses POSIX-style separators for stable cross-platform manifests.
|
||||||
|
Silently skips files that are not under *project_path*.
|
||||||
|
"""
|
||||||
entries: Dict[str, str] = {}
|
entries: Dict[str, str] = {}
|
||||||
|
project_root = project_path.resolve()
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
||||||
if abs_path.is_file():
|
if not abs_path.is_file():
|
||||||
rel = str(abs_path.relative_to(project_path))
|
continue
|
||||||
entries[rel] = _sha256(abs_path)
|
try:
|
||||||
|
rel = abs_path.resolve().relative_to(project_root)
|
||||||
|
except ValueError:
|
||||||
|
# File is outside the project root — skip it
|
||||||
|
continue
|
||||||
|
entries[rel.as_posix()] = _sha256(abs_path)
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@@ -607,9 +617,11 @@ def remove_tracked_files(
|
|||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Remove individual tracked files.
|
"""Remove individual tracked files.
|
||||||
|
|
||||||
If *files* is provided, exactly those files are removed (the values
|
If *files* is provided, those files are candidates for removal.
|
||||||
are ignored but accepted for forward compatibility). Otherwise the
|
Each file's current SHA-256 is compared against the recorded hash;
|
||||||
install manifest for *agent_id* is read.
|
files whose hash no longer matches (i.e. user-modified) are skipped
|
||||||
|
unless *force* is ``True``. When *files* is ``None``, the install
|
||||||
|
manifest for *agent_id* is read instead.
|
||||||
|
|
||||||
Raises :class:`AgentFileModifiedError` if any tracked file was
|
Raises :class:`AgentFileModifiedError` if any tracked file was
|
||||||
modified and *force* is ``False`` (only when reading from the
|
modified and *force* is ``False`` (only when reading from the
|
||||||
@@ -618,12 +630,16 @@ def remove_tracked_files(
|
|||||||
|
|
||||||
Directories are **never** deleted — only individual files.
|
Directories are **never** deleted — only individual files.
|
||||||
|
|
||||||
|
The install manifest is only deleted when every tracked file was
|
||||||
|
successfully removed. If some files were skipped (modified), the
|
||||||
|
manifest is preserved so they remain tracked.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_path: Project root directory.
|
project_path: Project root directory.
|
||||||
agent_id: Agent identifier.
|
agent_id: Agent identifier.
|
||||||
force: When ``True``, delete even modified files.
|
force: When ``True``, delete even modified files.
|
||||||
files: Explicit mapping of project-relative path → hash. When
|
files: Mapping of project-relative path → SHA-256 hash.
|
||||||
supplied, the install manifest is not consulted.
|
When supplied, the install manifest is not consulted.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of project-relative paths that were removed.
|
List of project-relative paths that were removed.
|
||||||
@@ -673,9 +689,13 @@ def remove_tracked_files(
|
|||||||
abs_path.unlink()
|
abs_path.unlink()
|
||||||
removed.append(rel_path)
|
removed.append(rel_path)
|
||||||
|
|
||||||
# Clean up the install manifest itself
|
# Clean up the install manifest only when all tracked files were
|
||||||
|
# removed. If some were skipped (modified), keep the manifest so
|
||||||
|
# those files remain tracked for future teardown attempts.
|
||||||
if manifest_file.is_file():
|
if manifest_file.is_file():
|
||||||
manifest_file.unlink(missing_ok=True)
|
remaining = len(entries) - len(removed)
|
||||||
|
if remaining == 0:
|
||||||
|
manifest_file.unlink(missing_ok=True)
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user