diff --git a/.gitignore b/.gitignore index e6fe104..a458968 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ gitea-mcp*.json .vscode/ graphify-out/ branches/ +# Throwaway agent payload/encoding helpers (#261): delete after the MCP +# commit completes or aborts; ignored here so strays cannot block issue locks. +_encode_*.py +_emit_*.py +_inline_*.py diff --git a/docs/llm-workflow-runbooks.md b/docs/llm-workflow-runbooks.md index 1b04cb3..765c55f 100644 --- a/docs/llm-workflow-runbooks.md +++ b/docs/llm-workflow-runbooks.md @@ -459,6 +459,33 @@ touching anything. files, detected secret, or any production/deploy behavior — **stop, report the blocker, and take no mutating action.** Fail closed; never work around a gate. +## Agent temp artifact cleanup (#261) + +Failed or aborted subagent runs have left throwaway helper scripts in the +shared working tree — for example `_encode_commit_payload.py` and +`_emit_payload.py` from the #152 commit-recovery attempt. These artifacts are +never part of any issue scope. They pollute `git status`, which trips +fail-closed gates: `gitea_lock_issue` refuses to lock over tracked edits, and +the pre-flight purity checks attribute the dirt to the current session. + +Rules: + +- **Delete throwaway helpers as soon as the MCP commit completes or aborts.** + Any file matching `_encode_*`, `_emit_*`, or `_inline_*` created as a + scratch payload/encoding helper must be removed in the same session that + created it — success and failure paths alike. +- **Prefer the scratchpad.** Payload preparation belongs in the session + scratchpad or a temp directory, never inside the repository worktree + (see the shell-free commit payload work in #263). +- **`.gitignore` backstop.** The patterns `_encode_*.py`, `_emit_*.py`, and + `_inline_*.py` are ignored so a leftover helper cannot show up as an + untracked file and block issue locks for later sessions. Ignoring them is a + safety net, not permission to leave them behind. +- **Cleanup checklist before ending any author session:** run + `git status --porcelain` in the worktree you mutated; remove any + `_encode_*` / `_emit_*` / `_inline_*` files you created; report the removal + in the final run report like any other local mutation. + ## Task/role alignment (#167) The **requested task** decides what a session may do — not the credential it diff --git a/tests/test_agent_temp_artifact_cleanup_docs.py b/tests/test_agent_temp_artifact_cleanup_docs.py new file mode 100644 index 0000000..d7d3a60 --- /dev/null +++ b/tests/test_agent_temp_artifact_cleanup_docs.py @@ -0,0 +1,76 @@ +"""Docs checks for agent temp-artifact cleanup guidance (#261). + +Failed subagent runs left throwaway helper scripts (e.g. +``_encode_commit_payload.py``, ``_emit_payload.py``) in the shared working +tree, polluting ``git status`` and tripping issue-lock / preflight gates. +These tests keep the cleanup runbook guidance present and the ignore +patterns in place so stray helpers can never dirty a worktree again. +""" +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +RUNBOOK = REPO_ROOT / "docs" / "llm-workflow-runbooks.md" +GITIGNORE = REPO_ROOT / ".gitignore" + +ARTIFACT_PATTERNS = ("_encode_*.py", "_emit_*.py", "_inline_*.py") + + +def _runbook_text(): + assert RUNBOOK.is_file(), "missing docs/llm-workflow-runbooks.md" + return RUNBOOK.read_text(encoding="utf-8") + + +def _gitignore_text(): + assert GITIGNORE.is_file(), "missing .gitignore" + return GITIGNORE.read_text(encoding="utf-8") + + +def test_runbook_has_agent_temp_artifact_cleanup_section(): + text = _runbook_text() + assert "## Agent temp artifact cleanup" in text, ( + "runbook lacks an 'Agent temp artifact cleanup' section") + assert "#261" in text, "cleanup guidance does not reference issue #261" + + +def test_runbook_names_known_stray_artifacts(): + text = _runbook_text() + for name in ("_encode_commit_payload.py", "_emit_payload.py"): + assert name in text, f"runbook does not name stray artifact {name!r}" + + +def test_runbook_covers_all_artifact_patterns(): + text = _runbook_text() + for pattern in ("_encode_*", "_emit_*", "_inline_*"): + assert pattern in text, f"runbook does not cover pattern {pattern!r}" + + +def test_runbook_requires_cleanup_after_commit_or_abort(): + text = _runbook_text().lower() + assert "delete" in text or "remove" in text + for phase in ("completes", "aborts"): + assert phase in text, ( + f"runbook does not require cleanup when the MCP commit {phase}") + + +def test_runbook_explains_lock_and_preflight_impact(): + text = _runbook_text().lower() + assert "gitea_lock_issue" in text + assert "preflight" in text or "pre-flight" in text + + +def test_gitignore_blocks_agent_helper_patterns(): + lines = [ + ln.strip() for ln in _gitignore_text().splitlines() + if ln.strip() and not ln.strip().startswith("#") + ] + for pattern in ARTIFACT_PATTERNS: + assert pattern in lines, f".gitignore lacks pattern {pattern!r}" + + +def test_no_stray_agent_helpers_in_repo_root(): + """Acceptance: artifacts stay removed from default worktrees.""" + stray = [ + p.name for pattern in ARTIFACT_PATTERNS + for p in REPO_ROOT.glob(pattern) + ] + assert not stray, f"stray agent helper artifacts present: {stray}"