Skip to content

Add reproducible PR review loop + enforce merge gate on land - #59

Merged
nutt-adam merged 20 commits into
mainfrom
wren/pr-review-loop-merge-gate
Mar 19, 2026
Merged

Add reproducible PR review loop + enforce merge gate on land#59
nutt-adam merged 20 commits into
mainfrom
wren/pr-review-loop-merge-gate

Conversation

@wren-nutthouse

@wren-nutthouse wren-nutthouse commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add docs/pr-review-loop.md documenting the exact Choir loop (resolve CodeRabbit threads -> auto re-review -> required checks green -> approvals -> merge)
  • enforce an automation merge gate for land steps: block when required checks are not green or PR review threads are unresolved
  • wire automation to always enable this gate for workflow land steps
  • update SDLC docs/example workflow and README references

Validation

  • cargo test -q

Notes

  • gate fails closed when no open PR exists for the branch, so automation cannot land unreviewed branches
  • direct tt land remains unchanged unless TT_ENFORCE_MERGE_GATE=1 is set

Summary by CodeRabbit

  • New Features

    • Merge gate enforcement for safe PR landing—requires all required checks to pass and all review threads to resolve before landing
    • Run tracking commands to list and display SDLC workflow run details and history
    • Milestone-based filtering for issue selection in automated workflows
  • Documentation

    • Added PR review loop and merge gate enforcement documentation
    • Updated workflow orchestration guidance for automation safety
  • Improvements

    • Enhanced agent state detection patterns
    • Improved automation script robustness for branch verification
    • Refined permission rule suggestion logic
    • Better prompt execution and output file handling

@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive PR reproducibility and review-loop enforcement system for Tutti's SDLC automation. It adds merge-gate validation (enforcing required checks and resolved review threads before landing), auto-recovery features for prompt steps, new run tracking CLI commands, milestone-based issue filtering, baseline verification and implementation-result tracking scripts, comprehensive workflow documentation, runtime pattern updates, and infrastructure for deterministic automation workflows.

Changes

