From 2cbb2ad94482b10f04792f611b770360bdc34f54 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Mon, 19 Oct 2026 12:39:40 +0000 Subject: [PATCH 1/2] Fold guide and manual sections for an initial overview (#804) --- site/docs/docs.css | 9 +- site/docs/index.html | 201 ++++++++++++++++++++++++++--------- site/generate-docs.mjs | 15 ++- site/how-it-works/guide.css | 4 +- site/how-it-works/index.html | 71 ++++++++----- site/main.js | 2 +- site/tests/pages.test.cjs | 84 ++++++++++++++- 7 files changed, 297 insertions(+), 89 deletions(-) diff --git a/site/docs/docs.css b/site/docs/docs.css index 93fe4968..3a8db490 100644 --- a/site/docs/docs.css +++ b/site/docs/docs.css @@ -5,9 +5,12 @@ .manual nav[aria-label="Site"] { display: flex; flex-wrap: wrap; gap: 12px 24px; } .manual main { padding-block: 32px 80px; overflow-wrap: anywhere; } .manual h1 { font-size: clamp(44px, 9vw, 80px); } -.manual h2 { font-size: clamp(30px, 5vw, 48px); margin-top: 56px; } -.manual h3 { font-size: 28px; margin-top: 32px; } -.manual h4 { font-size: 18px; margin-top: 28px; } +.manual h2 { font-size: clamp(30px, 5vw, 48px); } +.manual h3 { font-size: 28px; } +.manual h4 { font-size: 18px; } +.manual .fold { margin-top: 28px; } +.manual main > .fold { border-top: 1px solid var(--rule); padding-top: 16px; } +.manual .fold > summary :is(h2, h3, h4) { margin: 0; } .manual [id] { scroll-margin-top: 24px; } .manual pre { padding: 16px; background: var(--k2); border: 1px solid var(--rule); white-space: pre-wrap; overflow-wrap: anywhere; } .manual li { margin-block: 8px; } diff --git a/site/docs/index.html b/site/docs/index.html index fafec789..a77976da 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -22,6 +22,7 @@ + Skip to the manual @@ -53,7 +54,8 @@

A factory that turns a GitHub issue into a ready-for-review pull request, or on request a merged one, by running unattended agent sessions against it. The humans are the day shift; the agents work the third shift, overnight, on any server, while you do something else.

Quality over quantity: a pull request marked ready for review is one the factory stands behind. When it can't stand behind the work, the Run is a Failed run: the work is pushed so nothing is lost, and the pull request goes back to a draft.

Terms in bold are defined in CONTEXT.md.

-

Workflows

+

Workflows

+

A Run, one issue in and one pull request out, is the factory's first workflow. Five more build on it:

  • Merge run, thirdshift merge <Issue URL>: a Run that also merges its own reviewed, green pull request (see Usage).
  • @@ -64,7 +66,9 @@

    Workflows

Architect, Pickup and Security runs are Passes: they take no Issue URL, so your operating system's scheduler can start them. See A Pickup run on a schedule, Weeding for Architect runs, and Fencing for Security runs.

Every session runs on the Harness you choose: Claude Code by default, or Codex, Antigravity CLI, Grok Build, Muse Code or OpenCode. See Harness, Model and Effort.

-

What a Run does

+
+

What a Run does

+

From a clone of the issue's repository, thirdshift <Issue URL>:

  1. Checks that the Run makes sense, before creating anything:

    @@ -91,7 +95,8 @@

    What a Run does

  2. Cleans up: removes the worktree, the local Issue branch and the temporary directory the Factory skills were written out to, whether the Run succeeded or not. The one exception is a Failed run whose work could not be pushed: see below.

-

The Claim

+

The Claim

+

The Claim is the mark that the factory has taken an issue, so the issue list shows what it is working on. When a Run or a Spec run starts on an issue, thirdshift labels the issue in-progress, in place of ready-for-agent if it has that. An issue with no ready-for-agent label still gets in-progress, so the label means the same thing however the Run was started.

  • It is made once every check of step 1 has passed and the branches are picked, and before the worktree is created. A Run stopped by a check changes no label.
  • @@ -102,7 +107,8 @@

    The Claim

  • A Continuation on an issue that is already in-progress changes nothing and makes no request. One on an issue that isn't Claimed yet Claims it.
  • If the Claim can't be made, the Run stops there, before any work, with exit 1 and the cause: could not make the Claim on #<n>: <gh's error>.
-

When the Claim ends

+

When the Claim ends

+

How the Run or the Spec run ends decides what becomes of its Claim:

  • Released, except for a failed Security fix, when it fails with nothing on origin to take over: no Issue branch for the issue there and no pull request from one, open, merged or closed, or for a Spec run, no Spec branch and no Spec PR. That is a session that fails before it changes anything, as on a usage limit or an expired login, a Ctrl-C before any work, or a worktree that can't be created, on an issue that was never started before. A retry that fails that way on an issue an earlier Run left an Issue branch or a pull request for keeps its Claim. The issue's labels go back as they were before the Claim: in-progress comes off, and ready-for-agent goes back if the issue had it, and only then. So an outage costs a retry and doesn't strand the issue. stderr says releasing the Claim on #<n>: labelling it ready-for-agent, in place of in-progress, or releasing the Claim on #<n>: removing in-progress. A Failed run whose push failed is released too, since nothing of it reached origin: its work is only in the worktree it kept, which stops the next Run on that machine at the pre-flight checks until you push or delete the local Issue branch.
  • @@ -112,9 +118,15 @@

    When the Claim ends

    A release undoes only what the Claim changed, in one request that keeps the issue's other labels, any added during the Run included. An issue that was already in-progress when the Run started stays in-progress, with ready-for-agent back if the Claim took it off (releasing the Claim on #<n>: labelling it ready-for-agent again), and one whose in-progress someone took off during the Run is left as it is. A Claim that can't be released, or can't be removed after a merge, doesn't change how the Run ends: stderr gets a warning: line naming the command to run by hand.

    A process killed outright, with kill -9 or by a power cut, releases nothing: the issue stays in-progress for you to relabel.

    An issue closed some other way, as when you merge its pull request by hand, keeps in-progress until a Pickup run takes it off, in the Sweep.

    -

    Status

    +
+
+

Status

+

The designed behaviour described in this manual is implemented. Known gaps and planned work are tracked in the open issues.

-

Machine status

+
+
+

Machine status

+

thirdshift status names this host and lists live Commands across every repository in logs.dir, with their issue and title when present, Harness, Model, Effort, current stage and its age. Architect runs, Pickup runs and Security runs nest the Runs or Spec runs they dispatch beneath themselves, each Spec's Tickets and their Runs, and each Run's Base fix. Every Ticket is listed: running, merged into the Spec branch, failed, waiting on named blockers, or not started. A Pass with no issue is named by its kind and Command log stamp, such as architect-20261018T120000-0400 or secure-20261018T120000-0400. After the live Commands it lists those that ended in the last 24 hours, with their outcomes. thirdshift status --all lists every Command this machine has logs for, newest start first. A Command or child Run with no outcome whose process has gone shows as died at <stage> in either view. Process start identifiers distinguish a reused process id on Linux and macOS. Records remain beside their Command logs, as *.command.json, with child records named for their issue as *.issue-<n>.child.json. Each process writes only its own files, replacing them atomically. A record write failure warns once per recording process and does not change its outcome.

History uses each source only for Commands missing from the preceding sources: Command records, then Activity log start/end lines with their Command logs, then Session logs in the current <logs.dir>/<owner>/<repo>/commands/<kind>/ layout. Activity entries have start and end times and outcomes, with no stages. Session entries group logs by issue and exact Command owner; they have no known end or outcome and appear only with --all. A Spec run or Pass's Sessions share that Command's stamp and owner token. Legacy tokenless logs retain their existing grouping. The flat layout from before ADR 0011 is ignored.

For each repository and kind of Pass whose latest Activity log line is a skip, status also shows that Pass as idle with its reason, even when the skip is older than another kind's activity. A later start or end for that kind clears its idle entry. For example:

@@ -187,7 +199,9 @@

Machine status

}

Every entry has source: command_record, activity_log or session_log. The latter two carry repository, kind, nullable issue (number, URL, nullable title), harness, model, effort, started_at, nullable ended_at and outcome, nullable command_log, session_logs (paths), state, stages: [] and stage: null. They omit record-only fields such as process identity and branch information. Activity times use the UTC offset in the Command log's stamp; their outcome is the Activity log's full ending text. A Session entry has null Harness, Model, Effort, end time and outcome, and lists its grouped Session logs. Both flags can be combined as status --all --json.

-

Show and read one Command

+
+

Show and read one Command

+

Name a Command by widgets#7, acme/widgets#7, or its full Issue URL. The owner is needed only when repositories on this machine share the name; an ambiguous short name is refused with the matching repositories. When several Commands worked on the issue, the live one wins, otherwise the latest start wins. A Ticket's issue names its Run within the Spec run. A Pass with no issue is named by kind and stamp, such as architect-20261018T120000-0400, as listed by status.

