diff --git a/CLAUDE.md b/CLAUDE.md index 6056033..b420a57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,13 @@ - Prefer explicit imports over glob imports - Keep functions focused — if it's doing two things, split it +## Code Review (CodeRabbit) +- CodeRabbit is configured as a required reviewer on PRs +- When it finds issues, it submits a "Request changes" review to block merging +- Once all CodeRabbit comments are resolved and pre-merge checks pass, it automatically switches to "Approve" +- Do not manually dismiss CodeRabbit reviews — fix the issues and let it re-review +- PRs are only mergeable when CodeRabbit approves AND all CI checks are green + ## Dogfooding - This project uses itself (`tt`) to orchestrate development — tutti developing tutti - When a tutti feature doesn't work during a dogfood run, **fix the framework** (the `tt` command / runtime / session code), don't work around it by doing the task manually diff --git a/README.md b/README.md index a266b9c..68bc754 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ source ~/.zshrc - OpenClaw skill contract: `docs/OPENCLAW_SKILL_CONTRACT.md` - OpenClaw skill starter: `skills/openclaw/SKILL.md` - OpenClaw integration bundle: `integrations/openclaw/README.md` +- PR reproducibility loop (CodeRabbit + checks + merge gate): `docs/pr-review-loop.md` - Versioning and release policy: `VERSIONING.md` ## tutti.toml @@ -318,6 +319,7 @@ Reusable prompt components and skills are **phrases**. A phrase might be a CLAUD - Run checkpoints persisted at `.tutti/state/workflow-checkpoints/.json` + `tt run --resume ` - Workflow step types: `prompt`, `command`, `ensure_running`, `workflow` (nested), `review`, `land` - Workflow `review`/`land` steps auto-start required sessions when they are not already running +- Workflow `land` steps enforce a GitHub merge gate (required checks green + all PR review threads resolved) - `workflow_complete` hooks for deterministic chaining - Auto-reclaim of newly-started `persistent = false` sessions at workflow end - `tt serve` local control API endpoints: diff --git a/docs/CODEX_SDLC_ORCHESTRATION.md b/docs/CODEX_SDLC_ORCHESTRATION.md index 9066f8d..9f916d3 100644 --- a/docs/CODEX_SDLC_ORCHESTRATION.md +++ b/docs/CODEX_SDLC_ORCHESTRATION.md @@ -61,6 +61,8 @@ Use `docs/examples/tutti-codex-sdlc.toml` as a starting point. - Enforce docs/version updates in implementation prompt - Require test pass before PR open and before merge/land - If CodeRabbit fails, gather feedback and route to Codex fix step +- Follow the canonical PR reproducibility loop in `docs/pr-review-loop.md` +- Automation `land` steps enforce merge gate checks (required checks + resolved review threads) ## Suggested runbook @@ -74,4 +76,5 @@ Use `docs/examples/tutti-codex-sdlc.toml` as a starting point. - Start with `fail_mode = "closed"` on command steps - Keep PR creation and land as explicit steps (no hidden auto-merge) -- Add approval gate before land for now +- Keep approval checks in your reviewer/final summary prompts +- Keep merge gate enabled before `land` (required checks green + no unresolved review threads) diff --git a/docs/examples/tutti-codex-sdlc.toml b/docs/examples/tutti-codex-sdlc.toml index e002b96..544af1e 100644 --- a/docs/examples/tutti-codex-sdlc.toml +++ b/docs/examples/tutti-codex-sdlc.toml @@ -24,7 +24,7 @@ prompt = "You orchestrate workflow state and handoffs only. You do not implement [[agent]] name = "implementer" -runtime = "codex" +runtime = "claude-code" worktree = true persistent = true scope = "src/**" @@ -57,6 +57,11 @@ prompt = "Perform strict review and release-readiness validation. Block if risk name = "sdlc-smoke" description = "Preflight test run before unattended automation" +[[workflow.step]] +id = "ensure_implementer" +type = "ensure_running" +agent = "implementer" + [[workflow.step]] id = "select_issue" type = "command" @@ -71,6 +76,14 @@ cwd = "agent_worktree" run = "scripts/automation/create_issue_branch.sh .tutti/state/auto/selected_issue.json .tutti/state/auto/branch.json" fail_mode = "closed" +[[workflow.step]] +id = "verify_clean_baseline" +type = "command" +agent = "implementer" +cwd = "agent_worktree" +run = "scripts/automation/verify_clean_baseline.sh .tutti/state/auto/branch.json" +fail_mode = "closed" + [[workflow.step]] id = "validate_smoke" type = "command" @@ -122,6 +135,14 @@ cwd = "agent_worktree" run = "scripts/automation/create_issue_branch.sh .tutti/state/auto/selected_issue.json .tutti/state/auto/branch.json" fail_mode = "closed" +[[workflow.step]] +id = "verify_clean_baseline" +type = "command" +agent = "implementer" +cwd = "agent_worktree" +run = "scripts/automation/verify_clean_baseline.sh .tutti/state/auto/branch.json" +fail_mode = "closed" + # 3) Planning first (Claude) [[workflow.step]] id = "plan_issue" @@ -224,3 +245,10 @@ inject_files = [".tutti/state/auto/selected_issue.json", ".tutti/state/auto/bran wait_for_idle = true wait_timeout_secs = 900 text = "Summarize final readiness, residual risks, and merge recommendation." + +# 10) Land (merge gate enforced by automation) +[[workflow.step]] +id = "land_changes" +type = "land" +agent = "implementer" +fail_mode = "closed" diff --git a/docs/infographic-github.html b/docs/infographic-github.html new file mode 100644 index 0000000..49c6214 --- /dev/null +++ b/docs/infographic-github.html @@ -0,0 +1,278 @@ + + + + + +tutti — GitHub social preview (1280x640) + + + + +
+
+
+ +
+ +
+

