--- name: playbook-background-agent-prompts description: "REQUIRED before writing any background agent prompt: full template, 5 discipline rules, mandatory wrap-up block, and anti-patterns; read this before drafting any agent prompt" metadata: node_type: memory type: project originSessionId: 075715ba-4454-464c-ba39-dfbcdefc6590 --- ## When to use background agents Use a background agent when: - The task is bounded (clear start, clear end, defined output) - It does not require real-time decisions from the user - It can run in parallel with other work without file/resource conflicts - It fits within 15 tool calls Do NOT use a background agent for: - Open-ended research or design tasks (those belong in main session) - Tasks that require human decisions mid-way - Tasks that share files or DB rows with another running agent --- ## The 5 Prompt Discipline Rules Every background agent prompt must satisfy all 5. A prompt missing any of these will loop, stall, or silently fail. ### Rule 1 — Hard turn limit Always include `--max-turns 15` in the prompt (or in the claude -p invocation). Without this, a looping agent runs indefinitely and consumes unbounded SDK credits. ### Rule 2 — Loop detection instruction Include this verbatim: > "If you issue the same tool call or command twice with identical arguments, STOP immediately and output the mandatory wrap-up with status=partially_succeeded." ### Rule 3 — Credential map inline Every credential the agent needs must be provided inline in the prompt — exact Vault path and field name. Never send an agent to search for auth at runtime. Use the infrastructure auth map (`secret/data/claude/infrastructure-auth-map` → yaml field) as the source. Format: ``` ## Credentials you will need - sudo-bridge (primary): Vault path secret/data/sudo-bridge → api_key - Gitea token: Vault path secret/data/gitea → admin_token - AppRole bootstrap: role-id at /opt/appdata/docker/docker-compose/vault/approle/role-id ``` Always include the AppRole bootstrap pattern when Vault access is needed: ```bash VAULT_IP=$(docker inspect vault-iwaulpoi5hwirdlogshmul40 --format '{{json .NetworkSettings.Networks}}' | python3 -c "import sys,json; nets=json.load(sys.stdin); print(list(nets.values())[0]['IPAddress'])") ROLE_ID=$(cat /opt/appdata/docker/docker-compose/vault/approle/role-id | tr -d '[:space:]') SECRET_ID=$(cat /opt/appdata/docker/docker-compose/vault/approle/secret-id | tr -d '[:space:]') VAULT_TOKEN=$(curl -sf -X POST -H "Content-Type: application/json" \ -d "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \ http://$VAULT_IP:8200/v1/auth/approle/login | python3 -c "import sys,json; print(json.load(sys.stdin)['auth']['client_token'])") # Always revoke after use: curl -sf -X POST -H "X-Vault-Token: $VAULT_TOKEN" http://$VAULT_IP:8200/v1/auth/token/revoke-self ``` ### Rule 4 — Defined output format Specify the exact output the agent must produce. Never say "report what you did" — always define the schema. Use the mandatory wrap-up block (Rule 5 below) as the minimum output requirement. Add task-specific fields as needed. ### Rule 5 — Mandatory wrap-up block (structural, not instructional) Add this block verbatim to every prompt, immediately before the loop detection instruction. This is structural — the agent cannot satisfy the prompt without emitting it. ``` ## MANDATORY WRAP-UP (required regardless of success or failure) Before stopping for ANY reason — task complete, error, or approaching turn limit — output this JSON as your final message. Do not stop without it. { "status": "succeeded|partially_succeeded|failed", "actions_taken": ["action 1 — outcome", "action 2 — outcome"], "actions_failed": ["action — reason it failed"], "notes": "anything relevant for the next session" } If you hit --max-turns before finishing, set status="partially_succeeded" and list what remains in actions_failed. ``` --- ## Prompt template ``` You are [one sentence role description]. [One sentence scope — what you will and will not touch.] ## Task [Clear, bounded description of what to accomplish.] ## Context / pre-fetched data [Include inline any data the agent needs: DB rows, config values, file contents. Do not make the agent fetch things that can be included here.] ## Credentials you will need [List exact Vault paths and field names for every secret needed. Include AppRole bootstrap if Vault access required.] ## Step-by-step [Numbered steps if the task has a defined sequence.] ## MANDATORY WRAP-UP (required regardless of success or failure) Before stopping for ANY reason — task complete, error, or approaching turn limit — output this JSON as your final message. Do not stop without it. { "status": "succeeded|partially_succeeded|failed", "actions_taken": ["action 1 — outcome", "action 2 — outcome"], "actions_failed": ["action — reason it failed"], "notes": "anything relevant for the next session" } If you hit --max-turns before finishing, set status="partially_succeeded" and list what remains in actions_failed. --max-turns 15 If you issue the same tool call or command twice with identical arguments, STOP immediately and output the mandatory wrap-up with status=partially_succeeded. ``` --- ## Parallel agent rules - **Max comfortable parallel:** 4-5 agents (primary server has 18GB available RAM, ~300MB per agent) - **Max safe parallel:** 8 agents - **Do not parallelize:** Tasks that write to the same file, the same DB row, or the same Gitea repo - **Do parallelize:** Tasks that touch independent files, services, or repos - Token cost is the same whether serial or parallel — parallelism saves wall-clock time only --- ## Anti-patterns | Anti-pattern | Why it fails | Fix | |---|---|---| | "Report what you did when done" | Instructional — skipped under context pressure | Use mandatory wrap-up JSON block | | Sending agent to find its own credentials | Agent loops searching for auth | Provide all Vault paths inline | | Open-ended task ("investigate and fix anything wrong") | No defined completion state | Bound the task: specific file, specific check, specific output | | No --max-turns | Looping agent runs forever | Always include --max-turns 15 | | Sharing a file between two parallel agents | Race condition, one overwrites the other | Assign non-overlapping files/resources | | Making design decisions in the agent | Agent guesses wrong, wasted run | Pre-decide everything in main session, send agent only to execute | | `curl -sf ... \| python3 -c` for API calls | `-sf` suppresses body on error; inline python escaping breaks | Write a /tmp Python script using urllib instead | | secrets-proxy /shell with target="server-01" | Only accepts target=sandbox or target=production — 400 error | Server-01 ops go through sudo-bridge-server01 at http://192.168.1.90:8082 directly | | Pre-fetching everything before writing prompts | Burns context; most things agents can discover in 1 tool call | Pre-fetch only what's blocking — include discovery steps inside the agent prompt instead | | Writing files on server-01 without allowlist entry | 403 — command not in allowlist | Add pattern to allowlist (tier 1 = auto, tier 2 = NTFY approval) first; use base64 python3 -c for file writes | | Downgrading tier-2 wildcard to tier-1 to bypass NTFY | First-registered wildcard wins — adding tier-1 `python3 -c *` after a tier-2 `python3 -c *` has no effect | Use `docker exec python3 -c` instead (different prefix, not matched by `python3 -c *`) | | Pre-hashing keys in PROXY_CALLERS env var | secrets-proxy app.py expects raw keys in PROXY_CALLERS and hashes internally — pre-hashing causes double-hash, all auth returns 403 Invalid API key | Store raw keys: `PROXY_CALLERS=json.dumps(callers)` not `json.dumps({n: sha256(k) for n,k in callers.items()})` | | Relying on NTFY approvals for unattended agents | NTFY notifications may be missed or time out, stalling the agent indefinitely | Design server-01 file writes to use `docker exec` pattern (tier-1) for unattended runs; reserve tier-2 for explicit interactive sessions | --- ## Evolution Update when: new anti-patterns discovered; AppRole bootstrap pattern changes; parallel agent limits change based on server hardware; new credential types need standard inline format.