mod architect;
mod args;
mod asks;
mod base_fix;
mod branch;
mod child_run;
mod ci;
mod claim;
mod command;
mod config;
mod console;
mod delivery;
mod email;
mod failed_run;
mod git;
mod github;
mod harness;
mod host;
mod interrupt;
mod issue;
mod labels;
mod launch;
mod logs;
mod notification;
mod pass;
mod pickup;
mod poll;
mod process;
mod progress;
mod prompt;
#[cfg(test)]
mod prompts_page;
mod pull_request;
mod ready;
mod recovery;
mod resend_key;
mod run;
mod run_ending;
mod security;
mod session;
mod setup;
mod skills;
mod spec_run;
mod terminal;
#[cfg(test)]
mod test_support;
mod trust;
mod update;
mod worktree;
use std::process::ExitCode;
use architect::Outcome;
use args::{ArchitectArgs, Command, PassArgs, RunArgs};
use asks::Asks;
use command::{Ending, failure};
use config::UserConfig;
use logs::Begin;
use notification::About;
use pickup::Took;
use run::StartedBy;
const HELP: &str = "\
thirdshift turns a GitHub issue into a ready-for-review pull request, unattended.
usage: thirdshift <Issue URL> Run the factory on the issue
thirdshift <number> Resolve the issue from origin and run
thirdshift merge <number> Run it, then merge the pull request
thirdshift merge <Issue URL> Run, then merge the pull request
thirdshift --no-merge <Issue URL> Run, leaving the pull request for review
thirdshift --email <Issue URL> Run, then email the outcome
thirdshift architect [<focus>] Review architecture, publish a plan and run it
thirdshift architect [<focus>] --plan-only Publish a plan and stop
thirdshift architect base <branch> [<focus>] Use <branch> as the Base branch
thirdshift pickup Take the lowest-numbered Ready issue and run it
thirdshift pickup base <branch> Use <branch> as the Base branch
thirdshift secure Audit and record findings privately
thirdshift secure base <branch> Use <branch> as the Base branch
thirdshift email-test [<address>] Send a test email through Resend
thirdshift setup Choose defaults and write the User config
thirdshift status [--all] [--json] List this machine's Commands
thirdshift console [--no-sign-in] Serve this machine's Console on localhost
thirdshift console install Print a service and proxy install script for sudo
thirdshift console password [--email <address>] Set a Console password privately
thirdshift console sign-out [--email <address>] [--json] End an account's sessions
thirdshift console log [--json] Read this machine's Console log
thirdshift console theme <name|list> [--json] Choose or list Console Themes
thirdshift console theme import <file> Import a Theme JSON document
thirdshift console theme export <name> Export a Theme as JSON
thirdshift console sections [--hide <section>] [--show <section>] [--json] Choose visible sections
thirdshift show <command> [--json] Show one Command in full
thirdshift stop <command> [--requeue] [--json] Stop a live Command and wait for cleanup
thirdshift logs [--command <id>] [--json] List this machine's logs
thirdshift logs --file <id> [--follow] Read an exact listed log
thirdshift logs <command> [--session <kind>] [--follow] Read or follow a log
thirdshift update Update to the latest release
thirdshift version Print the version
thirdshift help Print this reference
Start from the issue's repository clone. The Base branch is the branch checked out.
Use a positive decimal number anywhere an Issue URL is accepted, e.g. thirdshift merge 106.
Numbers use origin's GitHub repository, including from nested directories in the clone.
An issue with sub-issues starts a Spec run: its Tickets merge into a Spec branch.
merge on a Spec merges the Spec PR. <focus> is one quoted free-text argument.
Shared options (before or after the Issue URL or number, or after a Pass command):
merge / no-merge Merge the pull request / leave it for review
email [<address>] / no-email Send a Run notification / skip it
base-fix / no-base-fix Allow / forbid fixing inherited CI failures
parallel <n> Run up to <n> Tickets at once (Spec runs; default 3)
harness <name> Choose claude, codex, agy, grok, muse, opencode
model <name> Choose the Harness's Model
effort <level> Choose the Harness's Effort
security-review / no-security-review Enable / disable a Run's Security review
security-fix / no-security-fix Allow / forbid fixing reproduced Security findings
Options also accept a -- prefix. Each option may be given once; opposite options
cannot be combined. Command options override the User config's defaults.
Pass commands: architect, pickup, secure (no Issue URL).
base <branch> Use an origin branch, whatever is checked out
--plan-only architect only; stops before dispatching a Run
With --plan-only, only base, email and no-email apply.
Defaults: ~/.thirdshift/config.toml; thirdshift setup writes every setting.
For workflow details, settings, Harness setup, email, logs and scheduling, see the README:
https://github.com/JacobStephens2/thirdshift#readme
";
fn main() -> ExitCode {
child_run::name_this_process();
let args: Vec<String> = std::env::args().skip(1).collect();
let RunArgs {
issue,
flags,
given,
} = match args::parse(&args) {
Ok(Command::Help) => {
print!("{HELP}");
return ExitCode::SUCCESS;
}
Ok(Command::Version) => {
println!("thirdshift {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
Ok(Command::Update) => return outcome(update::update()),
Ok(Command::Setup) => return outcome(setup::setup()),
Ok(Command::ConsoleInstall) => {
return match console::install() {
Ok(script) => {
print!("{script}");
ExitCode::SUCCESS
}
Err(error) => failure(&error),
};
}
Ok(Command::ConsolePassword(email)) => return outcome(console::password(email.as_deref())),
Ok(Command::ConsoleTheme { name, json }) => {
return printed(console::theme(name.as_deref(), json));
}
Ok(Command::ConsoleThemeImport(path)) => return printed(console::import_theme(&path)),
Ok(Command::ConsoleThemeExport(name)) => return printed(console::export_theme(&name)),
Ok(Command::ConsoleSections { hide, show, json }) => {
return printed(console::sections(&hide, &show, json));
}
Ok(Command::ConsoleSignOut { email, json }) => {
return match console::sign_out(email.as_deref(), json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::ConsoleLog { json }) => {
return match console::print_log(json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Console { no_sign_in }) => {
return match console::serve(no_sign_in) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Stop {
command,
requeue,
json,
}) => {
return match logs::stop::stop(&command, requeue, json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Show { command, json }) => {
return match logs::status::inspect::show(&command, json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Logs {
command,
session,
follow,
}) => {
return match logs::status::inspect::logs(&command, session.as_deref(), follow) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::LogsList { json, owner }) => {
return match logs::catalog::show(json, owner.as_deref()) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::LogFile { id, follow }) => {
return match logs::catalog::print_file(&id, follow) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Status { json, all }) => {
return match logs::status::show(json, all) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::ReleaseSummary) => return release_summary(),
Ok(Command::EmailTest(to)) => {
return outcome(
UserConfig::load().and_then(|config| email::send_test(to, &config.email)),
);
}
Ok(Command::Architect(architect_args)) => return architect(architect_args),
Ok(Command::Pickup(pickup_args)) => return pickup(pickup_args),
Ok(Command::Secure(args)) => return secure(args),
Ok(Command::Run(run_args)) => run_args,
Err(error) => return argument_error(format_args!("{error:#}")),
};
let issue = match launch::resolve_issue(issue) {
Ok(issue) => issue,
Err(error) => return failure(&error),
};
let begin = match &given {
Some(given) => Begin::ChildRun(&given.stamp, given.command),
None => Begin::Run(&issue),
};
let asks_of = |config: &UserConfig| match &given {
Some(given) => Asks::of_child_run(given, config),
None => Asks::of_run(&issue, &flags, config),
};
let ask = |config: &UserConfig| asks_of(config).notification;
let (config, mut started) = match command::start(begin, ask, About::Issue(&issue)) {
Ok(started) => started,
Err(failure) => return failure,
};
let mut asks = asks_of(&config);
let started_by = given
.as_ref()
.map_or(StartedBy::Command, |given| StartedBy::Child(&given.kind));
let ended = run::run_to_end(&issue, &mut asks, started_by);
started.built_with(&asks.harness);
started.finish(Ending::Run(ended))
}
fn architect(args: ArchitectArgs) -> ExitCode {
let ask = |config: &UserConfig| args.flags.notification(config);
let (config, mut started) = match command::start(Begin::ArchitectRun, ask, About::ArchitectRun)
{
Ok(started) => started,
Err(failure) => return failure,
};
let mut harness = args.flags.harness(&config);
let outcome = architect::run(
args.focus.as_deref(),
args.base.as_deref(),
args.plan_only,
&args.flags,
&config,
&mut harness,
);
started.built_with(&harness);
started.finish(match outcome {
Outcome::Skipped(skipped) => Ending::Skipped(skipped.into()),
Outcome::Ran { review, dispatched } => Ending::Architect { review, dispatched },
})
}
fn pickup(args: PassArgs) -> ExitCode {
let ask = |config: &UserConfig| args.flags.notification(config);
let (config, mut started) = match command::start(Begin::PickupRun, ask, About::PickupRun) {
Ok(started) => started,
Err(failure) => return failure,
};
let mut harness = args.flags.harness(&config);
let took = pickup::run(args.base.as_deref(), &args.flags, &config, &mut harness);
started.built_with(&harness);
match took {
Ok(pickup::Outcome::Took(Took {
issue,
title,
ended,
})) => {
started.took(&issue, title);
started.finish(Ending::Run(ended))
}
Ok(pickup::Outcome::Skipped(skipped)) => started.finish(Ending::Skipped(skipped.into())),
Err(error) => failure(&error),
}
}
fn secure(args: PassArgs) -> ExitCode {
let (config, mut started) = match command::start(
Begin::SecurityRun,
|config| args.flags.notification(config),
About::SecurityRun,
) {
Ok(started) => started,
Err(failure) => return failure,
};
let mut harness = args.flags.security_harness(&config);
let outcome = security::run(args.base.as_deref(), &args.flags, &config, &mut harness);
started.built_with(&harness);
started.finish(match outcome {
security::Outcome::Skipped(skipped) => Ending::Skipped(command::Skip {
reason: skipped.to_string(),
urls: Vec::new(),
}),
security::Outcome::Audited(audited) => Ending::Security(audited),
security::Outcome::Fixed { ended, findings } => Ending::SecurityFix { ended, findings },
})
}
fn release_summary() -> ExitCode {
use std::io::Read;
let summary = (|| -> anyhow::Result<String> {
interrupt::install()?;
let config = UserConfig::load()?;
let mut prompt = String::new();
std::io::stdin().read_to_string(&mut prompt)?;
harness::write_summary(&config.harness, &prompt)
})();
match summary {
Ok(summary) => {
println!("{summary}");
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
fn printed(result: anyhow::Result<String>) -> ExitCode {
match result {
Ok(text) => {
println!("{text}");
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
fn outcome(result: anyhow::Result<impl std::fmt::Display>) -> ExitCode {
match result {
Ok(outcome) => {
progress::step(outcome);
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
fn argument_error(error: std::fmt::Arguments) -> ExitCode {
progress::step(error);
eprint!("\n{HELP}");
ExitCode::from(2)
}