Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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
Expand Down Expand Up @@ -299,6 +300,7 @@ Reusable prompt components and skills are **phrases**. A phrase might be a CLAUD
- Run checkpoints persisted at `.tutti/state/workflow-checkpoints/<run_id>.json` + `tt run --resume <run_id>`
- 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:
Expand Down
5 changes: 4 additions & 1 deletion docs/CODEX_SDLC_ORCHESTRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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

Expand All @@ -72,4 +74,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)
7 changes: 7 additions & 0 deletions docs/examples/tutti-codex-sdlc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,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"
41 changes: 41 additions & 0 deletions docs/pr-review-loop.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 28 additions & 7 deletions src/automation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,10 +1004,13 @@ impl<'a> WorkflowExecutor<'a> {
let policy_decision =
evaluate_workflow_command_policy(policy_ctx, &rendered);
if !policy_decision.allowed {
let message = format!(
let mut message = format!(
"command blocked by permissions policy: '{}'",
policy_decision.command
);
if let Some(rule) = policy_decision.suggested_rule.as_deref() {
message.push_str(&format!(" (hint: add allow rule '{rule}')"));
}
match fail_mode {
WorkflowFailMode::Open => {
step_results.push(StepResult {
Expand Down Expand Up @@ -1462,7 +1465,7 @@ impl<'a> WorkflowExecutor<'a> {
}

match with_project_root(self.project_root, || {
crate::cli::land::run(agent, *pr, *force)
crate::cli::land::run_with_options(agent, *pr, *force, true)
}) {
Ok(()) => step_results.push(StepResult {
index: step_index,
Expand Down Expand Up @@ -1857,6 +1860,7 @@ fn evaluate_workflow_command_policy(
"step_index": ctx.step_index,
"command": decision.command,
"matched_rule": decision.matched_rule,
"suggested_rule": decision.suggested_rule,
})),
},
);
Expand Down Expand Up @@ -2692,7 +2696,8 @@ fn execute_control_step(
if *force {
args.push("--force".to_string());
}
let run_result = run_tt_subcommand(project_root, &args);
let run_result =
run_tt_subcommand_with_env(project_root, &args, &[("TT_ENFORCE_MERGE_GATE", "1")]);
match (run_result, *fail_mode) {
(Ok(_), _) => Ok(ControlStepOutcome {
index: step_index,
Expand Down Expand Up @@ -2753,11 +2758,21 @@ fn control_error_outcome(
}

fn run_tt_subcommand(project_root: &Path, args: &[String]) -> Result<()> {
run_tt_subcommand_with_env(project_root, args, &[])
}

fn run_tt_subcommand_with_env(
project_root: &Path,
args: &[String],
env: &[(&str, &str)],
) -> Result<()> {
let bin = std::env::current_exe()?;
let output = Command::new(bin)
.args(args)
.current_dir(project_root)
.output()?;
let mut cmd = Command::new(bin);
cmd.args(args).current_dir(project_root);
for (key, value) in env {
cmd.env(key, value);
}
let output = cmd.output()?;
if output.status.success() {
return Ok(());
}
Expand Down Expand Up @@ -3475,6 +3490,12 @@ mod tests {
.as_deref()
.is_some_and(|m| m.contains("blocked by permissions policy"))
);
assert!(
result.step_results[0]
.message
.as_deref()
.is_some_and(|m| m.contains("hint: add allow rule"))
);

let decisions = crate::state::load_policy_decisions(&dir).unwrap();
assert!(decisions.iter().any(|d| {
Expand Down
Loading
Loading