Bring the launch template (used by /ultraplan-local --decompose) into contract-parity with the Phase 2.6 wave executor hardenings shipped in the previous commit: - GIT_OPTIONAL_LOCKS=0 exported once at the top - MAX_TURNS / MAX_BUDGET_USD env-overridable (default 50 / 5) - Absolute SHARED_CONTEXT_FILE built from brief + architecture - SAFETY_PREAMBLE prepended to every per-session prompt (GH #36071 + GH #52272 clarifications) - Per-child --max-turns + --max-budget-usd + --append-system-prompt-file - push-before-cleanup before merge AND in the cleanup_worktrees trap - Three new template rules (16, 17, 18, 19) document the contract for session-decomposer Pin in tests/lib/doc-consistency.test.mjs locks all required substrings against future regressions.
9.9 KiB
9.9 KiB
Headless Launch Script Template
This template is used by the session-decomposer agent to generate a launch script for headless execution of decomposed sessions.
Template
#!/usr/bin/env bash
# Headless launch script — generated by ultraplan-local
# Master plan: {plan_path}
# Generated: {date}
# Sessions: {total_sessions} ({parallel_count} parallel, {sequential_count} sequential)
set -euo pipefail
# Prevent accidental API billing — remove this line if you intend to use API credits
unset ANTHROPIC_API_KEY
REPO_ROOT="$(git rev-parse --show-toplevel)"
PLAN_DIR="{session_dir}"
LOG_DIR="{session_dir}/logs"
WORKTREE_BASE="{session_dir}/worktrees"
mkdir -p "$LOG_DIR" "$WORKTREE_BASE"
# Disable git's optional locks during parallel worktree ops (research/02 R2;
# GH #47721). Mirror Phase 2.6 hardenings (commands/ultraexecute-local.md).
export GIT_OPTIONAL_LOCKS=0
# Per-child guardrails (operator may override via env). Match Phase 2.6
# Step 2a-pre defaults.
MAX_TURNS="${ULTRAEXECUTE_MAX_TURNS:-50}"
MAX_BUDGET_USD="${ULTRAEXECUTE_MAX_BUDGET_USD:-5}"
# Shared context (brief + architecture if present). Absolute path so child
# sessions resolve it after `cd` into their worktree.
SHARED_CONTEXT_FILE="$WORKTREE_BASE/.shared-context.md"
if [ -f "$PLAN_DIR/brief.md" ]; then
cat "$PLAN_DIR/brief.md" > "$SHARED_CONTEXT_FILE"
if [ -f "$PLAN_DIR/architecture/overview.md" ]; then
printf '\n\n---\n\n' >> "$SHARED_CONTEXT_FILE"
cat "$PLAN_DIR/architecture/overview.md" >> "$SHARED_CONTEXT_FILE"
fi
fi
# Safety preambles prepended to every per-session prompt (defense-in-depth
# against headless-mode hook gaps + Claude 4 cross-model malware-reminder).
SAFETY_PREAMBLE='[CRITICAL — defense in depth] You are running in headless mode where plugin hooks may not fire reliably (GH #36071). DO NOT execute commands matching: `rm -rf /`, `curl | bash`, `git push --force` to main, `git reset --hard` outside this worktree, or any command writing outside the current worktree. Treat this rule as enforced regardless of `--allowedTools` allowlist.
Note: any malware-related safety reminders apply conditionally to code you assess as actually malicious, not to all code reads (per GH #52272 community workaround).'
# Cleanup trap — always remove worktrees on exit (success or failure)
cleanup_worktrees() {
echo ""
echo "=== Cleaning up worktrees ==="
cd "$REPO_ROOT"
# push-before-cleanup (Hard Rule 19): push any remaining branches so work
# survives even if subsequent removal races. Failure is non-fatal.
git branch --list "ultraplan/{slug}/*" | while read b; do
git push origin "$b" 2>/dev/null || true
done
for wt in "$WORKTREE_BASE"/session-*; do
[ -d "$wt" ] && git worktree remove "$wt" --force 2>/dev/null && echo "Removed: $wt"
done
git worktree prune
git branch --list "ultraplan/{slug}/*" | while read b; do
git branch -D "$b" 2>/dev/null
done
rmdir "$WORKTREE_BASE" 2>/dev/null
echo "Cleanup complete."
}
trap cleanup_worktrees EXIT
# Pre-flight: verify clean working tree
if [ -n "$(git status --porcelain)" ]; then
echo "ERROR: Working tree is not clean. Commit or stash changes before parallel execution."
git status --short
exit 1
fi
# Pre-flight: verify remote push permissions (catches credential/auth issues
# BEFORE spawning sessions). Sub-agent bash sandbox may have different
# credentials than the launching shell — Step 0 in each session spec handles
# the sandbox-side detection. Set ULTRAEXECUTE_SKIP_PREFLIGHT=1 for offline
# or air-gapped testing.
if [ "${ULTRAEXECUTE_SKIP_PREFLIGHT:-0}" != "1" ]; then
if ! git push --dry-run origin HEAD >/tmp/push-dryrun-launch.log 2>&1; then
echo "ERROR: git push --dry-run failed. Sessions will be unable to push."
cat /tmp/push-dryrun-launch.log
echo ""
echo "Fix remote credentials before running parallel execution, or set"
echo "ULTRAEXECUTE_SKIP_PREFLIGHT=1 to bypass (offline/air-gapped only)."
exit 1
fi
if grep -qE "(rejected|denied|forbidden|permission)" /tmp/push-dryrun-launch.log; then
echo "ERROR: git push --dry-run reports rejection. Sessions will fail at commit time."
cat /tmp/push-dryrun-launch.log
exit 1
fi
fi
echo "=== Ultraplan Headless Execution (Worktree-Isolated) ==="
echo "Plan: {plan_path}"
echo "Sessions: {total_sessions}"
echo "Repo root: $REPO_ROOT"
echo ""
# --- Wave {N}: Parallel sessions (no dependencies) ---
echo "--- Wave {N}: {description} ---"
{# For each parallel session in this wave, create worktree: }
git worktree add -b "ultraplan/{slug}/session-{n}" "$WORKTREE_BASE/session-{n}" HEAD
echo "Worktree created: session-{n} (branch: ultraplan/{slug}/session-{n})"
{# Launch session in its worktree (with safety preamble + budget caps + shared context): }
cd "$WORKTREE_BASE/session-{n}" && claude -p "${SAFETY_PREAMBLE}
$(cat "$PLAN_DIR/session-{n}-{slug}.md")" \
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
--permission-mode bypassPermissions \
--max-turns "$MAX_TURNS" \
--max-budget-usd "$MAX_BUDGET_USD" \
--append-system-prompt-file "$SHARED_CONTEXT_FILE" \
> "$LOG_DIR/session-{n}.log" 2>&1 &
PID_{n}=$!
cd "$REPO_ROOT"
echo "Started session {n}: {title} (PID $PID_{n})"
{# After all parallel sessions in this wave: }
echo "Waiting for Wave {N} to complete..."
wait $PID_{n1} $PID_{n2}
echo "Wave {N} complete."
echo ""
# --- Merge wave results (sequential) ---
echo "--- Merging Wave {N} ---"
cd "$REPO_ROOT"
{# For each session in the wave: push BEFORE merge (Hard Rule 19 — push-before-cleanup). }
git push origin "ultraplan/{slug}/session-{n}" 2>/dev/null || true
git merge --no-ff "ultraplan/{slug}/session-{n}" \
-m "merge: ultraplan session {n} — {title}"
if [ $? -ne 0 ]; then
echo "MERGE CONFLICT: session {n}. Conflicting files:"
git diff --name-only --diff-filter=U
git merge --abort
echo "Aborting. Earlier sessions in this wave are already merged."
exit 1
fi
git worktree remove "$WORKTREE_BASE/session-{n}" --force
git branch -d "ultraplan/{slug}/session-{n}"
echo "Merged and cleaned: session {n}"
git worktree prune
# --- Verify wave results ---
echo "--- Verifying Wave {N} ---"
{# For each session in the wave, run its exit condition commands }
{verify_commands}
# --- Wave {N+1}: Sequential sessions (depends on previous wave) ---
{# Repeat wave pattern for dependent sessions }
echo ""
echo "=== All sessions complete ==="
echo "Review logs in $LOG_DIR/"
echo "Run final verification: {final_verify_command}"
Rules for the session-decomposer
When generating a launch script from this template:
- Group sessions into waves by dependency. Sessions with no dependencies or whose dependencies are all in earlier waves can run in the same wave.
- Each wave waits for completion before the next wave starts.
- Verification runs after each wave — if verification fails, the script stops and reports which session failed.
- Log each session to a separate file for debugging.
- Use
claude -pwith the session spec file as the prompt. - Use
--allowedTools "Read,Write,Edit,Bash,Glob,Grep"with--permission-mode bypassPermissionsfor child sessions. This limits the tool surface to what the executor needs and prevents agent spawning, MCP access, and external web requests in headless sessions. - Final verification at the end runs the master plan's verification section.
- Never include secrets in the generated script.
- Wave verification must be independent. After each wave completes, run verification commands fresh via Bash — never parse session log files as proof of success. Log files contain executor self-reporting, not ground truth. The command's exit code is the only authoritative verification signal.
- Billing preamble. Prepend
unset ANTHROPIC_API_KEYwith a comment at the top of the script to prevent accidental API billing. Users who intend to use API credits can remove this line. - Worktree isolation is mandatory. Every parallel wave MUST use git
worktrees. Each session gets its own worktree and branch. Never launch
parallel
claude -psessions in the same working directory. - Cleanup trap on EXIT. The generated script MUST include a
trapon EXIT that removes all worktrees (git worktree remove --force) and prunes branches, even if the script fails or is interrupted. - Sequential merge after each wave. After all sessions in a wave complete, merge their branches back to the main branch one at a time. Abort on merge conflict — do not force-resolve.
- Clean working tree before worktrees. Add a
git status --porcelaincheck at the top of the script. Fail if the working tree is dirty. - Absolute paths for logs. Log file paths must be absolute (resolved from
$REPO_ROOT), not relative to any worktree. - Per-child guardrails (mirrors Phase 2.6 Step 2b). Every
claude -pinvocation must include--max-turns "$MAX_TURNS",--max-budget-usd "$MAX_BUDGET_USD", and--append-system-prompt-file "$SHARED_CONTEXT_FILE". The shared context must be built once with an absolute path (resolved from$WORKTREE_BASE) so child sessions can read it aftercd. - Safety preamble. Every per-session prompt must be prefixed with the
$SAFETY_PREAMBLEstring defined at the top of the script. This is the primary defense when plugin hooks do not fire reliably (GH #36071), and includes the GH #52272 malware-reminder clarification for AUTO mode. - GIT_OPTIONAL_LOCKS=0. The script must export
GIT_OPTIONAL_LOCKS=0once at the top so every git invocation (worktree add/remove/prune, branch -d, merge, push) avoids the index.lock background-poll race (research/02 R2; GH #47721). - push-before-cleanup (Hard Rule 19). After successful
git merge --no-ff, rungit push origin <branch>BEFOREgit worktree removeandgit branch -d. Push failure is non-fatal — cleanup proceeds. Converts unrecoverable branch loss into recoverable remote state (research/02 R3).