diff --git a/plugins/ultraplan-local/templates/headless-launch-template.md b/plugins/ultraplan-local/templates/headless-launch-template.md index 7cc4479..0deaec0 100644 --- a/plugins/ultraplan-local/templates/headless-launch-template.md +++ b/plugins/ultraplan-local/templates/headless-launch-template.md @@ -23,11 +23,42 @@ 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 @@ -81,10 +112,15 @@ echo "--- Wave {N}: {description} ---" 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: } -cd "$WORKTREE_BASE/session-{n}" && claude -p "$(cat "$PLAN_DIR/session-{n}-{slug}.md")" \ +{# 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" @@ -99,7 +135,8 @@ echo "" # --- Merge wave results (sequential) --- echo "--- Merging Wave {N} ---" cd "$REPO_ROOT" -{# For each session in the wave, merge its branch: } +{# 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 @@ -166,3 +203,21 @@ When generating a launch script from this template: check at the top of the script. Fail if the working tree is dirty. 15. **Absolute paths for logs.** Log file paths must be absolute (resolved from `$REPO_ROOT`), not relative to any worktree. +16. **Per-child guardrails (mirrors Phase 2.6 Step 2b).** Every `claude -p` + invocation 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 after `cd`. +17. **Safety preamble.** Every per-session prompt must be prefixed with the + `$SAFETY_PREAMBLE` string 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. +18. **GIT_OPTIONAL_LOCKS=0.** The script must export `GIT_OPTIONAL_LOCKS=0` + once 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). +19. **push-before-cleanup (Hard Rule 19).** After successful `git merge --no-ff`, + run `git push origin ` BEFORE `git worktree remove` and + `git branch -d`. Push failure is non-fatal — cleanup proceeds. Converts + unrecoverable branch loss into recoverable remote state (research/02 R3). diff --git a/plugins/ultraplan-local/tests/lib/doc-consistency.test.mjs b/plugins/ultraplan-local/tests/lib/doc-consistency.test.mjs index bf8d56f..9051543 100644 --- a/plugins/ultraplan-local/tests/lib/doc-consistency.test.mjs +++ b/plugins/ultraplan-local/tests/lib/doc-consistency.test.mjs @@ -158,6 +158,26 @@ test('rule-catalogue has exactly 12 entries', async () => { ); }); +test('headless-launch-template.md mirrors Phase 2.6 hardenings', () => { + const tpl = read('templates/headless-launch-template.md'); + for (const needle of [ + 'GIT_OPTIONAL_LOCKS', + '--max-turns', + '--max-budget-usd', + '--append-system-prompt-file', + 'SHARED_CONTEXT_FILE', + 'SAFETY_PREAMBLE', + 'git push origin', + 'GH #36071', + 'push-before-cleanup', + ]) { + assert.ok( + tpl.includes(needle), + `templates/headless-launch-template.md should include "${needle}" (Step 10 mirrors Phase 2.6)`, + ); + } +}); + test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => { const cmd = read('commands/ultraplan-local.md'); const orch = read('agents/planning-orchestrator.md');