Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/components/agent-glyph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function AgentIcon({ agent, className }: { agent?: string; className?: st
return <SparkleIcon className={cn(cls, "text-[#6f9bff]")} strokeWidth={2.25} />;
case "pi":
return <span className={cn("shrink-0 font-semibold text-[#a78bfa] text-xs leading-none", className)}>π</span>;
case "omp":
return <span className={cn("shrink-0 font-semibold text-[#f472b6] text-xs leading-none", className)}>π</span>;
case "opencode":
return <SquareTerminalIcon className={cn(cls, "text-foreground/80")} />;
default:
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Location, Session, Stats, Worktree } from "@/lib/api";
// or "idle" for a plain shell.
export type SessionState = "ready" | "running" | "waiting" | "finished" | "exited" | "idle";

export const AGENTS = ["claude", "codex", "opencode", "gemini", "pi", "cursor-agent"];
export const AGENTS = ["claude", "codex", "opencode", "gemini", "pi", "omp", "cursor-agent"];

export function agentOf(s: Session): string | undefined {
// A service's terminal runs no agent, whatever its command.
Expand Down Expand Up @@ -148,7 +148,7 @@ export function sortedWorktrees(loc: Location): Worktree[] {
return [...(loc.worktrees ?? [])].sort((a, b) => Number(!!b.main) - Number(!!a.main) || a.name.localeCompare(b.name));
}

const labels: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", pi: "Pi", "cursor-agent": "Cursor Agent", cursor: "Cursor Agent" };
const labels: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", pi: "Pi", omp: "oh-my-pi", "cursor-agent": "Cursor Agent", cursor: "Cursor Agent" };

// agentLabel is an agent's product name: "claude" → "Claude Code".
export const agentLabel = (a: string) => labels[a] ?? a.charAt(0).toUpperCase() + a.slice(1);
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/session-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { activateTab, findSession, focusPane, openTab, refOf, removePane, select
// that worktree, and never takes the keyboard from them; otherwise a
// notification offers to open it.

const agentNames: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", cursor: "Cursor" };
const agentNames: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor" };

function agentLabel(box: string, agent?: string): string {
if (!agent) return "A terminal";
Expand Down
2 changes: 1 addition & 1 deletion app/src/views/automations/flows/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const TRIGGER_PHRASE: Record<string, string> = {
"box.upgraded": "the box is upgraded",
};

const AGENT_NAME: Record<string, string> = { claude: "Claude", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", cursor: "Cursor" };
const AGENT_NAME: Record<string, string> = { claude: "Claude", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor" };
export const agentName = (id?: string) => (id ? (AGENT_NAME[id] ?? id) : "an agent");

export const GITHUB_ONS: { on: GitHubOn; label: string; phrase: string; fields: string[] }[] = [
Expand Down
2 changes: 2 additions & 0 deletions app/src/views/settings/agents-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const INSTALL: Record<string, string> = {
opencode: "npm install -g opencode-ai",
gemini: "npm install -g @google/gemini-cli",
cursor: "curl https://cursor.com/install -fsS | bash",
omp: "bun install -g @oh-my-pi/pi-coding-agent",
};

const BUILTIN: Pick<AgentPreset, "id" | "name">[] = [
Expand All @@ -29,6 +30,7 @@ const BUILTIN: Pick<AgentPreset, "id" | "name">[] = [
{ id: "opencode", name: "OpenCode" },
{ id: "gemini", name: "Gemini CLI" },
{ id: "cursor", name: "Cursor Agent" },
{ id: "omp", name: "oh-my-pi" },
];

export function AgentsSection() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/views/settings/labs-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function LabsSection() {
{labs && (
<SettingsRow
label="Structured agents"
description="New Claude Code, Codex, OpenCode, Gemini CLI and Cursor Agent agents run without their terminal: the box drives them through their own protocol (Claude Code's stream, Codex's app server, ACP for the rest) and keeps its own record. They open as conversations; Claude Code's own terminal is one switch away."
description="New Claude Code, Codex, OpenCode, Gemini CLI, Cursor Agent and oh-my-pi agents run without their terminal: the box drives them through their own protocol (Claude Code's stream, Codex's app server, ACP for the rest) and keeps its own record. They open as conversations; Claude Code's own terminal is one switch away."
>
<Switch checked={structured} onCheckedChange={(on) => setPrefs({ structuredClaude: on })} />
</SettingsRow>
Expand Down
9 changes: 8 additions & 1 deletion docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ upgrades with `berth upgrade BOX`, or by running the
[install command](/getting-started/add-a-box#with-the-install-command)
again.


## Unreleased
- **oh-my-pi is a built-in agent.** Start it from a project like any other
(`omp`), with a model and thinking level. With *Structured agents* on, it runs
through the Agent Client Protocol: its replies and tool calls fill the chat
and its permission requests come to it. `berthd integrations install omp`
adds a small extension to `~/.omp/agent/extensions` so a terminal session
says when it is working, needs you or is done, and adds Shipyard's MCP
server to `~/.omp/agent/mcp.json` so omp can drive other sessions; a box
that has omp gets both with the other agents' hooks.
- **Settings › Boxes reads correctly to a screen reader.** A box with no
browsers running put its "No browsers running" note inside the browsers
list, so assistive tech announced an empty list with a stray line in it.
Expand Down
6 changes: 4 additions & 2 deletions docs/concepts/sessions-and-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ announces `task.created`.

An agent is a preset: an id, a name and a command. The box offers each
built-in whose command it finds on its `PATH` (ids `claude`, `codex`,
`opencode`, `gemini`, and `cursor`, which runs `cursor-agent`), plus any a
`opencode`, `gemini`, `omp` (oh-my-pi), and `cursor`, which runs
`cursor-agent`), plus any a
repository's config adds or replaces. `berth agents BOX` lists the
built-ins the box has; a repository's own presets come with its location
(`agents` in `berth locations BOX --json`).
Expand All @@ -121,7 +122,7 @@ A first prompt is passed as the command's last argument, or after its
Shipyard knows what an agent is doing from the agent's own hooks, which
`berthd install` sets up for the agent CLIs it finds on the box (later,
`berthd integrations install claude`, or `codex`, `cursor`, `gemini`,
`opencode`, `all`; an upgrade brings hooks Shipyard installed before up to
`opencode`, `omp`, `all`; an upgrade brings hooks Shipyard installed before up to
date):

| State | In the app | Meaning | From |
Expand Down Expand Up @@ -196,6 +197,7 @@ changing ends the turn).
| Cursor Agent | ✓ | ✓ | screen | ✓ | hooks in `~/.cursor/hooks.json` |
| Gemini CLI | ✓ | ✓ | ✓ | ✓ | hooks in `~/.gemini/settings.json` |
| OpenCode | | ✓ | ✓ | ✓ | a plugin in `~/.config/opencode/plugin/berth.js` |
| oh-my-pi | ✓ | ✓ | ✓ | ✓ | an extension in `~/.omp/agent/extensions/berth.ts` |
| anything else | screen | screen | screen | screen | the terminal |

The box lists these as `adapters` in `GET /v1/info`, with `capabilities`
Expand Down
7 changes: 6 additions & 1 deletion docs/guides/agent-integrations.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Agent integrations
description: Claude Code, Codex, Cursor, Gemini CLI and OpenCode hooks, the skills Shipyard teaches agents, and announcing events from any tool.
description: Claude Code, Codex, Cursor, Gemini CLI, OpenCode and oh-my-pi hooks, the skills Shipyard teaches agents, and announcing events from any tool.
---

Shipyard works with the tools around it in both directions: tools drive
Expand Down Expand Up @@ -44,6 +44,11 @@ with the command that installs them.
`AfterAgent` / `SessionEnd` hooks in `~/.gemini/settings.json`.
- **OpenCode**: a small plugin, `~/.config/opencode/plugin/berth.js`, that
runs the hook when a session gets busy, asks for a permission, or goes idle.
- **oh-my-pi**: a small extension, `~/.omp/agent/extensions/berth.ts`, that
runs the hook when a session starts, a turn starts or ends, or a tool asks
for approval. Subagents have no UI and report nothing. On a box it also
adds `berthd mcp` to `mcpServers` in `~/.omp/agent/mcp.json`, next to any
servers already there.

### Every account

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/labs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,8 @@ own record of the conversation. The box drives it from there:
chat. Its modes are default (asks to go past its workspace sandbox),
untrusted (asks before untrusted commands), read only, and bypass (no
sandbox, nothing asked). It has no terminal switch.
- **OpenCode, Gemini CLI and Cursor Agent** speak the Agent Client Protocol
(`opencode acp`, `gemini --acp`, `cursor-agent acp`), and so does any agent
- **OpenCode, Gemini CLI, Cursor Agent and oh-my-pi** speak the Agent Client
Protocol (`opencode acp`, `gemini --acp`, `cursor-agent acp`, `omp acp`), and so does any agent
whose preset in a repository's config has an [`acp`](/reference/config):
the box opens its session (with its own login, for Cursor Agent), streams
its reply, cancels its turn, switches its own modes, and brings its
Expand Down
1 change: 1 addition & 0 deletions docs/guides/project-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Shipyard passes them with each CLI's own flags, checked against its `--help`:
| `claude` | `--model` | `--effort` (low, medium, high, xhigh, max) | `opus`, `sonnet`, `haiku` (Claude Code's aliases for the latest of each) |
| `codex` | `--model` | `-c model_reasoning_effort=` (minimal, low, medium, high) | none: Codex's default |
| `opencode`, `cursor` | `--model` | none | none |
| `omp` | `--model` | `--thinking` (low, medium, high, xhigh, max) | none: omp's default |

Unset means the CLI's default. A value must be a plain name (letters,
digits, `.` `_` `:` `/` `-`); anything else is refused, so nothing reaches a
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/berthd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ flows, skills) can drive it directly. `berthd help` prints this list.
| `berthd doctor [--json]` | Check this box's setup and how to fix it. It looks for the agent CLIs afresh, and names each one's version, path and how it was installed |
| `berthd version` | Print this build's version |
| `berthd session attach NAME` | Attach to a session in this terminal |
| `berthd mcp` | A stdio MCP server of Shipyard's tools for agents on this box, talking to its socket; `integrations install` adds it to Claude Code, Codex and Gemini CLI ([runs](/guides/runs#from-an-agent-mcp)) |
| `berthd mcp` | A stdio MCP server of Shipyard's tools for agents on this box, talking to its socket; `integrations install` adds it to Claude Code, Codex, Gemini CLI and oh-my-pi ([runs](/guides/runs#from-an-agent-mcp)) |
| `berthd headless --agent A --out FILE.jsonl --prompt-file FILE [--read-only] [--add-dir D]` | One non-interactive agent turn, as runs start in tmux for judges and reviewers |

`~/.berth/runs.json` on the box sets `max_concurrent_runs` (10),
Expand Down Expand Up @@ -237,7 +237,7 @@ refuse a helper, and a `before:helper.attach` gate a look in a worktree's
| Command | What it does |
| --- | --- |
| `berthd integrations [status]` | Where Shipyard's hooks and skills are for each agent CLI on this box, per Claude Code and Codex account (`Claude Code: hooks and 7 skills in ~/.claude, ~/.berth/accounts/claude/personal`), and the command that adds them where they're missing |
| `berthd integrations install claude\|cursor\|codex\|gemini\|opencode\|all\|present` | Install Shipyard's skills and agent hooks for a tool (`present`: every agent CLI on this box, quietly skipping the rest), and `berthd mcp` as an MCP server for Claude Code, Codex and Gemini CLI. Claude Code and Codex get them in [every account folder](/guides/agent-integrations#every-account) |
| `berthd integrations install claude\|cursor\|codex\|gemini\|opencode\|omp\|all\|present` | Install Shipyard's skills and agent hooks for a tool (`present`: every agent CLI on this box, quietly skipping the rest), and `berthd mcp` as an MCP server for Claude Code, Codex, Gemini CLI and oh-my-pi. Claude Code and Codex get them in [every account folder](/guides/agent-integrations#every-account) |
| `berthd hook TOOL EVENT [PAYLOAD]` | What those hooks run: turns a tool's hook into a Shipyard event (`agent.finished`, `agent.waiting`), naming the session from `$BERTH_SESSION`. While berthd is down it keeps the event in `~/.berth/box/spool/`, published when berthd starts |

See [agent integrations](/guides/agent-integrations) for what the hooks and
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ See [orchestration](/guides/orchestration) and
| Command | What it does |
| --- | --- |
| `berth integrations [status]` | Where Shipyard's hooks and skills are for each agent CLI on this laptop, per Claude Code and Codex account |
| `berth integrations install claude\|cursor\|codex\|gemini\|opencode\|all` | Install Shipyard's skills and agent hooks for a tool, on this laptop, in every account folder it has |
| `berth integrations install claude\|cursor\|codex\|gemini\|opencode\|omp\|all` | Install Shipyard's skills and agent hooks for a tool, on this laptop, in every account folder it has |
| `berth hook TOOL EVENT [PAYLOAD]` | What those hooks run: turns a tool's hook into a Shipyard event |

See [agent integrations](/guides/agent-integrations).
4 changes: 2 additions & 2 deletions docs/reference/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ guide is in [project config](/guides/project-config).
| `acp` | string | What follows `command` to start the agent's Agent Client Protocol server (`acp`, `--acp`). With it the agent can run as a [structured agent](/guides/labs#structured-agents). Given with an `id` and no `command`, it adds that to a built-in agent. |

The built-ins are offered when their command is installed on the box:
`claude`, `codex`, `opencode` (`--prompt`), `gemini` (`-i`) and `cursor`
(`cursor-agent`).
`claude`, `codex`, `opencode` (`--prompt`), `gemini` (`-i`), `omp` (oh-my-pi) and
`cursor` (`cursor-agent`).

### `hooks`

Expand Down
3 changes: 3 additions & 0 deletions internal/agentcli/agentcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ var Catalog = []Agent{
{ID: "gemini", Name: "Gemini CLI", Command: "gemini",
Install: "npm install -g @google/gemini-cli",
Why: "it installs with npm and needs Node.js 20 or newer, which Shipyard doesn't install; install Node, then run the command"},
{ID: "omp", Name: "oh-my-pi", Command: "omp",
Install: "bun install -g @oh-my-pi/pi-coding-agent",
Why: "it installs with bun, which Shipyard doesn't install; install bun, then run the command"},
}

// ByID finds an agent in the catalog.
Expand Down
2 changes: 1 addition & 1 deletion internal/agentpath/agentpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import (
)

// Commands are the agent CLIs Shipyard starts, looked for together.
var Commands = []string{"claude", "codex", "opencode", "gemini", "cursor-agent", "pi"}
var Commands = []string{"claude", "codex", "opencode", "gemini", "cursor-agent", "pi", "omp"}

// Found is where an agent's command is.
type Found struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/box/agentpaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func agentChecks() []doctor.Check {
}
if found == 0 {
checks = append(checks, doctor.Check{Area: "Agents", Name: "agent CLIs", Status: doctor.Info,
Detail: "none found (claude, codex, opencode, gemini, cursor-agent), through your shell or where installers put them",
Detail: "none found (claude, codex, opencode, gemini, cursor-agent, omp), through your shell or where installers put them",
Fix: "Add agents from Shipyard (Settings → Boxes), or: berthd agents install claude"})
}
st := agentFinder().Status()
Expand Down
32 changes: 32 additions & 0 deletions internal/box/omp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package box

import "testing"

func TestOmpIsABuiltinAgentThatRunsOverACP(t *testing.T) {
var omp *AgentPreset
for i := range builtinAgents {
if builtinAgents[i].ID == "omp" {
omp = &builtinAgents[i]
}
}
if omp == nil {
t.Fatal("omp is not a built-in agent")
}
if omp.Command != "omp" || omp.ACP != "acp" || omp.ModelFlag != "--model" || omp.EffortFlag != "--thinking" {
t.Fatalf("preset = %+v", *omp)
}
}

func TestAnOmpCommandIsRecognisedAsOmp(t *testing.T) {
for command, want := range map[string]string{
"omp": "omp",
"env FOO=1 omp --model opus": "omp",
"/Users/a/.local/bin/omp -r": "omp",
"bunx @oh-my-pi/pi-coding-agent": "omp",
"npx @oh-my-pi/pi-coding-agent@1.2.3": "omp",
} {
if got := agentOf(command); got != want {
t.Errorf("agentOf(%q) = %q, want %q", command, got, want)
}
}
}
4 changes: 2 additions & 2 deletions internal/box/phoneui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ const GROUPS = [
["finished", "Done"],
["idle", "Ready"],
];
const AGENTS = { claude: "C", codex: "X", opencode: "O", gemini: "G", cursor: "U" };
const AGENT_NAMES = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", cursor: "Cursor" };
const AGENTS = { claude: "C", codex: "X", opencode: "O", gemini: "G", omp: "P", cursor: "U" };
const AGENT_NAMES = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor" };

// A worktree by its display name when it was given one (worktree_title).
function where(s) {
Expand Down
2 changes: 1 addition & 1 deletion internal/box/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (b *Box) handleServices(w http.ResponseWriter, r *http.Request) error {
// Tools reports which agent CLIs this box has.
func Tools() []string {
var have []string
for _, t := range []string{"claude", "codex", "opencode", "gemini", "pi"} {
for _, t := range []string{"claude", "codex", "opencode", "gemini", "pi", "omp"} {
if _, ok := agentFound(t); ok {
have = append(have, t)
}
Expand Down
6 changes: 5 additions & 1 deletion internal/box/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ type AgentPreset struct {
// The model and effort flags are each CLI's own, from its --help: Claude
// Code's --model takes aliases (opus, sonnet, haiku) and --effort low to max;
// Codex takes -m/--model and its reasoning effort as the config key
// model_reasoning_effort; OpenCode and Cursor Agent take --model.
// model_reasoning_effort; OpenCode and Cursor Agent take --model; oh-my-pi
// takes --model and --thinking.
var builtinAgents = []AgentPreset{
{ID: "claude", Name: "Claude Code", Command: "claude", ModelFlag: "--model", EffortFlag: "--effort",
Models: []string{"opus", "sonnet", "haiku"}, Efforts: []string{"low", "medium", "high", "xhigh", "max"}},
Expand All @@ -53,6 +54,8 @@ var builtinAgents = []AgentPreset{
{ID: "opencode", Name: "OpenCode", Command: "opencode", PromptFlag: "--prompt", ModelFlag: "--model", ACP: "acp"},
{ID: "gemini", Name: "Gemini CLI", Command: "gemini", PromptFlag: "-i", ACP: "--acp"},
{ID: "cursor", Name: "Cursor Agent", Command: "cursor-agent", ModelFlag: "--model", ACP: "acp"},
{ID: "omp", Name: "oh-my-pi", Command: "omp", ModelFlag: "--model", EffortFlag: "--thinking",
Efforts: []string{"low", "medium", "high", "xhigh", "max"}, ACP: "acp"},
}

// Presets are the built-in agents this box has, then the location's own from
Expand Down Expand Up @@ -214,6 +217,7 @@ var agentPackages = map[string]string{
"@openai/codex": "codex",
"@google/gemini-cli": "gemini",
"opencode-ai": "opencode",
"@oh-my-pi/pi-coding-agent": "omp",
}

// wrapperWord is a word that runs the command after it.
Expand Down
1 change: 1 addition & 0 deletions internal/box/tmux.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ var agentInstalls = map[string]string{
"opencode": "npm install -g opencode-ai",
"gemini": "npm install -g @google/gemini-cli",
"cursor": "curl https://cursor.com/install -fsS | bash",
"omp": "bun install -g @oh-my-pi/pi-coding-agent",
}

// tmuxRequirement is whether tmux is here and, when it isn't, how to get it.
Expand Down
2 changes: 1 addition & 1 deletion internal/boxcmd/boxcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ func Run(ctx context.Context, c *box.Client, args []string, out io.Writer) error
}
return show(out, *asJSON, i.Agents, func() {
if len(i.Agents) == 0 {
fmt.Fprintln(out, "No agent CLIs found on this box (claude, codex, opencode, gemini, cursor-agent).")
fmt.Fprintln(out, "No agent CLIs found on this box (claude, codex, opencode, gemini, cursor-agent, omp).")
return
}
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
Expand Down
2 changes: 1 addition & 1 deletion internal/integrations/adapters/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func For(name string) (*Adapter, bool) {

// All lists the adapters, for the API and docs.
func All() []*Adapter {
return []*Adapter{Claude, Codex, Cursor, Gemini, OpenCode, Screen}
return []*Adapter{Claude, Codex, Cursor, Gemini, OpenCode, Omp, Screen}
}

// CapsFor is an agent's capabilities: the screen's for anything unknown.
Expand Down
Loading