diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96a4692 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Marathon: keep host-specific/internal config and runtime state local +.marathon/env +.marathon/logs/ +.marathon/*-config/ + +# Beads: db is regenerable from issues.jsonl +.beads/*.db diff --git a/.marathon/env.example b/.marathon/env.example new file mode 100644 index 0000000..c2c7746 --- /dev/null +++ b/.marathon/env.example @@ -0,0 +1,20 @@ +# Copy to .marathon/env (untracked) and fill in. +# Model/proxy config for the marathon launcher. Sourced and exported by launch-marathon.sh. + +# Anthropic-compatible endpoint (e.g. an Anthropic-API-compatible proxy in front of your model). +export ANTHROPIC_BASE_URL="https://your-anthropic-compatible-endpoint" +# Auth token for that endpoint (a placeholder is fine if the proxy handles auth). +export ANTHROPIC_AUTH_TOKEN="set-me" + +# Pin every model slot to one model so the loop is deterministic. +export ANTHROPIC_MODEL="glm-4.7" +export ANTHROPIC_DEFAULT_OPUS_MODEL="glm-4.7" +export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-4.7" +export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-4.7" +export CLAUDE_CODE_SUBAGENT_MODEL="glm-4.7" + +# If the endpoint uses a self-signed cert on a VPN entrypoint, you may need: +# export NODE_TLS_REJECT_UNAUTHORIZED=0 + +export DISABLE_AUTOUPDATER=1 +export DISABLE_TELEMETRY=1 diff --git a/.marathon/instruction.md b/.marathon/instruction.md new file mode 100644 index 0000000..1c1d7bb --- /dev/null +++ b/.marathon/instruction.md @@ -0,0 +1,99 @@ +# Trail Boss — Marathon Coding Instruction + +You are an autonomous developer implementing **Trail Boss**. Each iteration you read this +file, do **one coherent unit of work** toward the plan, verify it, commit, push, and the loop +restarts you fresh. Work is driven by `docs/plan/plan.md` (the authoritative design). + +## Project overview + +Trail Boss is a single-pane attention router for interactive AI coding-agent sessions +(Claude Code, running in tmux panes). When a session blocks waiting on the human — a +permission prompt, or a finished turn awaiting the next instruction — Trail Boss surfaces it +in a flat FIFO dead-letter queue and **navigates the operator to the live tmux pane** to act. +It never injects input ("navigator, not relay"). The human is on the loop, engaged only by +exception. + +## Working directory + +This repository (the marathon runs with its CWD set to the repo root). Use **relative paths**. + +## Authoritative docs — read the relevant parts before coding + +- `docs/plan/plan.md` — the complete design: capabilities, detection model, architecture, + the daemon/presentation split, the switching & keybindings surface, phases, open questions. +- `docs/research/claude-code-mechanics.md` — the Claude Code hook/env/payload facts + (probe-confirmed): `$TMUX_PANE` is in the hook env; the single `trailboss-emit.sh`; + `Stop` + `PermissionRequest` are the two enqueue triggers. +- `docs/notes/decisions.md` — settled decisions. **Do not re-litigate these** (navigator-not- + relay, flat FIFO/no-priority, Notification dropped, collector-is-the-daemon, etc.). + +## Current state + +Design is complete; **no implementation code exists yet**. The marathon starts at Phase 1. +Track progress in `PROGRESS.md` at the repo root (create it on the first iteration). + +## Iteration protocol + +Each iteration: + +1. **Orient** — read `PROGRESS.md` (if present) to see what's done and what's next. Skim the + relevant section of `docs/plan/plan.md`. +2. **Pick one unit** — the next unblocked item in the phase order below. Don't skip ahead; + foundational work first. Stay focused on a single coherent change. +3. **Implement** — write the code/scripts. Follow the settled design in the docs; if you make + a new design decision, record it in `docs/notes/decisions.md`. +4. **Verify** — actually exercise what you built (run the script, hit the endpoint, fire the + hook). Don't mark something done on code-read alone. For hook/tmux behavior, test in a + throwaway tmux session under `~/scratch` or a temp dir — never disturb other tmux sessions, + and never `send-keys` into panes you don't own. +5. **Commit + push** — mandatory every iteration: `git add` the specific files, commit with a + conventional message (`feat(scope): …`, `fix(scope): …`, `docs(scope): …`), then + `git push origin main`. Origin mirrors to GitHub automatically. +6. **Update `PROGRESS.md`** — what you did, what's next, any new known issues. + +If a phase's work is genuinely blocked, write down why in `PROGRESS.md` and pick the next +most-impactful unblocked unit. + +## Phase order (see `docs/plan/plan.md` "Implementation phases" for detail) + +1. **Probe `PermissionRequest`** — confirm it fires for the gate types in use and capture its + payload shape (the one remaining unknown). `Stop`/`SessionStart`/`$TMUX_PANE` are already + probe-confirmed. Record findings in `docs/research/claude-code-mechanics.md`. +2. **Emitter** — `trailboss-emit.sh`: forwards the hook stdin payload to the collector and + injects `$TMUX_PANE`. Plus the `settings.json` hook wiring (all hooks → this one script). +3. **Daemon (control plane)** — ingest endpoint (loopback only), SQLite state, the self-healing + `session_id → pane` registry, the transcript reconcile loop, and the FIFO queue. Expose + `/next` and `/skip`. Behind the normalized stuck/unstuck adapter contract so detection stays + harness-agnostic. Default stack: fork disler's hook event store (Bun + SQLite) unless a + better fit emerges — record the choice in `docs/notes/decisions.md`. +4. **Navigation** — resolve a pane id to `switch-client`/`select-window`/`select-pane`; + compute the next head on resolve/skip (operator-initiated jump, no forced focus-steal). +5. **Presentation** — `display-popup` queue picker + the Next/Skip keybindings + an optional + status-line "N stuck" segment. +6. **Walking skeleton** — close the loop end-to-end: stuck pane → loaded into focus → interact + → reconcile dequeues → next stuck session loads. This is the first real milestone. +7. **Iterate** — auto-jump toggle, skip/cooldown tuning, embedded `link-window` view, a second + harness adapter to validate the abstraction. + +## Settled invariants (enforce; do not violate) + +- **Navigator, not relay.** Never send synthesized input into a session. Delivery is + navigation to the live pane; any `send-keys` is human-authored text only and secondary. +- **The queue never lies.** The transcript JSONL is ground truth; reconcile is authoritative + over hook events. +- **Flat FIFO, no priority.** `permission` and `stopped` are treated identically; `reason` is + display-only. +- **Loopback-only ingest.** The collector binds `127.0.0.1` only. + +## Git workflow + +- All work goes to `main` (no feature branches for marathon work). +- Commit + push every iteration. Conventional commit messages. +- **Never force-push; never amend published commits.** + +## Constraints + +- Don't run a TUI inside this agent's own terminal — test TUIs in a separate tmux session. +- Keep the repo **public-clean**: no internal infrastructure hostnames, cluster names, tokens, + or absolute home paths in committed files. Use relative paths and env/config indirection. +- Don't add dependencies or abstractions the current phase doesn't need. diff --git a/.marathon/launch-marathon.sh b/.marathon/launch-marathon.sh new file mode 100755 index 0000000..9d39bac --- /dev/null +++ b/.marathon/launch-marathon.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Trail Boss marathon launcher. +# Runs claude headless in a loop: each iteration pipes instruction.md to +# `claude --print`, which does one unit of work, commits, pushes, and exits; +# the loop restarts it fresh. Model/proxy config comes from .marathon/env +# (untracked — copy env.example to env and fill in). +# +# Usage: +# ./.marathon/launch-marathon.sh # session "trailboss-marathon" +# ./.marathon/launch-marathon.sh my-session +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +INSTRUCTION_FILE="$SCRIPT_DIR/instruction.md" +CONFIG_DIR="$SCRIPT_DIR/glm47-config" +ENV_FILE="$SCRIPT_DIR/env" +LOG_DIR="$SCRIPT_DIR/logs" +SESSION_NAME="${1:-trailboss-marathon}" + +[ -f "$ENV_FILE" ] || { echo "Missing $ENV_FILE — copy env.example to env and fill it in." >&2; exit 1; } +[ -f "$INSTRUCTION_FILE" ] || { echo "Missing $INSTRUCTION_FILE" >&2; exit 1; } + +# shellcheck disable=SC1090 +set -a; source "$ENV_FILE"; set +a # exports ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, model slots, etc. +mkdir -p "$LOG_DIR" "$CONFIG_DIR" + +if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + echo "Session '$SESSION_NAME' already exists. Attach: tmux attach -t $SESSION_NAME" + exit 0 +fi + +MODEL="${ANTHROPIC_MODEL:-glm-4.7}" +LOG="$LOG_DIR/session_$(date +%Y%m%d_%H%M%S).jsonl" + +LOOP_CMD="cd '$REPO_DIR' && unset CLAUDECODE && export CLAUDE_CONFIG_DIR='$CONFIG_DIR' && while true; do + echo \"[trailboss-marathon] iteration start \$(date -Iseconds)\" + cat '$INSTRUCTION_FILE' | claude --model '$MODEL' --dangerously-skip-permissions --output-format stream-json --verbose --print 2>&1 | tee -a '$LOG' + ec=\$? + echo \"[trailboss-marathon] iteration done (exit \$ec) \$(date -Iseconds)\" + if [ \$ec -ne 0 ]; then sleep 30; else sleep 5; fi +done" + +tmux new-session -d -s "$SESSION_NAME" -c "$REPO_DIR" +tmux send-keys -t "$SESSION_NAME" "$LOOP_CMD" Enter + +echo "Marathon running: $SESSION_NAME (model: $MODEL)" +echo " Attach: tmux attach -t $SESSION_NAME Stop: tmux kill-session -t $SESSION_NAME" +echo " Logs: $LOG"