#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:
AIOSAI
2026-07-10 03:07:01 -07:00
parent 739dada015
commit 0878afbc81
5 changed files with 155 additions and 0 deletions
+1
View File
@@ -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.
+15
View File
@@ -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`.
+14
View File
@@ -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,
}
+111
View File
@@ -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"] == []