#!/bin/bash # Run archiving — preserves prd.json, progress.md, and contracts from completed runs. # # Two archive triggers: # 1. Branch change: check_archive detects a new branch and archives the staged snapshot. # 2. Completed run: archive_and_reset is called by the /run skill when prd.json shows # all stories passed (or the branch was deleted). This handles the common workflow # of merging a feature branch back to main and starting a new feature. # # Archive layout: # .loop/archive/ # runs.log — one-line-per-run index for quick lookup # 2026-03-15-auth-system/ — full artifacts from that run # prd.json, progress.md, contracts/ LAST_BRANCH_FILE="$LOOP_DIR/.last-branch" STAGING_DIR="$LOOP_DIR/.archive-staging" # Snapshot current artifacts so they can be archived later if the branch changes. # Call this at the END of a successful run or before exit. snapshot_for_archive() { rm -rf "$STAGING_DIR" mkdir -p "$STAGING_DIR" [ -f "$LOOP_DIR/prd.json" ] && cp "$LOOP_DIR/prd.json" "$STAGING_DIR/" [ -f "$LOOP_DIR/progress.md" ] && cp "$LOOP_DIR/progress.md" "$STAGING_DIR/" [ -d "$LOOP_DIR/contracts" ] && cp -r "$LOOP_DIR/contracts" "$STAGING_DIR/" } # Check if we need to archive and do so if branch changed. # Reads the NEW branch from live prd.json and the OLD branch from the staging # snapshot (which was saved at the end of the previous run). This avoids the # bug where both branches read from the same (already-overwritten) prd.json. check_archive() { local current_branch current_branch=$(prd_branch_name 2>/dev/null) [ -z "$current_branch" ] && return # Determine the previous branch from the staging snapshot (most reliable) # or fall back to .last-branch file local last_branch="" if [ -f "$STAGING_DIR/prd.json" ]; then if command -v jq &>/dev/null; then last_branch=$(jq -r '.branchName // empty' "$STAGING_DIR/prd.json" 2>/dev/null) else last_branch=$(LOOP_PRD="$STAGING_DIR/prd.json" python3 -c " import json, os print(json.load(open(os.environ['LOOP_PRD'])).get('branchName', ''), end='') " 2>/dev/null) fi fi [ -z "$last_branch" ] && [ -f "$LAST_BRANCH_FILE" ] && last_branch=$(cat "$LAST_BRANCH_FILE") if [ -n "$last_branch" ] && [ "$last_branch" != "$current_branch" ]; then archive_run "$last_branch" fi echo "$current_branch" > "$LAST_BRANCH_FILE" } # Archive the previous run's staged artifacts (NOT current prd.json) archive_run() { local branch_name="$1" local feature_name feature_name=$(echo "$branch_name" | sed 's|.*/||') local archive_dir="$LOOP_DIR/archive/$(date +%Y-%m-%d)-${feature_name}" mkdir -p "$archive_dir" if [ -d "$STAGING_DIR" ]; then # Use the staged snapshot (correct artifacts from the previous run) cp -r "$STAGING_DIR"/* "$archive_dir/" 2>/dev/null || true rm -rf "$STAGING_DIR" else # Fallback: no snapshot exists (first run or upgrade from old version). # Current artifacts may belong to the new feature — archive what we have # but warn the user. log "WARNING: No archive snapshot found. Archiving current artifacts (may be from new feature)." [ -f "$LOOP_DIR/prd.json" ] && cp "$LOOP_DIR/prd.json" "$archive_dir/" [ -f "$LOOP_DIR/progress.md" ] && cp "$LOOP_DIR/progress.md" "$archive_dir/" [ -d "$LOOP_DIR/contracts" ] && cp -r "$LOOP_DIR/contracts" "$archive_dir/" fi # Verify the archive received content before deleting originals if ! find "$archive_dir" -maxdepth 1 -type f | read -r; then log "WARNING: Archive directory $archive_dir is empty after copy — skipping deletion of originals to prevent data loss" return fi # Clean up old run's artifacts (progress.md, contracts — NOT prd.json which belongs to new feature) rm -f "$LOOP_DIR/progress.md" rm -rf "$LOOP_DIR/contracts" append_runs_log "$branch_name" "$archive_dir" log "Archived previous run to $archive_dir" } # Archive current run artifacts and reset for a new run. # Called by the /run skill when a completed run is detected (all stories passed # or the feature branch no longer exists). Unlike check_archive (which reads from # staging), this archives the LIVE artifacts directly since we know they belong # to the completed run. archive_and_reset() { local loop_dir="${1:-.loop}" local prd="$loop_dir/prd.json" [ -f "$prd" ] || return 0 # Read branch name from current prd.json local branch_name="" if command -v jq &>/dev/null; then branch_name=$(jq -r '.branchName // empty' "$prd" 2>/dev/null) elif command -v python3 &>/dev/null; then branch_name=$(LOOP_PRD="$prd" python3 -c " import json, os print(json.load(open(os.environ['LOOP_PRD'])).get('branchName', ''), end='') " 2>/dev/null) fi local feature_name feature_name=$(echo "${branch_name:-unknown}" | sed 's|.*/||') local archive_dir="$loop_dir/archive/$(date +%Y-%m-%d)-${feature_name}" mkdir -p "$archive_dir" # Archive live artifacts [ -f "$prd" ] && cp "$prd" "$archive_dir/" [ -f "$loop_dir/progress.md" ] && cp "$loop_dir/progress.md" "$archive_dir/" [ -f "$loop_dir/progress-archive.md" ] && cp "$loop_dir/progress-archive.md" "$archive_dir/" [ -d "$loop_dir/contracts" ] && cp -r "$loop_dir/contracts" "$archive_dir/" # Verify archive has content before deleting originals if ! find "$archive_dir" -maxdepth 1 -type f | read -r; then echo "[archive] WARNING: Archive directory is empty — skipping reset to prevent data loss" return 1 fi append_runs_log "$branch_name" "$archive_dir" # Reset run-specific files (keep config.json, init.sh, harness files) rm -f "$loop_dir/prd.json" rm -f "$loop_dir/progress.md" rm -f "$loop_dir/progress-archive.md" rm -rf "$loop_dir/contracts" rm -rf "$loop_dir/.archive-staging" rm -f "$loop_dir/.last-branch" rm -f "$loop_dir/.verdict" echo "[archive] Archived completed run to $archive_dir" echo "[archive] .loop/ reset — ready for new stories" } # Archive a completed run from a worktree back to the main project's .loop/archive/. # Called by the /run skill's completion handler after the loop finishes in a worktree. # # Usage: archive_from_worktree # worktree_loop_dir: absolute path to the worktree's .loop/ (source) # main_loop_dir: absolute path to the main project's .loop/ (destination) archive_from_worktree() { local wt_loop_dir="$1" local main_loop_dir="$2" local wt_prd="$wt_loop_dir/prd.json" [ -f "$wt_prd" ] || { echo "[archive] WARNING: No prd.json in worktree — nothing to archive"; return 1; } # Read branch name from worktree's prd.json local branch_name="" if command -v jq &>/dev/null; then branch_name=$(jq -r '.branchName // empty' "$wt_prd" 2>/dev/null) elif command -v python3 &>/dev/null; then branch_name=$(LOOP_PRD="$wt_prd" python3 -c " import json, os print(json.load(open(os.environ['LOOP_PRD'])).get('branchName', ''), end='') " 2>/dev/null) fi local feature_name feature_name=$(echo "${branch_name:-unknown}" | sed 's|.*/||') local archive_dir="$main_loop_dir/archive/$(date +%Y-%m-%d)-${feature_name}" mkdir -p "$archive_dir" # Copy artifacts from worktree [ -f "$wt_prd" ] && cp "$wt_prd" "$archive_dir/" [ -f "$wt_loop_dir/progress.md" ] && cp "$wt_loop_dir/progress.md" "$archive_dir/" [ -f "$wt_loop_dir/progress-archive.md" ] && cp "$wt_loop_dir/progress-archive.md" "$archive_dir/" [ -d "$wt_loop_dir/contracts" ] && cp -r "$wt_loop_dir/contracts" "$archive_dir/" [ -d "$wt_loop_dir/triage" ] && cp -r "$wt_loop_dir/triage" "$archive_dir/" # Verify archive has content if ! find "$archive_dir" -maxdepth 1 -type f | read -r; then echo "[archive] WARNING: Archive directory is empty — copy may have failed" return 1 fi append_runs_log "$branch_name" "$archive_dir" echo "[archive] Archived worktree run to $archive_dir" } # Append a one-line summary to the runs log. append_runs_log() { local branch_name="$1" local archive_dir="$2" local runs_log runs_log="$(dirname "$archive_dir")/runs.log" # Read story counts from the archived prd.json local total=0 passed=0 blocked=0 local archived_prd="$archive_dir/prd.json" if [ -f "$archived_prd" ]; then if command -v jq &>/dev/null; then total=$(jq '.userStories | length' "$archived_prd" 2>/dev/null || echo 0) passed=$(jq '[.userStories[] | select(.passes == true)] | length' "$archived_prd" 2>/dev/null || echo 0) blocked=$(jq '[.userStories[] | select(.blocked == true)] | length' "$archived_prd" 2>/dev/null || echo 0) elif command -v python3 &>/dev/null; then eval "$(LOOP_PRD="$archived_prd" python3 -c " import json, os d = json.load(open(os.environ['LOOP_PRD'])) s = d.get('userStories', []) print(f'total={len(s)} passed={sum(1 for x in s if x.get(\"passes\"))} blocked={sum(1 for x in s if x.get(\"blocked\"))}') " 2>/dev/null)" || true fi fi printf '%s %-30s %s/%s passed %s blocked\n' \ "$(date +%Y-%m-%d)" "${branch_name:-unknown}" "$passed" "$total" "$blocked" \ >> "$runs_log" }