Cohort / File(s) Summary
Documentation & Workflow Specs
README.md, docs/CODEX_SDLC_ORCHESTRATION.md, docs/pr-review-loop.md, docs/examples/tutti-codex-sdlc.toml, tutti.toml
Comprehensive workflow documentation including new PR reproducibility loop spec, merge-gate enforcement requirements, and complete sdlc-smoke/sdlc-auto workflow definitions with agent configurations and step orchestration.
Merge Gate Enforcement
src/cli/land.rs, scripts/automation/write_implement_result.py, scripts/automation/verify_clean_baseline.sh
Implements GitHub-based merge gate validation querying required checks status and review-thread resolution, with supporting baseline verification and result-tracking scripts for the automated workflow.
Prompt Execution & Auto-Recovery
src/automation/mod.rs
Extends prompt step execution with session auto-start, activity/output waiting with timeouts, buffered prompt submission for codex/claude-code runtimes, and branch-progress verification with automatic retry logic.
Automation Scripts & State Tracking
scripts/automation/create_issue_branch.sh, scripts/automation/verify_branch_has_changes.py, scripts/automation/wait_coderabbit.sh, scripts/automation/write_implement_result.py
Enhances branch creation/verification with worktree guards, state capture (base_branch/base_sha), conditional fetch handling, CodeRabbit feedback collection fallback, and implementation-result JSON generation with validation.
Run Tracking & Ledger Access
src/cli/runs.rs
Introduces list and show commands for SDLC run tracking, displaying active runs with state/timestamps and detailed run information including transitions and failure details.
Issue Claiming with Milestone Filter
src/cli/issue_claim.rs, src/cli/mod.rs, src/main.rs
Extends issue-claim acquire subcommand with optional `--milestone` parameter for filtering issue candidates by milestone title.
Worktree & Branch State
src/worktree/mod.rs, src/cli/up.rs
Adds current-branch tracking to WorktreeSnapshot, branch-aware worktree preservation when fresh flag is set, and updated git-branch resolution for existing worktrees.
Permissions Suggestion Refinement
src/permissions/mod.rs, src/cli/permissions.rs
Changes suggested-rule generation from wildcard-suffixed (e.g., "git stash *") to token-only format (e.g., "git stash"), affecting both policy evaluation and permission-suggestion messaging.
Runtime Patterns & Idle Detection
src/runtime/mod.rs, src/runtime/claude_code.rs, tests/fixtures/runtime/*
Updates Claude-code runtime patterns (working, idle, completion recognition), refines completion-detection logic to exclude spinner-glyph cases, and adds fixture files for idle-prompt-bar and prompt-marker testing.
Visual Assets
docs/infographic.html, docs/infographic-github.html, .gitignore
Adds dark-themed GitHub preview and architecture infographics as standalone HTML, plus gitignore entry for `.tutti-worktrees/`.

Sequence Diagram(s)

sequenceDiagram
    participant Agent as Agent (Prompt)
    participant Session as Tmux Session
    participant Executor as Step Executor
    participant Automation as Automation Runtime
    participant File as Output/State Files
    participant Retry as Retry Logic

    Executor->>Automation: start_and_wait_ready()
    Automation->>Session: Create/start session
    Session-->>Automation: Ready
    Automation-->>Executor: Session started

    Executor->>Executor: Capture baseline pane hash
    Executor->>Session: Send prompt
    Executor->>Automation: wait_for_prompt_activity_or_output(20s)
    
    alt Activity/Output Detected
        Automation->>Session: Poll pane for activity
        Session-->>Automation: Output detected
        Automation-->>Executor: Success
    else Timeout
        Automation-->>Executor: Timeout (fail if no output file)
    end

    Executor->>File: Check for output_json
    
    alt Output File Exists
        File-->>Executor: File loaded
        Executor->>Executor: Record Success
    else File Missing
        Executor->>Retry: Prepare retry prompt
        Retry->>Session: Auto-start + send retry
        Retry->>Automation: wait_for_prompt_activity_or_output(60s)
        Automation-->>Retry: Activity detected
        Retry->>File: wait_for_prompt_output_file(120s)
        File-->>Retry: File appears
        Retry-->>Executor: Load and store output
    end
Loading
sequenceDiagram
    participant User as User/Automation
    participant Land as Land Command
    participant GH as GitHub (gh CLI)
    participant Checks as Required Checks
    participant Reviews as Review Threads
    participant Merge as Merge Gate

    User->>Land: tt land (with TT_ENFORCE_MERGE_GATE=1)
    
    Land->>GH: gh pr list --branch <target>
    GH-->>Land: PR number
    
    Land->>Checks: gh pr checks <pr> --required
    Checks-->>Land: Check statuses
    
    alt Any Check Not Green
        Land->>Merge: ❌ BLOCK
        Merge-->>User: Landing failed: required checks not green
    end
    
    Land->>Reviews: Query GraphQL reviewThreads (paginated)
    Reviews-->>Land: Thread list + resolution status
    
    alt Unresolved Threads Exist
        Land->>Merge: ❌ BLOCK
        Merge-->>User: Landing failed: unresolved review threads
    else All Resolved
        Land->>Merge: ✅ ALLOW
        Merge-->>User: Landing approved
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops through PR gates with glee,
Merge threads resolved—now can we be free?
Auto-start sessions, retry with grace,
Workflows dance at a brisk pace,
Reproducibility's the rabbit's embrace!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers the main changes, addresses validation via cargo test, and includes relevant notes about gate behavior. However, it does not include the required Versioning section with SemVer choice and version number, nor does it explicitly justify why versioning updates were not made. Add the Versioning section with SemVer choice (likely MINOR for new capability), version number, and either update Cargo.toml/CHANGELOG.md or explicitly justify why they were not updated.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: adding a reproducible PR review loop and enforcing a merge gate on the land command. It directly relates to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wren/pr-review-loop-merge-gate
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/land.rs (1)

35-43: ⚠️ Potential issue | 🔴 Critical

Gate the landed SHA, not just the PR's branch name.

Line 39 validates GitHub state before the --pr early return and before Line 40 can create a new local commit. That breaks tt land <agent> --pr under TT_ENFORCE_MERGE_GATE=1, and the normal land path can still cherry-pick local-only commits — including the auto-commit from commit_wip_if_needed() — even though checks/review only covered the older PR head. Skip the gate on the --pr path, and make the gate compare the local branch tip to the PR head SHA before landing.

💡 Safer ordering
     if !force {
         ensure_git_clean(&resolved.project_root)?;
     }
     ensure_branch_exists(&resolved.project_root, &branch)?;
-    maybe_enforce_merge_gate(&resolved.project_root, &branch, enforce_merge_gate)?;
     let wip_committed = commit_wip_if_needed(&worktree_path, &resolved.agent_name)?;
 
     if pr {
         push_and_open_pr(&resolved.project_root, &branch)?;
         if wip_committed {
@@
         }
         return Ok(());
     }
+
+    maybe_enforce_merge_gate(&resolved.project_root, &branch, enforce_merge_gate)?;
Please also fail closed when `git rev-parse ` does not match the PR head SHA.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/land.rs` around lines 35 - 43, The merge-gate check is currently run
unconditionally before handling the --pr path and before commit_wip_if_needed(),
which allows local-only commits to bypass review; modify the flow in land.rs so
that when pr is true you skip calling maybe_enforce_merge_gate() before
push_and_open_pr(), and instead, when enforcing the merge gate for the normal
land path, have maybe_enforce_merge_gate() (or a new helper it calls) fetch the
PR head SHA and compare it against the local branch tip (git rev-parse
<branch>); if they differ, fail closed with an error; also ensure the check
happens after commit_wip_if_needed() for the non-PR path so the gated SHA is the
actual commit being landed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/land.rs`:
- Around line 227-237: The GraphQL query assigned to variable `query` only
requests `reviewThreads(first: 100)` which can miss threads on large PRs; change
the query to accept an `after: String` variable and request
`reviewThreads(first: 100, after: $after)`, include `pageInfo { hasNextPage
endCursor }` alongside `nodes { isResolved }`, and then update the calling code
that queries the `pullRequest` (the loop/logic that counts unresolved threads)
to paginate: repeatedly execute the query using `endCursor` while `hasNextPage`
is true, aggregating `nodes` and counting where `isResolved == false` until all
pages are fetched so the unresolved-thread gate is accurate.

In `@src/cli/permissions.rs`:
- Around line 204-209: The current fallback synthesizes "{cmd} *" which is too
broad; change the unwrap_or_else that uses format!("{cmd} *") so it does not
append a wildcard—use cmd.clone() (or preserve None) instead so
PermissionSuggestion.suggested_rule is the command itself rather than a
star-suffixed prefix; update the construction of PermissionSuggestion (and its
suggested_rule type if you want to represent absence) so the code uses
decision.suggested_rule.unwrap_or_else(|| cmd.clone()) instead of the
wildcard-forming string.

In `@src/permissions/mod.rs`:
- Around line 73-85: The suggestion builder currently emits wildcard prefix
rules via suggested_wildcard_prefix_rule which creates entries like "cargo test
*" and thus over-broadens permissions; change suggested_wildcard_prefix_rule to
emit an exact prefix suggestion instead (e.g., return Some(format!("{} {}",
tokens[0], tokens[1])) for two-token commands) so callers get "cargo test"
(which matching_allow_rule already treats as a valid prefix match) rather than a
wildcard rule; leave the function signature and tokenization logic intact but
remove the trailing " *" and ensure it still returns None for short inputs.

---

Outside diff comments:
In `@src/cli/land.rs`:
- Around line 35-43: The merge-gate check is currently run unconditionally
before handling the --pr path and before commit_wip_if_needed(), which allows
local-only commits to bypass review; modify the flow in land.rs so that when pr
is true you skip calling maybe_enforce_merge_gate() before push_and_open_pr(),
and instead, when enforcing the merge gate for the normal land path, have
maybe_enforce_merge_gate() (or a new helper it calls) fetch the PR head SHA and
compare it against the local branch tip (git rev-parse <branch>); if they
differ, fail closed with an error; also ensure the check happens after
commit_wip_if_needed() for the non-PR path so the gated SHA is the actual commit
being landed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fb1c405b-6442-4937-b514-83e549ac1c66

📥 Commits

Reviewing files that changed from the base of the PR and between c8f5ae7 and 6338dfc.

📒 Files selected for processing (8)
  • README.md
  • docs/CODEX_SDLC_ORCHESTRATION.md
  • docs/examples/tutti-codex-sdlc.toml
  • docs/pr-review-loop.md
  • src/automation/mod.rs
  • src/cli/land.rs
  • src/cli/permissions.rs
  • src/permissions/mod.rs

Comment thread src/cli/land.rs Outdated
Comment thread src/cli/permissions.rs
Comment thread src/permissions/mod.rs Outdated
wren-nutthouse and others added 3 commits March 17, 2026 10:41
- Paginate reviewThreads GraphQL query in land.rs to handle PRs with
  >100 review threads (request pageInfo, loop with cursor)
- Use cmd.clone() instead of format!("{cmd} *") as fallback in
  permissions.rs to avoid overly broad suggested rules
- Rename suggested_wildcard_prefix_rule to suggested_prefix_rule and
  emit exact prefix suggestions without trailing " *"
- Update all affected test expectations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam nutt-adam closed this Mar 18, 2026
@nutt-adam nutt-adam reopened this Mar 18, 2026
@nutt-adam

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/automation/mod.rs (1)

2699-2700: Avoid duplicating the merge-gate env var literal across modules.

"TT_ENFORCE_MERGE_GATE" is now defined in more than one place; centralizing it prevents drift if the key ever changes.

♻️ Suggested refactor
diff --git a/src/cli/land.rs b/src/cli/land.rs
@@
-const ENFORCE_MERGE_GATE_ENV: &str = "TT_ENFORCE_MERGE_GATE";
+pub(crate) const ENFORCE_MERGE_GATE_ENV: &str = "TT_ENFORCE_MERGE_GATE";
diff --git a/src/automation/mod.rs b/src/automation/mod.rs
@@
-            let run_result =
-                run_tt_subcommand_with_env(project_root, &args, &[("TT_ENFORCE_MERGE_GATE", "1")]);
+            let run_result = run_tt_subcommand_with_env(
+                project_root,
+                &args,
+                &[(crate::cli::land::ENFORCE_MERGE_GATE_ENV, "1")],
+            );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` around lines 2699 - 2700, Extract the environment
variable literal "TT_ENFORCE_MERGE_GATE" into a single shared constant and use
that constant where it's referenced instead of the string literal; e.g., define
a pub(crate) const TT_ENFORCE_MERGE_GATE: &str = "TT_ENFORCE_MERGE_GATE" in a
central module (such as a new or existing constants/mod or the parent automation
module) and replace the literal in the call to
run_tt_subcommand_with_env(project_root, &args, &[("TT_ENFORCE_MERGE_GATE",
"1")]) with &[ (TT_ENFORCE_MERGE_GATE, "1") ] so all modules reference the
single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/automation/mod.rs`:
- Around line 2699-2700: Extract the environment variable literal
"TT_ENFORCE_MERGE_GATE" into a single shared constant and use that constant
where it's referenced instead of the string literal; e.g., define a pub(crate)
const TT_ENFORCE_MERGE_GATE: &str = "TT_ENFORCE_MERGE_GATE" in a central module
(such as a new or existing constants/mod or the parent automation module) and
replace the literal in the call to run_tt_subcommand_with_env(project_root,
&args, &[("TT_ENFORCE_MERGE_GATE", "1")]) with &[ (TT_ENFORCE_MERGE_GATE, "1") ]
so all modules reference the single source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 19b17786-6391-44dc-b085-ac14789a499a

📥 Commits

Reviewing files that changed from the base of the PR and between 6338dfc and c8cfff8.

📒 Files selected for processing (4)
  • src/automation/mod.rs
  • src/cli/land.rs
  • src/cli/permissions.rs
  • src/permissions/mod.rs

nutt-adam added a commit that referenced this pull request Mar 19, 2026
…anges

Cover startup grace window (#67), persistent memory (#62/#63), merge gate
enforcement (#59), permissions suggest (#53), orchestration state machine
(#54/#55), and all fixes shipped since 0.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nutt-adam and others added 4 commits March 19, 2026 11:41
Fix dirty worktree contamination, false idle detection, and resource
limits that prevented sdlc-auto from completing end-to-end with --strict.

Scripts:
- create_issue_branch.sh: add worktree safety guard, pre/post-clean,
  and clean-state assertion to prevent carried dirty state
- verify_clean_baseline.sh: new script asserting HEAD==base_sha and
  clean porcelain after branch creation
- wait_coderabbit.sh: treat timeout as soft exit (exit 0), detect
  CodeRabbit comments and rate-limit responses as fallback

Idle detection (health/mod.rs):
- Add startup_grace parameter to suppress false saw_activity from
  initial pane capture during agent initialization
- Require idle stability window before acting on completion signals,
  preventing false completion between Claude Code tool calls
- After startup grace, allow stable completion signal to trigger idle
  detection even without prior saw_activity (fixes case where agent
  finishes before wait_for_agent_idle starts)

Workflow engine (automation/mod.rs):
- ensure_running waits for agent to reach idle/ready before declaring
  success, preventing prompt delivery to uninitialized sessions
- Update all wait_for_agent_idle call sites with startup_grace param

Workflow config (tutti.toml, docs/examples):
- Add verify_clean_baseline step after create_branch
- Add stop_idle_agents step before final_review to free max_concurrent
  slots for the reviewer agent

Verified: ISSUE_LABEL=dogfood-now ISSUE_MILESTONE=v0.4.0 tt run sdlc-auto
--strict completes all 23 steps successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…improvements

- issue-claim acquire: add --milestone flag to filter by GitHub milestone
- up: preserve agent worktree on auto/issue-* branches instead of
  resetting, preventing mid-workflow state loss
- worktree: add current_branch to WorktreeSnapshot for branch inspection
- claude-code runtime: add "Searching for", "Unravelling", "(thinking)"
  working patterns; add "don't ask on", "shift+tab to cycle" idle and
  completion patterns for better prompt-bar detection
- runtime detection: completion markers only outrank working patterns
  when no spinner is active
- verify_branch_has_changes.py: handle local-only branches and fetch
  remote before comparison
- write_implement_result.py: new script to record implementation output
- runs subcommand: add tt runs for listing workflow run history
- Test fixtures for claude/codex idle prompt bar detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the pub(crate) constant from land.rs instead of duplicating the
string literal in automation/mod.rs.

Addresses CodeRabbit nitpick on PR #59.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts in permissions (keep non-wildcard suggestion fix),
worktree (keep current_branch field + add tests from main), and
update test assertions for WorktreeSnapshot.current_branch field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/automation/verify_branch_has_changes.py`:
- Around line 41-42: The code currently builds branch_ref and calls
git_output(["log", "--oneline", f"origin/main..{branch_ref}"]) which will raise
CalledProcessError if neither origin/<branch> nor local <branch> exist; update
verify_branch_has_changes.py to first check existence of both refs using
ref_exists (for "origin/<branch>" and "<branch>") and if neither exists return a
controlled error (or raise a clear exception) instead of invoking git_output, or
alternatively wrap the git_output call in a try/except that catches
subprocess.CalledProcessError and returns/raises a descriptive error; reference
branch_ref, ref_exists(), and git_output() when making the change.
- Around line 34-39: The dynamic-branch git fetch currently runs with
check=False and ignores all failures; update the subprocess.run call that
fetches ["git", "fetch", "origin", branch] to capture the CompletedProcess
result, inspect result.returncode and result.stderr, and if returncode != 0 and
stderr does not include "couldn't find remote ref" treat it as a hard failure
(log/raise/exit) so network/auth/server errors stop execution while
missing-remote-ref remains acceptable; keep the existing behavior for the
missing-ref case and mirror the stricter handling used for the earlier fetch of
"main".

In `@scripts/automation/write_implement_result.py`:
- Around line 60-66: The current changed_files computation only lists files from
the HEAD commit; update the run_git call that builds changed_files (the list
comprehension using run_git) to use git diff --name-only between base_sha and
HEAD (e.g., run_git(["diff", "--name-only", f"{base_sha}..HEAD"])) so it
collects all files changed in the commit range validated earlier; keep the
filtering (splitlines and if line.strip()) the same and ensure base_sha variable
is referenced in that run_git invocation.
- Around line 45-53: Replace the simple equality check between commit_sha and
base_sha with an explicit ancestor check: call git merge-base --is-ancestor
base_sha HEAD (via the run_git helper or a new helper) before using
run_git(["log", "--oneline", f"{base_sha}..HEAD"]) and if merge-base indicates
base_sha is not an ancestor of HEAD, print an error (include base_branch and
both SHAs) to stderr and return 1; keep the existing early-return for the
equal-SHA case but ensure the new merge-base check runs when they differ to
guard against divergent branches (update variables commit_sha, base_sha,
progress_log where applicable).

In `@src/automation/mod.rs`:
- Around line 815-844: The current auto-start path sleeps for 3 seconds and can
race with agent startup; create a shared helper
start_and_wait_ready(project_root: &Path, session_name: &str, agent: AgentType)
that calls with_project_root(self.project_root, || crate::cli::up::run(...)) to
start the session and then waits for readiness using the existing
wait_for_agent_idle/ensure_running logic (or polling TmuxSession::session_exists
+ wait_for_agent_idle) instead of a fixed sleep; replace the inline match/sleep
blocks in the TmuxSession auto-start site (the block using
with_project_root/crate::cli::up::run) and the other control-DAG startup path
(lines around ensure_running and 1573-1590) to call start_and_wait_ready(...)
and propagate errors into failed_steps/step_results the same way.
- Around line 32-39: The code currently ignores WorkflowBranchState.branch and
treats any commit ahead of base_sha as progress; update
prompt_step_has_branch_progress() to read the current HEAD branch (e.g., from
the worktree or repo) and compare it to WorkflowBranchState.branch, returning
failure/false if they differ, and only then consider base_sha advancement as
success; ensure WorkflowBranchState.branch is referenced to avoid the
unused-field warning and preserve existing semantics for base_branch and
base_sha (use base_branch/default_base_branch and base_sha Option<String> as
before) so CI/clippy warnings are resolved.
- Around line 1201-1247: The retry block for the implement_code step currently
hardcodes the "codex" adapter and fixed timeouts (300s/20s); change the calls
that reference "codex" and the magic timeout values so they use the existing
runtime variable and wait_timeout_secs (and any related timeout values derived
from it) instead; specifically update occurrences in the implement_code retry
path including the wait_for_prompt_activity_or_output call and the
health::wait_for_agent_idle invocation (and any associated sleep/timeout checks)
to pass runtime and wait_timeout_secs rather than the literal "codex" and
hardcoded seconds so the retry behavior matches the agent runtime in use.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5530cf32-693c-4c29-b82c-bfa52ca274ee

📥 Commits

Reviewing files that changed from the base of the PR and between c8cfff8 and 5e04b51.

📒 Files selected for processing (24)
  • README.md
  • docs/CODEX_SDLC_ORCHESTRATION.md
  • docs/examples/tutti-codex-sdlc.toml
  • scripts/automation/create_issue_branch.sh
  • scripts/automation/verify_branch_has_changes.py
  • scripts/automation/verify_clean_baseline.sh
  • scripts/automation/wait_coderabbit.sh
  • scripts/automation/write_implement_result.py
  • src/automation/mod.rs
  • src/cli/issue_claim.rs
  • src/cli/land.rs
  • src/cli/mod.rs
  • src/cli/runs.rs
  • src/cli/send.rs
  • src/cli/up.rs
  • src/health/mod.rs
  • src/main.rs
  • src/runtime/claude_code.rs
  • src/runtime/mod.rs
  • src/worktree/mod.rs
  • tests/fixtures/runtime/claude_idle_prompt_bar.txt
  • tests/fixtures/runtime/codex_idle_prompt_bar.txt
  • tests/fixtures/runtime/codex_idle_prompt_marker.txt
  • tutti.toml
✅ Files skipped from review due to trivial changes (1)
  • tests/fixtures/runtime/codex_idle_prompt_bar.txt
🚧 Files skipped from review as they are similar to previous changes (3)
  • README.md
  • docs/CODEX_SDLC_ORCHESTRATION.md
  • src/cli/land.rs

Comment thread scripts/automation/verify_branch_has_changes.py Outdated
Comment thread scripts/automation/verify_branch_has_changes.py Outdated
Comment thread scripts/automation/write_implement_result.py
Comment thread scripts/automation/write_implement_result.py
Comment thread src/automation/mod.rs
Comment thread src/automation/mod.rs Outdated
Comment thread src/automation/mod.rs
Scripts:
- verify_branch_has_changes.py: distinguish fetch failures from missing
  remote refs; guard against missing local+remote branch refs before
  git log
- write_implement_result.py: use merge-base --is-ancestor to validate
  base_sha ancestry; use git diff --name-only for full range of changed
  files instead of just HEAD commit

Rust:
- prompt_step_has_branch_progress: check current branch matches
  WorkflowBranchState.branch before declaring progress, fixing the
  unused `branch` field clippy warning
- Extract start_and_wait_ready() helper used from all auto-start sites
  (prompt, ensure_running, implement_code retry, control-DAG) replacing
  fixed 3-second sleeps
- implement_code retry: use agent's actual runtime instead of hardcoded
  "codex"; use config-driven timeout instead of fixed 300s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/automation/mod.rs (1)

1143-1148: ⚠️ Potential issue | 🟡 Minor

Fix error message to match actual timeout.

The timeout at line 1134 is 60 seconds, but the error message at line 1146 says "20s". This appears to be a copy-paste error from the earlier activity check at line 902.

🐛 Proposed fix
                                     message: Some(
-                                        "implement_code retry did not start activity or produce output within 20s"
+                                        "implement_code retry did not start activity or produce output within 60s"
                                             .to_string(),
                                     ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` around lines 1143 - 1148, The error message for the
"implement_code retry" timeout is incorrect (says "20s" but the actual timeout
is 60 seconds); update the message associated with the timed_out case (the
struct instance that sets exit_code: None, timed_out: true, message: Some(...))
to reflect "60s" (or, better, interpolate the timeout constant/variable used for
the 60s check) so the text matches the actual timeout value and avoid future
copy-paste discrepancies.
🧹 Nitpick comments (2)
src/automation/mod.rs (2)

1239-1245: Consider using wait_timeout_secs instead of hardcoded 3600s for consistency.

The first implement_code retry block (lines 1155-1161) uses Duration::from_secs((*wait_timeout_secs).max(1)), but this block uses a hardcoded 1-hour timeout. This inconsistency could lead to unexpected behavior depending on how the workflow is configured.

♻️ Proposed consistency fix

Note: This would require restructuring to access wait_timeout_secs outside the if let ResolvedStep::Prompt block, which may not be straightforward. If the 1-hour timeout is intentional for this specific case, consider adding a comment explaining why.

+                            // Use longer timeout for implement_code without output_json,
+                            // as the agent may need more time to complete complex implementations.
                             let retry_wait = health::wait_for_agent_idle(
                                 agent_runtime,
                                 session_name,
                                 Duration::from_secs(3600),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` around lines 1239 - 1245, The call to
health::wait_for_agent_idle currently uses a hardcoded
Duration::from_secs(3600); replace this with the configured wait_timeout_secs
for consistency (e.g., Duration::from_secs((*wait_timeout_secs).max(1))) or
otherwise thread the wait_timeout_secs value into the scope where
wait_for_agent_idle is invoked (referencing wait_for_agent_idle, agent_runtime,
session_name, and wait_timeout_secs) so the timeout behavior matches the other
retry block; if the 3600s value is intentional, add a brief comment on why this
case must differ.

794-794: Remove unused _auto_started_session variable.

This variable is assigned at line 822 but never read. Consider removing it to reduce dead code.

♻️ Suggested removal
-                        let mut _auto_started_session = false;
                         let rendered = match render_template(text, &outputs, false) {

And remove the assignment at line 822:

-                                Ok(()) => _auto_started_session = true,
+                                Ok(()) => {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` at line 794, Remove the dead variable
_auto_started_session: delete its declaration (let mut _auto_started_session =
false;) and any subsequent assignments to it (the assignment around where it is
set at the later block), since the variable is never read; ensure no other logic
depends on that variable so simply remove the declaration and the assignment to
eliminate dead code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/automation/verify_branch_has_changes.py`:
- Around line 40-49: The code allows selecting a stale remote tracking ref
(remote_ref = f"origin/{branch}") even when fetch_branch failed with "couldn't
find remote ref"; update the logic so that when fetch_branch.returncode != 0 and
the stderr contains "couldn't find remote ref" you do NOT consider remote_ref in
the subsequent ref_exists check. Concretely, use the existing fetch_branch error
check to set a flag (or set remote_ref to None) when that specific error occurs,
then only call ref_exists(remote_ref) and assign branch_ref = remote_ref if the
fetch succeeded for that ref; otherwise skip to checking the local branch with
ref_exists(branch) and fail if neither exists, ensuring stale refs aren't used
by ref_exists(remote_ref) after a "couldn't find remote ref" fetch.

In `@scripts/automation/write_implement_result.py`:
- Around line 80-82: The error message incorrectly says "HEAD commit" while the
check uses the computed changed_files for the range base_sha..HEAD; update the
print message in the branch that checks the variable changed_files (the
conditional that returns 1) to reflect the actual range being validated (e.g.,
mention "no changed files in base_sha..HEAD" or interpolate the range variables
used to compute changed_files) so the message matches the check performed by the
function that computes changed_files.

---

Duplicate comments:
In `@src/automation/mod.rs`:
- Around line 1143-1148: The error message for the "implement_code retry"
timeout is incorrect (says "20s" but the actual timeout is 60 seconds); update
the message associated with the timed_out case (the struct instance that sets
exit_code: None, timed_out: true, message: Some(...)) to reflect "60s" (or,
better, interpolate the timeout constant/variable used for the 60s check) so the
text matches the actual timeout value and avoid future copy-paste discrepancies.

---

Nitpick comments:
In `@src/automation/mod.rs`:
- Around line 1239-1245: The call to health::wait_for_agent_idle currently uses
a hardcoded Duration::from_secs(3600); replace this with the configured
wait_timeout_secs for consistency (e.g.,
Duration::from_secs((*wait_timeout_secs).max(1))) or otherwise thread the
wait_timeout_secs value into the scope where wait_for_agent_idle is invoked
(referencing wait_for_agent_idle, agent_runtime, session_name, and
wait_timeout_secs) so the timeout behavior matches the other retry block; if the
3600s value is intentional, add a brief comment on why this case must differ.
- Line 794: Remove the dead variable _auto_started_session: delete its
declaration (let mut _auto_started_session = false;) and any subsequent
assignments to it (the assignment around where it is set at the later block),
since the variable is never read; ensure no other logic depends on that variable
so simply remove the declaration and the assignment to eliminate dead code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d95bc93c-94ec-4693-aa08-c3552a566850

📥 Commits

Reviewing files that changed from the base of the PR and between 5e04b51 and 300677e.

📒 Files selected for processing (3)
  • scripts/automation/verify_branch_has_changes.py
  • scripts/automation/write_implement_result.py
  • src/automation/mod.rs

Comment thread scripts/automation/verify_branch_has_changes.py
Comment thread scripts/automation/write_implement_result.py
- verify_branch_has_changes.py: skip stale origin/<branch> ref when
  fetch reported "couldn't find remote ref"
- write_implement_result.py: fix error message to reflect base_sha..HEAD
  range instead of "HEAD commit"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/automation/write_implement_result.py (1)

26-27: Use explicit UTF-8 for file I/O in automation artifacts.

Relying on implicit locale encoding can make runs nondeterministic across environments.

Proposed fix
-    selected_issue = json.loads(Path(sys.argv[1]).read_text())
-    branch_info = json.loads(Path(sys.argv[2]).read_text())
+    selected_issue = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
+    branch_info = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
@@
-    output_path.write_text(json.dumps(payload, indent=2) + "\n")
+    output_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")

Also applies to: 105-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/automation/write_implement_result.py` around lines 26 - 27, The file
I/O currently uses Path.read_text() without specifying encoding which can yield
nondeterministic results; update the calls that populate selected_issue and
branch_info (and the similar call around line where result writing occurs) to
read with an explicit UTF-8 encoding (e.g., use
Path(...).read_text(encoding="utf-8") or open(..., encoding="utf-8") and
.read()) so all artifact reads/writes are deterministic across locales; ensure
every Path.read_text()/open usage in this module (including the instance
referenced at line ~105) is changed to explicitly specify "utf-8".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/automation/write_implement_result.py`:
- Around line 9-14: Several subprocess.run calls (the ones assigning to result
at the occurrences around lines 9-14, 34-39, 45-50, and 87-92) may hang
indefinitely; add a timeout parameter to each subprocess.run invocation (e.g.,
timeout=30 or an appropriate constant) and catch subprocess.TimeoutExpired
around those calls; on timeout, log an error (including the command and timeout
value) and fail/exit gracefully (e.g., processLogger.error or sys.exit(1)) so
the automation does not stall. Ensure you update every subprocess.run call shown
in write_implement_result.py and handle TimeoutExpired consistently.

---

Nitpick comments:
In `@scripts/automation/write_implement_result.py`:
- Around line 26-27: The file I/O currently uses Path.read_text() without
specifying encoding which can yield nondeterministic results; update the calls
that populate selected_issue and branch_info (and the similar call around line
where result writing occurs) to read with an explicit UTF-8 encoding (e.g., use
Path(...).read_text(encoding="utf-8") or open(..., encoding="utf-8") and
.read()) so all artifact reads/writes are deterministic across locales; ensure
every Path.read_text()/open usage in this module (including the instance
referenced at line ~105) is changed to explicitly specify "utf-8".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 66156d60-46fa-404e-8512-1f8c74ec9266

📥 Commits

Reviewing files that changed from the base of the PR and between 300677e and aa011c8.

📒 Files selected for processing (2)
  • scripts/automation/verify_branch_has_changes.py
  • scripts/automation/write_implement_result.py

Comment thread scripts/automation/write_implement_result.py
nutt-adam and others added 2 commits March 19, 2026 18:31
Prevent hung automation runs by adding a 60-second timeout to all
git subprocess calls (fetch, merge-base, push, and the run_git helper).

Addresses CodeRabbit feedback on PR #59.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lt.py

Prevents nondeterministic behavior across environments with different
locale settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nutt-adam
nutt-adam previously approved these changes Mar 19, 2026
@nutt-adam

Copy link
Copy Markdown
Contributor

@coderabbitai all requested changes have been made. please re-review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown

@nutt-adam Sure! I'll kick off a full re-review of the PR now to cover all the updated changes.

✅ Actions performed

Full review triggered.

nutt-adam added a commit that referenced this pull request Mar 19, 2026
…choir runs (#71)

* feat(health): add startup grace window to wait_for_agent_idle (#67)

Prevent fresh prompt steps from falsely completing before the agent
has consumed the prompt. The startup grace period (default 30s) gates
completion detection until real working activity is observed.

Key changes:
- wait_for_agent_idle accepts a startup_grace Duration parameter
- AgentStatus::Working counts as activity even without pane hash change,
  requiring 2+ consecutive polls to avoid flicker false positives
- First pane capture no longer counts as a hash "change"
- Completion signals before any activity are held until grace expires
- "Unravelling" added to claude-code working patterns
- startup_grace_secs field threaded through config and automation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add 0.3.0 changelog entry for issue #67 and prior unreleased changes

Cover startup grace window (#67), persistent memory (#62/#63), merge gate
enforcement (#59), permissions suggest (#53), orchestration state machine
(#54/#55), and all fixes shipped since 0.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce startup grace to 10s and validate wait settings

- Reduce DEFAULT_STARTUP_GRACE_SECS from 30 to 10 so the
  completion-before-activity path fires before typical wait timeouts
- Validate that wait_timeout_secs/startup_grace_secs are only set when
  wait_for_idle is true, failing fast with actionable guidance

Addresses CodeRabbit feedback on PR #71.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/cli/permissions.rs (1)

587-588: Make the persisted-config assertion exact.

contains("echo blocked") still passes if the file regresses to echo blocked *, so this test no longer proves the on-disk wildcard removal.

Suggested fix
-        let saved = std::fs::read_to_string(global_config_path()).unwrap();
-        assert!(saved.contains("echo blocked"));
+        let saved = GlobalConfig::load().unwrap();
+        assert_eq!(
+            saved.permissions.unwrap_or_default().allow,
+            vec!["echo blocked".to_string()]
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/permissions.rs` around lines 587 - 588, The test currently asserts
that the persisted config "saved" contains "echo blocked", which would still
pass if a wildcard slip-in like "echo blocked *" appears; change the assertion
to check the exact persisted content instead (e.g., compare saved to the exact
expected string or assert equality/trimming rather than using contains). Locate
the test that reads the file via global_config_path() (the variable named saved)
and replace the loose contains check with an exact match against the expected
on-disk config text to ensure wildcard removal is validated precisely.
tutti.toml (1)

76-77: Complex shell command is correct but consider extracting to a script.

The inline bash command with conditional milestone handling works correctly, but the complexity makes it harder to maintain. Consider extracting this pattern to a reusable script if used frequently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tutti.toml` around lines 76 - 77, The inline complex bash in the run key (the
cargo run ... issue-claim acquire command with conditional ISSUE_MILESTONE
handling and output to .tutti/state/auto/selected_issue.json) should be
extracted to a small executable script (e.g., scripts/acquire-issue.sh) that
accepts ISSUE_LABEL, ISSUE_MILESTONE, lease TTL and output path as args or
environment variables; update the run value to call that script with the same
env vars (ISSUE_LABEL, ISSUE_MILESTONE, ISSUE_MILESTONE check moved into the
script) so the conditional logic and command composition live in a reusable,
tested script rather than a single long inline bash line.
src/automation/mod.rs (1)

794-794: Consider removing unused _auto_started_session variable.

This variable is set at line 822 but never read. If it's intended for future use, consider adding a TODO comment; otherwise, it can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` at line 794, The variable `_auto_started_session` is
declared and later assigned but never read; remove its declaration and any
assignments to `_auto_started_session` (search for `_auto_started_session` in
this module) to eliminate the unused variable, or if you intend to keep it for
future logic, replace the declaration with a clearly marked TODO comment (e.g.,
`// TODO: preserve _auto_started_session for future session-tracking`) so
linters and reviewers know it's intentional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/automation/verify_clean_baseline.sh`:
- Line 25: The BASE_SHA assignment interpolates $BRANCH_JSON directly into the
python -c string, risking shell injection; change it so the script passes
BRANCH_JSON as an argument to Python (use sys.argv or argparse inside the
one-liner) and open that argument instead of embedding the variable, e.g. invoke
python3 -c with sys.argv[1] and supply "$BRANCH_JSON" as the external argument;
update the line that sets BASE_SHA to reference BRANCH_JSON only as a shell
argument and parse it inside the Python code to safely read the 'base_sha'
field.

In `@scripts/automation/wait_coderabbit.sh`:
- Around line 31-32: The script currently stores the full PR JSON in DATA and
passes it as a single argv to the embedded Python (via RESULT=$(python3 - <<'PY'
"$DATA")), which can hit argument-length limits for large `comments`; change the
flow to avoid passing the full payload on the command line — either pipe DATA
into Python's stdin or write DATA to a temporary file and have the Python
snippet read from stdin or that file. Update the use-sites of DATA/RESULT and
the python invocation in wait_coderabbit.sh so the Python code reads from
standard input (or reads the temp file path provided) instead of receiving the
full JSON as an argument, and ensure the temporary file (if used) is created
securely and cleaned up.
- Around line 53-65: The fallback loop currently treats any CodeRabbit comment
as a completed review; update the logic in the comments loop (variables:
comments, for c in comments, author, body) to ignore kickoff-only comments
emitted by the bot (e.g. messages containing "review triggered", "Review
triggered.", "kickoff", or other kickoff marker) by checking body
(case-insensitive) and continuing the loop instead of printing PASS/exit for
those; keep the existing rate-limit check ("rate limit") but only treat
non-kickoff CodeRabbit comments as a real review and then print PASS and exit.

In `@src/automation/mod.rs`:
- Around line 1128-1153: The error message in the StepResult created after
wait_for_prompt_activity_or_output incorrectly says "within 20s" while the call
uses Duration::from_secs(60); update the message string in that StepResult (the
one containing "implement_code retry did not start activity or produce output
within 20s") to reflect the actual timeout (e.g., "within 60s" or interpolate
the timeout), referencing the wait_for_prompt_activity_or_output call and the
StepResult construction so both stay consistent.

In `@src/permissions/mod.rs`:
- Around line 78-84: The helper suggested_prefix_rule currently returns None for
single-token command lines because it requires at least two tokens; update
suggested_prefix_rule(command_line: &str) so that if tokens.is_empty() it
returns None, if tokens.len() == 1 it returns Some(tokens[0].to_string()) and
otherwise returns Some(format!("{} {}", tokens[0], tokens[1])); this ensures
single-token suggestions (e.g., "make") are returned while preserving the
two-token behaviour used elsewhere.

In `@src/worktree/mod.rs`:
- Around line 199-212: git_current_branch currently returns the literal "HEAD"
in a detached HEAD state which breaks direct branch comparisons in
automation/mod.rs; change git_current_branch to detect when the output equals
"HEAD" and return an explicit detached indicator (e.g., change its signature to
return Result<Option<String>> where Ok(None) means detached) so callers (like
automation/mod.rs) can handle detached state explicitly (update callers to treat
None as detached or compare against commit SHA if they prefer); reference
git_current_branch, automation/mod.rs, and cli/up.rs when making the
adjustments.

In `@tests/fixtures/runtime/codex_idle_prompt_bar.txt`:
- Around line 1-16: The fixture codex_idle_prompt_bar.txt contains Claude UI
output and is unused by tests; either delete this file or replace its contents
with real Codex idle-prompt output and update/rename it to match intent (or
rename to claude_idle_prompt_bar.txt if it’s intended for Claude) so tests can
reference it; locate the fixture file (codex_idle_prompt_bar.txt) and either
remove it from the fixtures directory and any test fixture lists, or overwrite
its content with authentic Codex output matching the existing
claude_idle_prompt_bar.txt structure and ensure any tests/reference names are
updated accordingly.

In `@tutti.toml`:
- Around line 286-289: The "stop_idle_agents" command uses chained && in the run
string which aborts remaining stops if any "cargo run ... down" fails; update
the run for id "stop_idle_agents" so each agent stop is attempted independently
(e.g., separate sequential commands or use a no-fail postfix like "|| true" or
";" between the four cargo run down invocations) so planner, conductor, tester
and docs-release are each executed even if one fails.

---

Nitpick comments:
In `@src/automation/mod.rs`:
- Line 794: The variable `_auto_started_session` is declared and later assigned
but never read; remove its declaration and any assignments to
`_auto_started_session` (search for `_auto_started_session` in this module) to
eliminate the unused variable, or if you intend to keep it for future logic,
replace the declaration with a clearly marked TODO comment (e.g., `// TODO:
preserve _auto_started_session for future session-tracking`) so linters and
reviewers know it's intentional.

In `@src/cli/permissions.rs`:
- Around line 587-588: The test currently asserts that the persisted config
"saved" contains "echo blocked", which would still pass if a wildcard slip-in
like "echo blocked *" appears; change the assertion to check the exact persisted
content instead (e.g., compare saved to the exact expected string or assert
equality/trimming rather than using contains). Locate the test that reads the
file via global_config_path() (the variable named saved) and replace the loose
contains check with an exact match against the expected on-disk config text to
ensure wildcard removal is validated precisely.

In `@tutti.toml`:
- Around line 76-77: The inline complex bash in the run key (the cargo run ...
issue-claim acquire command with conditional ISSUE_MILESTONE handling and output
to .tutti/state/auto/selected_issue.json) should be extracted to a small
executable script (e.g., scripts/acquire-issue.sh) that accepts ISSUE_LABEL,
ISSUE_MILESTONE, lease TTL and output path as args or environment variables;
update the run value to call that script with the same env vars (ISSUE_LABEL,
ISSUE_MILESTONE, ISSUE_MILESTONE check moved into the script) so the conditional
logic and command composition live in a reusable, tested script rather than a
single long inline bash line.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b2f0e6da-23ad-4227-8122-88e975ba34fd

📥 Commits

Reviewing files that changed from the base of the PR and between 715c39c and b712479.

📒 Files selected for processing (27)
  • README.md
  • docs/CODEX_SDLC_ORCHESTRATION.md
  • docs/examples/tutti-codex-sdlc.toml
  • docs/pr-review-loop.md
  • scripts/automation/create_issue_branch.sh
  • scripts/automation/verify_branch_has_changes.py
  • scripts/automation/verify_clean_baseline.sh
  • scripts/automation/wait_coderabbit.sh
  • scripts/automation/write_implement_result.py
  • src/automation/mod.rs
  • src/cli/issue_claim.rs
  • src/cli/land.rs
  • src/cli/mod.rs
  • src/cli/permissions.rs
  • src/cli/runs.rs
  • src/cli/send.rs
  • src/cli/up.rs
  • src/health/mod.rs
  • src/main.rs
  • src/permissions/mod.rs
  • src/runtime/claude_code.rs
  • src/runtime/mod.rs
  • src/worktree/mod.rs
  • tests/fixtures/runtime/claude_idle_prompt_bar.txt
  • tests/fixtures/runtime/codex_idle_prompt_bar.txt
  • tests/fixtures/runtime/codex_idle_prompt_marker.txt
  • tutti.toml

Comment thread scripts/automation/verify_clean_baseline.sh Outdated
Comment thread scripts/automation/wait_coderabbit.sh Outdated
Comment thread scripts/automation/wait_coderabbit.sh
Comment thread src/automation/mod.rs
Comment thread src/permissions/mod.rs Outdated
Comment thread src/worktree/mod.rs Outdated
Comment thread tests/fixtures/runtime/codex_idle_prompt_bar.txt Outdated
Comment thread tutti.toml
Take main's idle detection (startup_grace, consecutive working polls)
from merged PR #71. Restore expanded Claude Code completion/idle
patterns (don't ask on, shift+tab to cycle) and working patterns
(Searching for, (thinking)) needed for reliable prompt-bar detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam
nutt-adam force-pushed the wren/pr-review-loop-merge-gate branch from c20e9af to 072af0b Compare March 19, 2026 08:06
- verify_clean_baseline.sh: pass branch.json path via argv instead of
  interpolating into Python string (shell injection risk)
- wait_coderabbit.sh: pipe PR JSON via stdin instead of argv (arg length
  limits); filter out kickoff-only CodeRabbit comments before declaring
  review available
- automation/mod.rs: fix stale "20s" in error message (actual timeout
  is 60s)
- permissions/mod.rs: return suggestion for single-token commands
  instead of None
- worktree/mod.rs: handle detached HEAD (returns "HEAD") by returning
  None instead of the literal string
- Remove misnamed unused fixture codex_idle_prompt_bar.txt
- tutti.toml: use semicolons instead of && for stop_idle_agents so all
  agents are stopped even if one is already down

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam

Copy link
Copy Markdown
Contributor

@coderabbitai I've addressed your requested changed. Re-review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown

@nutt-adam Sure! Kicking off a full re-review now to cover all the updated changes.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/land.rs (1)

35-44: ⚠️ Potential issue | 🔴 Critical

Run the merge gate against the final branch state.

maybe_enforce_merge_gate() executes before commit_wip_if_needed() and before the --pr push path. That lets plain land auto-commit fresh worktree changes after the PR was already approved/green, and it makes land --pr fail before it can create/update the PR the gate expects. Gate after the branch is in its final pushed state, or reject dirty worktrees when the gate is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/land.rs` around lines 35 - 44, The merge gate is being run too early;
move maybe_enforce_merge_gate(&resolved.project_root, &branch,
enforce_merge_gate) so it runs after commit_wip_if_needed(&worktree_path,
&resolved.agent_name) has finalized the worktree, and for the PR flow call
maybe_enforce_merge_gate only after push_and_open_pr(&resolved.project_root,
&branch) so the gate evaluates the final pushed branch state; additionally
ensure that when enforce_merge_gate is true you call
ensure_git_clean(&resolved.project_root) (or keep the existing check) before
running the gate to reject dirty worktrees rather than allowing auto-commits
after the gate check.
♻️ Duplicate comments (1)
src/automation/mod.rs (1)

1199-1205: ⚠️ Potential issue | 🟠 Major

Reuse the resolved runtime and configured timeout in the fallback retry path.

This retry block still re-derives runtime from agent.runtime with a "claude-code" fallback and then waits a fixed 3600s. That diverges from the already-resolved runtime/wait_timeout_secs, so agents inheriting a non-claude-code default runtime can be polled with the wrong adapter, and retries can hang for an hour despite a tighter workflow timeout.

Also applies to: 1245-1251, 2273-2278

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/automation/mod.rs` around lines 1199 - 1205, The fallback retry path
re-derives the adapter runtime and hardcodes a 3600s sleep, which diverges from
the already-resolved runtime and configured timeout; update the retry branches
to use the previously-resolved runtime variable (agent_runtime or runtime as
used earlier) and the resolved wait_timeout_secs instead of recalculating from
config.agents and using 3600, so polling uses the correct adapter and honors the
configured timeout across the retry logic (apply the same change for the other
occurrences referenced around the retry blocks).
🧹 Nitpick comments (2)
src/permissions/mod.rs (1)

78-84: Add a regression test for the single-token branch.

Line 82 adds important behavior (Some(token)), but there isn’t a direct test for a one-token command like make. Adding one prevents silent regressions.

Proposed test addition
 #[test]
 fn evaluate_command_policy_suggests_rule_from_first_two_tokens_only() {
     let policy = PermissionsConfig {
         allow: vec!["git status".to_string()],
     };
     let decision = evaluate_command_policy(Some(&policy), "cargo test --quiet --all");
     assert_eq!(decision.suggested_rule.as_deref(), Some("cargo test"));
 }
+
+#[test]
+fn evaluate_command_policy_suggests_single_token_rule() {
+    let policy = PermissionsConfig {
+        allow: vec!["git status".to_string()],
+    };
+    let decision = evaluate_command_policy(Some(&policy), "make");
+    assert_eq!(decision.suggested_rule.as_deref(), Some("make"));
+}

As per coding guidelines, "Write unit tests in each module using #[cfg(test)] mod tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/permissions/mod.rs` around lines 78 - 84, The function
suggested_prefix_rule has a single-token branch returning Some(token) but lacks
a regression test; add a unit test inside the module's #[cfg(test)] mod tests
that calls suggested_prefix_rule with a one-token command (e.g., "make") and
asserts it returns Some("make") to prevent future regressions—place the test
alongside other tests in the same file and use
assert_eq!(suggested_prefix_rule("make"), Some("make".to_string())) (referencing
the suggested_prefix_rule function name to locate where to add the test).
src/cli/runs.rs (1)

110-124: Add module tests for the new formatting helpers.

This file introduces output-shaping logic in truncate_run_id() and format_issue() but no local tests. A couple of narrow assertions here would lock the CLI output down cheaply.

As per coding guidelines, "Write unit tests in each module using #[cfg(test)] mod tests."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/runs.rs` around lines 110 - 124, Add a local test module
(#[cfg(test)] mod tests) in the same file that contains unit tests for
truncate_run_id and format_issue: write tests that assert truncate_run_id
returns the original string when length <= 12 and returns the first 9 chars +
"..." when longer; and tests for format_issue that cover: issue_title = None,
issue_title = Some("") (or whitespace) and issue_title = Some("Title") to verify
outputs "#<number>" and "#<number> <title>" respectively (construct a minimal
crate::state::SdlcRunLedgerRecord value for format_issue inputs), using
assert_eq! to validate results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Line 11: The .gitignore entry `.tutti-worktrees/` is incorrect because
worktrees are created under `.tutti/worktrees/<agent_name>` per
src/worktree/mod.rs, and the existing `.tutti/` rule already covers that path;
remove the `.tutti-worktrees/` line from .gitignore (or if a different directory
was intended, replace it with the correct directory name) so the ignore rules
accurately reflect the actual worktree path.

In `@docs/examples/tutti-codex-sdlc.toml`:
- Around line 74-80: The new workflow step with id "verify_clean_baseline" uses
cwd = "agent_worktree" for agent "implementer" but sdlc-smoke never boots or
ensures the implementer worktree exists; update the sdlc-smoke workflow to
bootstrap the implementer before any steps that use agent_worktree (e.g., add an
ensure_running / "tt up" or existing ensure step for implementer prior to the
"create_branch" and "verify_clean_baseline" steps) so src/automation/mod.rs's
agent_worktree resolver finds .tutti/worktrees/implementer.

In `@scripts/automation/create_issue_branch.sh`:
- Around line 33-52: The script's use of git clean -fd leaves ignored files in
place so the worktree can still differ from origin; update both occurrences of
git clean -fd to remove ignored files as well (e.g., git clean -fdx or git clean
-ffdx) so the post-clean truly matches origin/$BASE_BRANCH; keep the existing
git reset --hard and git checkout -B "$BRANCH" "origin/$BASE_BRANCH" flow and
leave the DIRTY check (git status --porcelain) and BASE_SHA assignment
unchanged.
- Around line 11-16: The script's default ISSUE_JSON and OUT_FILE currently
resolve under the agent worktree (breaking the no-arg flow consumed by
src/automation/mod.rs expecting project_root/.tutti/state/auto/branch.json);
update create_issue_branch.sh to compute defaults relative to the repository
top-level (use the existing TOPLEVEL from git rev-parse --show-toplevel) and set
ISSUE_JSON and OUT_FILE to "$TOPLEVEL/.tutti/state/auto/branch.json" (and the
corresponding .out path) while still allowing callers to override via
environment/args; modify any code paths in create_issue_branch.sh that currently
default to agent-relative paths to use these TOPLEVEL-based defaults so
src/automation/mod.rs consumers continue to find the file.

In `@scripts/automation/wait_coderabbit.sh`:
- Around line 31-34: The heredoc Python block is reading stdin but the script
also pipes DATA into python, causing the heredoc to win and json.load(sys.stdin)
to parse the Python source; fix by passing DATA via an environment variable or
temp file instead of piping: set/export the shell variable DATA before invoking
the heredoc (or write DATA to a temp file) and modify the Python here-doc in the
script (the block that calls python3 with <<'PY' and uses json.load(sys.stdin'))
to read os.environ['DATA'] (or open the temp file) and json.loads() so it parses
the actual JSON string from the DATA variable rather than the heredoc source.

In `@scripts/automation/write_implement_result.py`:
- Around line 49-57: The current logic only verifies base_sha is an ancestor of
HEAD, which still allows accidentally being on the wrong branch (e.g., main)
before pushing HEAD into target_branch; after retrieving commit_sha and before
using target_branch, retrieve the current branch name (e.g., via git rev-parse
--abbrev-ref HEAD or run_git equivalent), compare it to target_branch and abort
with an error if they differ, and keep the existing ancestor check
(variables/functions to touch: commit_sha, base_sha, target_branch, run_git,
GIT_TIMEOUT_SECONDS, and the is_ancestor check) so we never push an unintended
HEAD into the issue branch.

In `@src/automation/mod.rs`:
- Around line 2151-2219: The helpers wait_for_prompt_activity_or_output and
wait_for_prompt_output_file currently accept any existing output file via
path.exists(), which can ingest stale JSON on reruns; change them to record a
baseline (file nonexistence or its modified time/hash) before submitting the
prompt (or proactively remove the old file) and then only treat the path as
success if it either did not exist at baseline and now exists, or its mtime/hash
is newer/different than the baseline; update wait_for_prompt_activity_or_output
(use baseline_pane_hash logic pattern) and wait_for_prompt_output_file to check
the file's creation/modification timestamp or content hash against the baseline
and only return Ok when the file is updated after the attempt started.
- Around line 2263-2286: The call to health::wait_for_agent_idle in
start_and_wait_ready currently discards its Result (let _ = ...), allowing
startup failures to be ignored; change that call to propagate errors (e.g.,
remove "let _ =" and append ? to the health::wait_for_agent_idle(...)
invocation) so start_and_wait_ready returns Err when the agent fails to become
ready, ensuring callers cannot proceed on a half-started session.

In `@src/cli/issue_claim.rs`:
- Around line 466-476: The empty-result error in acquire currently reports only
the label and hides the milestone filter; update the error path after calling
gh_list_issues in acquire to include milestone (when Some) in the user-facing
message so it reports both label and milestone (or "no milestone" / None) —
update the error string construction in acquire (referencing variables
milestone, label and the gh_list_issues result) to show the effective filters so
automation misconfiguration is obvious.

In `@src/cli/runs.rs`:
- Around line 2-12: The import and call to load_active_runs in list() is invalid
because crate::state only exports load_sdlc_run_ledger; either add a
load_active_runs function to the state module that returns the same shape
expected by list(), or update list() to call the existing load_sdlc_run_ledger
(and adapt variable names/usage). Locate list(), replace the
load_active_runs(project_root)? call with a call to
load_sdlc_run_ledger(project_root)? (or implement a new pub fn
load_active_runs(...) in crate::state that delegates to load_sdlc_run_ledger) so
the symbol resolves and the return types match.

In `@tutti.toml`:
- Around line 79-93: The workflow fails because steps "create_branch" and
"verify_clean_baseline" run with cwd="agent_worktree" before the implementer
worktree exists; add a preliminary step that ensures/creates the implementer
worktree (call the same logic as ensure_implementer in src/automation/mod.rs)
and place it before the first agent_worktree usage (i.e., before the step with
id="create_branch"); update the workflow entries around ids "create_branch" and
"verify_clean_baseline" (and the similar blocks at the other ranges) to run only
after that new ensure-implementer step so .tutti/worktrees/implementer is
present when those commands run.

---

Outside diff comments:
In `@src/cli/land.rs`:
- Around line 35-44: The merge gate is being run too early; move
maybe_enforce_merge_gate(&resolved.project_root, &branch, enforce_merge_gate) so
it runs after commit_wip_if_needed(&worktree_path, &resolved.agent_name) has
finalized the worktree, and for the PR flow call maybe_enforce_merge_gate only
after push_and_open_pr(&resolved.project_root, &branch) so the gate evaluates
the final pushed branch state; additionally ensure that when enforce_merge_gate
is true you call ensure_git_clean(&resolved.project_root) (or keep the existing
check) before running the gate to reject dirty worktrees rather than allowing
auto-commits after the gate check.

---

Duplicate comments:
In `@src/automation/mod.rs`:
- Around line 1199-1205: The fallback retry path re-derives the adapter runtime
and hardcodes a 3600s sleep, which diverges from the already-resolved runtime
and configured timeout; update the retry branches to use the previously-resolved
runtime variable (agent_runtime or runtime as used earlier) and the resolved
wait_timeout_secs instead of recalculating from config.agents and using 3600, so
polling uses the correct adapter and honors the configured timeout across the
retry logic (apply the same change for the other occurrences referenced around
the retry blocks).

---

Nitpick comments:
In `@src/cli/runs.rs`:
- Around line 110-124: Add a local test module (#[cfg(test)] mod tests) in the
same file that contains unit tests for truncate_run_id and format_issue: write
tests that assert truncate_run_id returns the original string when length <= 12
and returns the first 9 chars + "..." when longer; and tests for format_issue
that cover: issue_title = None, issue_title = Some("") (or whitespace) and
issue_title = Some("Title") to verify outputs "#<number>" and "#<number>
<title>" respectively (construct a minimal crate::state::SdlcRunLedgerRecord
value for format_issue inputs), using assert_eq! to validate results.

In `@src/permissions/mod.rs`:
- Around line 78-84: The function suggested_prefix_rule has a single-token
branch returning Some(token) but lacks a regression test; add a unit test inside
the module's #[cfg(test)] mod tests that calls suggested_prefix_rule with a
one-token command (e.g., "make") and asserts it returns Some("make") to prevent
future regressions—place the test alongside other tests in the same file and use
assert_eq!(suggested_prefix_rule("make"), Some("make".to_string())) (referencing
the suggested_prefix_rule function name to locate where to add the test).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76982083-61b7-4199-ba81-2e116cd7abf2

📥 Commits

Reviewing files that changed from the base of the PR and between a1beda9 and 5bc0762.

⛔ Files ignored due to path filters (1)
  • docs/social-preview.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • .gitignore
  • README.md
  • docs/CODEX_SDLC_ORCHESTRATION.md
  • docs/examples/tutti-codex-sdlc.toml
  • docs/infographic-github.html
  • docs/infographic.html
  • docs/pr-review-loop.md
  • scripts/automation/create_issue_branch.sh
  • scripts/automation/verify_branch_has_changes.py
  • scripts/automation/verify_clean_baseline.sh
  • scripts/automation/wait_coderabbit.sh
  • scripts/automation/write_implement_result.py
  • src/automation/mod.rs
  • src/cli/issue_claim.rs
  • src/cli/land.rs
  • src/cli/mod.rs
  • src/cli/permissions.rs
  • src/cli/runs.rs
  • src/cli/up.rs
  • src/main.rs
  • src/permissions/mod.rs
  • src/runtime/claude_code.rs
  • src/runtime/mod.rs
  • src/worktree/mod.rs
  • tests/fixtures/runtime/claude_idle_prompt_bar.txt
  • tests/fixtures/runtime/codex_idle_prompt_marker.txt
  • tutti.toml

Comment thread .gitignore Outdated
Comment thread docs/examples/tutti-codex-sdlc.toml
Comment thread scripts/automation/create_issue_branch.sh
Comment thread scripts/automation/create_issue_branch.sh
Comment thread scripts/automation/wait_coderabbit.sh Outdated
Comment thread src/automation/mod.rs
Comment thread src/automation/mod.rs
Comment thread src/cli/issue_claim.rs
Comment thread src/cli/runs.rs
Comment thread tutti.toml
nutt-adam and others added 3 commits March 20, 2026 06:03
- .gitignore: remove redundant .tutti-worktrees/ rule (covered by .tutti/)
- CLAUDE.md: document CodeRabbit request-changes workflow
- create_issue_branch.sh: resolve defaults relative to PROJECT_ROOT
  not cwd; use git clean -ffdx for exact baseline including ignored files
- wait_coderabbit.sh: fix critical stdin conflict — pass JSON via env
  var instead of piping stdin into heredoc
- write_implement_result.py: validate current branch matches target
  before pushing HEAD
- docs/examples: add ensure_running for implementer before worktree
  steps in sdlc-smoke
- automation/mod.rs: delete stale output_json before sending prompt to
  prevent wait helpers from short-circuiting on leftover files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
land.rs: move merge gate after commit_wip_if_needed and push, so it
evaluates the final branch state instead of gating prematurely.

automation/mod.rs: use step's resolved runtime and wait_timeout_secs in
fallback retry path instead of re-deriving from config; propagate
wait_for_agent_idle errors in start_and_wait_ready with warning on
timeout; delete stale output_json before prompt send.

issue_claim.rs: include milestone in "no issues found" error message.

runs.rs: fix load_active_runs reference (add function to state module);
add issue_title, branch, failure_message fields to SdlcRunLedgerRecord;
add unit tests for truncate_run_id and format_issue.

permissions/mod.rs: add regression test for single-token command
suggestion.

tutti.toml + docs/examples: add ensure_running for implementer before
agent_worktree steps in sdlc-smoke workflow.

CLAUDE.md: document CodeRabbit request-changes workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam
nutt-adam merged commit 20f0089 into main Mar 19, 2026
10 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 20, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants