#[cfg(not(target_os = "linux"))]
compile_error!(
"thirdshift supports Linux only. On macOS, run it inside an Ubuntu 24.04 Linux VM (such as Lima). On Windows, use WSL2."
);
#[cfg(not(target_os = "linux"))]
fn main() {}
#[cfg(target_os = "linux")]
mod architect;
#[cfg(target_os = "linux")]
mod args;
#[cfg(target_os = "linux")]
mod asks;
#[cfg(target_os = "linux")]
mod base_fix;
#[cfg(target_os = "linux")]
mod branch;
#[cfg(target_os = "linux")]
mod child_run;
#[cfg(target_os = "linux")]
mod ci;
#[cfg(target_os = "linux")]
mod claim;
#[cfg(target_os = "linux")]
mod command;
#[cfg(target_os = "linux")]
mod command_limit;
#[cfg(target_os = "linux")]
mod config;
#[cfg(target_os = "linux")]
mod console;
#[cfg(target_os = "linux")]
mod cronjobs;
#[cfg(target_os = "linux")]
mod delivery;
#[cfg(target_os = "linux")]
mod email;
#[cfg(target_os = "linux")]
mod evals;
#[cfg(target_os = "linux")]
mod failed_run;
#[cfg(target_os = "linux")]
mod fix;
#[cfg(target_os = "linux")]
mod git;
#[cfg(target_os = "linux")]
mod github;
#[cfg(target_os = "linux")]
mod harness;
#[cfg(target_os = "linux")]
mod host;
#[cfg(target_os = "linux")]
mod implement;
#[cfg(target_os = "linux")]
mod interrupt;
#[cfg(target_os = "linux")]
mod issue;
#[cfg(target_os = "linux")]
mod labels;
#[cfg(target_os = "linux")]
mod launch;
#[cfg(target_os = "linux")]
mod local_time;
#[cfg(target_os = "linux")]
mod logs;
#[cfg(target_os = "linux")]
mod notification;
#[cfg(target_os = "linux")]
mod otlp_jsonl;
#[cfg(target_os = "linux")]
mod pass;
#[cfg(target_os = "linux")]
mod pickup;
#[cfg(target_os = "linux")]
mod poll;
#[cfg(target_os = "linux")]
mod process;
#[cfg(target_os = "linux")]
mod processes;
#[cfg(target_os = "linux")]
mod progress;
#[cfg(target_os = "linux")]
mod prompt;
#[cfg(test)]
#[cfg(target_os = "linux")]
mod prompts_page;
#[cfg(target_os = "linux")]
mod proxy;
#[cfg(target_os = "linux")]
mod pull_request;
#[cfg(target_os = "linux")]
mod ready;
#[cfg(target_os = "linux")]
mod recovery;
#[cfg(target_os = "linux")]
mod resend_key;
#[cfg(target_os = "linux")]
mod reservation;
#[cfg(target_os = "linux")]
mod resources;
#[cfg(target_os = "linux")]
mod review;
#[cfg(target_os = "linux")]
mod run;
#[cfg(target_os = "linux")]
mod run_ending;
#[cfg(target_os = "linux")]
mod sandbox;
#[cfg(target_os = "linux")]
mod scope;
#[cfg(target_os = "linux")]
mod scoring;
#[cfg(target_os = "linux")]
mod security;
#[cfg(target_os = "linux")]
mod session;
#[cfg(target_os = "linux")]
mod setup;
#[cfg(target_os = "linux")]
mod skill;
#[cfg(target_os = "linux")]
mod skills;
#[cfg(target_os = "linux")]
mod spec_run;
#[cfg(target_os = "linux")]
mod terminal;
#[cfg(test)]
#[cfg(target_os = "linux")]
mod test_support;
#[cfg(target_os = "linux")]
mod triage;
#[cfg(target_os = "linux")]
mod trial;
#[cfg(target_os = "linux")]
mod trust;
#[cfg(target_os = "linux")]
mod update;
#[allow(dead_code)]
#[cfg(target_os = "linux")]
mod verdict;
#[cfg(target_os = "linux")]
mod worktree;
#[cfg(target_os = "linux")]
use std::process::ExitCode;
#[cfg(target_os = "linux")]
use architect::Outcome;
#[cfg(target_os = "linux")]
use args::{ArchitectArgs, Command, PassArgs, RunArgs};
#[cfg(target_os = "linux")]
use asks::Asks;
#[cfg(target_os = "linux")]
use command::{Ending, failure};
#[cfg(target_os = "linux")]
use config::UserConfig;
#[cfg(target_os = "linux")]
use logs::Begin;
#[cfg(target_os = "linux")]
use notification::About;
#[cfg(target_os = "linux")]
use pickup::Took;
#[cfg(target_os = "linux")]
use run::StartedBy;
#[cfg(target_os = "linux")]
const HELP: &str = "\
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 [lowest-first|highest-first] Take the Ready issue at that end of the issue numbers (default lowest-first)
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 review <PR URL> | --author <login> Review and fix one PR; --recommend-only posts findings
thirdshift fix [<PR URL or number>] Repair conflicts and CI; no target selects a PR Pass
thirdshift triage [--recommend-only] [--file <path>] [--read-machine] Investigate a Report and file triaged issues
thirdshift skill <skill> [issue <issue>] Run one skill and print its final message
thirdshift process [list] | processes List Processes, marking Built-in processes (experimental)
thirdshift process show <name> Print a Process file exactly (experimental)
thirdshift process <name> [issue <issue>] Run a Custom process; pull-request <PR URL> selects a pull request (experimental)
thirdshift process check <name> [issue <issue>] Print Step choices and startup results; also accepts pull-request <PR URL> (experimental)
thirdshift score Sample merged factory work and propose Bug origins
thirdshift scores [<pull request>] [--mine] [--json] Show Scores; --mine enters yours
thirdshift bug-origins [<issue>] [--json] Show origins; --confirm / --correct <pr> / --none settles one
thirdshift email-test [<address>] Send a test email through Resend
thirdshift setup Choose defaults and write the User config
thirdshift evals <action> Manage Eval data and Arms, including import-replays; see the manual
thirdshift experiment [--release] <candidate> [options] Compare Arms; --release uses the last tag
thirdshift experiments [<id>] [--json] List Experiments or show one; <id> --name <text> renames it; <id> --record-difficulty records A/A Difficulty
thirdshift recheck <id> <trial>|--pending|--batch ... Review or record Score rechecks; see the manual
thirdshift status [--all] [--json] List this machine's Commands
thirdshift cronjobs [--json] List this account's thirdshift cron jobs
thirdshift resources [--json] Show host memory, CPU load and session limits
thirdshift console [--no-sign-in] Serve this machine's Console on localhost
thirdshift console install [--output <file>] Print, or save, 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 token <create|list|revoke> ... | scopes ... Manage tokens or address scopes; CLI-only
thirdshift console whoami [--json] | openapi Identify caller or print the API's OpenAPI description
thirdshift console theme <name|list|import|export> ... Choose, list, import or export Console Themes
thirdshift console sections [--hide <section>] [--show <section>] [--json] Choose visible sections
thirdshift console other-accounts [none|readable|name,name] [--json] Show or set local account visibility
thirdshift show <command> [--spans | --span <id>] [--json] Show a Command, its tree or a span
thirdshift metrics [<command>] [--json] Show Run metrics or weekly trends
thirdshift stop <command> [--requeue] [--json] Stop a live Command and wait for cleanup
thirdshift dismiss <command> [--undo] [--json] Dismiss an Inbox row, or take it back
thirdshift logs [--command <id>] [--json] List this machine's logs
thirdshift logs --file <id> [--follow | --json] Read a log
thirdshift logs <command> [--session <kind>] [--follow | --json] Read a log; --export <dir> writes its whole Trace
thirdshift update Update to the latest release
thirdshift version Print the version
thirdshift help Print this reference
Start from the issue's clone. The Base branch is the branch checked out; positive issue numbers use origin, even in nested directories.
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; -- prefixes accepted, once each, without opposites):
merge / no-merge Merge the pull request / leave it for review
email [<address>] / no-email Send a Run notification / skip it (email-unless-merged [<address>]: unless merged)
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>, effort <level> Choose the Harness's Model and Effort
security-review / no-security-review Enable / disable a Ticket or Review run's Security review
security-fix / no-security-fix Allow / forbid fixing reproduced Security findings (security-close / no-security-close: closing after merge defaults on)
Pass commands: architect, pickup, secure, review --author <login>, fix. For architect, pickup, secure and fix, base <branch> selects an origin branch; fix also accepts author <login>.
--plan-only architect or a Custom process's architecture-review; publish and stop (architect: only base and email flags apply)
Defaults: ~/.thirdshift/config.toml; thirdshift setup writes every setting; commands override User config defaults. Full manual:
https://thirdshift.app/docs/
";
#[cfg(target_os = "linux")]
fn main() -> ExitCode {
child_run::name_this_process();
let args: Vec<String> = std::env::args().skip(1).collect();
if let Some(result) = setup::sandbox::child(&args) {
return result;
}
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::Score) => return score(),
Ok(Command::BugOrigins(options)) => return printed(scoring::origins::execute(options)),
Ok(Command::Scores {
pull_request,
mine,
json,
}) => {
return printed(scoring::show(pull_request.as_deref(), mine, json));
}
Ok(Command::Evals(action)) => return printed(evals::execute(action)),
Ok(Command::Experiment(options)) => {
return match evals::experiment::run(options) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Experiments(args)) => return printed(evals::experiment::show(&args)),
Ok(Command::Recheck(args)) => return printed(evals::experiment::recheck(&args)),
Ok(Command::CronJobs { json }) => return printed(cronjobs::show(json)),
Ok(Command::Resources { json }) => return printed(resources::show(json)),
Ok(Command::ConsoleInstall { output: None }) => {
return match console::install() {
Ok(script) => {
print!("{script}");
ExitCode::SUCCESS
}
Err(error) => failure(&error),
};
}
Ok(Command::ConsoleInstall { output: Some(path) }) => {
return outcome(console::install_to(std::path::Path::new(&path)));
}
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::ConsoleOtherAccounts { value, json }) => {
return printed(config::console::other_accounts(value.as_deref(), json));
}
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::ConsoleToken(action)) => {
return match console::tokens::run(action) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::ConsoleWhoami { json }) => return printed(console::whoami(json)),
Ok(Command::ConsoleScopes(command)) => return printed(command.run()),
Ok(Command::ConsoleOpenApi) => return printed(console::openapi()),
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::Dismiss {
command,
undo,
json,
}) => {
return match logs::dismiss::dismiss(&command, undo, json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Spans {
command,
span,
json,
}) => {
return match logs::spans::show(&command, span.as_deref(), json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::Metrics(asked)) => {
return match logs::metrics::show(&asked) {
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,
json,
}) => {
return match logs::status::inspect::logs(&command, session.as_deref(), follow, json) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => failure(&error),
};
}
Ok(Command::LogsExport { command, directory }) => {
return match logs::export::export(&command, &directory) {
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, json }) => {
return match logs::catalog::print_file(&id, follow, json) {
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::Skill { name, issue }) => return printed(skill::run(&name, issue)),
Ok(Command::Review(command)) => return printed(processes::review::run(command)),
Ok(Command::Fix(command)) => return fix(command),
Ok(Command::Process(action)) => {
let verbatim = matches!(action, args::ProcessAction::Show(_));
return match processes::execute(action) {
Ok(text) if verbatim => {
print!("{text}");
ExitCode::SUCCESS
}
Ok(text) if text.is_empty() => ExitCode::SUCCESS,
other => printed(other),
};
}
Ok(Command::Trial(request)) => return printed(trial::run(&request)),
Ok(Command::OomExec { program, args }) => match scope::oom_exec(&program, &args) {
Ok(()) => return ExitCode::SUCCESS,
Err(error) => return failure(&error),
},
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::Triage(args)) => return triage(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 _suppress_otlp = given
.as_ref()
.filter(|given| given.suppress_otlp)
.map(|_| logs::SuppressOtlp::new());
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 process = given
.as_ref()
.and_then(|given| given.process.as_deref())
.unwrap_or("ticket-run");
let ended = run::run_process_to_end(&issue, &mut asks, started_by, process);
started.built_with(&asks.harness);
started.finish(Ending::Run(ended))
}
#[cfg(target_os = "linux")]
fn fix(args: args::FixArgs) -> ExitCode {
let (config, mut started) = match command::start(
Begin::FixRun {
pass: matches!(args.target, fix::Target::Pass { .. }),
},
|config| args.flags.notification(config),
About::FixRun,
) {
Ok(started) => started,
Err(failure) => return failure,
};
let mut harness = args.flags.harness(&config);
let ended = fix::run(&args, &config, &mut harness);
started.built_with(&harness);
started.finish(ended)
}
#[cfg(target_os = "linux")]
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 },
})
}
#[cfg(target_os = "linux")]
fn triage(args: args::TriageArgs) -> ExitCode {
let report = match triage::Report::read(args.file.as_deref()) {
Ok(report) => report,
Err(error) => return failure(&error),
};
let flags = asks::Flags {
email: args.email,
harness: args.harness,
..Default::default()
};
let (config, mut started) = match command::start(
Begin::TriageRun,
|config| flags.notification(config),
About::TriageRun,
) {
Ok(started) => started,
Err(failure) => return failure,
};
let mut harness = flags.harness(&config);
started.built_with(&harness);
let options = triage::Options {
recommend_only: args.recommend_only,
allow_ready_for_agent: args.allow_ready_for_agent || config.triage_allow_ready_for_agent,
read_machine: args.read_machine
|| launch::repo().is_ok_and(|repo| config.triage_reads_machine(&repo)),
};
started.finish(Ending::Triage(triage::run(&report, options, &mut harness)))
}
#[cfg(target_os = "linux")]
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 order = args.order.unwrap_or(config.pickup_order);
let took = pickup::run(
args.base.as_deref(),
&args.flags,
&config,
&mut harness,
order,
);
started.built_with(&harness);
match took {
Ok(pickup::Outcome::Took(Took {
issue,
is_spec,
title,
ended,
})) => {
started.took(&issue, title, is_spec);
started.finish(Ending::Run(ended))
}
Ok(pickup::Outcome::Skipped(skipped)) => started.finish(Ending::Skipped(skipped.into())),
Err(error) => failure(&error),
}
}
#[cfg(target_os = "linux")]
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,
finding,
audit,
close,
} => Ending::SecurityFix {
ended,
finding,
audit,
close,
},
})
}
#[cfg(target_os = "linux")]
fn score() -> ExitCode {
let _lock = match scoring::lock() {
Ok(Some(lock)) => lock,
Ok(None) => {
progress::step("Scoring run skipped: another Scoring run is running on this machine");
return ExitCode::SUCCESS;
}
Err(error) => return failure(&error),
};
let started_at = chrono::Utc::now();
let (config, started) = match command::start(
Begin::ScoringRun,
|_| notification::NotificationAsk::Skip,
About::ScoringRun,
) {
Ok(started) => started,
Err(failure) => return failure,
};
started.finish(Ending::Scoring(
scoring::sample(&config, started_at).map_err(Into::into),
))
}
#[cfg(target_os = "linux")]
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)?;
resources::wait()?;
harness::write_summary(&config.harness, &prompt)
})();
match summary {
Ok(summary) => {
println!("{summary}");
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
#[cfg(target_os = "linux")]
fn printed(result: anyhow::Result<String>) -> ExitCode {
match result {
Ok(text) => {
if !text.is_empty() {
println!("{text}");
}
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
#[cfg(target_os = "linux")]
fn outcome(result: anyhow::Result<impl std::fmt::Display>) -> ExitCode {
match result {
Ok(outcome) => {
progress::step(outcome);
ExitCode::SUCCESS
}
Err(error) => failure(&error),
}
}
#[cfg(target_os = "linux")]
fn argument_error(error: std::fmt::Arguments) -> ExitCode {
progress::step(error);
eprint!("\n{HELP}");
ExitCode::from(2)
}