asciinema-streaming-backup
Complete system for streaming asciinema recordings to GitHub with automatic brotli archival. Uses idle-detection for intelligent chunking, zstd for concatenatable streaming compression, and GitHub Actions for final brotli recompression.
Platform: macOS, Linux Isolation: Uses Git orphan branch (separate history, cannot pollute main)
Architecture Overview ┌─────────────────┐ zstd chunks ┌─────────────────┐ Actions ┌─────────────────┐ │ asciinema rec │ ──────────────────▶ │ GitHub Orphan │ ───────────────▶ │ brotli archive │ │ + idle-chunker │ (concatenatable) │ gh-recordings │ │ (300x compress)│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ Idle ≥30s triggers chunk │ Separate history ▼ │ Cannot PR to main ~/asciinema_recordings/ ▼ └── repo-name/ .github/workflows/ └── chunks/*.zst └── recompress.yml
Requirements Component Required Installation Version asciinema CLI Yes brew install asciinema 3.0+ (Rust) zstd Yes brew install zstd Any brotli Yes brew install brotli Any git Yes Pre-installed 2.20+ gh CLI Yes brew install gh Any fswatch Optional brew install fswatch For real-time Workflow Phases Phase 0: Preflight Validation
Purpose: Verify all tools installed, offer self-correction if missing.
/usr/bin/env bash << 'PREFLIGHT_EOF'
preflight-check.sh - Validates all requirements
MISSING=()
Check each tool
for tool in asciinema zstd brotli git gh; do if ! command -v "$tool" &>/dev/null; then MISSING+=("$tool") fi done
if [[ ${#MISSING[@]} -gt 0 ]]; then echo "Missing tools: ${MISSING[]}" echo "" echo "Install with:" echo " brew install ${MISSING[]}" exit 1 fi
Check asciinema version (need 3.0+ for Rust version)
ASCIINEMA_VERSION=$(asciinema --version 2>&1 | grep -oE '[0-9]+.[0-9]+' | head -1) if [[ "${ASCIINEMA_VERSION%%.*}" -lt 3 ]]; then echo "Warning: asciinema $ASCIINEMA_VERSION detected. Version 3.0+ recommended." echo "Upgrade: brew upgrade asciinema" fi
echo "All requirements satisfied" PREFLIGHT_EOF
AskUserQuestion (if tools missing):
AskUserQuestion: question: "Required tools are missing. How would you like to proceed?" header: "Preflight Check" options: - label: "Install all missing tools (Recommended)" description: "Run: brew install ${MISSING[*]}" - label: "Show manual installation commands" description: "Display commands without executing" - label: "Continue anyway (may fail later)" description: "Skip installation and proceed"
Self-Correction: If tools are missing, generate installation command and offer to run it.
Phase 1: GitHub Account Detection
Purpose: Detect available GitHub accounts and let user choose which to use for recording storage.
Detection Sources
Probe these 5 sources to detect GitHub accounts:
Source Command What it finds SSH config grep -A5 "Host github" ~/.ssh/config Match directives with IdentityFile SSH keys ls ~/.ssh/id_ed25519_* Account-named keys (e.g., id_ed25519_terrylica) gh CLI gh auth status Authenticated accounts mise env grep GH_ACCOUNT .mise.toml GH_ACCOUNT variable git config git config user.name Global git username Detection Script /usr/bin/env bash << 'DETECT_ACCOUNTS_EOF'
detect-github-accounts.sh - Probe all sources for GitHub accounts
Uses portable parallel arrays (works in bash 3.2+ and when wrapped for zsh)
ACCOUNT_NAMES=() ACCOUNT_SOURCES=()
log() { echo "[detect] $*"; }
Helper: add account with source (updates existing or appends new)
add_account() { local account="$1" source="$2" local idx for idx in "${!ACCOUNT_NAMES[@]}"; do if [[ "${ACCOUNT_NAMES[$idx]}" == "$account" ]]; then ACCOUNT_SOURCES[$idx]+="$source " return fi done ACCOUNT_NAMES+=("$account") ACCOUNT_SOURCES+=("$source ") }
1. SSH config Match directives
if [[ -f ~/.ssh/config ]]; then while IFS= read -r line; do if [[ "$line" =~ IdentityFile.*id_ed25519_([a-zA-Z0-9_-]+) ]]; then add_account "${BASH_REMATCH[1]}" "ssh-config" fi done < ~/.ssh/config fi
2. SSH key filenames
for keyfile in ~/.ssh/id_ed25519_; do if [[ -f "$keyfile" && "$keyfile" != .pub ]]; then account=$(basename "$keyfile" | sed 's/id_ed25519_//') add_account "$account" "ssh-key" fi done
3. gh CLI authenticated accounts
if command -v gh &>/dev/null; then while IFS= read -r account; do [[ -n "$account" ]] && add_account "$account" "gh-cli" done < <(gh auth status 2>&1 | grep -oE 'Logged in to github.com account [a-zA-Z0-9_-]+' | awk '{print $NF}') fi
4. mise env GH_ACCOUNT
if [[ -f .mise.toml ]]; then account=$(grep -E 'GH_ACCOUNT\s=' .mise.toml 2>/dev/null | sed 's/.=\s"([^"])".*/\1/') [[ -n "$account" ]] && add_account "$account" "mise-env" fi
5. git config user.name
git_user=$(git config user.name 2>/dev/null) [[ -n "$git_user" ]] && add_account "$git_user" "git-config"
Score and display
log "=== Detected GitHub Accounts ===" RECOMMENDED="" MAX_SOURCES=0 for idx in "${!ACCOUNT_NAMES[@]}"; do account="${ACCOUNT_NAMES[$idx]}" sources="${ACCOUNT_SOURCES[$idx]}" count=$(echo "$sources" | wc -w | tr -d ' ') log "$account: $count sources ($sources)" if (( count > MAX_SOURCES )); then MAX_SOURCES=$count RECOMMENDED="$account" RECOMMENDED_SOURCES="$sources" fi done
echo "" echo "RECOMMENDED=$RECOMMENDED" echo "SOURCES=$RECOMMENDED_SOURCES" DETECT_ACCOUNTS_EOF
AskUserQuestion AskUserQuestion: question: "Which GitHub account should be used for recording storage?" header: "GitHub Account Selection" options: - label: "${RECOMMENDED} (Recommended)" description: "Detected via: ${SOURCES}" # Additional detected accounts appear here dynamically - label: "Enter manually" description: "Type a GitHub username not listed above"
Post-Selection: If user selects an account, ensure gh CLI is using that account:
/usr/bin/env bash << 'POST_SELECT_EOF'
Ensure gh CLI is authenticated as selected account
SELECTED_ACCOUNT="${1:?Usage: provide selected account}"
if ! gh auth status 2>&1 | grep -q "Logged in to github.com account $SELECTED_ACCOUNT"; then echo "Switching gh CLI to account: $SELECTED_ACCOUNT" gh auth switch --user "$SELECTED_ACCOUNT" 2>/dev/null || \ echo "Warning: Could not switch accounts. Manual auth may be needed." fi POST_SELECT_EOF
Phase 1.5: Current Repository Detection
Purpose: Detect current git repository context to provide intelligent defaults for Phase 2 questions.
Detection Script /usr/bin/env bash << 'DETECT_REPO_EOF'
Detect current repository context for intelligent defaults
CURRENT_REPO_URL="" CURRENT_REPO_OWNER="" CURRENT_REPO_NAME="" DETECTED_FROM=""
Check if we're in a git repository
if git rev-parse --git-dir &>/dev/null; then # Try origin remote first if git remote get-url origin &>/dev/null; then CURRENT_REPO_URL=$(git remote get-url origin) DETECTED_FROM="origin remote" # Fallback to first available remote elif [[ -n "$(git remote)" ]]; then REMOTE=$(git remote | head -1) CURRENT_REPO_URL=$(git remote get-url "$REMOTE") DETECTED_FROM="$REMOTE remote" fi
# Parse owner and name from URL (SSH or HTTPS) if [[ -n "$CURRENT_REPO_URL" ]]; then if [[ "$CURRENT_REPO_URL" =~ github.com://([^/.]+) ]]; then CURRENT_REPO_OWNER="${BASH_REMATCH[1]}" CURRENT_REPO_NAME="${BASH_REMATCH[2]%.git}" fi fi fi
Output for Claude to parse
echo "CURRENT_REPO_URL=$CURRENT_REPO_URL" echo "CURRENT_REPO_OWNER=$CURRENT_REPO_OWNER" echo "CURRENT_REPO_NAME=$CURRENT_REPO_NAME" echo "DETECTED_FROM=$DETECTED_FROM" DETECT_REPO_EOF
Claude Action: Store detected values (CURRENT_REPO_OWNER, CURRENT_REPO_NAME, DETECTED_FROM) for use in subsequent AskUserQuestion calls. If no repo detected, proceed without defaults.
Phase 2: Core Configuration
Purpose: Gather essential configuration from user.
2.1 Repository URL
If current repo detected (from Phase 1.5):
AskUserQuestion: question: "Which repository should store the recordings?" header: "Repository" options: - label: "${CURRENT_REPO_OWNER}/${CURRENT_REPO_NAME} (Recommended)" description: "Current repo detected from ${DETECTED_FROM}" - label: "Create dedicated repo: ${GITHUB_ACCOUNT}/asciinema-recordings" description: "Separate repository for all recordings" - label: "Enter different repository" description: "Specify another repository (user/repo format)"
If no current repo detected:
AskUserQuestion: question: "Enter the GitHub repository URL for storing recordings:" header: "Repository URL" options: - label: "Create dedicated repo: ${GITHUB_ACCOUNT}/asciinema-recordings" description: "Separate repository for all recordings (Recommended)" - label: "Enter repository manually" description: "SSH (git@github.com:user/repo.git), HTTPS, or shorthand (user/repo)"
URL Normalization (handles multiple formats):
/usr/bin/env bash << 'NORMALIZE_URL_EOF'
Normalize to SSH format for consistent handling
normalize_repo_url() { local url="$1"
# Shorthand: user/repo -> git@github.com:user/repo.git if [[ "$url" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$ ]]; then echo "git@github.com:${url}.git" # HTTPS: https://github.com/user/repo -> git@github.com:user/repo.git elif [[ "$url" =~ ^https://github.com/([^/]+)/([^/]+)/?$ ]]; then echo "git@github.com:${BASH_REMATCH[1]}/${BASH_REMATCH[2]%.git}.git" # Already SSH format else echo "$url" fi }
URL="${1:?Usage: provide URL to normalize}" normalize_repo_url "$URL" NORMALIZE_URL_EOF
Confirmation for free-form input (if user selected "Enter different/manually"):
AskUserQuestion: question: "You entered '${USER_INPUT}'. Normalized to: ${NORMALIZED_URL}. Is this correct?" header: "Confirm Repository" options: - label: "Yes, use ${NORMALIZED_URL}" description: "Proceed with this repository" - label: "No, let me re-enter" description: "Go back to repository selection"
2.2 Recording Directory AskUserQuestion: question: "Where should recordings be stored locally?" header: "Recording Directory" options: - label: "~/asciinema_recordings/${RESOLVED_REPO_NAME} (Recommended)" description: "Example: ~/asciinema_recordings/alpha-forge" - label: "Custom path" description: "Enter a different directory path"
Note: ${RESOLVED_REPO_NAME} is the actual repo name from Phase 1.5 or Phase 2.1, not a variable placeholder. Display the concrete path to user.
2.3 Branch Name AskUserQuestion: question: "What should the orphan branch be named?" header: "Branch Name" options: - label: "asciinema-recordings (Recommended)" description: "Matches ~/asciinema_recordings/ parent directory pattern" - label: "gh-recordings" description: "GitHub-prefixed alternative (gh = GitHub storage)" - label: "recordings" description: "Minimal name" - label: "Custom" description: "Enter a custom branch name"
Naming Convention: The default asciinema-recordings matches the parent directory ~/asciinema_recordings/ for consistency.
Phase 3: Advanced Configuration
Purpose: Allow customization of compression and behavior parameters.
Configuration Parameters Parameter Default Options Idle threshold 30s 15s, 30s (Recommended), 60s, Custom (5-300) zstd level 3 1 (fast), 3 (Recommended), 6, Custom (1-22) Brotli level 9 6, 9 (Recommended), 11, Custom (1-11) Auto-push Yes Yes (Recommended), No Poll interval 5s 2s, 5s (Recommended), 10s AskUserQuestion Sequence
3.1 Idle Threshold:
AskUserQuestion: question: "How long should the chunker wait before creating a chunk?" header: "Idle Threshold" options: - label: "15 seconds" description: "More frequent chunks, smaller files" - label: "30 seconds (Recommended)" description: "Balanced chunk size and frequency" - label: "60 seconds" description: "Larger chunks, less frequent uploads" - label: "Custom (5-300 seconds)" description: "Enter a custom threshold"
3.2 zstd Compression Level:
AskUserQuestion: question: "What zstd compression level for streaming chunks?" header: "zstd Level" options: - label: "1 (Fast)" description: "Fastest compression, larger files" - label: "3 (Recommended)" description: "Good balance of speed and compression" - label: "6 (Better compression)" description: "Slower but smaller chunks" - label: "Custom (1-22)" description: "Enter a custom level"
3.3 Brotli Compression Level:
AskUserQuestion: question: "What brotli compression level for final archives?" header: "Brotli Level" options: - label: "6" description: "Faster archival, slightly larger files" - label: "9 (Recommended)" description: "Great compression with reasonable speed" - label: "11 (Maximum)" description: "Best compression, slowest (may timeout on large files)" - label: "Custom (1-11)" description: "Enter a custom level"
3.4 Auto-Push:
AskUserQuestion: question: "Should chunks be automatically pushed to GitHub?" header: "Auto-Push" options: - label: "Yes (Recommended)" description: "Push immediately after each chunk" - label: "No" description: "Manual push when ready"
3.5 Poll Interval:
AskUserQuestion: question: "How often should the chunker check for idle state?" header: "Poll Interval" options: - label: "2 seconds" description: "More responsive, slightly higher CPU" - label: "5 seconds (Recommended)" description: "Good balance" - label: "10 seconds" description: "Lower resource usage"
Phase 4: Orphan Branch Setup
Purpose: Create or configure the orphan branch with GitHub Actions workflow.
Check for Existing Branch /usr/bin/env bash << 'CHECK_BRANCH_EOF'
Check if branch exists on remote
REPO_URL="${1:?Usage: provide repo URL}" BRANCH="${2:-asciinema-recordings}" # From Phase 2 (default changed)
if git ls-remote --heads "$REPO_URL" "$BRANCH" 2>/dev/null | grep -q "$BRANCH"; then echo "Branch '$BRANCH' already exists on remote" echo "BRANCH_EXISTS=true" else echo "Branch '$BRANCH' does not exist" echo "BRANCH_EXISTS=false" fi CHECK_BRANCH_EOF
AskUserQuestion (if branch exists) AskUserQuestion: question: "Branch '${BRANCH}' already exists on remote. How should we proceed?" header: "Existing Branch" options: - label: "Clone locally (Recommended)" description: "Use existing branch, clone to local directory" - label: "Reset and recreate fresh" description: "Delete remote branch and start over (DESTRUCTIVE)" - label: "Keep existing and verify" description: "Check existing setup matches configuration" - label: "Show manual instructions" description: "Display commands without executing"
Branch Creation (if new) /usr/bin/env bash << 'SETUP_ORPHAN_EOF'
setup-orphan-branch.sh - Creates asciinema-recordings orphan branch
REPO_URL="${1:?Usage: setup-orphan-branch.sh
Create temporary clone for setup
TEMP_DIR=$(mktemp -d) trap "rm -rf $TEMP_DIR" EXIT
git clone --depth 1 "$REPO_URL" "$TEMP_DIR" cd "$TEMP_DIR"
Create orphan branch
git checkout --orphan "$BRANCH" git rm -rf .
Setup directory structure
mkdir -p .github/workflows chunks archives
Create workflow with user-selected brotli level (EMBEDDED at creation time)
cat > .github/workflows/recompress.yml << WORKFLOW_EOF name: Recompress to Brotli
on: push: branches: [$BRANCH] paths: ['chunks/*/.zst'] workflow_dispatch:
jobs: recompress: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4
- name: Install compression tools
run: sudo apt-get update && sudo apt-get install -y zstd brotli
- name: Recompress chunks to brotli
run: |
if compgen -G "chunks/*.zst" > /dev/null; then
mkdir -p archives
ARCHIVE_NAME="archive_\$(date +%Y%m%d_%H%M%S).cast.br"
ls -1 chunks/*.zst | sort | xargs cat | zstd -d | brotli -${BROTLI_LEVEL} -o "archives/\$ARCHIVE_NAME"
rm -f chunks/*.zst
echo "Created: archives/\$ARCHIVE_NAME"
echo "ARCHIVE_NAME=\$ARCHIVE_NAME" >> \$GITHUB_ENV
else
echo "No chunks to process"
fi
- name: Commit archive
if: env.ARCHIVE_NAME != ''
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: archive recording to brotli (\${{ env.ARCHIVE_NAME }})"
file_pattern: 'archives/*.br chunks/'
WORKFLOW_EOF
Create placeholder files
echo '# Recording chunks (zstd compressed)' > chunks/README.md echo '# Brotli archives (final compressed)' > archives/README.md
Create README
cat > README.md << 'README_EOF'
Recording Storage (Orphan Branch)
This branch stores asciinema recording backups. It is completely isolated from the main codebase.
Structure
chunks/- Streaming zstd-compressed chunks (auto-deleted after archival)archives/- Final brotli-compressed recordings (~300x compression)
How It Works
- Local idle-chunker monitors asciinema recording
- When idle ≥30s, creates zstd chunk and pushes here
- GitHub Action concatenates chunks and recompresses to brotli
- Chunks are deleted, archive is retained
Isolation Guarantee
This is an orphan branch with no shared history with main. Git refuses to merge: "refusing to merge unrelated histories" README_EOF
Commit and push
git add . git commit -m "init: recording storage (orphan branch)" git push -u origin "$BRANCH"
cd -
Clone to local recordings directory
mkdir -p "$(dirname "$LOCAL_DIR")" git clone --single-branch --branch "$BRANCH" --depth 1 "$REPO_URL" "$LOCAL_DIR" echo "Setup complete: $LOCAL_DIR" SETUP_ORPHAN_EOF
Phase 5: Local Environment Setup
Purpose: Configure local directory and generate chunker script with user parameters.
Setup Local Directory /usr/bin/env bash << 'SETUP_LOCAL_EOF' REPO_NAME="${1:?Usage: provide repo name}" REPO_URL="${2:?Usage: provide repo URL}" BRANCH="${3:-asciinema-recordings}"
LOCAL_DIR="$HOME/asciinema_recordings/${REPO_NAME}"
Ensure directories exist
mkdir -p "$LOCAL_DIR/chunks" mkdir -p "$LOCAL_DIR/archives"
Clone if not present
if [[ ! -d "$LOCAL_DIR/.git" ]]; then git clone --single-branch --branch "$BRANCH" --depth 1 "$REPO_URL" "$LOCAL_DIR" fi
echo "LOCAL_DIR=$LOCAL_DIR" SETUP_LOCAL_EOF
Generate Customized idle-chunker.sh
Generate the chunker script with user-selected parameters embedded:
/usr/bin/env bash << 'GEN_CHUNKER_EOF'
Parameters from Phase 3 (passed as arguments)
LOCAL_DIR="${1:?Usage: provide LOCAL_DIR}" IDLE_THRESHOLD="${2:-30}" ZSTD_LEVEL="${3:-3}" POLL_INTERVAL="${4:-5}" PUSH_ENABLED="${5:-true}"
cat > "$LOCAL_DIR/idle-chunker.sh" << CHUNKER_EOF
!/usr/bin/env bash
idle-chunker.sh - Generated with user configuration
Configuration (embedded from setup):
IDLE_THRESHOLD=${IDLE_THRESHOLD}
ZSTD_LEVEL=${ZSTD_LEVEL}
POLL_INTERVAL=${POLL_INTERVAL}
PUSH_ENABLED=${PUSH_ENABLED}
set -euo pipefail
CAST_FILE="\${1:?Usage: idle-chunker.sh
Embedded configuration
IDLE_THRESHOLD=${IDLE_THRESHOLD} ZSTD_LEVEL=${ZSTD_LEVEL} POLL_INTERVAL=${POLL_INTERVAL} PUSH_ENABLED=${PUSH_ENABLED}
cd "\$(dirname "\$0")" last_pos=0
echo "Monitoring: \$CAST_FILE" echo "Idle threshold: \${IDLE_THRESHOLD}s | zstd level: \${ZSTD_LEVEL} | Poll: \${POLL_INTERVAL}s"
while [[ -f "\$CAST_FILE" ]] || sleep 2; do [[ -f "\$CAST_FILE" ]] || continue mtime=\$(stat -f%m "\$CAST_FILE" 2>/dev/null || stat -c%Y "\$CAST_FILE") idle=\$((\$(date +%s) - mtime)) size=\$(stat -f%z "\$CAST_FILE" 2>/dev/null || stat -c%s "\$CAST_FILE")
if (( idle >= IDLE_THRESHOLD && size > last_pos )); then chunk="chunks/chunk_\$(date +%Y%m%d_%H%M%S).cast" tail -c +\$((last_pos + 1)) "\$CAST_FILE" > "\$chunk" zstd -\${ZSTD_LEVEL} --rm "\$chunk"
if [[ "\$PUSH_ENABLED" == "true" ]]; then
git add chunks/ && git commit -m "chunk \$(date +%H:%M)" && git push
fi
last_pos=\$size
echo "[\$(date +%H:%M:%S)] Created: \${chunk}.zst"
fi
sleep \$POLL_INTERVAL done CHUNKER_EOF
chmod +x "$LOCAL_DIR/idle-chunker.sh" echo "Generated: $LOCAL_DIR/idle-chunker.sh" GEN_CHUNKER_EOF
Display Configuration Summary /usr/bin/env bash << 'SETUP_EOF' echo "" echo "=== Setup Complete ===" echo "" echo "Configuration:" echo " Repository: $REPO_URL" echo " Branch: $BRANCH" echo " Local directory: $LOCAL_DIR" echo "" echo "Parameters:" echo " Idle threshold: ${IDLE_THRESHOLD}s" echo " zstd level: $ZSTD_LEVEL" echo " Brotli level: $BROTLI_LEVEL" echo " Auto-push: $PUSH_ENABLED" echo " Poll interval: ${POLL_INTERVAL}s" echo "" echo "To start recording:" echo " 1. asciinema rec /path/to/session.cast" echo " 2. $LOCAL_DIR/idle-chunker.sh /path/to/session.cast" SETUP_EOF
Phase 6: Autonomous Validation
Purpose: Claude executes validation tests automatically, displaying results in CLI. Only interrupts user when human action is required.
Validation Test Categories Test Autonomous? Reason 1. Tool preflight ✅ YES Bash checks tools 2. zstd round-trip ✅ YES Synthetic test data 3. Brotli round-trip ✅ YES Synthetic test data 4. zstd concatenation ✅ YES Critical for streaming 5. Git/gh auth check ✅ YES Query auth status 6. Orphan branch validation ✅ YES Check remote/local 7. Workflow file check ✅ YES Read file contents 8. GitHub Actions trigger ✅ YES gh workflow run + watch 9. Recording test ❌ USER Requires starting asciinema 10. Chunker live test ❌ USER Requires active recording Autonomous Execution
Claude runs the validation script and displays formatted results:
╔════════════════════════════════════════════════════════════════╗ ║ AUTONOMOUS VALIDATION - Claude Code Executes All Tests ║ ╠════════════════════════════════════════════════════════════════╣ ║ ║ ║ Phase 1: Tool Check ║ ║ ───────────────── ║ ║ [RUN] Checking asciinema... ✓ installed (v3.0.0) ║ ║ [RUN] Checking zstd... ✓ installed (v1.5.5) ║ ║ [RUN] Checking brotli... ✓ installed (v1.1.0) ║ ║ [RUN] Checking git... ✓ installed (v2.43.0) ║ ║ [RUN] Checking gh... ✓ installed (v2.40.0) ║ ║ ║ ║ Phase 2: Compression Tests ║ ║ ──────────────────────── ║ ║ [RUN] zstd round-trip... ✓ PASSED ║ ║ [RUN] brotli round-trip... ✓ PASSED ║ ║ [RUN] zstd concatenation... ✓ PASSED (critical for streaming) ║ ║ ║ ║ Phase 3: Repository Validation ║ ║ ───────────────────────────── ║ ║ [RUN] Checking gh auth... ✓ authenticated as terrylica ║ ║ [RUN] Checking orphan branch... ✓ gh-recordings exists ║ ║ [RUN] Checking local clone... ✓ ~/asciinema_recordings/repo ║ ║ [RUN] Checking workflow file... ✓ recompress.yml present ║ ║ ║ ║ Phase 4: GitHub Actions Test ║ ║ ───────────────────────────── ║ ║ [RUN] Triggering workflow_dispatch... ✓ triggered ║ ║ [RUN] Watching run #12345... ⏳ in_progress ║ ║ [RUN] Watching run #12345... ✓ completed (success) ║ ║ ║ ║ ═══════════════════════════════════════════════════════════ ║ ║ AUTONOMOUS TESTS: 8/8 PASSED ║ ║ ═══════════════════════════════════════════════════════════ ║ ╚════════════════════════════════════════════════════════════════╝
User-Required Tests
Only TWO tests require user action:
Test 9: Recording Validation
AskUserQuestion: question: "Ready to test recording? This requires you to start asciinema in another terminal." header: "Recording Test" options: - label: "Guide me through it (Recommended)" description: "Step-by-step instructions" - label: "Skip this test" description: "I'll verify manually later" - label: "I've already verified recording works" description: "Mark as passed"
If "Guide me through it" selected, display:
╔════════════════════════════════════════════════════════════════╗ ║ USER ACTION REQUIRED: Recording Test ║ ╠════════════════════════════════════════════════════════════════╣ ║ ║ ║ In a NEW terminal, run: ║ ║ ┌────────────────────────────────────────────────────────┐ ║ ║ │ asciinema rec ~/asciinema_recordings/test_session.cast │ ║ ║ └────────────────────────────────────────────────────────┘ ║ ║ ║ ║ Then type a few commands and exit with Ctrl+D ║ ║ ║ ║ Come back here when done. ║ ╚════════════════════════════════════════════════════════════════╝
Then Claude autonomously validates the created file:
Claude runs after user confirms:
[RUN] Checking test_session.cast exists... ✓ [RUN] Validating JSON header... ✓ {"version": 2, ...} [RUN] Checking line count... ✓ 23 events recorded
Test 10: Chunker Live Test
AskUserQuestion: question: "Ready to test live chunking? This requires running recording + chunker simultaneously." header: "Chunker Test" options: - label: "Guide me (Recommended)" description: "Two-terminal workflow instructions" - label: "Skip - I trust the setup" description: "Skip live test"
Full Validation Script
See references/autonomous-validation.md for the complete validation script.
Troubleshooting on Failure
If any test fails, Claude displays inline troubleshooting:
[RUN] Checking gh auth... ✗ FAILED
Troubleshooting:
1. Run: gh auth login
2. Select: GitHub.com
3. Choose: HTTPS or SSH
4. Follow prompts to authenticate
Then re-run validation.
Quick Start First-Time Setup /usr/bin/env bash << 'PREFLIGHT_EOF'
1. Check requirements
for tool in asciinema zstd brotli git gh; do command -v "$tool" &>/dev/null && echo "$tool: OK" || echo "$tool: MISSING" done
2. Create orphan branch (replace with your repo)
REPO="git@github.com:YOUR/REPO.git" ./setup-orphan-branch.sh "$REPO"
3. Validate setup
./validate-setup.sh "$HOME/asciinema_recordings/REPO" PREFLIGHT_EOF
Recording Session /usr/bin/env bash << 'SKILL_SCRIPT_EOF'
Terminal 1: Start recording
WORKSPACE=$(basename "$PWD") asciinema rec $PWD/tmp/${WORKSPACE}$(date +%Y-%m-%d%H-%M).cast
Terminal 2: Start idle-chunker
~/asciinema_recordings/REPO/idle-chunker.sh $PWD/tmp/${WORKSPACE}_*.cast SKILL_SCRIPT_EOF
TodoWrite Task Templates Template: Full Setup 1. [Preflight] Validate all tools installed (asciinema, zstd, brotli, git, gh) 2. [Preflight] AskUserQuestion: offer installation for missing tools 3. [Account] Detect GitHub accounts from 5 sources 4. [Account] AskUserQuestion: select GitHub account 5. [Config] AskUserQuestion: repository URL 6. [Config] AskUserQuestion: recording directory 7. [Config] AskUserQuestion: branch name 8. [Advanced] AskUserQuestion: idle threshold 9. [Advanced] AskUserQuestion: zstd level 10. [Advanced] AskUserQuestion: brotli level 11. [Advanced] AskUserQuestion: auto-push 12. [Advanced] AskUserQuestion: poll interval 13. [Branch] Check if orphan branch exists on remote 14. [Branch] AskUserQuestion: handle existing branch 15. [Branch] Create orphan branch if needed 16. [Branch] Create GitHub Actions workflow with embedded parameters 17. [Local] Clone orphan branch to ~/asciinema_recordings/ 18. [Local] Generate idle-chunker.sh with embedded parameters 19. [Validate] Run autonomous validation (8 tests) 20. [Validate] AskUserQuestion: recording test (user action) 21. [Validate] AskUserQuestion: chunker live test (user action) 22. [Guide] Display configuration summary and usage instructions
Template: Recording Session 1. [Context] Detect workspace from $PWD 2. [Context] Generate datetime for filename 3. [Context] Ensure tmp/ directory exists 4. [Command] Generate asciinema rec command 5. [Command] Generate idle-chunker command 6. [Guide] Display two-terminal workflow instructions
Troubleshooting "Cannot push to orphan branch"
Cause: Authentication or permissions issue.
Fix:
Check gh auth status
gh auth status
Re-authenticate if needed
gh auth login
"Chunks not being created"
Cause: Idle threshold not reached, or file not growing.
Fix:
Verify recording is active: tail -f $CAST_FILE Lower threshold: IDLE_THRESHOLD=15 Check file permissions "GitHub Action not triggering"
Cause: Workflow file missing or wrong branch filter.
Fix:
Verify workflow exists
cat ~/asciinema_recordings/REPO/.github/workflows/recompress.yml
Check branch filter includes gh-recordings
grep -A2 "branches:" ~/asciinema_recordings/REPO/.github/workflows/recompress.yml
"Brotli archive empty or corrupted"
Cause: zstd chunks not concatenating properly (overlapping data).
Fix: Ensure idle-chunker uses last_chunk_pos to avoid overlap:
/usr/bin/env bash << 'PREFLIGHT_EOF_2'
Check for overlaps - each chunk should be sequential
for f in chunks/*.zst; do zstd -d "$f" -c | head -1 done PREFLIGHT_EOF_2
Key Design Decisions Decision Rationale zstd for streaming Supports frame concatenation (brotli doesn't) brotli for archival Best compression ratio (~300x for .cast files) Orphan branch Complete isolation, can't pollute main history Idle-based chunking Semantic breakpoints, not mid-output splits Shallow clone Minimal disk usage, can't accidentally access main 30s idle threshold Balances chunk frequency vs semantic completeness Post-Change Checklist
After modifying this skill:
Orphan branch creation scripts use heredoc wrapper All bash blocks compatible with zsh (no declare -A, no grep -P) GitHub Actions workflow validates brotli recompression Idle chunker handles both macOS and Linux stat syntax Detection flow outputs parseable key=value format References validate links to external documentation Reference Documentation Idle Chunker Script - Complete chunker implementation GitHub Workflow - Full Actions workflow Setup Scripts - All setup and validation scripts Autonomous Validation - Validation script and user-required tests asciinema 3.0 Docs zstd Frame Format Git Orphan Branches