Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Use `thirdshift help` for the command reference, or read [Ticket run behavior](d
| A security audit | `thirdshift secure` | A [Security run](docs/guide.md#security-runs) records findings privately; fixing a reproduced finding is optional. |
| Merged factory work | `thirdshift score` | A [Scoring run](docs/guide.md#scoring-runs) samples and grades it, and proposes Bug origins; `thirdshift scores --json` lists samples, five criterion Scores, calibration and proposals by week; `thirdshift scores <pull request> --mine` records yours before revealing the model's. |
| One skill | `thirdshift skill <skill> [issue <issue>]` | A [standalone skill](docs/guide.md#standalone-skills) runs headlessly and prints its final message. |
| A Custom process | `thirdshift process <name> [issue <issue>]` | An experimental [Custom process](docs/guide.md#custom-processes) runs skill or command Steps from `~/.thirdshift/processes/<name>.toml`, in named throwaway worktrees when the file declares them. An optional Step can fail without stopping the process. `issue <issue>` gives every session Step that issue. With no issue it is a Pass and skips while another Pass on the repository is running. |
| A Custom process | `thirdshift process <name> [issue <issue>]` | An experimental [Custom process](docs/guide.md#custom-processes) runs skill or command Steps from `~/.thirdshift/processes/<name>.toml`, in named throwaway worktrees when the file declares them. An optional Step can fail without stopping the process. `issue <issue>` gives every session Step that issue. With no issue it is a Pass and skips while another Pass on the repository is running. Status, the Console and the Activity log list it as Process `<name>`. A failed one is in Needs you, and it sends a Run notification under the same email rules as a Ticket run. A skipped Process sends none. |

Each Architect, Pickup or Security run makes one pass. Use cron or another operating system scheduler to repeat it. See [scheduling](docs/guide.md#on-a-schedule), [Architect runs every few minutes](docs/guide.md#architect-runs-every-few-minutes) and [Security runs on a schedule](docs/guide.md#security-runs-on-a-schedule-with-fixing-allowed).

Expand Down
13 changes: 12 additions & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ thirdshift secure # audit the Base branch and record findings
thirdshift secure base <branch> # do that with <branch> as the Base branch, from a clone on any branch
thirdshift skill <skill> [issue <issue>] # run one skill headlessly and print its final message
thirdshift process # list Custom processes (experimental)
thirdshift process <name> [issue <issue>] # run a Custom process, optionally on an issue (experimental)
thirdshift process <name> [issue <issue>] # run a Custom process, optionally on an issue; email flags match a Ticket run (experimental)
thirdshift process check <name> # validate one and print its plan (experimental)
thirdshift score # sample merged factory pull requests (see Scoring runs)
thirdshift scores [<pull request>] [--json] # show Scores and calibration
Expand Down Expand Up @@ -1003,6 +1003,17 @@ command that exits non-zero is failed, and the line names its exit status.
Stop and Ctrl-C interrupt a command Step the way they interrupt a session,
and `[resources]` is checked before each Step.

`thirdshift status` and `status --json` list a running or finished Process as
Process `<name>`, with the ending succeeded, failed, skipped or interrupted.
The stored kind stays `Process`, and the selector used by `show` and `stop`
stays `process-<name>-<stamp>`. `thirdshift show` prints each Step's Command
log line and, when that Step wrote a result, the path of the result file. The
Console lists the Process, and a failed one is in Needs you. The Activity log
says Process `<name>`. A succeeded, failed or interrupted Process sends a Run
notification under the same `email`, `email-unless-merged` and `no-email`
flags and the same User config rules as a Ticket run. A missing address or
Resend key stops the Process before any Step. A skipped Process sends none.

A Custom process started with no issue is a [Pass](#one-at-a-time). While another Pass on the repository is still running, its Spec run or Ticket run included, `thirdshift process <name>` is skipped: it prints `another Pass is already running on <owner>/<repo>`, prints nothing on stdout, exits `0` and sends no Run notification. It creates no worktree and starts no Step. While it runs, a Pickup run on the same repository is skipped. It does not take an Architect run's Ready-issue or Architect-idea gates, so a Ready issue or an Architect idea waiting for triage does not skip it. The Command record's outcome is `skipped`, and its cause is that reason. The Activity log records `Process <name> skipped: <reason>` when that is not already the last Process line. With `activity.quiet_skips` set, a skipped Process prints nothing. `thirdshift process <name> issue <issue>` is not a Pass: this rule does not skip it, and it does not hold the lock.

### User config
Expand Down
12 changes: 11 additions & 1 deletion site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@
thirdshift secure base &lt;branch&gt; # do that with &lt;branch&gt; as the Base branch, from a clone on any branch
thirdshift skill &lt;skill&gt; [issue &lt;issue&gt;] # run one skill headlessly and print its final message
thirdshift process # list Custom processes (experimental)
thirdshift process &lt;name&gt; [issue &lt;issue&gt;] # run a Custom process, optionally on an issue (experimental)
thirdshift process &lt;name&gt; [issue &lt;issue&gt;] # run a Custom process, optionally on an issue; email flags match a Ticket run (experimental)
thirdshift process check &lt;name&gt; # validate one and print its plan (experimental)
thirdshift score # sample merged factory pull requests (see Scoring runs)
thirdshift scores [&lt;pull request&gt;] [--json] # show Scores and calibration
Expand Down Expand Up @@ -911,6 +911,16 @@
command that exits non-zero is failed, and the line names its exit status.
Stop and Ctrl-C interrupt a command Step the way they interrupt a session,
and <code>[resources]</code> is checked before each Step.</p>
<p><code>thirdshift status</code> and <code>status --json</code> list a running or finished Process as
Process <code>&lt;name&gt;</code>, with the ending succeeded, failed, skipped or interrupted.
The stored kind stays <code>Process</code>, and the selector used by <code>show</code> and <code>stop</code>
stays <code>process-&lt;name&gt;-&lt;stamp&gt;</code>. <code>thirdshift show</code> prints each Step&#39;s Command
log line and, when that Step wrote a result, the path of the result file. The
Console lists the Process, and a failed one is in Needs you. The Activity log
says Process <code>&lt;name&gt;</code>. A succeeded, failed or interrupted Process sends a Run
notification under the same <code>email</code>, <code>email-unless-merged</code> and <code>no-email</code>
flags and the same User config rules as a Ticket run. A missing address or
Resend key stops the Process before any Step. A skipped Process sends none.</p>
<p>A Custom process started with no issue is a <a href="#one-at-a-time">Pass</a>. While another Pass on the repository is still running, its Spec run or Ticket run included, <code>thirdshift process &lt;name&gt;</code> is skipped: it prints <code>another Pass is already running on &lt;owner&gt;/&lt;repo&gt;</code>, prints nothing on stdout, exits <code>0</code> and sends no Run notification. It creates no worktree and starts no Step. While it runs, a Pickup run on the same repository is skipped. It does not take an Architect run&#39;s Ready-issue or Architect-idea gates, so a Ready issue or an Architect idea waiting for triage does not skip it. The Command record&#39;s outcome is <code>skipped</code>, and its cause is that reason. The Activity log records <code>Process &lt;name&gt; skipped: &lt;reason&gt;</code> when that is not already the last Process line. With <code>activity.quiet_skips</code> set, a skipped Process prints nothing. <code>thirdshift process &lt;name&gt; issue &lt;issue&gt;</code> is not a Pass: this rule does not skip it, and it does not hold the lock.</p>
</div></details>
<details class="fold"><summary><h3>User config</h3></summary>
Expand Down
2 changes: 1 addition & 1 deletion site/how-it-works/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ <h1>How thirdshift works</h1>
<div class="guide-content" id="scheduled">
<p>A direct Issue URL starts work you have already chosen. Other commands find work, then dispatch the same Ticket run or Spec run you just followed. Each invocation makes one pass; your operating system's scheduler starts the next one.</p>
<p>To run one skill directly, use <code>thirdshift skill &lt;skill&gt; [issue &lt;issue&gt;]</code>. It starts one headless session on your default Harness, Model and Effort in a temporary worktree at your checkout's committed HEAD, and prints the final message. Worktrees with local work are kept. <a href="/docs/#standalone-skills">Standalone skills</a>.</p>
<p><code>thirdshift process</code> is experimental, beside <code>thirdshift skill</code>. It runs the Steps in <code>~/.thirdshift/processes/&lt;name&gt;.toml</code>, a skill session or a command. Consecutive Steps can run together, each in a throwaway worktree the file names, and the last finished Step's result is printed. An optional Step can fail without stopping the process. <code>thirdshift process &lt;name&gt; issue &lt;issue&gt;</code> gives the process an issue, so each session Step works from that issue. With no issue it is a Pass and skips while another Pass on the repository is running, without an Architect run's Ready-issue or Architect-idea gates. With an issue, it is not a Pass. <a href="/docs/#custom-processes">Custom processes</a>.</p>
<p><code>thirdshift process</code> is experimental, beside <code>thirdshift skill</code>. It runs the Steps in <code>~/.thirdshift/processes/&lt;name&gt;.toml</code>, a skill session or a command. Consecutive Steps can run together, each in a throwaway worktree the file names, and the last finished Step's result is printed. An optional Step can fail without stopping the process. <code>thirdshift process &lt;name&gt; issue &lt;issue&gt;</code> gives the process an issue, so each session Step works from that issue. With no issue it is a Pass and skips while another Pass on the repository is running, without an Architect run's Ready-issue or Architect-idea gates. With an issue, it is not a Pass. Status, the Console and the Activity log list it as Process <code>&lt;name&gt;</code>, and a failed one is in Needs you. It sends a Run notification under the same email rules as a Ticket run, except a skipped Process sends none. <a href="/docs/#custom-processes">Custom processes</a>.</p>
<figure class="guide-diagram grid-bg">
<figcaption>Different entry points, the same implementation and Delivery</figcaption>
<div class="guide-entrypoints">
Expand Down
2 changes: 1 addition & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ <h1 class="misreg">Your issues become pull requests overnight.</h1>
<ol class="chain"><li><a href="/prompts/#skill-thirdshift-implement">/thirdshift-implement</a></li><li><a href="/prompts/#skill-thirdshift-code-review">/thirdshift-code-review</a></li><li><a href="/prompts/#skill-thirdshift-pr">/thirdshift-pr</a></li></ol>
<p>thirdshift takes an Issue URL: one issue starts a Ticket run; an issue with sub-issues starts a Spec run. Its Factory skills, adapted to run with no human in the loop, implement the issue with <code>/thirdshift-implement</code>, review the work with <code>/thirdshift-code-review</code> and write the pull request with <code>/thirdshift-pr</code>.</p>
<p>Issues labelled <code>bug</code> first use <a href="/prompts/#skill-thirdshift-diagnosing-bugs"><code>/thirdshift-diagnosing-bugs</code></a> to reproduce the reported failure and write a failing regression test before fixing it. If it cannot reproduce the bug, the Ticket run leaves triage notes and <code>needs-info</code> on the issue, ending successfully without a new pull request. Security fixes and Base fixes keep their own flow.</p>
<p><code>thirdshift skill</code> runs one skill. <code>thirdshift process</code> is experimental: it runs skill or command Steps from <code>~/.thirdshift/processes</code>, including Steps that run together in throwaway worktrees the file names. An optional Step can fail without stopping the process. Pass <code>issue</code> to give the process an issue, so each session Step works from that issue. With no issue it is a Pass: it skips, exiting 0 with no Run notification, while another Pass on the repository is running, and it does not take an Architect run's Ready-issue or Architect-idea gates. With an issue, it is not a Pass. <a href="/docs/#custom-processes">Custom processes</a>.</p>
<p><code>thirdshift skill</code> runs one skill. <code>thirdshift process</code> is experimental: it runs skill or command Steps from <code>~/.thirdshift/processes</code>, including Steps that run together in throwaway worktrees the file names. An optional Step can fail without stopping the process. Pass <code>issue</code> to give the process an issue, so each session Step works from that issue. With no issue it is a Pass: it skips, exiting 0 with no Run notification, while another Pass on the repository is running, and it does not take an Architect run's Ready-issue or Architect-idea gates. With an issue, it is not a Pass. Status, the Console and the Activity log list it as Process <code>&lt;name&gt;</code>, and a failed one is in Needs you. It sends a Run notification under the same email rules as a Ticket run, except a skipped Process sends none. <a href="/docs/#custom-processes">Custom processes</a>.</p>
<p class="more"><a href="/prompts/">Read every prompt and skill the agents get →</a></p></div>
</div>
</div>
Expand Down
111 changes: 72 additions & 39 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum ProcessAction {
Run {
name: String,
issue: Option<IssueInput>,
/// `email`, `email-unless-merged` or `no-email`, when the command gave one.
email: Option<NotificationAsk>,
},
}

Expand Down Expand Up @@ -835,32 +837,81 @@ fn metrics(args: &[String]) -> Result<MetricsArgs> {
Ok(asked)
}

const PROCESS_USAGE: &str = "usage: thirdshift process [check <name> | <name> [issue <issue>] [email [address] | email-unless-merged [address] | no-email]]";

fn parse_process(args: &[String]) -> Result<ProcessAction> {
match args {
[] => Ok(ProcessAction::List),
[keyword] if keyword == "check" => bail!("usage: thirdshift process check <name>"),
[name] => Ok(ProcessAction::Run {
name: process_name(name)?,
issue: None,
}),
[keyword, name] if keyword == "check" => Ok(ProcessAction::Check(process_name(name)?)),
[name, keyword] if keyword == "issue" => {
process_name(name)?;
bail!("usage: thirdshift process <name> [issue <issue>]");
}
[name, keyword, issue] if keyword == "issue" => Ok(ProcessAction::Run {
name: process_name(name)?,
issue: Some(IssueInput::parse(issue)?),
}),
_ => bail!("usage: thirdshift process [check <name> | <name> [issue <issue>]]"),
if args.is_empty() {
return Ok(ProcessAction::List);
}
let mut name = None;
let mut issue = None;
let mut email = None;
let mut check = false;
let mut expect_issue = false;
let mut words = args.iter().peekable();
while let Some(arg) = words.next() {
if take_email_flag(&mut email, arg, &mut words)? {
continue;
}
// `check` is the subcommand only before the name. A later `check` is
// the name, so a reserved file called check is still refused as one.
if arg == "check" && !check && name.is_none() && issue.is_none() && email.is_none() {
check = true;
continue;
}
if arg == "issue" {
if check || name.is_none() || expect_issue || issue.is_some() {
bail!(PROCESS_USAGE);
}
expect_issue = true;
continue;
}
if expect_issue {
issue = Some(IssueInput::parse(arg)?);
expect_issue = false;
continue;
}
if name.is_some() || arg.starts_with('-') {
bail!(PROCESS_USAGE);
}
name = Some(process_name(arg)?);
}
if expect_issue || (email.is_some() && (check || name.is_none())) {
bail!(PROCESS_USAGE);
}
match (check, name) {
(true, Some(name)) if issue.is_none() => Ok(ProcessAction::Check(name)),
(true, _) => bail!(PROCESS_USAGE),
(false, Some(name)) => Ok(ProcessAction::Run { name, issue, email }),
(false, None) => bail!(PROCESS_USAGE),
}
}

/// Record an email flag in `email`. False, taking nothing, when `arg` is not one.
fn take_email_flag<'a>(
email: &mut Option<NotificationAsk>,
arg: &str,
rest: &mut Peekable<impl Iterator<Item = &'a String>>,
) -> Result<bool> {
let asked = match arg {
"email" | "--email" => {
NotificationAsk::Send(rest.next_if(|next| is_address(next)).cloned())
}
"email-unless-merged" | "--email-unless-merged" => {
NotificationAsk::SendUnlessMerged(rest.next_if(|next| is_address(next)).cloned())
}
"no-email" | "--no-email" => NotificationAsk::Skip,
_ => return Ok(false),
};
ask_once(email, asked, arg, EMAIL_FLAGS)?;
Ok(true)
}

/// A process name is one path component. A slash is an argument error, not a
/// file rule: the file is `~/.thirdshift/processes/<name>.toml`.
fn process_name(name: &str) -> Result<String> {
if name.is_empty() || name == "." || name == ".." || name.contains(['/', '\\', '\0']) {
bail!("usage: thirdshift process [check <name> | <name> [issue <issue>]]");
bail!(PROCESS_USAGE);
}
Ok(name.to_string())
}
Expand Down Expand Up @@ -975,32 +1026,14 @@ fn take_flag<'a>(
arg: &str,
rest: &mut Peekable<impl Iterator<Item = &'a String>>,
) -> Result<bool> {
if take_email_flag(&mut flags.email, arg, rest)? {
return Ok(true);
}
match arg {
"merge" | "--merge" => ask_once(&mut flags.goal, Goal::Merged, arg, MERGE_FLAGS)?,
"no-merge" | "--no-merge" => {
ask_once(&mut flags.goal, Goal::ReadyForReview, arg, MERGE_FLAGS)?
}
"email" | "--email" => {
let to = rest.next_if(|next| is_address(next)).cloned();
ask_once(
&mut flags.email,
NotificationAsk::Send(to),
arg,
EMAIL_FLAGS,
)?
}
"email-unless-merged" | "--email-unless-merged" => {
let to = rest.next_if(|next| is_address(next)).cloned();
ask_once(
&mut flags.email,
NotificationAsk::SendUnlessMerged(to),
arg,
EMAIL_FLAGS,
)?
}
"no-email" | "--no-email" => {
ask_once(&mut flags.email, NotificationAsk::Skip, arg, EMAIL_FLAGS)?
}
"parallel" | "--parallel" => ask_parallel(&mut flags.parallel, arg, rest.next())?,
BASE_FIX | "--base-fix" => {
ask_once(&mut flags.base_fix, BaseFixAsk::Allow, arg, BASE_FIX_FLAGS)?
Expand Down
Loading
Loading