tutti

+

multi-agent orchestration

+
+ +
+
+
{ }
+

tt up

+

Config-driven launch with dependency ordering & worktree isolation

+
+
+
+ +
+

tt watch

+

Live TUI dashboard with crash restart & auto-handoff

+
+
+
+ +
+

tt run

+

Declarative workflows mixing prompts & shell commands

+
+
+
+ +
+

tt usage

+

Token capacity tracking across all agents & runtimes

+
+
+
+ +
+

tt health

+

Agent health probes with CLI & HTTP endpoints

+
+
+ +
+ Claude Code + Codex + Aider + | + Rust + tmux + | + local-first + | + config-driven + | + open source +
+ +
+ +
+ + github.com/nutthouse/tutti +
+ + + diff --git a/docs/infographic.html b/docs/infographic.html new file mode 100644 index 0000000..9ddae33 --- /dev/null +++ b/docs/infographic.html @@ -0,0 +1,594 @@ + + + + + +tutti — architecture + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

tutti

+

multi-agent orchestration

+
+ +
+ + nutthouse/tutti +
+ + + + + + + + + + + + +
+
+
{ }
+ tutti.toml +
+
Agents, workflows, hooks, permissions, env vars
+
+ + +
+
+
>_
+ tt up +
+
Topological launch with dep ordering
+
+ + +
+ ISOLATED GIT WORKTREES + + +
+
+
>_
+ backend +
+ Claude Code +
+ + +
+
+
>_
+ frontend +
+ Claude Code +
+ + +
+
+
>_
+ etl +
+ Codex +
+
+ + + + + + +
+
+
+ +
+ tt watch +
+
Live TUI dashboard
+
    +
  • Real-time agent status
  • +
  • Crash detection + auto-restart
  • +
  • Context monitoring + auto-handoff
  • +
  • PLAN / CTX columns
  • +
+
+ + +
+
+
+ +
+ tt run +
+
Workflow automation engine
+
    +
  • Prompt steps → agent sessions
  • +
  • Command steps → shell execution
  • +
  • Hooks: agent_stop → verify
  • +
  • Strict / lenient fail modes
  • +
