#!/usr/bin/env zsh # Standalone plan-build launcher. This file is intentionally sourceable so its # command seams can be replaced by the test suite. _plan_build_usage() { print -r -- "Usage: plan_build [--yolo] [--prompt] plan_build --architect [--new] [--yolo]" } _plan_build_require_cli() { local binary="$1" local label="$2" local resolved resolved="$(command -v "$binary" 2>/dev/null)" || resolved="" if [[ -n "$resolved" && -x "$resolved" ]]; then return 0 fi print -u2 -r -- "Error: ${label} ('${binary}') is not installed or not in your PATH." return 1 } _plan_build_confirm_indexing() { local project_root="$1" local reply if ! (: /dev/null; then print -u2 -r -- "Notice: No interactive terminal available; enhancing without project indexing." return 1 fi printf "Allow Auggie to index and use project context from '%s'? (y/N) " "$project_root" >/dev/tty if ! IFS= read -r reply | "$output_file" } _plan_build_platform() { command uname -s } _plan_build_start_auggie_script() { local platform="$1" log_file="$2" workspace="$3" cache_dir="$4" local auth_file="$5" prompt_file="$6" use_project_context="$7" shift 7 if [[ "$platform" == Darwin ]]; then command script -q -t 0 "$log_file" auggie "$@" /dev/null 2>&1 & else AUGGIE_WORKSPACE="$workspace" AUGGIE_CACHE_DIR="$cache_dir" \ AUGGIE_AUTH_FILE="$auth_file" AUGGIE_PROMPT_FILE="$prompt_file" \ AUGGIE_INDEXING="$use_project_context" \ command script -q -f -e -O "$log_file" -c ' if [ "$AUGGIE_INDEXING" = 1 ]; then exec auggie --print --enhance-prompt --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --allow-indexing --wait-for-indexing --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE" else exec auggie --print --enhance-prompt --no-discover-workspaces --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE" fi ' /dev/null 2>&1 & fi REPLY=$! } _plan_build_enhance_prompt() { emulate -L zsh setopt localtraps local prompt="$1" local output_file="$2" local run_dir prompt_file workspace cache_dir auth_file log_file parsed_file local project_root use_project_context timeout_seconds script_pid= waited child_status=0 local -a auggie_args trap 'return 130' INT trap 'return 143' TERM trap 'return 129' HUP if [[ -z "$prompt" ]]; then print -u2 -r -- "Error: Prompt was empty." return 1 fi if [[ ! -s "$HOME/.augment/session.json" ]]; then print -u2 -r -- "Error: Auggie session file not found. Run 'auggie login' first." return 1 fi run_dir="$(mktemp -d "${TMPDIR:-/tmp}/plan-build-auggie.XXXXXX")" || return 1 trap "rm -rf -- ${(q)run_dir}" EXIT prompt_file="$run_dir/prompt.txt" workspace="$run_dir/workspace" cache_dir="$run_dir/cache" auth_file="$HOME/.augment/session.json" log_file="$run_dir/auggie.log" parsed_file="$run_dir/enhanced.txt" mkdir -p "$workspace" "$cache_dir" || { return 1 } print -r -- "$prompt" >| "$prompt_file" || return 1 project_root="$(command git rev-parse --show-toplevel 2>/dev/null)" || project_root="$PWD" use_project_context=0 timeout_seconds=90 if _plan_build_confirm_indexing "$project_root"; then use_project_context=1 timeout_seconds=300 workspace="$project_root" cache_dir="$HOME/.augment" print -u2 -r -- "Indexing approved; enhancing with project context from '${project_root}'." else print -u2 -r -- "Enhancing without project indexing." fi auggie_args=(--print --enhance-prompt --workspace-root "$workspace" --augment-cache-dir "$cache_dir" --augment-session-json "$auth_file" --dont-save-session --instruction-file "$prompt_file") if (( use_project_context )); then auggie_args+=(--allow-indexing --wait-for-indexing) else auggie_args+=(--no-discover-workspaces) fi _plan_build_start_auggie_script "$(_plan_build_platform)" "$log_file" \ "$workspace" "$cache_dir" "$auth_file" "$prompt_file" \ "$use_project_context" "${auggie_args[@]}" || return 1 script_pid="$REPLY" trap "kill ${(q)script_pid} >/dev/null 2>&1 wait ${(q)script_pid} >/dev/null 2>&1 rm -rf -- ${(q)run_dir}" EXIT waited=0 while kill -0 "$script_pid" >/dev/null 2>&1; do if command grep -aq "🤖" "$log_file" 2>/dev/null || command grep -aq "Tool call:" "$log_file" 2>/dev/null; then kill "$script_pid" >/dev/null 2>&1 || true wait "$script_pid" >/dev/null 2>&1 || true script_pid= trap "rm -rf -- ${(q)run_dir}" EXIT child_status=0 break fi if (( waited >= timeout_seconds )); then kill "$script_pid" >/dev/null 2>&1 || true wait "$script_pid" >/dev/null 2>&1 || true script_pid= trap "rm -rf -- ${(q)run_dir}" EXIT print -u2 -r -- "Error: Timed out waiting for Auggie to enhance the prompt." return 124 fi sleep 1 waited=$((waited + 1)) done if [[ -n "$script_pid" ]]; then wait "$script_pid" child_status=$? script_pid= trap "rm -rf -- ${(q)run_dir}" EXIT fi if (( child_status != 0 )); then print -u2 -r -- "Error: Auggie process failed with status ${child_status}." return "$child_status" fi _plan_build_parse_auggie_output "$log_file" "$parsed_file" if [[ ! -s "$parsed_file" ]]; then print -u2 -r -- "Error: Auggie did not return an enhanced prompt." return 1 fi command cp "$parsed_file" "$output_file" || { print -u2 -r -- "Error: Unable to save the enhanced prompt." return 1 } } _plan_build_confirm_enhanced_prompt() { local reply if ! (: /dev/null; then print -r -- "❌ Error: Cannot review the enhanced prompt without an interactive terminal; Claude was not launched." return 2 fi printf "Proceed with this enhanced prompt? (y/N) " >/dev/tty if ! IFS= read -r reply /dev/tty print -r -- "🛑 Review cancelled; Claude was not launched." return 2 fi [[ "$reply" == (y|Y|yes|YES|Yes) ]] } _plan_build_claude_plugin_state() { local plugin_id="$1" awk -v plugin_id="$plugin_id" ' $0 ~ plugin_id { found = 1 next } found && /Status:/ { if ($0 ~ /enabled/) print "enabled" else print "disabled" printed = 1 exit } found && /❯/ { print "installed" printed = 1 exit } END { if (!printed) print found ? "installed" : "missing" } ' } _plan_build_safe_mode_preflight() { case "${CLAUDE_CODE_SAFE_MODE:-}" in 1|true|TRUE|yes|YES|on|ON) print -r -- "Error: Claude Code safe mode disables the Matt Pocock and AgentMemory plugins." print -r -- "Unset CLAUDE_CODE_SAFE_MODE before using plan_build." return 1 ;; esac } _plan_build_matt_preflight() { local plugin_json plugin_state if ! plugin_json="$(command claude plugin list 2>/dev/null)"; then print -r -- "Error: Unable to inspect Claude Code plugins." print -r -- "Run 'claude plugin list' to diagnose the problem." return 1 fi plugin_state="$(print -r -- "$plugin_json" | _plan_build_claude_plugin_state \ "mattpocock-skills@mattpocock")" case "$plugin_state" in enabled) return 0 ;; disabled) print -r -- "Error: Matt Pocock skills are installed in Claude Code but disabled." print -r -- "Enable the mattpocock-skills@mattpocock plugin, then retry." ;; missing) print -r -- "Error: Matt Pocock skills are required for the default workflow." print -r -- "Install the mattpocock-skills@mattpocock Claude Code plugin, then retry." ;; *) print -r -- "Error: Unable to determine Matt Pocock plugin status." print -r -- "Run 'claude plugin list' to diagnose the problem." ;; esac return 1 } _plan_build_agentmemory_health() { local pid exit_status waited=0 command -v agentmemory >/dev/null 2>&1 || return 2 command agentmemory status >/dev/null 2>&1 & pid=$! while kill -0 "$pid" >/dev/null 2>&1; do if (( waited >= 10 )); then kill "$pid" >/dev/null 2>&1 || true wait "$pid" >/dev/null 2>&1 || true return 124 fi sleep 1 waited=$((waited + 1)) done wait "$pid" exit_status=$? return "$exit_status" } _plan_build_claude_agentmemory_state() { local plugin_json plugin_json="$(command claude plugin list 2>/dev/null)" || return 1 print -r -- "$plugin_json" | _plan_build_claude_plugin_state \ "agentmemory@agentmemory" } _plan_build_codex_agentmemory_state() { local plugin_list plugin_list="$(command codex plugin list 2>/dev/null)" || return 1 print -r -- "$plugin_list" | _plan_build_codex_plugin_state } _plan_build_codex_plugin_state() { awk ' $1 == "agentmemory@agentmemory" { if ($0 ~ /installed, enabled/) print "enabled" else print "disabled" found = 1 exit } END { if (!found) print "missing" } ' } _plan_build_agentmemory_check() { local claude_state codex_state failed=0 health_status _plan_build_agentmemory_health health_status=$? case "$health_status" in 0) ;; 2) print -r -- "AgentMemory CLI is not installed or not in PATH." print -r -- "Install @agentmemory/agentmemory, then retry." failed=1 ;; 124) print -r -- "AgentMemory status check timed out after 10 seconds." failed=1 ;; *) print -r -- "AgentMemory server is unavailable." failed=1 ;; esac claude_state="$(_plan_build_claude_agentmemory_state)" || claude_state="unknown" if [[ "$claude_state" != enabled ]]; then print -r -- "Claude Code AgentMemory plugin state: $claude_state." failed=1 fi codex_state="$(_plan_build_codex_agentmemory_state)" || codex_state="unknown" if [[ "$codex_state" != enabled ]]; then print -r -- "Codex AgentMemory plugin state: $codex_state." failed=1 fi (( failed == 0 )) } _plan_build_confirm_memory_action() { local reply if ! _plan_build_has_tty; then print -r -- "Error: AgentMemory preflight failed without an interactive terminal." return 1 fi printf "AgentMemory preflight failed. [r]etry, [c]ontinue without memory, or [s]top? " >/dev/tty if ! IFS= read -r reply /dev/null } _plan_build_project_id_is_valid() { [[ "$1" =~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{1,127}$' ]] } _plan_build_default_project_id() { local root="$1" remote is_url=0 remote="$(command git -C "$root" remote get-url origin 2>/dev/null)" || return 1 [[ "$remote" == *://* ]] && is_url=1 remote="${remote#*://}" remote="${remote#*@}" if (( ! is_url )) && [[ "$remote" == *:* ]]; then remote="${remote%%:*}/${remote#*:}" fi remote="${remote%.git}" [[ -n "$remote" ]] || return 1 print -r -- "$remote" } _plan_build_initialize_project_id() { local root="$1" candidate reply if ! _plan_build_has_tty; then print -r -- "Error: Missing $root/.agentmemory-project and no interactive terminal is available." return 1 fi candidate="$(_plan_build_default_project_id "$root")" || candidate="" if [[ -n "$candidate" ]] && ! _plan_build_project_id_is_valid "$candidate"; then candidate="" fi if [[ -n "$candidate" ]]; then printf "Create .agentmemory-project with ID '%s'? (Y/n) " "$candidate" >/dev/tty if ! IFS= read -r reply /dev/tty if ! IFS= read -r candidate | "$root/.agentmemory-project" || return 1 print -r -- "Created $root/.agentmemory-project. Commit it, then rerun plan_build." return 1 } _plan_build_resolve_project_id() { local root project_file project_id committed_id root="$(_plan_build_project_root)" || return 1 project_file="$root/.agentmemory-project" if [[ ! -e "$project_file" ]]; then _plan_build_initialize_project_id "$root" return $? fi if [[ ! -f "$project_file" || ! -r "$project_file" ]]; then print -r -- "Error: AgentMemory project ID file is not a readable regular file: $project_file" return 1 fi project_id="$(<"$project_file")" if ! _plan_build_project_id_is_valid "$project_id"; then print -r -- "Error: Invalid AgentMemory project ID in $project_file" return 1 fi committed_id="$(command git -C "$root" show HEAD:.agentmemory-project 2>/dev/null)" || { print -r -- "Error: .agentmemory-project must be committed before plan_build can use it." return 1 } if [[ "$project_id" != "$committed_id" ]]; then print -r -- "Error: Working-tree .agentmemory-project differs from the committed identity." return 1 fi REPLY="$project_id" } _plan_build_is_worktree() { local inside_work_tree inside_work_tree="$(command git rev-parse --is-inside-work-tree 2>/dev/null)" || return 1 [[ "$inside_work_tree" == true ]] } _plan_build_has_tty() { (: /dev/null } _plan_build_common_preflight() { local skill_path="$1" require_matt="$2" if ! _plan_build_has_tty; then print -r -- "Error: plan_build requires an interactive terminal." return 1 fi _plan_build_require_cli claude "Claude Code CLI" || return 1 _plan_build_require_cli codex "Codex CLI" || return 1 _plan_build_require_cli coderabbit "CodeRabbit CLI" || return 1 _plan_build_require_cli git "Git CLI" || return 1 if ! _plan_build_is_worktree; then print -r -- "Error: plan_build must run inside a Git worktree." print -r -- "Change to a checked-out Git worktree, then retry." return 1 fi if [[ ! -r "$skill_path" || ! -s "$skill_path" ]]; then print -r -- "Error: Workflow skill is missing, unreadable, or empty: $skill_path" return 1 fi _plan_build_safe_mode_preflight || return 1 if (( require_matt )); then _plan_build_matt_preflight || return 1 fi } _plan_build_launch_claude() { local use_yolo="$1" project_id="$2" memory_mode="$3" local inject_context="true" shift 3 [[ "$memory_mode" == degraded ]] && inject_context="false" if (( use_yolo )); then AGENTMEMORY_PROJECT_NAME="$project_id" AGENTMEMORY_INJECT_CONTEXT="$inject_context" \ PLAN_BUILD_MEMORY_MODE="$memory_mode" \ command claude --dangerously-skip-permissions "$@" else AGENTMEMORY_PROJECT_NAME="$project_id" AGENTMEMORY_INJECT_CONTEXT="$inject_context" \ PLAN_BUILD_MEMORY_MODE="$memory_mode" command claude "$@" fi } _plan_build_read_payload() { local line payload="" while IFS= read -r line; do [[ "$line" == EOF ]] && break if [[ -z "$payload" ]]; then payload="$line" else payload+=$'\n'"$line" fi done print -r -- "$payload" } plan_build() { emulate -L zsh setopt localtraps local use_yolo=0 use_architect=0 use_new=0 enhance_payload=0 local payload enhanced_file= confirm_status enhance_status architect_start_mode local project_id memory_mode local standard_skill="${PLAN_BUILD_SKILL_PATH:-$HOME/.claude/skills/plan-build/SKILL.md}" local architect_skill="${PLAN_BUILD_ARCHITECT_SKILL_PATH:-$HOME/.claude/skills/plan-build-architect/SKILL.md}" trap 'return 130' INT trap 'return 143' TERM trap 'return 129' HUP while (( $# )); do case "$1" in --yolo) (( use_yolo )) && { print -r -- "Error: Duplicate argument: --yolo" _plan_build_usage return 1 } use_yolo=1 ;; --architect) (( use_architect )) && { print -r -- "Error: Duplicate argument: --architect" _plan_build_usage return 1 } use_architect=1 ;; --new) (( use_new )) && { print -r -- "Error: Duplicate argument: --new" _plan_build_usage return 1 } use_new=1 ;; --prompt) (( enhance_payload )) && { print -r -- "Error: Duplicate argument: --prompt" _plan_build_usage return 1 } enhance_payload=1 ;; *) print -r -- "Error: Unknown argument: $1" _plan_build_usage return 1 ;; esac shift done if (( use_new && ! use_architect )); then print -r -- "Error: --new requires --architect." _plan_build_usage return 1 fi if (( use_architect && enhance_payload )); then print -r -- "Error: --architect may combine only with --new and --yolo." _plan_build_usage return 1 fi if (( use_architect )); then _plan_build_common_preflight "$architect_skill" 0 || return 1 else _plan_build_common_preflight "$standard_skill" 1 || return 1 if (( enhance_payload )); then _plan_build_require_cli auggie "Auggie CLI" || return 1 _plan_build_require_cli script "script utility" || return 1 _plan_build_require_cli perl "Perl" || return 1 fi fi _plan_build_resolve_project_id || return 1 project_id="$REPLY" _plan_build_agentmemory_preflight || return 1 memory_mode="$REPLY" print -r -- "📥 Reading payload... Type 'EOF' on a new line and press Enter when finished." payload="$(_plan_build_read_payload)" if [[ -z "$payload" ]]; then print -r -- "❌ Error: Payload was empty." return 1 fi if (( enhance_payload )); then enhanced_file="$(mktemp "${TMPDIR:-/tmp}/plan-build-enhanced.XXXXXX")" || return 1 trap "rm -f -- ${(q)enhanced_file}" EXIT print -r -- "✨ Enhancing payload with Auggie..." _plan_build_enhance_prompt "$payload" "$enhanced_file" enhance_status=$? if (( enhance_status != 0 )); then print -r -- "❌ Error: Prompt enhancement failed; Claude was not launched." return "$enhance_status" fi payload="$(<"$enhanced_file")" rm -f -- "$enhanced_file" enhanced_file= trap - EXIT if [[ -z "$payload" ]]; then print -r -- "❌ Error: Enhanced payload was empty; Claude was not launched." return 1 fi printf '\n%s\n%s\n%s\n\n' \ "━━━━━━━━━━━━━━━━ Auggie enhanced prompt ━━━━━━━━━━━━━━━━" \ "$payload" \ "━━━━━━━━━━━━━━━━ End enhanced prompt ━━━━━━━━━━━━━━━━━" _plan_build_confirm_enhanced_prompt confirm_status=$? if (( confirm_status == 2 )); then return 1 elif (( confirm_status != 0 )); then print -r -- "🛑 Enhanced prompt not approved; Claude was not launched." return 0 fi print -r -- "✅ Enhanced prompt approved." fi if (( use_architect )); then if (( use_new )); then architect_start_mode="Start mode: archive-and-start-new. Explicitly archive the prior planning state as the skill directs, then begin a new architecture plan." else architect_start_mode="Start mode: safe-resume-detection. Safely detect whether an existing architecture planning session should be resumed; do not archive or replace it automatically." fi print -r -- "🚀 Launching Claude Code in architect mode..." _plan_build_launch_claude "$use_yolo" "$project_id" "$memory_mode" "Read \`$architect_skill\` and follow it strictly, including every gate even when permissive Claude execution is active. Stay in the documentation-first architect/orchestrator role: produce and coordinate documentation and planning only, and do not implement the requirement. AgentMemory project: $project_id Memory mode: $memory_mode. Run the skill's mandatory recall gate before workflow detection or repository modification. Pass this same project identity to every Codex process. $architect_start_mode Use the following payload as the initial requirement: $payload" return $? fi print -r -- "🚀 Launching Claude Code with the memory-aware Matt workflow..." _plan_build_launch_claude "$use_yolo" "$project_id" "$memory_mode" "Read \`$standard_skill\` and strictly follow its memory-aware Matt Pocock workflow. AgentMemory project: $project_id Memory mode: $memory_mode. Run the mandatory recall gate before routing or modifying the repository. Pass this same project identity to every Codex process. $payload" } _plan_build_main() { plan_build "$@" } if [[ "${ZSH_EVAL_CONTEXT:-}" == toplevel ]]; then _plan_build_main "$@" fi