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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ One statically linked Linux binary covers the first three, so it doesn't depend
The user account that runs thirdshift needs:

- **`claude`** (Claude Code), logged in.
- **`codex`** (Codex CLI), logged in, only for `harness codex`.
- **`gh`** (GitHub CLI), logged in.
- **`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.

Expand All @@ -108,7 +109,7 @@ Sessions run headless in Claude Code's auto mode (`claude -p --permission-mode a

### 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-*`, 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:
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 Codex, 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.
Expand Down Expand Up @@ -257,7 +258,7 @@ effort = "" # how hard that Model reasons; default blank, for Codex's own

Every key holds its real value, so a Run reading it does exactly what it does with no file. `email.to` has no default, so `setup` suggests one: the public email of your GitHub profile (from `gh api user`), else your global git `user.email`, unless that is a `@users.noreply.github.com` address, which can't receive mail. With neither, `email.to` is the only line written commented out, as above. `setup` never asks `gh` for more scopes, so a private GitHub email is not read, and a Run never looks the suggestion up: `--email` with no address and no `email.to` still stops the Run. From a terminal (stdin and stderr both terminals), `setup` first asks, on stderr:

1. The Harness for every Run's sessions (`harness.default`), listing both, each marked where sessions can't run on it here: `claude (not installed)` when `claude` isn't on `PATH`, and `codex (not supported yet)` until Codex sessions are. A marked Harness, or a name that isn't one, is refused and asked again. The default is the one sessions can run on, `claude` when both can. Then the Model (`harness.<name>.model`) and the Effort (`harness.<name>.effort`) for the Harness chosen only, each defaulting to the User config's, where Enter on none keeps the Harness's own default and `-` clears one. A named Model gets the test call a Run makes (see [Harness, Model and Effort](#harness-model-and-effort)); if Claude refuses it, Setup shows what Claude said and asks the Model and Effort again. The other Harness's section is left as it is. With no Harness sessions can run on here, Setup says so, asks none of this, and leaves the harness settings as they are.
1. The Harness for every Run's sessions (`harness.default`), listing both, each marked where sessions can't run on it here: `claude (not installed)` when `claude` isn't on `PATH`, and `codex (not supported yet)` until Setup offers Codex. A marked Harness, or a name that isn't one, is refused and asked again. The default is the one sessions can run on, `claude` when both can. Then the Model (`harness.<name>.model`) and the Effort (`harness.<name>.effort`) for the Harness chosen only, each defaulting to the User config's, where Enter on none keeps the Harness's own default and `-` clears one. A named Model gets the test call a Run makes (see [Harness, Model and Effort](#harness-model-and-effort)); if Claude refuses it, Setup shows what Claude said and asks the Model and Effort again. The other Harness's section is left as it is. With no Harness sessions can run on here, Setup says so, asks none of this, and leaves the harness settings as they are.
2. Every Run a Merge run? (`merge.always`)
3. With that on, every Run may start a Base fix when the Base branch's CI is red? (`base.fix`). With it off, this is not asked, and `base.fix` is written at its default, `false`.
4. Every Run first fast-forwards your checkout of the Base branch? (`launch.pull`)
Expand Down Expand Up @@ -294,9 +295,11 @@ Every session of a command, its implement session, each Repair and Resume, a Spe
thirdshift https://github.com/<owner>/<repo>/issues/<n> model claude-opus-5-5 effort high
```

For each of the three, the command wins, then the [User config](#user-config), then the default. The Harness's default is `claude`; the Model's and the Effort's are nothing, which leaves them to the Harness, so with no flag and no `[harness]` section sessions run exactly as `claude -p` would. A Model and Effort set in the User config come from the chosen Harness's own section, so `harness claude` over a `codex` default takes `[harness.claude]`, while `model` and `effort` on the command apply to whichever Harness is chosen. Names are passed to Claude Code as given: `--model` and `--effort` when set. Only `claude` works for now: `harness codex` fails with `not supported yet`.
For each of the three, the command wins, then the [User config](#user-config), then the default. The Harness's default is `claude`; the Model's and the Effort's are nothing, which leaves them to the Harness, so with no flag and no `[harness]` section sessions run exactly as `claude -p` would. A Model and Effort set in the User config come from the chosen Harness's own section, so `harness claude` over a `codex` default takes `[harness.claude]`, while `model` and `effort` on the command apply to whichever Harness is chosen. Names are passed to Claude Code as given: `--model` and `--effort` when set. Codex takes them as it names them, which the check below settles: `-m <model>` and `-c model_reasoning_effort="<effort>"` when set.

Before any work, where an Origin match failure would stop it, and so before the Claim, the worktree and any Command log, a command checks what it chose: the Harness's CLI must be on `PATH`, else it fails naming the CLI and the setting that chose it; and a named Model gets a minimal test call, `claude -p --model <model>` with the Effort, if any, which must succeed, else the command fails with what Claude said. No test call is made when no Model is named. A Pickup run or an Architect run makes these checks only once it has decided not to skip, and a Spec run makes them once for all its Tickets.
With `harness codex`, every session runs `codex exec --json --dangerously-bypass-approvals-and-sandbox` in the Run's worktree, with stdin set to null, and with `-c project_doc_fallback_filenames=["CLAUDE.md"]`, so Codex reads `CLAUDE.md` where a repository has no `AGENTS.md`. Codex's sandbox is off: on a stock Ubuntu machine it can't start, and commits and pushes from a worktree write outside it, so the worktree is the only boundary, as with Claude's auto mode ([ADR-0012](docs/adr/0012-factory-skills-linked-into-the-worktree-codex-unsandboxed.md)). The Factory skills are linked into the worktree's `.agents/skills/`, kept out of git with `/.agents/skills/thirdshift-*` in `.git/info/exclude`, and each prompt's first line loads its skill as `$thirdshift-<skill>`. A Resume runs `codex exec … resume <session id>`, with every flag given again. Progress lines come from Codex's `item.*` events, the session-ended line gives its token totals, as Codex reports no turns or cost, and the final message is the last `agent_message`. A session whose turn fails (`turn.failed`), or that exits non-zero, fails the Run with Codex's error in the cause; an `error` event Codex retries does not. Each Codex session adds a `trust_level = "trusted"` entry for the repository to `~/.codex/config.toml`, and your `~/.claude/` instructions and hooks don't reach it.

Before any work, where an Origin match failure would stop it, and so before the Claim, the worktree and any Command log, a command checks what it chose: the Harness's CLI must be on `PATH`, else it fails naming the CLI and the setting that chose it; and on Claude a named Model gets a minimal test call, `claude -p --model <model>` with the Effort, if any, which must succeed, else the command fails with what Claude said. No test call is made when no Model is named. On Codex a named Model and Effort are checked against `codex debug models`, which costs no tokens: each matches regardless of case, a Model by its slug or its display name, and becomes the name Codex takes, so `model GPT-6.1-Sol effort Max` reaches Codex as `gpt-6.1-sol` and `max`. An unknown Model, or an Effort that Model doesn't support (with no Model, one no Model supports), fails naming the valid choices. What the command records is what the check settled on. A Pickup run or an Architect run makes these checks only once it has decided not to skip, and a Spec run makes them once for all its Tickets.

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.

Expand Down
4 changes: 3 additions & 1 deletion prompts/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- Generated by src/prompts_page.rs from the prompts' titles and when sentences in src/prompts_page.rs and the claude arguments in src/session.rs; don't edit. Regenerate with UPDATE_PROMPTS=1 cargo test prompts_page -->
<!-- Generated by src/prompts_page.rs from the prompts' titles and when sentences in src/prompts_page.rs and the claude and codex arguments in src/session.rs; don't edit. Regenerate with UPDATE_PROMPTS=1 cargo test prompts_page -->

# Session prompts

Expand All @@ -16,6 +16,8 @@ A [Resume](resume.md) continues the session that ended:
claude -p --permission-mode auto --output-format stream-json --verbose --resume <session id> <prompt>
```

With `harness codex`, each session runs `codex exec --json --dangerously-bypass-approvals-and-sandbox -m <model> -c model_reasoning_effort="<effort>" -c project_doc_fallback_filenames=["CLAUDE.md"] <prompt>` instead, without `-m <model>` or the Effort's `-c` where they are not set, and with `resume <session id>` before the prompt for a Resume. The Factory skills are linked into `.agents/skills/`, and a prompt's first line loads its skill as `$thirdshift-<skill>`.

| Prompt | When it is sent | File |
| --- | --- | --- |
| Fresh | Starts the implement session, which goes on to review and open the pull request, when the Run starts a new Issue branch. | [fresh.md](fresh.md) |
Expand Down
1 change: 1 addition & 0 deletions site/prompts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h1 class="misreg">Prompts and skills</h1>
<pre class="job-text" aria-label="A new session"><code>claude -p --permission-mode auto --output-format stream-json --verbose <mark class="ph">&lt;prompt&gt;</mark></code></pre>
<p class="lede">A <a href="#prompt-resume">Resume</a> continues the session that ended:</p>
<pre class="job-text" aria-label="A Resume"><code>claude -p --permission-mode auto --output-format stream-json --verbose --resume <mark class="ph">&lt;session id&gt;</mark> <mark class="ph">&lt;prompt&gt;</mark></code></pre>
<p class="lede">With <code>harness codex</code>, each session runs <code>codex exec --json --dangerously-bypass-approvals-and-sandbox -m <mark class="ph">&lt;model&gt;</mark> -c model_reasoning_effort="<mark class="ph">&lt;effort&gt;</mark>" -c project_doc_fallback_filenames=["CLAUDE.md"] <mark class="ph">&lt;prompt&gt;</mark></code> instead, without <code>-m <mark class="ph">&lt;model&gt;</mark></code> or the Effort's <code>-c</code> where they are not set, and with <code>resume <mark class="ph">&lt;session id&gt;</mark></code> before the prompt for a Resume. The Factory skills are linked into <code>.agents/skills/</code>, and a prompt's first line loads its skill as <code>$thirdshift-&lt;skill&gt;</code>.</p>
</div>
</section>
<section class="sec" aria-labelledby="prompts-title">
Expand Down
3 changes: 2 additions & 1 deletion src/architect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub fn run(
focus: Option<&str>,
base: Option<&str>,
launch_pull: bool,
harness: &Choice,
harness: &mut Choice,
) -> Result<Outcome, FailedRun> {
let started = Utc::now();
let Launch {
Expand All @@ -242,6 +242,7 @@ pub fn run(
return Ok(skip(&repo, skipped));
}
harness.check()?;
let harness = &*harness;
logs::started(Work::ArchitectRun(&repo), harness);
if launch_pull {
directory.pull(&base);
Expand Down
6 changes: 6 additions & 0 deletions src/base_fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ impl BaseFix {
}
}

/// Have any Base fix it starts run its sessions on `harness`: the Run's,
/// once the Run has checked it.
pub fn runs_on(&mut self, harness: Choice) {
self.harness = harness;
}

/// Whether the Run compares its red checks with the Base branch's, so
/// that some may be Inherited failures.
pub fn sees_inherited_failures(&self) -> bool {
Expand Down
Loading
Loading