+
+ + + probes + triggers + hooks + + + + + + +
+
+
+ +
+ tt usage +
+
Capacity tracking & token attribution
+
    +
  • Per-agent token breakdown
  • +
  • Weekly reset tracking
  • +
  • Multi-runtime aggregation
  • +
+
+
+ 0% + ~68% of 45h + 100% +
+
+ + +
+
+
+ +
+ Claude Code +
+
~/.claude/ JSONL logs
+
+ + +
+
+
+ +
+ Codex +
+
~/.codex/ session logs
+
+ + tokens + + + + + + +
+
+
+ +
+ OpenClaw Skill +
+
JSON API for external agent loops
16 actions, stable envelope contract
+
+ + +
+
+
+ +
+ .tutti/state/ +
+
Agent state, automation runs, verify summaries, handoff packets, logs
+
+ + +
+
+
+ +
+ tt health +
+
Agent health probes
CLI + HTTP endpoint
+
+ + +
+ Supports: + Claude Code + Codex + Aider + | + Rust + tmux + | + local-first + | + config-driven +
+ +
+ + diff --git a/docs/pr-review-loop.md b/docs/pr-review-loop.md new file mode 100644 index 0000000..1407e46 --- /dev/null +++ b/docs/pr-review-loop.md @@ -0,0 +1,41 @@ +# PR Review Loop ("Choir" Reproducibility) + +This is the exact merge loop for Tutti automation. + +## Objective + +Make PR handling deterministic: +1. Resolve CodeRabbit threads. +2. Let CodeRabbit auto re-review after push. +3. Wait for required checks to be green. +4. Confirm approvals are present. +5. Merge/land only after gates pass. + +## Canonical loop + +1. Open or update PR from the issue branch. +2. Wait for CodeRabbit feedback (`wait_and_collect_coderabbit.py`). +3. Apply actionable feedback and push. +4. Repeat until no unresolved feedback remains. +5. Run final validation (`cargo test --quiet` + reviewer packet). +6. **Before `land`/merge, enforce merge gate**: + - Required checks must be green. + - PR review threads must all be resolved (including CodeRabbit threads). +7. Merge/land. + +## Enforcement + +Automation land steps now run with: + +- `TT_ENFORCE_MERGE_GATE=1` + +When enabled, `tt land` fails closed if: +- no open PR exists for the landed branch, +- any required check is not green, +- any PR review thread remains unresolved. + +## Notes + +- CodeRabbit re-review is push-triggered; do not manually force review unless required. +- Keep this gate in automation even if humans occasionally merge manually. +- If a gate fails, fix the PR state and rerun the land step. diff --git a/docs/social-preview.png b/docs/social-preview.png new file mode 100644 index 0000000..e065b03 Binary files /dev/null and b/docs/social-preview.png differ diff --git a/scripts/automation/create_issue_branch.sh b/scripts/automation/create_issue_branch.sh index 2cd013f..32171cd 100755 --- a/scripts/automation/create_issue_branch.sh +++ b/scripts/automation/create_issue_branch.sh @@ -4,8 +4,16 @@ set -euo pipefail # Create a feature branch from selected issue JSON. # Usage: create_issue_branch.sh [selected_issue_json] [output_json] -ISSUE_JSON="${1:-.tutti/state/auto/selected_issue.json}" -OUT_FILE="${2:-.tutti/state/auto/branch.json}" +# Safety: refuse to run destructive commands outside an agent worktree +TOPLEVEL=$(git rev-parse --show-toplevel) +case "$TOPLEVEL" in + */.tutti/worktrees/*) ;; + *) echo "FATAL: create_issue_branch.sh must run inside an agent worktree, not $TOPLEVEL" >&2; exit 1 ;; +esac + +PROJECT_ROOT=$(cd "$TOPLEVEL/../.." && pwd) +ISSUE_JSON="${1:-$PROJECT_ROOT/.tutti/state/auto/selected_issue.json}" +OUT_FILE="${2:-$PROJECT_ROOT/.tutti/state/auto/branch.json}" BASE_BRANCH="${BASE_BRANCH:-main}" mkdir -p "$(dirname "$OUT_FILE")" @@ -22,12 +30,38 @@ STAMP=$(date +%Y%m%d%H%M%S) BRANCH="auto/issue-${ISSUE_NUM}-${STAMP}" git fetch origin "$BASE_BRANCH" + +# Pre-clean: discard any carried state from the current branch +git reset --hard HEAD +git clean -ffdx + +# Switch to the automation branch from a clean baseline git checkout -B "$BRANCH" "origin/$BASE_BRANCH" -python3 - <<'PY' "$OUT_FILE" "$BRANCH" "$ISSUE_NUM" +# Post-clean: guarantee working tree matches origin exactly (including ignored files) +git reset --hard "origin/$BASE_BRANCH" +git clean -ffdx + +BASE_SHA=$(git rev-parse HEAD) + +# Assert clean baseline, including ignored files +DIRTY=$(git status --porcelain --ignored) +if [ -n "$DIRTY" ]; then + echo "FATAL: worktree is not clean after reset:" >&2 + echo "$DIRTY" >&2 + exit 1 +fi + +python3 - <<'PY' "$OUT_FILE" "$BRANCH" "$ISSUE_NUM" "$BASE_BRANCH" "$BASE_SHA" import json,sys out, branch, issue = sys.argv[1], sys.argv[2], int(sys.argv[3]) +base_branch, base_sha = sys.argv[4], sys.argv[5] with open(out, 'w', encoding='utf-8') as f: - json.dump({"branch": branch, "issue_number": issue}, f, indent=2) + json.dump({ + "branch": branch, + "issue_number": issue, + "base_branch": base_branch, + "base_sha": base_sha, + }, f, indent=2) print(out) PY diff --git a/scripts/automation/verify_branch_has_changes.py b/scripts/automation/verify_branch_has_changes.py old mode 100755 new mode 100644 index a027426..deea37b --- a/scripts/automation/verify_branch_has_changes.py +++ b/scripts/automation/verify_branch_has_changes.py @@ -4,13 +4,57 @@ import sys from pathlib import Path + +def git_output(args: list[str]) -> str: + result = subprocess.run( + ["git", *args], + text=True, + capture_output=True, + check=True, + ) + return result.stdout.strip() + + +def ref_exists(ref: str) -> bool: + result = subprocess.run( + ["git", "rev-parse", "--verify", ref], + text=True, + capture_output=True, + check=False, + ) + return result.returncode == 0 + + branch_file = Path(sys.argv[1] if len(sys.argv) > 1 else ".tutti/state/auto/branch.json") with branch_file.open("r", encoding="utf-8") as f: branch = json.load(f)["branch"] -subprocess.check_call(["git", "fetch", "origin", "main"]) -log = subprocess.check_output(["git", "log", "--oneline", f"origin/main..{branch}"], text=True).strip() +subprocess.run(["git", "fetch", "origin", "main"], check=True, capture_output=True, text=True) +fetch_branch = subprocess.run( + ["git", "fetch", "origin", branch], + check=False, + capture_output=True, + text=True, +) +remote_ref_missing = False +if fetch_branch.returncode != 0: + err = (fetch_branch.stderr or "").lower() + if "couldn't find remote ref" not in err: + print(fetch_branch.stderr.strip() or f"Failed to fetch origin/{branch}", file=sys.stderr) + sys.exit(1) + remote_ref_missing = True + +remote_ref = f"origin/{branch}" +if not remote_ref_missing and ref_exists(remote_ref): + branch_ref = remote_ref +elif ref_exists(branch): + branch_ref = branch +else: + print(f"Branch ref not found: {remote_ref} or {branch}", file=sys.stderr) + sys.exit(1) + +log = git_output(["log", "--oneline", f"origin/main..{branch_ref}"]) if not log: print("No commits found on automation branch vs origin/main") sys.exit(1) diff --git a/scripts/automation/verify_clean_baseline.sh b/scripts/automation/verify_clean_baseline.sh new file mode 100755 index 0000000..64903f9 --- /dev/null +++ b/scripts/automation/verify_clean_baseline.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verify that an agent worktree has a clean baseline matching branch.json. +# Usage: verify_clean_baseline.sh [branch_json_path] + +# Safety: must be in an agent worktree +TOPLEVEL=$(git rev-parse --show-toplevel) +case "$TOPLEVEL" in + */.tutti/worktrees/*) ;; + *) echo "FATAL: verify_clean_baseline.sh must run inside an agent worktree, not $TOPLEVEL" >&2; exit 1 ;; +esac + +# Resolve branch.json — try argument as-is, then relative to project root +BRANCH_JSON="${1:-.tutti/state/auto/branch.json}" +if [ ! -f "$BRANCH_JSON" ]; then + ROOT=$(dirname "$(git rev-parse --git-common-dir)") + BRANCH_JSON="$ROOT/$BRANCH_JSON" +fi +if [ ! -f "$BRANCH_JSON" ]; then + echo "FATAL: branch.json not found at $BRANCH_JSON" >&2 + exit 1 +fi + +BASE_SHA=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['base_sha'])" "$BRANCH_JSON") +HEAD_SHA=$(git rev-parse HEAD) + +if [ "$HEAD_SHA" != "$BASE_SHA" ]; then + echo "FATAL: HEAD ($HEAD_SHA) != base_sha ($BASE_SHA)" >&2 + exit 1 +fi + +DIRTY=$(git status --porcelain) +if [ -n "$DIRTY" ]; then + echo "FATAL: worktree not clean:" >&2 + echo "$DIRTY" >&2 + exit 1 +fi + +echo "Baseline verified: HEAD=$HEAD_SHA, clean=true" diff --git a/scripts/automation/wait_coderabbit.sh b/scripts/automation/wait_coderabbit.sh index a52d159..f9e271e 100755 --- a/scripts/automation/wait_coderabbit.sh +++ b/scripts/automation/wait_coderabbit.sh @@ -24,30 +24,52 @@ with open(out,'w',encoding='utf-8') as f: json.dump({"pr_number":pr,"status":"timeout","found":False},f,indent=2) print(out) PY - exit 1 + # Timeout is a soft outcome — downstream steps handle missing feedback gracefully + exit 0 fi - DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json statusCheckRollup) - RESULT=$(python3 - <<'PY' "$DATA" -import json,sys -obj=json.loads(sys.argv[1]) + DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json statusCheckRollup,comments) + RESULT=$(DATA="$DATA" python3 - <<'PY' +import json,os +obj=json.loads(os.environ["DATA"]) + +# Check status checks first checks=obj.get("statusCheckRollup") or [] cr=[] for c in checks: name=(c.get("name") or "").lower() if "coderabbit" in name or "code rabbit" in name: cr.append(c) -if not cr: - print("NONE") +if cr: + states=[(c.get("status") or "", c.get("conclusion") or "") for c in cr] + if any(s != "COMPLETED" for s,_ in states): + print("PENDING") + else: + allowed=("SUCCESS",) + bad=[x for x in states if x[1] not in allowed] + print("FAIL" if bad else "PASS") raise SystemExit -# if multiple, require all completed and explicitly successful -states=[(c.get("status") or "", c.get("conclusion") or "") for c in cr] -if any(s != "COMPLETED" for s,_ in states): - print("PENDING") -else: - allowed=("SUCCESS",) - bad=[x for x in states if x[1] not in allowed] - print("FAIL" if bad else "PASS") + +# Fallback: check for CodeRabbit PR comments (review may appear as comment, not check) +KICKOFF_MARKERS = ["review triggered", "walkthrough", "