thirdshift show 'widgets#7'
 thirdshift show https://github.com/acme/widgets/issues/7 --json
@@ -218,7 +232,9 @@ 

Show and read one Command

The Command excerpt omits the common fields documented for status --json; all those fields are retained, including process identity, issue, Harness, Model, Effort and nullable outcome. children and dispatched retain the same recursive shape. session_logs lists the selected Run's Session log paths, including original sessions and Resumes that share a stage. latest_event is optional, present only when a live Run has a readable event, including in child Runs. History without a Command record uses the same Activity-log or Session-log shape as status --all --json, with Session logs discovered by issue/kind and exact Command owner, without invented stages or liveness.

logs writes the Command log verbatim. --session <kind> writes that kind's latest Session log as raw JSON lines; implement and spec-review include a later Resume, and repair selects the latest numbered Repair. A missing log or session kind gives an error. --follow first prints the selected log, then flushes appended bytes until the owning Command ends or dies, including its final output. A Ticket shares its Command's Command log but selects its own Session logs. The selected Command and Session log stay fixed while following.

-

Install

+
+

Install

+
curl -LsSf https://github.com/JacobStephens2/thirdshift/releases/latest/download/thirdshift-installer.sh | sh
 

The shorter curl -fsSL https://thirdshift.app/install.sh | sh, shown on thirdshift.app, redirects to the same installer. The shell installer and the binary both come from this repository's GitHub Releases. It puts thirdshift in ~/.local/bin, where Claude Code's installer puts claude, and needs no Rust toolchain. thirdshift is a single binary with the Factory skills compiled in, so it runs without a checkout of this repository (ADR-0001, ADR-0003).

@@ -226,13 +242,17 @@

Install

If you use Rust, you can install it from crates.io instead:

cargo install thirdshift
 
-

Updating

+

Updating

+
thirdshift update
 

It replaces the installed binary with the latest stable GitHub Release, or says it is already on it. Messages go to stderr and stdout stays empty. It exits 0 when it updated or was already up to date, and 1 on any failure, such as no network. Updating is safe while a Run or a Spec run is using the old binary: it keeps running it. On Linux so do the child Runs it starts afterwards, a Ticket's Run or a Base fix, which it starts from the binary it is running rather than from the install path, so they run its version even once that path holds the new one or nothing at all. On macOS a child Run is started from the install path, so one started after the update runs the new binary. A Run never checks for updates or updates itself.

thirdshift update only replaces a copy put in place by the shell installer (the curl command above), which leaves an install receipt in ~/.config/thirdshift/. It refuses to touch any other copy and lists the command that updates each other kind of install: cargo install thirdshift for a crates.io install, pulling and reinstalling for a build from source (see Building from source), and the curl command for a copy placed by hand.

Keep a single copy: with copies in more than one directory on your PATH (say ~/.local/bin and ~/.cargo/bin), you can end up running a stale one without noticing. type -a thirdshift lists every copy on your PATH.

-

Supported platforms

+
+
+

Supported platforms

+
  • Rocky Linux 9.8 and later, x86_64.
  • Ubuntu 24.04.3 and later, x86_64.
  • @@ -240,7 +260,9 @@

    Supported platforms

  • macOS Tahoe 26.5.2 and later, Apple Silicon.

One statically linked Linux binary covers the first three, so it doesn't depend on the host's glibc; macOS gets a native build.

-

Prerequisites

+
+

Prerequisites

+

