Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9856b1f
Gather Claude and Codex behind Harness adapters (#434)
JacobStephens2 Oct 6, 2026
860f48e
Merge pull request #441 from JacobStephens2/issue-434
JacobStephens2 Oct 6, 2026
0dc42af
Stop every Harness session process tree on interruption (#435)
JacobStephens2 Oct 6, 2026
3778911
Share detached-command exit assertions after Standards review
JacobStephens2 Oct 6, 2026
3ef66d5
Add the agy Harness and installed-only Setup (#436)
JacobStephens2 Oct 6, 2026
e83a16a
Add Muse Code Harness (#438)
JacobStephens2 Oct 6, 2026
5ac1f71
Avoid inferring killed agy work from active tool steps (#436)
JacobStephens2 Oct 6, 2026
6fc043a
Include agy settings in the config key regression check (#436)
JacobStephens2 Oct 6, 2026
7136bcd
Merge pull request #443 from JacobStephens2/issue-435
JacobStephens2 Oct 6, 2026
b683026
Cover Muse's multi-reply stream fallback (#438)
JacobStephens2 Oct 6, 2026
6888812
Merge remote-tracking branch 'origin/issue-432' into issue-436
JacobStephens2 Oct 6, 2026
a910f35
Merge remote-tracking branch 'origin/issue-432' into issue-438
JacobStephens2 Oct 6, 2026
347dfe7
Add Grok Build Harness (#437)
JacobStephens2 Oct 6, 2026
ab32200
Address Grok Harness Standards and Spec findings
JacobStephens2 Oct 6, 2026
baeee64
Merge pull request #445 from JacobStephens2/issue-436
JacobStephens2 Oct 6, 2026
84c76e1
Merge issue-432 and verify Grok Resume with documented frames
JacobStephens2 Oct 6, 2026
dd94dd5
Merge remote-tracking branch 'origin/issue-432' into issue-438
JacobStephens2 Oct 6, 2026
78403c0
Emit one terminal result in Grok failure scenarios
JacobStephens2 Oct 6, 2026
45de69e
Merge pull request #444 from JacobStephens2/issue-438
JacobStephens2 Oct 6, 2026
f4a25db
Add the OpenCode Harness with standalone sessions and export recovery…
JacobStephens2 Oct 6, 2026
219ef35
Expect the OpenCode section in Setup defaults (#439)
JacobStephens2 Oct 6, 2026
d00d69b
Merge origin/issue-432 into issue-437
JacobStephens2 Oct 6, 2026
d526116
Merge pull request #446 from JacobStephens2/issue-437
JacobStephens2 Oct 6, 2026
5e85210
Address Standards and Spec findings for OpenCode (#439)
JacobStephens2 Oct 6, 2026
380f4ed
Merge remote-tracking branch 'origin/issue-432' into issue-439
JacobStephens2 Oct 6, 2026
a6c6a3b
Use documented OpenCode stream events for Resume coverage (#439)
JacobStephens2 Oct 6, 2026
58085dc
Merge pull request #447 from JacobStephens2/issue-439
JacobStephens2 Oct 6, 2026
a739d3c
Fix Harness interruption and progress gaps (#432)
JacobStephens2 Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The message thirdshift starts or resumes an agent session with. It carries the *
_Avoid_: instructions, system prompt

**Harness**:
The headless agent CLI that runs every agent session of a **Command**: Claude Code (`claude`) or Codex (`codex`). One per Command, its child Runs and every **Resume** and **Repair** included. Chosen by the command, else the **User config**, else Claude Code.
The headless agent CLI that runs every agent session of a **Command**: Claude Code (`claude`), Codex (`codex`), Antigravity CLI (`agy`), Grok Build (`grok`), Muse Code (`muse`) or OpenCode (`opencode`). One per Command, its child Runs and every **Resume** and **Repair** included. Chosen by the command, else the **User config**, else Claude Code.
_Avoid_: agent, provider, backend

**Model**:
Expand Down
59 changes: 52 additions & 7 deletions README.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions docs/adr/0013-every-harness-runs-unattended-and-fully-trusted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Every Harness runs unattended and fully trusted in the worktree

Sessions run with no human to answer an approval prompt or a trust question. Their commits and pushes write to the main repository's shared `.git`, outside the worktree. So every **Harness** runs its sessions, and every Resume of one, with approvals off, its sandbox off, and the worktree trusted. Each Harness carries the flags and environment that do this. They are fixed, and no user setting changes them:

| Harness | Flags and environment |
|---|---|
| Claude Code | `--permission-mode auto` |
| Codex | `--dangerously-bypass-approvals-and-sandbox` |
| agy | `--dangerously-skip-permissions`, never `--sandbox` |
| Grok Build | `--always-approve`, `GROK_FOLDER_TRUST=0` |
| Muse Code | `--yolo` |
| OpenCode | `--auto`, `--standalone` |

The worktree is the only boundary. This generalises the Codex part of ADR 0012. Sources: `docs/research/harness-candidates.md`, `docs/research/codex-headless-harness.md`.

## Considered Options

- **Each CLI's own sandbox.** Rejected. On stock Ubuntu (`kernel.apparmor_restrict_unprivileged_userns = 1`), bubblewrap can't start, so every sandboxed command fails under Codex and Muse. The sandboxes of agy and Grok make the main checkout's `.git` read-only, which blocks commits. A sandboxed mode for machines where it works would be a separate issue.
- **A one-time `grok --trust` in each repository's main checkout**, done or documented by Setup. Rejected. A repository nobody had trusted would run its sessions without `AGENTS.md` or the Factory skills, and nothing would fail. A scheduled Pickup run is exactly the case where nobody would notice.
- **Using the CLI's settings for approval and trust**, such as a user config that already sets always-approve. Rejected. A session would then behave differently on every machine. Passing the flags each time makes them explicit.
- **OpenCode through the user's shared background service** (its default). Rejected. A SIGTERM leaves the turn and its shell command running in the service, and thirdshift doesn't control that service's version or environment. `--standalone` keeps the server in the session's process group, and costs 2–3 s per session.

## Consequences

- A Harness's self-update is turned off in each session's environment too (`MUSE_NO_AUTO_UPDATE=1`, `GROK_DISABLE_AUTOUPDATER=1`, `AGY_CLI_DISABLE_AUTO_UPDATE=true`, `OPENCODE_DISABLE_AUTOUPDATE=1`). An update in the middle of a Run could replace the binary a Resume needs. agy ignores `=1`; only `=true` works.
- Each Harness reads its own instruction file and falls back to the other. thirdshift adds the fallback where a CLI lacks one: a `GEMINI.md` link for agy, and an `AGENTS.md` link for OpenCode, both to `CLAUDE.md` at the worktree root and kept out of git. Grok reads both files when both exist, and thirdshift leaves that as it is.
- Instructions in the user's `~/.claude/` reach only the Harnesses that read them: Claude, Grok and Muse. They don't reach Codex, agy or OpenCode.
6 changes: 3 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ fn ask_harness(harness: &mut Option<Harness>, arg: &str, name: Option<&String>)
}
let Some(named) = name.and_then(|name| Harness::named(name)) else {
let given = name.map(|name| format!(", not {name}")).unwrap_or_default();
bail!("{arg} must be followed by {}{given}", harness::NAMES);
bail!("{arg} must be followed by {}{given}", harness::names());
};
*harness = Some(named);
Ok(())
Expand Down Expand Up @@ -995,11 +995,11 @@ mod tests {
for (args, error) in [
(
vec![URL, "harness"],
"harness must be followed by claude or codex",
"harness must be followed by claude or codex or agy or grok or muse or opencode",
),
(
vec![URL, "harness", "gemini"],
"harness must be followed by claude or codex, not gemini",
"harness must be followed by claude or codex or agy or grok or muse or opencode, not gemini",
),
(
vec!["--harness", "claude", URL, "harness", "codex"],
Expand Down
1 change: 1 addition & 0 deletions src/asks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ mod tests {
default: Some(Harness::Codex),
claude: model_and_effort("opus", "high"),
codex: model_and_effort("gpt-6.1-sol", "max"),
..harness::Settings::default()
},
..no_settings()
}
Expand Down
35 changes: 30 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl UserConfig {
Some(harness) => config.harness.default = Some(harness),
None => bail!(
"harness.default must be {}, not {name:?}, in {file}",
harness::NAMES
harness::names()
),
},
("harness", "default", _) => {
Expand Down Expand Up @@ -387,7 +387,7 @@ parallel = 3 # how many Tickets a Spec run runs at once; default 3
limit = 3 # how many open issues labelled in-progress stop a Pickup run taking another; default 3

[harness]
default = "claude" # the Harness every Run's sessions run on, claude or codex; default claude
default = "claude" # the Harness every Run's sessions run on, claude, codex, agy, grok, muse or opencode; default claude

[harness.claude]
model = "" # the Model Claude Code's sessions run on; default blank, for Claude Code's own
Expand All @@ -396,6 +396,22 @@ effort = "" # how hard that Model reasons; default blank, for Claude Code's ow
[harness.codex]
model = "" # the Model Codex's sessions run on; default blank, for Codex's own
effort = "" # how hard that Model reasons; default blank, for Codex's own

[harness.agy]
model = "" # the Model Antigravity CLI's sessions run on; default blank, for agy's own
effort = "" # how hard that Model reasons; default blank, for agy's own

[harness.grok]
model = "" # the Model Grok Build's sessions run on; default blank, for Grok Build's own
effort = "" # how hard that Model reasons; default blank, for Grok Build's own

[harness.muse]
model = "" # the Model Muse Code's sessions run on; default blank, for Muse Code's own
effort = "" # how hard that Model reasons; default blank, for Muse Code's own

[harness.opencode]
model = "" # the Model OpenCode's sessions run on, provider/model; default blank, for OpenCode's own
effort = "" # the Model's variant, passed as #effort; default blank, for OpenCode's own
"#;

/// The line `DEFAULTS` holds for `email.to`, which has no default.
Expand Down Expand Up @@ -527,7 +543,15 @@ mod tests {
"harness.claude.model",
"harness.claude.effort",
"harness.codex.model",
"harness.codex.effort"
"harness.codex.effort",
"harness.agy.model",
"harness.agy.effort",
"harness.grok.model",
"harness.grok.effort",
"harness.muse.model",
"harness.muse.effort",
"harness.opencode.model",
"harness.opencode.effort"
]
);
let commented_out: Vec<&str> = DEFAULTS
Expand Down Expand Up @@ -577,6 +601,7 @@ mod tests {
model: Some("gpt-6.1-sol".to_string()),
effort: Some("max".to_string()),
},
..harness::Settings::default()
}
);
assert_eq!(parse("").unwrap().harness, harness::Settings::default());
Expand All @@ -587,7 +612,7 @@ mod tests {
for (text, error) in [
(
"[harness]\ndefault = \"gemini\"\n",
"harness.default must be claude or codex, not \"gemini\"",
"harness.default must be claude or codex or agy or grok or muse or opencode, not \"gemini\"",
),
(
"[harness]\ndefault = true\n",
Expand Down Expand Up @@ -868,7 +893,7 @@ mod tests {
let config: toml::Table = completed.parse().unwrap();
let harness = config["harness"].as_table().unwrap();
assert!(harness.contains_key("default"), "{text:?}:\n{completed}");
for name in ["claude", "codex"] {
for name in ["claude", "codex", "agy", "grok"] {
let settings = harness[name].as_table().unwrap();
for key in ["model", "effort"] {
assert!(settings.contains_key(key), "{text:?}:\n{completed}");
Expand Down
Loading
Loading