thirdshift 0.20.5

Turn a GitHub issue into a ready-for-review pull request, unattended
//! The compiled Architect Process, through the Pass's existing adapters so its
//! session identity, child asks, progress and endings stay with the Command.

use anyhow::{Context, Result, anyhow, bail, ensure};
use chrono::{DateTime, Utc};

use super::{DraftKind, load_inherited_builtin};
use crate::architect::{Outcome, Reviewed, conclude};
use crate::failed_run::FailedRun;
use crate::issue::Repo;
use crate::logs::Work;
use crate::pass::{Dispatch, Outside};
use crate::prompt;

#[cfg(test)]
mod tests;

/// The Command's already resolved choices, after its lock and skip gates.
pub(crate) struct ArchitectRun<'a> {
    pub repo: &'a Repo,
    pub base: &'a str,
    pub origin: &'a str,
    pub focus: Option<&'a str>,
    pub started: DateTime<Utc>,
    pub pull: bool,
    pub plan_only: bool,
}

pub(crate) fn run(outside: &mut impl Outside, run: &ArchitectRun<'_>) -> Outcome {
    match run_steps(outside, run) {
        Ok(outcome) => outcome,
        Err(failed) => Outcome::Ran {
            review: Err(failed),
            dispatched: None,
        },
    }
}

fn run_steps<O: Outside>(outside: &mut O, run: &ArchitectRun<'_>) -> Result<Outcome, FailedRun> {
    let steps = steps()?;
    outside.check_harness(!run.plan_only)?;
    outside.started(Work::ArchitectRun(run.repo));
    if run.pull {
        outside.pull();
    }
    if outside.interrupted() {
        return Err(FailedRun {
            interrupted: true,
            ..FailedRun::from(anyhow!("interrupted"))
        });
    }
    let mut reviewed = None;
    let mut dispatched = None;
    // Keep the plan reserved from before marking it Ready through the
    // dispatched run's ending, or until a plan-only run ends here.
    let mut _reservation = None;
    for step in steps {
        match step {
            DraftKind::ArchitectureReview { .. } => {
                let (review, reservation) = review(outside, run)?;
                _reservation = reservation;
                let end = run.plan_only || !matches!(review, Reviewed::PlanReady(..));
                reviewed = Some(review);
                if end {
                    break;
                }
            }
            DraftKind::RunPublishedIssue { .. } => {
                let Some(Reviewed::PlanReady(plan, _)) = &reviewed else {
                    return Err(anyhow!(
                        "architect-run needs an Architecture review before dispatch"
                    )
                    .into());
                };
                outside.step(format!(
                    "dispatching the plan {url}, as thirdshift {url} would",
                    url = plan.url
                ));
                dispatched = Some(outside.dispatch(Dispatch::ArchitectPlan {
                    plan,
                    base: run.base,
                }));
            }
            DraftKind::ReviewAndAddress { .. }
            | DraftKind::Skill { .. }
            | DraftKind::Command { .. }
            | DraftKind::Implement { .. }
            | DraftKind::SpecReview { .. }
            | DraftKind::SecurityReview { .. }
            | DraftKind::SecurityAudit { .. }
            | DraftKind::ReproduceFindings { .. }
            | DraftKind::Delivery { .. }
            | DraftKind::SelfMerge { .. }
            | DraftKind::LandTickets { .. }
            | DraftKind::FixFindings { .. }
            | DraftKind::CloseFindings { .. } => {
                unreachable!("the compiled Architect Steps were checked")
            }
        }
    }
    Ok(Outcome::Ran {
        review: Ok(reviewed.context("architect-run ended without an Architecture review")?),
        dispatched,
    })
}

fn review<O: Outside>(
    outside: &mut O,
    run: &ArchitectRun<'_>,
) -> Result<(Reviewed, Option<O::Reservation>), FailedRun> {
    let starting = match run.focus {
        Some(focus) => format!(
            "starting the Architecture review of {}, focused on: {focus}",
            run.base
        ),
        None => format!("starting the Architecture review of {}", run.base),
    };
    let prompt = prompt::architecture_review(run.base, run.focus);
    let (reviewed, log) = outside.review(run.base, starting, &prompt, |outside, final_message| {
        conclude(outside, final_message, run.origin, run.started, O::reserve)
    });
    reviewed.map_err(|error| FailedRun {
        log,
        ..FailedRun::from(error)
    })
}

fn steps() -> Result<Vec<DraftKind>> {
    let file = load_inherited_builtin("architect-run")?;
    // The adapter retains the Pass's checked choices and specialized review
    // worktree. Refuse settings it cannot carry out rather than ignore them.
    ensure!(
        file.worktrees.is_empty(),
        "architect-run owns its review worktree"
    );
    file.steps
        .into_iter()
        .map(|step| {
            ensure!(
                step.worktree.is_none() && step.process.is_none(),
                "architect-run Step {} must inherit the Command's choices and run in sequence",
                step.name
            );
            match step.kind {
                DraftKind::ArchitectureReview { .. } | DraftKind::RunPublishedIssue { .. } => {
                    Ok(step.kind)
                }
                DraftKind::ReviewAndAddress { .. }
                | DraftKind::Skill { .. }
                | DraftKind::Command { .. }
                | DraftKind::Implement { .. }
                | DraftKind::SpecReview { .. }
                | DraftKind::SecurityReview { .. }
                | DraftKind::SecurityAudit { .. }
                | DraftKind::ReproduceFindings { .. }
                | DraftKind::Delivery { .. }
                | DraftKind::SelfMerge { .. }
                | DraftKind::LandTickets { .. }
                | DraftKind::FixFindings { .. }
                | DraftKind::CloseFindings { .. } => {
                    bail!("unsupported architect-run Step {}", step.name)
                }
            }
        })
        .collect()
}