#676 aipass: init update now refreshes AGENTS.md + prunes stale managed cruft. GAP1 — update_project synced AGENTS.md from a .aipass/project_AGENTS.md template that never existed, so AGENTS.md was a SILENT no-op on every 'aipass init update' (only CLAUDE.md, whose template exists, synced). Added the template + reconciled create/update to one source. GAP2 — update was additive-only; added a whitelist-scoped cleanup pass (_STALE_MANAGED_FILES, currently the retired aipass_global_prompt.md) that removes only positively-identified managed artifacts, logs each removal, never touches user files (registry/README/.gitignore/src/.trinity). SHIP-GAP caught in verify: .aipass/project_AGENTS.md was gitignored by the .aipass/.gitignore allowlist — added !project_AGENTS.md negation or the template would never distribute (the fix would be inert on real installs). Built by @aipass, verified by devpulse: 83 bootstrap tests green (+7), seedgo 31/31, live repro proves update emits AGENTS.md (name-interpolated) AND removes a planted cruft file.
This commit is contained in:
@@ -6,5 +6,6 @@
|
||||
!README.md
|
||||
!PROMPT_STYLE.md
|
||||
!project_CLAUDE.md
|
||||
!project_AGENTS.md
|
||||
!project_hooks.json
|
||||
#Do not add other exceptions here without careful consideration. Developer permissions0ns needed.
|
||||
@@ -0,0 +1,15 @@
|
||||
# {name}
|
||||
|
||||
Agent workspace powered by AIPass.
|
||||
|
||||
# Startup protocol
|
||||
|
||||
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||
|
||||
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`
|
||||
|
||||
Use drone commands for all operations. Never raw git, gh, or file access when drone provides it.
|
||||
|
||||
# Memories
|
||||
|
||||
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
||||
@@ -63,6 +63,20 @@ PyPI version — not the changelog header.
|
||||
split under the 600-line limit (`pid_cache` extracted). +25 tests.
|
||||
(built by @prax, verified by devpulse)
|
||||
|
||||
- **`aipass init update` now refreshes `AGENTS.md` and prunes stale managed
|
||||
cruft (issue #676).** Two gaps: (1) `update_project` synced `AGENTS.md` from a
|
||||
`.aipass/project_AGENTS.md` template that never existed, so the branch silently
|
||||
no-op'd and `AGENTS.md` was never refreshed on update (only `CLAUDE.md`, whose
|
||||
template exists, synced) — added the template and reconciled create/update to
|
||||
one source; (2) the update was additive-only — added a whitelist-scoped cleanup
|
||||
pass (`_STALE_MANAGED_FILES`, currently the retired `aipass_global_prompt.md`)
|
||||
that removes only positively-identified managed artifacts, logs every removal,
|
||||
and never touches user-owned files. The template also had to be un-ignored in
|
||||
`.aipass/.gitignore` (allowlist) or it would never have shipped — caught in
|
||||
verify. +7 tests; live repro confirms update emits `AGENTS.md` and clears a
|
||||
planted cruft file. (built by @aipass, verified by devpulse — incl. the
|
||||
gitignore ship-gap)
|
||||
|
||||
---
|
||||
|
||||
## [2026-07-09]
|
||||
|
||||
@@ -43,6 +43,10 @@ from aipass.aipass.apps.handlers.init import scaffold_content as sc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_STALE_MANAGED_FILES: list[Path] = [
|
||||
Path(".aipass") / "aipass_global_prompt.md",
|
||||
]
|
||||
|
||||
|
||||
def _sanitize_name(raw: str) -> str:
|
||||
"""Sanitize a project name for use in filenames.
|
||||
@@ -474,6 +478,7 @@ def update_project(target: Path) -> dict:
|
||||
updated: list[str] = []
|
||||
already_current: list[str] = []
|
||||
skipped: list[str] = []
|
||||
removed: list[str] = []
|
||||
aipass_home: str | None = None
|
||||
|
||||
# Managed directories — create if missing (graceful recovery).
|
||||
@@ -595,11 +600,20 @@ def update_project(target: Path) -> dict:
|
||||
venv_link.symlink_to(aipass_venv)
|
||||
updated.append(f".venv (symlink to AIPass runtime: {aipass_venv})")
|
||||
|
||||
# --- Cruft cleanup: remove known-stale AIPass-managed artifacts ---
|
||||
for rel in _STALE_MANAGED_FILES:
|
||||
stale_path = target / rel
|
||||
if stale_path.is_file():
|
||||
stale_path.unlink()
|
||||
removed.append(str(stale_path))
|
||||
logger.info("Removed stale managed file: %s", stale_path)
|
||||
|
||||
return {
|
||||
"project_name": name,
|
||||
"target": str(target),
|
||||
"updated_files": updated,
|
||||
"already_current": already_current,
|
||||
"skipped_files": skipped,
|
||||
"removed_files": removed,
|
||||
"aipass_home": aipass_home,
|
||||
}
|
||||
|
||||
@@ -440,6 +440,7 @@ def test_update_project_return_dict_structure(tmp_path):
|
||||
"updated_files",
|
||||
"already_current",
|
||||
"skipped_files",
|
||||
"removed_files",
|
||||
"aipass_home",
|
||||
}
|
||||
assert result["project_name"] == "UPD"
|
||||
@@ -1097,3 +1098,113 @@ def test_with_source_header_is_first_line():
|
||||
lines = result.split("\n")
|
||||
assert lines[0] == "<!-- Source: /test.md -->"
|
||||
assert lines[1] == "content"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GAP 1: AGENTS.md sync on update (#676)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_update_project_syncs_agents_md(tmp_path):
|
||||
"""update restores AGENTS.md when its content has been altered."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="sync")
|
||||
|
||||
agents_md = target / "AGENTS.md"
|
||||
agents_md.write_text("# Corrupted\n", encoding="utf-8")
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert str(agents_md.resolve()) in result["updated_files"]
|
||||
restored = agents_md.read_text(encoding="utf-8")
|
||||
assert "# SYNC" in restored
|
||||
assert "Startup protocol" in restored
|
||||
|
||||
|
||||
def test_update_project_creates_missing_agents_md(tmp_path):
|
||||
"""update creates AGENTS.md if it was deleted from the project."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="miss")
|
||||
|
||||
agents_md = target / "AGENTS.md"
|
||||
agents_md.unlink()
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert agents_md.exists()
|
||||
assert str(agents_md.resolve()) in result["updated_files"]
|
||||
content = agents_md.read_text(encoding="utf-8")
|
||||
assert "# MISS" in content
|
||||
|
||||
|
||||
def test_update_project_agents_md_already_current(tmp_path):
|
||||
"""update reports AGENTS.md as already_current when unchanged."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="cur")
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert any("AGENTS.md" in f for f in result["already_current"])
|
||||
assert not any("AGENTS.md" in f for f in result["updated_files"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GAP 2: Cruft cleanup on update (#676)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_update_project_removes_stale_global_prompt(tmp_path):
|
||||
"""update removes retired .aipass/aipass_global_prompt.md."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="cruft")
|
||||
|
||||
stale = target / ".aipass" / "aipass_global_prompt.md"
|
||||
stale.write_text("# old\n", encoding="utf-8")
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert not stale.exists()
|
||||
assert str(stale) in result["removed_files"]
|
||||
|
||||
|
||||
def test_update_project_cleanup_does_not_touch_user_files(tmp_path):
|
||||
"""Cruft cleanup never removes user-owned files."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="safe")
|
||||
|
||||
readme = target / "README.md"
|
||||
registry = target / "SAFE_REGISTRY.json"
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert readme.exists()
|
||||
assert registry.exists()
|
||||
assert len(result["removed_files"]) == 0
|
||||
|
||||
|
||||
def test_update_project_removed_files_in_result(tmp_path):
|
||||
"""Return dict always contains the removed_files key."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="rkey")
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert "removed_files" in result
|
||||
assert isinstance(result["removed_files"], list)
|
||||
|
||||
|
||||
def test_update_project_cleanup_no_stale_is_noop(tmp_path):
|
||||
"""When no stale files exist, removed_files is empty."""
|
||||
target = tmp_path / "proj"
|
||||
target.mkdir()
|
||||
init_project(target, project_name="clean")
|
||||
|
||||
result = update_project(target)
|
||||
|
||||
assert result["removed_files"] == []
|
||||
|
||||
Reference in New Issue
Block a user