The user account that runs thirdshift needs:

  • The selected Harness installed and authenticated: claude (Claude Code, the default), codex (Codex CLI), agy (Google Antigravity CLI), grok (Grok Build), muse (Muse Code), or opencode (OpenCode, with a configured Model provider). See Harness, Model and Effort for each CLI's flags and quirks.
  • @@ -248,18 +270,26 @@

    Prerequisites

  • git with a global user.name and user.email, and credentials that can push to the repository (gh auth setup-git makes git use gh's login). The agents commit as this identity; without it, an agent may borrow the author of the last commit.
  • node (Node.js) on PATH, for a Security run only: the Security audit's report validators run on it.
-

Unattended sessions

+

Unattended sessions

+

Sessions run headless with the full permissions of that user account, including sudo if the user has it, and nobody is there to approve anything. On Claude Code they run in auto mode (claude -p --permission-mode auto): its classifier checks each action and may block ones it judges risky, such as destructive commands or actions outside the task. A blocked action the agent can't work around can end the Run as a Failed run. The other Harnesses run with approvals and their sandbox turned off (ADR-0013), so no approval step or sandbox stands between an action and the account.

claude -p exits as soon as the agent ends its turn, killing any background task it started. So every prompt tells the agent to run long commands, such as tests, in the foreground, and to stop any background task it no longer needs, with the TaskStop tool, before it ends its turn. If a session still ends with background work running, thirdshift takes it as work the session was waiting on and gives it a Resume: it continues that same session once (claude -p --resume), asking the agent to re-run the work in the foreground and finish. If the Resume ends the same way, the work may have been abandoned, such as a hung test run the agent could not stop: thirdshift says which task was killed and carries on, so the pull request check, CI and the rest decide how the Run ends. If the Run then fails, its cause names the killed task ahead of what failed.

-

What sessions leave behind

+
+

What sessions leave behind

+

Cleanup removes only thirdshift's own worktree, local Issue branch and the temporary directory it wrote the Factory skills out to. The .git/info/exclude entry that keeps the linked skills out of git, /.claude/skills/thirdshift-*, or /.agents/skills/thirdshift-* on the other Harnesses, is added once and left in place, as every worktree of the repository shares the file. Anything else an agent does as your user stays. For example, one Run downloaded a JDK to ~/.local/jdk/ because the server had no Java, installed Playwright in /tmp/pw, and left a pull request body draft in /tmp/. This is by design, but worth knowing:

  • Install the target repository's toolchains up front (language runtimes, package managers, test tools), so the agents don't download their own on every Run.
  • For UI work, install the headless browser dependencies, e.g. libgbm1 on Ubuntu server, which headless Chromium needs. Without it, agents can't take screenshots to check a UI change and fall back to reading the code.
-

Usage and cost

+
+

Usage and cost

+

When claude is logged in with a claude.ai subscription, Runs use the plan's usage allowance, not API billing. The total_cost_usd in the session logs is the API-price equivalent, not a charge. An unattended Run that hits the plan's usage limit stalls or fails partway through.

-

Usage

+
+
+

Usage

+
cd ~/repos/widgets          # a clone of the issue's repository, on the Base branch
 thirdshift https://github.com/acme/widgets/issues/7
 
@@ -315,7 +345,8 @@

Usage

help lists commands and shared options, with a link to the README, which points here for workflow details, settings, Harness setup, email, logs and scheduling.

version and help print to stdout and exit 0. update, setup and email-test follow the Run's rule: stdout stays empty, messages go to stderr. architect follows it too: stdout carries the pull request's URL of the run it dispatches, or, in its place, the URL of the issue it ended on: its plan with --plan-only, or its idea when the review published no plan. A skipped Architect run prints nothing there when another is still running, the URL of each Architect plan that is still open, the URL of each Architect idea waiting for triage, and the URL of the Ready issue that goes first. pickup follows it as well: stdout carries the pull request's URL of the run it dispatches, and nothing when the Pickup run is skipped. So does secure: stdout carries the pull request's URL of a fix it dispatches, and nothing otherwise.

Uncommitted changes in your clone are fine: the Run works in its own worktree from origin, so they are simply left out. Unpushed commits on the Base branch are not: push them first, or the Run stops.

-

User config

+

User config

+

A User config at ~/.thirdshift/config.toml sets this machine's defaults for every Run. It is optional: with no file, or one that says nothing about a setting, a Run does only what its command asks for.

[merge]
 always = true   # every Run is a Merge run, without the merge word
@@ -445,7 +476,9 @@ 

User config

logs.dir sets the root of the logs, with each repository's in its <owner>/<repo>/ folder, created if missing. It must be an absolute path, ~ or a path starting with ~/, where ~ stands for $HOME. A relative path stops the Run before any work, since the directory a Run is launched from is no base for a setting that holds for every Run.

[harness] chooses the Harness, the agent CLI every session of a command runs on, and for each Harness the Model its sessions run on and their Effort, how hard that Model reasons. See Harness, Model and Effort.

A Run reads the file before any work. One that isn't valid TOML, or that has a key or section thirdshift doesn't know, such as alway for always, or a value of the wrong type, such as anything but true or false for always, or 0 for spec.parallel or pickup.limit, stops the Run with an error naming the file and the offending key, so a typo can't silently leave a setting off. email-test and setup read it the same way. update, version and help never read it, so a broken User config can't block them, and they never offer Setup; nor does email-test.

-

Harness, Model and Effort

+
+

Harness, Model and Effort

+

The available Harnesses are Claude Code (claude), Codex (codex), Antigravity CLI (agy), Grok Build (grok), Muse Code (muse) and OpenCode (opencode).

Every session of a command, its implement session, each Repair and Resume, a Spec review, an Architecture review, each Ticket's Run and a Base fix, runs on one Harness, with one Model and one Effort. harness <name>, model <name> and effort <level> (or --harness, --model and --effort) choose them for one command, before or after the Issue URL, and on architect (but not with --plan-only) and pickup too:

thirdshift https://github.com/<owner>/<repo>/issues/<n> model claude-opus-5-5 effort high
@@ -466,7 +499,9 @@ 

Harness, Model and Effort

OpenCode takes the Model id from your provider config, as <provider>/<model>, including MiMo-V2.6-Pro when your provider routes it. Model and Effort are passed together as -m <provider>/<model>#<effort>; an explicit Effort replaces a variant already in the Model id, and needs a Model set too. Before any work, thirdshift makes a minimal standalone test call with the same stdin, flags and environment. provider.no-route identifies a bad Model or Effort without a model turn; a successful check uses a model call because OpenCode has no free catalog without its service. Setup checks its answers the same way, proposing no default Model.

The session id comes from sessionID in the stream, and a Resume uses -s <id> with every flag and environment override again. OpenCode creates a new session for an unknown id, so thirdshift only resumes an id it read from the stream. Progress comes from step_start, tool_use, step_finish and error. After exit, thirdshift reads opencode session export --standalone <id>: an exported failed outcome fails the Run even with exit 0, the final message is the last assistant message's last text part, and usage and cost are summed over assistant messages to exclude the automatic title call. Input totals include cache reads and writes, output totals include reasoning, and each subtotal is shown. If export fails or is unreadable, the last stream text supplies the final message and no usage is shown. A stream error or nonzero exit also fails the Run with OpenCode's error. An interrupt sends SIGTERM through the whole process tree, then SIGKILL after the grace period if needed.

The Command log's opening lines (sessions run on claude · claude-opus-5-5 · high), the Activity log's start line, the pull request's body and the Run notification each name the Harness, Model and Effort, default model or default effort where the Harness's own is used. In the pull request's body, the Spec PR's included, thirdshift writes the line itself once the opening session has ended, such as Built with claude · claude-opus-5-5 · high, replacing any it wrote there before.

-

Email

+
+

Email

+

thirdshift sends email itself, with one HTTPS request to Resend's API, so it needs no mail server on the machine and works where SMTP ports are blocked (ADR 0005). It needs a Resend account and an API key:

  • The API key comes from the RESEND_API_KEY environment variable when it is set and not empty, and otherwise from the Credentials, ~/.thirdshift/credentials.toml, a file only you should be able to read (mode 600):

    @@ -493,7 +528,8 @@

    Email

    (a crontab line, CI secret, or a shell profile the Run's shell reads)

    The Credentials are read only when RESEND_API_KEY is unset or empty, and only by email-test, setup, and a Run or a Pass that asks for a notification. A missing file just means no key from it. One that isn't valid TOML, or holds anything but a quoted resend.key, such as a typo like kye, stops the command with exit 1, naming the file and the offending key, and nothing is sent. One that others can read is still used, with a warning: line on stderr saying to chmod 600 it. Nothing is sent to check the key itself. When Resend accepts the email, it prints accepted by Resend; check your inbox and exits 0; that is all it can verify, so check that the email arrives. When Resend refuses it, for example for a bad key or a sender it won't send from, it prints Resend's error text word for word, with where the key came from, and exits 1. It gives up after 30 seconds without an answer.

    -

    Run notifications

    +

    Run notifications

    +

    --email (or email) asks a Run for a Run notification: one email, sent when the Run ends, whatever the outcome. The word after the flag is the address only if it contains @ and doesn't start with https://, so the Issue URL is never taken for it; otherwise the email goes to email.to. With email.always = true in the User config, a Run asks for one without the flag, and --no-email (or no-email) skips it for that Run; an address after --email still wins over email.to. Giving a flag twice, or --email together with --no-email, is an argument error.

    A Run that asks for a notification, by the flag or by email.always, makes the same checks as email-test before any other work: an address is known, and a key is found, in RESEND_API_KEY or else the Credentials. If either fails, or the Credentials are broken, the Run stops, exits 1 naming what's wrong (with no key, the message above listing every way to give one), and sends nothing. A Run that asks for no notification never reads the Credentials, so a broken file can't stop it, and update, version and help never read it either. Once they pass, every way the Run ends sends exactly one notification, after its outcome is final and its cleanup done: ready for review, merged, a Failed run (including a later preflight failure such as an origin mismatch), or interrupted by Ctrl-C, SIGTERM or a closed terminal.

      @@ -505,7 +541,10 @@

      Run notifications

      An Architect run takes the same flags and the same email.always, and sends one notification covering its review and the run it dispatched.

      A Pickup run takes the same flags and the same email.always too. One that took an issue sends one notification, the one the run it dispatched would have sent by hand, and a skipped one sends none.

      A notification that can't be sent is a warning: line on stderr with Resend's error. It never changes the Run's outcome, stdout or exit code.

      -

      Stopping a Command

      +
    +
+

Stopping a Command

+

thirdshift stop widgets#7 stops a live Run and waits for its cleanup. It accepts the same Command names as show: owner/repo#number, an Issue URL, or a Pass's kind-stamp name. It works from any directory and never prompts. Ended and died @@ -527,7 +566,9 @@

Stopping a Command

and whether to requeue. The Command writes <subject> stopped from the CLI by <who> to its Activity log before its ended: line. Stop prints still ending each minute while cleanup runs, then the outcome and any Issue branch or pull request left behind.

-

Logs

+
+

Logs

+

Everything thirdshift logs goes under ~/.thirdshift/logs/, or the logs.dir set in the User config, in a folder per repository, <owner>/<repo>/, named for the GitHub repository rather than your checkout, so a fork and its upstream never share one and every checkout or worktree of a repository logs to the same place. thirdshift creates each folder as it needs it, so a repository's first pass needs nothing made by hand:

~/.thirdshift/logs/<owner>/<repo>/
 ├── activity.log              the Activity log
@@ -557,7 +598,10 @@ 

Logs

Each line is appended whole, so passes that run at once on one repository never garble it. One that can't be written is one warning: line on stderr, and the command carries on as for a Command log. thirdshift never rotates it: collapsed skips keep it small. Existing logs, including Session logs in <owner>/<repo>/sessions/ and logs from before the per-repository layout, are not moved.

With quiet_skips = true in the [activity] section of the User config, a skipped Pass prints nothing on stdout or stderr, its dated first line included, and leaves only its Activity log line. A pass that does work, or fails, prints as ever, so a scheduler's log file catches only what went wrong. Without it, a skipped pass prints as it always has, for a pass you type by hand.

-

Foreign commits in a Merge run

+
+
+

Foreign commits in a Merge run

+

A Merge run merges only code an agent wrote or reviewed. If someone else pushes to the Issue branch during the Run, their Foreign commits are reviewed before they can be merged:

  1. Each round of step 6 starts by fetching the Issue branch from origin. Any new commits there are merged into the Run's branch, by fast-forward or a merge commit, never a rebase, each logged on stderr as merging new commit <sha> from origin/<branch>. Foreign commits that arrive while CI runs on a green head send the Run round again rather than on to the merge. A merge that conflicts with the Run's own work gets a conflict Repair.
  2. @@ -566,7 +610,9 @@

    Foreign commits in a Merge run

Review Repairs are logged as repair-<i>, numbered with the other Repairs, count against the cap of 5 Repairs, and get a Resume like any session. A round that picks up Foreign commits counts against the same 5, like a Base branch move. A Run that spends either budget fails.

A Run without merge does none of this: it never fetches someone else's commits into its branch.

-

Base fix

+
+

Base fix

+

A Base branch whose CI is broken fails every Run on it with an Inherited failure. To have a Run fix the Base branch itself and carry on, give it base-fix (or --base-fix), before or after the URL:

thirdshift base-fix https://github.com/acme/widgets/issues/7
 
@@ -580,7 +626,8 @@

Base fix

If the Base fix fails, the Run is a Failed run with the cause Base fix <issue URL> failed: <its cause>. If the checks are still Inherited failures once the Base fix has merged, there is no second one, and the cause is CI red on <check>, which also fails on <base> at <short sha>, even after Base fix <issue URL> merged; fix <base> first. The Run's last progress lines include Base fix: <issue URL> merged, or not merged, and its own Run notification has the same Base fix: line.

On a Spec, base-fix, no-base-fix or the User config's base.fix holds for each Ticket's Run, whose Base fix goes into the Spec branch, its Base branch, and for the Spec PR, whose Base fix goes into the Base branch. Tickets running at once that meet the same Inherited failure share one Base fix: the first starts it, and the others wait on it. A Ticket's Run sends no Run notification, so the Spec run reports its Base fix: a Ticket that landed after one has #<n> landed with <PR URL>, after Base fix <issue URL> merged, or closed for one it waited on, as its line in the Tickets checklist and in the Spec run's Run notification, and one whose Base fix failed has the cause naming the issue.

-

One Base fix per Base branch

+

One Base fix per Base branch

+

A Run that finds an open Base fix issue for its Base branch and checks starts none of its own. After waiting on Base fix #<n>, already open: <issue URL>, it waits for that issue to close, as the Base fix's Self-merge leaves it, then merges the Base branch in again and watches CI. Waiting is the Run's one Base fix: if the checks are still Inherited failures once the issue has closed, the cause is CI red on <check>, which also fails on <base> at <short sha>, even after Base fix <issue URL> closed; fix <base> first, and its Run notification's Base fix: line says closed, or not closed if the Run ended first.

An issue that covers more checks than the Run's counts; one for another Base branch, or that leaves out one of the Run's checks, does not. The wait has no time limit: a Base fix issue from another clone or machine that nobody is working on has to be closed, or the Run interrupted, by hand.

Runs from one Launch directory, as a Spec run's Tickets are, look for the issue and write it one at a time, under a lock in the clone's git directory, so those that meet the same Inherited failure at once get one Base fix issue and one Base fix. There, a Run also knows whether a Base fix started from that Launch directory is still running:

@@ -590,7 +637,10 @@

One Base fix per Base branch

Across clones and machines the look is only a best-effort lock, and two Runs that look at the same moment may each start a Base fix.

Without base-fix or base.fix, or with no-base-fix, an Inherited failure fails the Run as described in What a Run does, and the Run says where the checks fail on the Base branch, offering a Base fix if nobody decided against one.

-

Continuation

+
+
+

Continuation

+

Running thirdshift again on an issue picks up where the last Run stopped (ADR-0002). It looks at the highest-numbered Issue branch:

  • No Issue branch yet: a fresh Run creates issue-<n> from the Base branch.
  • @@ -599,7 +649,9 @@

    Continuation

So you can retry a Failed run with the same command, or start an issue on one server and continue it on another.

A Run is not idempotent: re-running builds on whatever is already on the branch, including a Failed run's work-in-progress commit.

-

Failed runs

+
+

Failed runs

+

A Failed run is one that ends, including by Ctrl-C or a closed terminal, without an open pull request from its Issue branch that targets the Base branch, is mergeable and has passing CI, or, for a Merge run, without that pull request merged. Causes include the session exiting non-zero, no pull request or one with the wrong base, running out of Repairs, CI red only on Inherited failures (CI red on <check>[, <check>…], which also fails on <base> at <short sha>; fix <base> first), a CI-fix Repair that finds nothing on the branch to fix (a declined CI fix: the head is unchanged after it, and its one Check re-run left CI red or could not happen), and a Base branch that keeps moving while CI runs, or in a Merge run, an Issue branch that keeps getting Foreign commits.

A Failed run:

    @@ -609,7 +661,8 @@

    Failed runs

  1. Releases its Claim, unless it is a failed Security fix, if it left nothing on origin: no Issue branch and no pull request. Otherwise the issue stays in-progress.

Merges, never rebases or force-pushes: a branch worked on from several servers never loses history.

- +

An Inherited failure links the Base branch's checks

+
+
+

Spec runs

+

A Spec is an issue with sub-issues, its Tickets. thirdshift <Issue URL> on a Spec is a Spec run: it works through the Tickets in the order their GitHub "blocked by" links allow, each Ticket's Run a Merge run into the Spec branch, then leaves one Spec PR from the Spec branch into the Base branch ready for review (ADR-0006). An issue with no sub-issues is an ordinary Run.

A Spec run makes the Claim on the Spec, as a Run does on its issue: the Spec is labelled in-progress, in place of ready-for-agent. Its Tickets' Runs change no label on their Tickets. The Claim ends as a Run's does: it is released if the Spec run fails with no Spec branch on origin and no Spec PR, except for a failed Security fix, removed once a Self-merge of the Spec PR has left the Spec closed, and kept otherwise. The Spec branch is pushed before the first Ticket starts, so a Spec run that got as far as its Tickets keeps its Claim.

The Spec PR opens as a draft as soon as the first Ticket lands, titled from the Spec, with Closes #<spec> and a Tickets checklist: one line per Ticket, ticked once it is done, with its pull request, or saying it is running, failed, blocked or unready. thirdshift rewrites the checklist between its <!-- thirdshift:tickets --> markers as each Ticket starts and ends, leaving the rest of the body as it is. Once every Ticket is done, the Spec review rewrites the body, and thirdshift puts the checklist back, appending it if the markers are gone, before marking the Spec PR ready.

@@ -644,7 +700,9 @@

Spec runs

thirdshift: 03:12:40 Tickets not done: #21, #22, #24, #25, #26, #27

Running the Spec again picks up where the last Spec run stopped. The Spec branch is on origin, so the Spec run continues it (ADR-0002) and updates its draft Spec PR rather than opening another. Closed Tickets are done and not run again, and a Ticket that failed still has its Issue branch and draft pull request, so its Run is a Continuation into the Spec branch. With every Ticket closed, the Spec run goes straight to the Spec review and the Spec PR. With every Ticket closed and no Spec branch, as when the Spec was done some other way, it stops with every Ticket is closed and there is no Spec branch; nothing to do and exits 1, creating no branch or pull request and leaving the Spec open.

-

Architect runs

+
+

Architect runs

+

thirdshift architect starts an Architect run: the factory looks for architecture work itself, with no Issue URL, publishes a plan for the top opportunity, and implements it. Start it from a clone of the repository, on the Base branch, or name the Base branch with base <branch>:

cd ~/repos/widgets
 thirdshift architect                              # review the whole codebase, then run the plan
@@ -673,20 +731,25 @@ 

Architect runs

With --plan-only, it instead exits 0 with the plan's URL alone on stdout, and plan <url> is ready for an agent as stderr's last line. Read or edit the plan, then run it with thirdshift <Issue URL>. The plan is labelled architect-plan here too, so until it is finished or closed the next Architect run is skipped.

Progress lines on stderr say when the review starts, which plan it reported, when the labels are swapped, and when the plan is dispatched: dispatching the plan <url>, as thirdshift <url> would. The dispatched run's own progress lines follow. With no Strong candidate, the last line says which issue the Architect run ended on instead.

A review session that fails or is interrupted, a final message without one of the lines the prompt asks for, or a plan that fails a check ends the Architect run as a failure: exit 1, nothing on stdout, the cause on stderr and then the paths of the session log and the Command log. Nothing is dispatched and no label is changed, so a plan the review did publish stays needs-triage, without architect-plan, for you to finish or close, and doesn't skip the next Architect run. A review that finds no deepening opportunity at all has no issue to name, so it ends without one of those lines and the Architect run fails this way too; its session log says what it looked at.

-

One Run notification

+

One Run notification

+

An Architect run asked for a Run notification, by email or by email.always = true in the User config without no-email, sends exactly one, whatever its outcome, with or without --plan-only, unless it is skipped. A skipped Architect run, however it is skipped, sends none, even when asked, so a schedule can start one every few minutes without flooding your inbox: its line is in the log its scheduler keeps. It makes a Run's checks before any other work all the same, an address and a Resend API key, and stops with exit 1 if either is missing, so a run that would have been skipped fails on them too. The email goes after the outcome is final and printed, and a failed send is only a warning: line on stderr: it changes neither the exit code nor stdout.

  • Subject: [thirdshift] <owner>/<repo> Architect run: <outcome>. With a dispatched run, the outcome is that run's: ready for review, merged, failed or interrupted. Without one, it is the review's: plan published (with --plan-only), idea filed, idea already filed, review failed (also for a plan that fails a check) or interrupted. The repository is left out if origin doesn't name one on GitHub.
  • Body, plain text: a Result: line with the same outcome as the subject; a Review: line saying how the Architecture review ended, with the URL of the plan or idea issue it named (plan published: <url>, idea filed: <url>, idea already filed: <url>, failed or interrupted); a Description: line with that issue's title, when known, so you can see what the review proposed without opening the link; when the plan was dispatched, a Dispatched: line with that run's outcome; then what a Run's notification holds, for the dispatched run or else the failed review: the pull request URL (if any), the failure cause (if failed), with the lines after it of a dispatched run that failed on an Inherited failure, a Base fix: line for a dispatched run that started or waited on a Base fix, the session log path (if any), the Command log: path (if any), the hostname and how long the Architect run took. After a dispatched Spec run, it ends with a line per Ticket, as a Spec run's notification does.
-

No Strong candidate

+
+

No Strong candidate

+

Only a Strong top recommendation becomes a plan. When the review's top recommendation is Worth exploring or Speculative, it publishes no plan, and the Architect run ends in one of two ways, both a success, with or without --plan-only: exit 0, with one issue's URL alone on stdout. thirdshift labels that issue an Architect idea and dispatches nothing: there is nothing to run.

  • An idea issue. The review files its top recommendation as one issue labelled needs-triage, for the Day shift to flesh out, and ends its final message with Architecture review idea: <Issue URL>. stdout carries the idea issue's URL, and stderr's last line is no Strong candidate: the Architecture review filed the idea <url>.
  • Already filed. An open issue already covers that recommendation, so the review files nothing and ends its final message with Architecture review already filed: <Issue URL>. stdout carries that issue's URL, and stderr's last line is no Strong candidate: <url> already covers the Architecture review's top recommendation, so it filed nothing.

Either way, thirdshift then puts architect-idea and needs-triage on that issue, in one request that keeps its other labels, with a progress line saying so: labelling #<n> an Architect idea: adding needs-triage and architect-idea. thirdshift adds architect-idea, never the agent, and first creates it if the repository has none, as it does architect-plan. needs-triage goes back on an issue that already covered the idea even if it had been triaged, ready-for-human say, since the factory again takes it for the best next move. If thirdshift can't label the issue, the Architect run fails as a failed review does, with the cause on stderr, could not label the Architect idea #<n>: <why>, and in its Run notification.

-

An Architect idea waits for triage

+
+

An Architect idea waits for triage

+

An Architect idea means the factory has run out of Strong ideas, so while any Architect idea is open and still labelled needs-triage, every later Architect run on the repository is skipped, before any review, instead of filing the next weaker idea or landing on the same issue again. Both labels count in any case, as every label rule does. The check comes after the lock and the open Architect plan check, so with both an open plan and a waiting idea, the skip reports the plan:

  • stdout carries each waiting Architect idea's URL, one a line, newest first.
  • @@ -694,7 +757,9 @@

    An Architect idea waits for triageExit code 0: skipped is not a failure. No agent session starts, and nothing else is done.

Any triage decision releases the pause: take needs-triage off, whatever replaces it (ready-for-agent, ready-for-human, needs-info, wontfix or nothing), or close the issue. The issue keeps architect-idea, which only counts alongside needs-triage. An idea triaged ready-for-agent is then a Ready issue, once it has settled, so the next Architect run is skipped for it in turn, until a Pickup run, or you, takes it.

-

A Ready issue goes first

+
+

A Ready issue goes first

+

Work a human shaped goes ahead of the factory's own: while the repository has a Ready issue, by the very rule a Pickup run takes one by, every Architect run on it is skipped, before any review. The check comes last, after the lock, the open Architect plan check and the waiting Architect idea check, so with an open plan or a waiting idea as well, the skip reports that instead. It is only the search: an Architect run makes no Sweep and holds no Claim limit, which are the Pickup run's.

  • stdout carries the URL of the lowest-numbered Ready issue, the one a Pickup run would take.
  • @@ -703,12 +768,16 @@

    A Ready issue goes first

An issue labelled ready-for-agent that is not a Ready issue never skips an Architect run: one that carries a Claim, was labelled or shaped within the configured settling window (thirty minutes by default), has an open blocker, is a Ticket inside a Spec that is not itself a Ready issue, or is a Base fix's issue, among the rest of the rule. Those get their lines on stderr all the same, and the Architect run goes on.

The rule doesn't ask whether anything will take the Ready issue. A ready-for-agent issue that you mean to run by hand, on a repository with no Pickup line in its crontab, keeps every Architect run on it from starting until you run it, which makes the Claim, or take the label off.

-

One at a time

+
+

One at a time

+

A Pass is a command started with no Issue URL that decides before any work whether it is skipped. Only one Pass per repository runs at a time on a machine. A Pass started while another on the same repository is still running, the Spec run or Run it dispatched included, is skipped: it prints another Pass is already running on <owner>/<repo> as a progress line on stderr, prints nothing on stdout, and exits 0. Skipped is not a failure. A skipped Pass does nothing else: no launch pull, no worktree, no agent session, and no issue filed or labelled. This holds for every Pass, however it was started, so two never build one repository at once.

The repository is the one origin names, so two clones of one repository count as the same, and Passes on different repositories run at the same time. A Run or a Spec run started on an Issue URL is never skipped this way, and never makes a Pass skip.

thirdshift holds the rule with an operating-system lock on a file under ~/.thirdshift/architect-locks/, shared by every kind of Pass, which the Pass's process takes without waiting, after its checks in step 1, and holds until it exits. The directory keeps its original name so older thirdshift versions share the lock too. The operating system releases the lock when that process ends for any reason, a crash, a kill or a reboot included, so there is never a stale lock to clear: the next Pass on the repository runs normally. The file itself stays, and means nothing on its own. A review worktree that a killed Architect run left behind is removed by the next one.

A skipped Architect run sends no Run notification, even when asked, and neither does a skipped Pickup run (sends none). Both make the notification's checks before the lock is tried all the same, so a broken setup fails the pass with exit 1 rather than being skipped.

-

One Architect plan at a time

+
+

One Architect plan at a time

+

An Architect run is also skipped while an earlier Architect plan is still open, so a new refactor is never planned while the last one is unfinished: its pull request waiting for review, its run failed, or its plan published with --plan-only and not started.

thirdshift marks every Architect plan with the label architect-plan, in step 6: the Spec, or the standalone Ticket, never a Spec's Tickets. Once the one-at-a-time lock is its own, and before the launch pull, the worktree and the Architecture review, an Architect run lists the repository's open issues labelled architect-plan. If there are any, it is skipped:

    @@ -719,7 +788,9 @@

    One Architect plan at a time

    It applies to every Architect run, with or without --plan-only, and no flag overrides it. The rule is released by finishing the Architect plan, since merging its work closes the issue, by closing the issue, or by removing its architect-plan label. The check reads GitHub, so unlike the lock it holds across machines.

    An Architect run never retries or dispatches an existing Architect plan. One whose dispatched run failed stays open for you: run thirdshift <plan URL> to continue whatever branch and pull request it left, or remove its in-progress Claim and then apply ready-for-agent to hand it back to Pickup after settling. The other Ready issue gates still apply. What isn't labelled architect-plan never counts here: a plan that a failed or interrupted review left needs-triage, or an Architect idea, which pauses Architect runs by its own rule, only while it waits for triage.

    The check comes after the lock so that the plan of an Architect run that is still running, labelled already while its Spec run or Run goes on, is reported as already running, not as an unfinished plan. A skipped run sends no Run notification, even when asked: its stdout carries the open Architect plans' URLs, and its stderr line their commands.

    -

    On a schedule

    +
+

On a schedule

+

thirdshift has no scheduler of its own: the operating system's scheduler runs the ordinary command. This crontab entry, written for a Linux machine with cron, starts an Architect run every night at 02:00. Add it with crontab -e, with your own paths in place of /home/you and ~/repos/thirdshift:

PATH=/home/you/.local/bin:/home/you/.cargo/bin:/usr/local/bin:/usr/bin:/bin
 0 2 * * * cd ~/repos/thirdshift && thirdshift architect base main >> ~/.thirdshift/logs/cron.log 2>&1
@@ -739,7 +810,9 @@ 

On a schedule

The first leaves the pull request for you to review, and the second stops at the Architect plan, for you to read and run with thirdshift <Issue URL>.

A scheduled Architect run is skipped when another Pass on the repository is still running, the Spec run or Run it dispatched included, when an Architect plan is still open, when an Architect idea waits for triage, or when the repository has a Ready issue, so a ready-for-agent issue with no Pickup line to take it holds off every night until you run it: no new refactor is planned until last night's Architect plan is closed or loses its architect-plan label. Merging its pull request closes it; if its run failed, pick it up with thirdshift <plan URL> first. Closing the pull request alone leaves the Architect plan open. A skip exits 0, so the scheduler sees no failure, and the repository's Activity log has the line that says why, as does the log file unless activity.quiet_skips is set. With Run notifications on, by email.always = true in the User config or email on the line, an Architect run that did work sends one email, and a skipped one sends none, so a crontab line can fire every few minutes, as for Weeding, without flooding your inbox. A night without an email means it was skipped, or failed before the notification's checks passed: look in the log file.

Any scheduler that runs the command works; a systemd user timer needs lingering on (loginctl enable-linger) to fire while you are logged out.

-

Weeding

+
+

Weeding

+

Weeding is the factory clearing what features leave behind in a codebase as they land, with nobody starting it: Architect runs started often enough that each one begins as soon as the last one's Architect plan is merged. thirdshift still never schedules itself (ADR-0009): it is the line from On a schedule, fired every five minutes instead of nightly, with merge.always = true in the User config so each Architect plan's pull request is merged without waiting for you:

PATH=/home/you/.local/bin:/home/you/.cargo/bin:/usr/local/bin:/usr/bin:/bin
 */5 * * * * cd ~/repos/thirdshift && thirdshift architect base main >> ~/.thirdshift/logs/cron.log 2>&1
@@ -754,7 +827,10 @@ 

Weeding

A skip exits 0 and sends no email, even with Run notifications on, so the scheduler sees no failure and your inbox only hears of passes that did work. The repository's Activity log has the line that says why, once for each change of reason. An Architect plan whose run failed stays open, so Weeding pauses for the Day shift until you pick it up with thirdshift <plan URL>, close it, or take its architect-plan label off; an Architect idea pauses it until you triage the idea.

On a repository that also has a Pickup line, put --plan-only on the Weeding line instead: the Architect run publishes the Architect plan and marks it ready, and a Pickup run builds it as an ordinary Ready issue, under the Claim limit, and merges it with merge.always, as in the whole crontab below.

-

Security runs

+
+
+

Security runs

+

thirdshift secure starts a Security run, report-only by default: it audits the Base branch at its head on origin and records each Security finding privately as a draft repository security advisory. Start it from a clone on the Base branch, or name that branch from any checkout:

thirdshift secure
 thirdshift secure base main harness claude
@@ -772,17 +848,23 @@ 

Security runs

The Security audit runs in a throwaway worktree detached at origin's Base branch head, leaving the Launch directory and local work untouched, including when launch.pull is set. Its Session prompt runs thirdshift-security-audit in full audit mode with the quick profile, auditing the whole repository except vendored and third-party code. It names a SECURITY.md or conventional threat-model document when present, reads compatible earlier runs and ends incomplete instead of asking for input.

Each audit keeps a new output directory under <logs.dir>/<owner>/<repo>/audits/, outside the worktree. The audit artifacts and private advisory descriptions contain evidence; keep the logs directory private. thirdshift runs both embedded validators before recording anything. A missing final line, an incomplete session or an invalid report fails the run and names its Session log. Command logs and Session logs are in commands/secure/; Session logs are named secure-<stamp>~<token>-security-audit.jsonl; the Activity log records the start and ending as for other Passes.

A finding's title becomes the advisory summary. Its description preserves its write-up, trace, evidence and validation plan, with the fingerprint and audited commit. thirdshift claims no severity, CWE or affected version range. It uses the package in the repository's manifest, or the other ecosystem when none is identified. Matching fingerprints in any advisory state are kept rather than recorded again. Rejected candidates stay in the local report and produce no advisory. Draft advisories require GitHub repository security manager or administrator access. On a private repository whose advisory endpoint is unavailable, the private record is an issue labelled security-finding and needs-triage. It is never replaced by a public finding issue.

-

Security review

+

Security review

+

security-review, or review = true under [security], enables one guidance review after a Run's opening session or a Spec run's Spec review, before Delivery pushes and enters the Repair loop. A Spec run reviews the whole Spec branch against its Base branch once; its Tickets' Runs get none. no-security-review overrides the setting. Both words, with or without dashes, are accepted on commands that start Runs and follow dispatched Runs and Base fixes. Setup asks about the setting; it is off by default.

The session reads the relevant attack-class guidance and supporting code without delegated auditors or the full audit workflow. It fixes only findings reproduced by a failing proof-of-concept test, keeps that test and commits the fix. Unaddressed introduced findings appear under Security in the PR's Unaddressed findings. A Merge run, including a Spec run asked to merge its Spec PR, leaves the PR ready for review and fails with the findings named if any remain, or if the review was refused or incomplete. A Run or Spec run not asked to merge records the outcome and continues. An old finding, whose proof-of-concept also fails at the pinned merge base, is reported in a private file under the repository's logs (security-reviews/). thirdshift records it as a draft advisory, or a security-finding/needs-triage issue on a private repository, reusing existing fingerprints and preserving the Day shift's grades. New records include scored reproduction and fix-size metadata so a later Security run can take the fix when allowed. Its details stay out of the PR and it does not hold Self-merge; fixing belongs to a separate Security run. The separate Security audit retains its full audit workflow.

-

Fencing

+
+

Fencing

+

Fencing is the factory finding and fixing vulnerabilities with nobody starting it: Security runs started on a schedule with fixing allowed. thirdshift never schedules itself (ADR-0009). This Linux crontab entry starts one every five minutes; use your own paths, with claude and gh already logged in and Git able to push:

PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin
 */5 * * * * cd /home/you/repos/widgets && thirdshift secure base main harness claude security-fix >> /home/you/thirdshift-secure.log 2>&1
 

Each pass yields to a Ready issue, so work the Day shift shaped goes first. It is skipped while another Pass on the repository is running, while a fix has failed and its issue stays open, while a finding waits for triage and no reproduced finding can be fixed, or while the Base branch is unchanged and no fix is waiting. A failed fix pauses Fencing for the Day shift until its issue is closed; it is never retried by a later Security run or Pickup run. Triaging a finding lets audits go on; fixing allowed can still take a reproduced finding ahead of a different finding waiting for triage.

A skip exits 0 and sends no Run notification. Its reason appears in the Activity log once per change of reason. Set activity.quiet_skips = true in the User config to keep those skips out of the scheduler's log. merge on the command, or merge.always = true in the User config, lets each fix's Run merge its pull request; otherwise it stays ready for review.

-

Pickup runs

+
+
+

Pickup runs

+

thirdshift pickup starts a Pickup run: one pass, with no Issue URL, that takes the lowest-numbered Ready issue in the repository and runs it, so that an issue you have already marked ready needs no command typed for it. Start it from a clone of the repository, on the Base branch, or name the Base branch with base <branch>:

cd ~/repos/widgets
 thirdshift pickup                      # take the lowest-numbered Ready issue and run it
@@ -816,7 +898,8 @@ 

Pickup runs

pickup is a command only as the first argument, and takes nothing but those flags, each at most once: a focus, --plan-only, an Issue URL, a repeated or contradictory flag, or any other argument is an argument error (exit 2).

The dispatched run's ending is the Pickup run's: its exit code, its pull request's URL alone on stdout, and its last line on stderr, PR <url> is ready for review or PR <url> is merged. If it fails, the Pickup run fails as that Failed run or Failed spec run does. After an Inherited failure, the command it offers is thirdshift <Issue URL> with the dispatched run's flags and base-fix, since another Pickup run needs you to remove its Claim and then apply ready-for-agent before it can take started work again.

A pass is skipped when the lock is held, when the repository is at its Claim limit, when the repository has no Ready issue, or when the selected issue is reserved or stale: it exits 0 with stdout empty and a reason on stderr. A reserved or stale selection says #<n> is reserved by another Pickup run or is no longer Ready, with the reservation ref to inspect if it stays reserved. Skipped is not a failure. A skipped pass starts no agent session, makes no Claim, and sends no Run notification. One skipped for the lock changes no label at all; the others have made the Sweep, whose lines come before the reason.

-

The Claim limit

+

The Claim limit

+

A Pickup run takes nothing while the repository is at its Claim limit: as many open issues carry a Claim, that is, are labelled in-progress, as the limit, which is 3 unless you set it. It keeps a broken Base branch from failing every Ready issue in turn, one a pass, and pull requests from piling up unreviewed: once that many issues wait on you, the factory waits too.

  • The count is every open issue labelled in-progress in the repository, whoever started it: a Run you started by hand counts as one a Pickup run dispatched does. Closed issues don't count.
  • @@ -825,14 +908,18 @@

    The Claim limit

  • An issue leaves the count when it is closed, as merging its pull request does, or when you take its in-progress label off.
  • If the issues can't be counted, the pass stops with exit 1 and gh's error.
-

The Sweep

+
+

The Sweep

+

In the Sweep, each Pickup run, once it holds the lock and before it counts, lists the repository's closed issues labelled in-progress and takes the label off each, so an issue whose pull request you merged by hand doesn't look taken for ever.

  • Only in-progress is removed, in one request per issue: the closed issue's other labels stay. stderr says taking in-progress off #<n>, which is closed.
  • It runs on every pass that gets the lock, one that is then skipped for the Claim limit or for having no Ready issue included, and not on one skipped because the lock is held.
  • A failure, to list the closed issues or to take the label off one, is a warning: line on stderr, such as warning: could not take in-progress off #<n>: <gh's error>, and the pass carries on: the next pass tries again.
-

Why an issue was passed over

+
+

Why an issue was passed over

+

Each open issue labelled ready-for-agent that a pass looks at and does not take gets one line on stderr, naming the issue and the first reason that applies, in the order of the Ready issue rule above. The lines come before the line that says what the pass did, so the cron log explains why nothing started:

thirdshift: 03:00:01 Pickup run starting, 2026-10-03 -0400
 thirdshift: 03:00:02 #18 labelled needs-info
@@ -855,7 +942,9 @@ 

Why an issue was passed over

thirdshift: 03:00:04 taking Ready issue #22 "Sharpen the widgets", as thirdshift https://github.com/acme/widgets/issues/22 would thirdshift: 03:00:04 logging this command to ~/.thirdshift/logs/acme/widgets/commands/pickup/22-20261003T030001-0400~aB3cD4eF5gH6iJ7k.log
-

One Run notification for the issue taken

+
+

One Run notification for the issue taken

+

A Pickup run asked for a Run notification, by email or by email.always = true in the User config without no-email, sends exactly one when it took a Ready issue: the notification the Spec run or Run it dispatched would have sent started by hand. An address after email wins over email.to, as for a Run.

  • Subject: that run's, [thirdshift] <owner>/<repo>#<n> <issue title>: <outcome>, naming the issue taken, with the outcome ready for review, merged, failed or interrupted.
  • @@ -864,7 +953,9 @@

    One Run notification for the i

    The email goes after the outcome is final and printed, and a failed send is only a warning: line on stderr: it changes neither the exit code nor stdout. The dispatched run sends no notification of its own, whatever the User config says, as with an Architect run, nor do a Spec run's Ticket Runs.

    A skipped pass sends no notification, even when one was asked for: a pass every half hour would otherwise send dozens a day. A skipped Architect run sends none either.

    The notification's checks, an address and a Resend API key, are made before any other work on every pass, before the checks in step 1, the lock and any label read or changed. A broken setup therefore stops the pass with exit 1, naming what is missing, even a pass that would have been skipped, so it shows in the scheduler's log on the first pass, not only once an issue is ready. A pass that asks for no notification makes none of these checks.

    -

    A Pickup run on a schedule

    +
+

A Pickup run on a schedule

+

thirdshift has no scheduler of its own (ADR-0009): the operating system's scheduler runs the ordinary command, one pass each time. This crontab entry, written for a Linux machine with cron, starts a pass every half hour. Add it with crontab -e, with your own paths in place of /home/you and ~/repos/widgets:

PATH=/home/you/.local/bin:/home/you/.cargo/bin:/usr/local/bin:/usr/bin:/bin
 */30 * * * * cd ~/repos/widgets && thirdshift pickup base main >> ~/.thirdshift/logs/cron.log 2>&1
@@ -892,7 +983,10 @@ 

A Pickup run on a schedule

An issue whose run failed keeps its Claim and waits for the Day shift: it stays in-progress, with the Issue branch and any draft pull request the run left, no later pass takes it, and it counts towards the Claim limit until it is closed or you take the label off. To send it round again by hand, run thirdshift <Issue URL> from the clone, with the Base branch checked out, since that command takes no base <branch>: it picks up where the failed run stopped, as a Continuation does. To hand it back to Pickup instead, remove in-progress and then apply ready-for-agent: after settling, a pass can continue it if every other Ready issue gate still permits it. Applying the ready label before the last Claim ends does not hand it back. For an ordinary Run, the one failure a later pass does retry is one that left nothing on origin, such as a usage limit or an expired login: its Claim is released, so the issue is a Ready issue again once it has settled. A failed Security fix is always left for the Day shift, even with nothing on origin.

When shaping issues with to-spec and to-tickets from mattpocock/skills, keep a Spec labelled needs-triage until every Ticket is published, attached as a sub-issue and linked to its blockers; then replace it with ready-for-agent. A Spec with no Tickets yet looks exactly like a standalone Ticket, so a pass would start a plain Run on it. This repository's Day shift follows the publication procedure in docs/agents/triage-labels.md, required by AGENTS.md, which overrides the imported skills' defaults and survives their updates. The settling window (thirty minutes by default) is only a second line of defence: it covers Tickets attached within minutes of the label, not a Spec left labelled and without Tickets for longer.

-

Building from source

+
+
+

Building from source

+

This Markdown is the source for the site's manual. After changing it, run cd site && npm ci && npm run docs from a repository checkout and commit site/docs/index.html in the same pull request. cd site && npm test checks that the generated page is current and readable in a browser; the deployed site needs no build step.

Building needs the Rust toolchain. From a clone of this repository:

cargo install --path .
@@ -911,14 +1005,18 @@ 

Building from source

Editing a skill in skills/ has no effect until you rebuild and reinstall (ADR-0001).

The Prompts and skills page is generated from the prompts, the claude and codex arguments and the skills, and each Session prompt is also generated as a Markdown file in prompts/, with a README.md that lists them with the claude and codex command lines. cargo test fails until they are regenerated after a change to any of them. Regenerate the page and prompts/ with UPDATE_PROMPTS=1 cargo test prompts_page, which also deletes any file in prompts/ that it doesn't generate.

The integration tests swap in a fake gh, claude, codex, agy, grok, muse and opencode, from tests/fakes.rs. The first test that needs them builds them with rustc (the one on PATH, or $RUSTC), so the suite needs nothing else on PATH beyond git and bash.

-

The Rust version

+

The Rust version

+

rust-toolchain.toml pins the exact Rust version this repository builds with, and the rustfmt and clippy components CI runs. rustup reads it, so cargo in a checkout, CI, the release builds and the crates.io publish all use that version, and rustup installs it the first time it is needed. No workflow names a version of its own. The file is not in the crates.io package, so cargo install thirdshift builds with whatever Rust the machine has.

A newer Rust arrives as a pull request that changes channel in that file, so it runs through CI before main builds with it. Dependabot opens that pull request within a week of a stable release (.github/dependabot.yml). To move the pin by hand, set channel to the new version in full, patch number included, run the three checks CI runs, and open a pull request with the change and whatever the new compiler's lints asked for:

cargo fmt --check
 cargo clippy --all-targets -- -D warnings
 cargo test
 
-

Releasing

+
+
+

Releasing

+

From a clone of this repo, signed in to gh and the User config's default Harness, run the release script with the new version:

scripts/release.sh 0.4.0
 
@@ -926,10 +1024,13 @@

Releasing

Before it pushes anything, it refuses a release that can't be cut and says why: a version that isn't plain X.Y.Z (no leading v), a version that isn't higher than the one in Cargo.toml on origin/main, a v<version> tag that already exists locally or on origin, or a latest CI run on origin/main that is missing for its tip or ended without success. If that CI run hasn't finished, it prints a progress line and waits for it, with no overall timeout. Once CI succeeds, it starts the release automatically; if CI ends without success, it stops without pushing anything.

If it's interrupted, run it again with the same version: it picks up the existing branch or pull request and carries on from the first step not yet done, without the checks above, which the first run passed. With --review, it asks about a new summary before opening a pull request for a branch already pushed. Once the tag is on the merge of the pull request, it says the release is already tagged and picks up from the release workflow run: it waits for a run still going, prints the GitHub Release's URL and exits 0 for one that succeeded, and exits 1 as above for one that failed, reporting the run's latest attempt, so a rerun of its failed jobs counts. So you can stop it with Ctrl-C while it waits and run it again to carry on waiting. A v<version> tag anywhere else on origin is still refused.

The tag starts the release workflow, generated by dist from dist-workspace.toml, with the event policy applied by python3 scripts/generate-release.py (--check verifies freshness). It builds the Linux and macOS binaries and the installer, checks that the Linux binary starts on Rocky Linux 9 and in WSL2 with Ubuntu 24.04, and only then publishes the GitHub Release. It then publishes the same version to crates.io, skipping it if it is already there, and finally replaces the Release's body with the bump pull request's summary, then notes generated from the merged pull requests, then the install instructions. scripts/release-notes.sh builds that body; for a tag pushed by hand, with no bump pull request summary, it is the generated notes and the install instructions alone.

-

Credits and license

+
+

Credits and license

+

thirdshift is released under the MIT License.

Most Factory skills in skills/ are adapted from Matt Pocock's mattpocock/skills, changed to run headless with no human in the loop. They are used under the MIT License; its copyright notice is kept in skills/LICENSE and is embedded in the binary and written out with the skills on every Run. The thirdshift-pr skill also reproduces part of Dex Horthy's show-me skill; see skills/thirdshift-pr/CREDITS.md.

The thirdshift-security-audit Factory skill is copied from Cloudflare's security-audit-skill at commit c1c8a8c, with only its frontmatter name changed. It is used under the MIT License; Cloudflare's copyright notice and licence are kept in skills/thirdshift-security-audit/LICENSE, with the upstream repository and full commit in CREDITS.md. Both are embedded and written out with the skill.

+
diff --git a/site/generate-docs.mjs b/site/generate-docs.mjs index 27b672df..57873016 100644 --- a/site/generate-docs.mjs +++ b/site/generate-docs.mjs @@ -7,13 +7,23 @@ const source = new URL("../docs/guide.md", import.meta.url); const output = new URL("docs/index.html", import.meta.url); const slugger = new GithubSlugger(); const sections = []; +const folds = []; const markdown = new Marked({ renderer: { heading({ tokens, depth, text }) { const id = slugger.slug(text); const content = this.parser.parseInline(tokens); if (depth === 2) sections.push(`
  • ${content}
  • `); - return `${content}\n`; + if (depth === 1) return `

    ${content}

    \n`; + let closed = ""; + while (folds.length && folds.at(-1) >= depth) { + folds.pop(); + closed += "\n"; + } + folds.push(depth); + // Keep the existing fragment on the content: native fragment navigation + // reveals its containing disclosures even when JavaScript is disabled. + return `${closed}
    ${content}\n
    \n`; }, }, walkTokens(token) { @@ -24,7 +34,7 @@ const markdown = new Marked({ token.href = new URL(token.href, "https://github.com/JacobStephens2/thirdshift/blob/main/docs/").href; }, }); -const content = markdown.parse(await readFile(source, "utf8")); +const content = markdown.parse(await readFile(source, "utf8")) + "
    \n".repeat(folds.length); const page = ` @@ -49,6 +59,7 @@ const page = ` + Skip to the manual diff --git a/site/how-it-works/guide.css b/site/how-it-works/guide.css index f1632683..f57b2fda 100644 --- a/site/how-it-works/guide.css +++ b/site/how-it-works/guide.css @@ -11,10 +11,10 @@ .guide-map > .eyebrow { margin-bottom: 20px; } .guide-next { margin: 20px 0 0; font-size: 13px; color: var(--dim); } .guide-section { max-width: 1000px; } -.guide-section > p { max-width: 76ch; margin-block: 22px; } +.guide-content > p { max-width: 76ch; margin-block: 22px; } .guide-section .sec-head h2 { font-size: clamp(36px, 5.5vw, 64px); } .guide-section code { overflow-wrap: anywhere; } -.guide section, .guide li[id] { scroll-margin-top: 24px; } +.guide [id] { scroll-margin-top: 24px; } .guide-diagram { margin: 30px 0; padding: clamp(16px, 3vw, 30px); border: 1px solid var(--rule); } .guide-diagram figcaption { margin-bottom: 20px; color: var(--c); font-size: 13px; } .guide-flow { display: flex; gap: 28px; list-style: none; margin: 0; padding: 0; } diff --git a/site/how-it-works/index.html b/site/how-it-works/index.html index d1371ece..39cb847b 100644 --- a/site/how-it-works/index.html +++ b/site/how-it-works/index.html @@ -24,6 +24,7 @@ + Skip to the process @@ -52,9 +53,10 @@

    How thirdshift works

    Then scale up: 06 / Specs and Tickets · 07 / Scheduled work

    -
    -
    -

    01 / The human handoff

    Start with work you can describe

    +
    +
    +

    01 / The human handoff

    Start with work you can describe

    +

    The Day shift is you: settle what should change, what success looks like, and what is out of scope. Put that in a GitHub issue. One session's worth of work can stand alone; a larger Spec is an issue with Tickets as GitHub sub-issues. We'll follow a standalone issue first.

    Human intent becomes the agent's brief
    @@ -68,12 +70,14 @@

    How thirdshift works

    cd ~/repos/widgets
     thirdshift --no-merge https://github.com/acme/widgets/issues/7

    --no-merge explicitly leaves the pull request for review, even if your User config normally requests merging. Before starting, install and authenticate GitHub CLI (gh) and your chosen Harness (the agent CLI, such as Claude Code or Codex), configure Git's commit identity and push access, and install the repository's test toolchain. thirdshift setup chooses machine defaults.

    -
    +
    +
    -
    -
    -

    02 / Where the work happens

    Give the Run its own checkout

    +
    +
    +

    02 / Where the work happens

    Give the Run its own checkout

    +

    thirdshift checks that the Issue URL matches the clone's origin, that the issue is open, and that the Base branch, Harness and Git identity are usable. It selects an Issue branch, marks the issue in-progress (its Claim), then acquires a sibling Git Worktree. A fresh Issue branch starts from the fetched Base branch on origin.

    Example: two checkouts, one Git repository
    @@ -85,12 +89,14 @@

    How thirdshift works

    The work happens in the Worktree rather than switching your Launch directory to the Issue branch. If an existing Issue branch has no pull request or an open one, thirdshift continues it instead of starting over. An open pull request supplies its Base branch. Once a branch's pull request is closed or merged, later work uses a new numbered Issue branch.

    -
    +
    +
    -
    -
    -

    03 / What the agent does

    Let the agent implement and review

    +
    +
    +

    03 / What the agent does

    Let the agent implement and review

    +

    thirdshift starts a headless session in the selected Harness, in the Worktree. The Session prompt supplies the issue, Base branch, Issue branch and review instructions. Factory skills are linked where that Harness discovers them. The opening session carries the change from implementation through review to a committed branch and pull request.

    Inside the opening agent session
    @@ -102,12 +108,14 @@

    How thirdshift works

    The Standards axis checks repository coding standards and code smells. The Spec axis checks the change against the issue. For findings that say behavior is wrong, the implementing agent must run the reviewer's reproducer before deciding: a demonstrated failure gets fixed with its test kept; a declined finding needs evidence that exercises it. Skipped findings go in the pull request's Unaddressed findings section, and decisions for the Day shift get a linked triage issue.

    These are agent reviews, followed by deterministic Git and CI checks in Delivery. They do not guarantee correctness or replace your judgment. You can inspect the exact prompts and Factory skills, including the review procedure.

    -
    +
    +
    -
    -
    -

    04 / From a published change to a checked result

    Deliver against the latest Base branch

    +
    +
    +

    04 / From a published change to a checked result

    Deliver against the latest Base branch

    +

    A pull request existing is not the finish line. Delivery runs the opening session, then keeps the captured pull request mergeable and checks CI. This orchestration is thirdshift's code; a Repair is another agent session started when the branch needs work.

    The Repair loop, after the opening session publishes
    @@ -124,12 +132,14 @@

    How thirdshift works

    Repair and upstream-move budgets keep the loop bounded. If a CI-fix Repair leaves the same failing head, that head gets one Check re-run; if its own checks stay red, the Run fails rather than spending more Repairs on unchanged code.

    An Inherited failure is a check that also failed under the same name on the Base branch commit last merged in. thirdshift does not ask a Repair to fix that on this Issue branch. It fails the Run, or, when allowed, starts a separate Base fix and waits before trying again.

    An optional Security review runs after the opening session (or after the Spec review for a Spec PR). It fixes reproduced vulnerabilities introduced by this change; an introduced finding left unfixed holds Self-merge. Security review details.

    -
    +
    +
    -
    -
    -

    05 / The result and your next move

    Know what comes back

    +
    +
    +

    05 / The result and your next move

    Know what comes back

    +

    Ready for review

    The usual successful result: an open pull request into the Base branch, ready, mergeable and passing CI (or with no checks). You review the diff, tests and Unaddressed findings before merging.

    Merged, when requested

    A thirdshift merge <Issue URL> command requests a Merge run. thirdshift itself merges the exact head whose CI it watched. Foreign commits on the Issue branch are reconciled and their resulting changes reviewed first. Required human reviews can still block it, leaving the pull request ready for review.

    @@ -141,12 +151,14 @@

    How thirdshift works

    thirdshift show https://github.com/acme/widgets/issues/7 thirdshift logs https://github.com/acme/widgets/issues/7 --follow

    The Command log records the whole command; Session logs record the individual agent sessions, and the Activity log records starts and outcomes per repository. Optional email notifications report how the Run ended. Inspecting logs.

    -
    +
    +
    -
    -
    -

    06 / The same primitive, composed

    Scale one Run into a Spec run

    +
    +
    +

    06 / The same primitive, composed

    Scale one Run into a Spec run

    +

    When the Issue URL has Tickets as GitHub sub-issues, thirdshift starts a Spec run. GitHub's “blocked by” links determine when a Ticket can start. Independent Tickets can run in parallel, each in its own Worktree; a blocked Ticket waits for its prerequisites to land.

    Example dependency graph: A and B are independent; C needs both
    @@ -162,12 +174,14 @@

    How thirdshift works

    Each Ticket is a Merge run into the Spec branch, not directly into main. The Spec PR gathers the result into the Base branch. A Ticket closing means its work landed on the Spec branch; the whole Spec has not shipped yet.

    A failed Ticket stops only its dependents; other reachable Tickets can still run. Tickets marked for human work or triage also hold their dependents. An incomplete Spec leaves a draft Spec PR with a Tickets checklist. Rerunning continues existing branches. Spec run controls and limits.

    -
    +
    +
    -
    -
    -

    07 / Where the next issue comes from

    Choose how the next work starts

    +
    +
    +

    07 / Where the next issue comes from

    Choose how the next work starts

    +

    A direct Issue URL starts work you have already chosen. Other commands find work, then dispatch the same Run or Spec run you just followed. Each invocation makes one pass; your operating system's scheduler starts the next one.

    Different entry points, the same implementation and Delivery
    @@ -180,7 +194,8 @@

    How thirdshift works

    Pickup respects issue readiness, open blockers and the Claim limit. Architect and Security runs yield to ready human-shaped work. Gates can skip a pass without failure, and unresolved plans, ideas or findings can leave the next decision with the Day shift. This is scheduled CLI work on your machine, using your authenticated tools.

    Next: inspect every prompt and skill, see a real Run's recorded output, or use the manual for installation, commands and configuration.

    -
    +
    +