1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
= "omp"
= "omp"
# A release binary, not a package manager. oh-my-pi ships one static executable
# per platform and publishes no npm package, so this is `curl` where the other
# two adapters are `npm install -g`. `latest` rather than the pinned `v17.3.3`
# these paths were read against, for the same reason the others are unpinned:
# an adapter that installs an old harness is a different lie from one that
# points at the wrong path, and only the second is omh's to tell.
= """curl -fsSL https://github.com/can1357/oh-my-pi/releases/latest/download/omp-linux-$(case "$(uname -m)" in aarch64|arm64) echo arm64 ;; *) echo x64 ;; esac) -o /usr/local/bin/omp && chmod +x /usr/local/bin/omp"""
# The whole directory, and it has to be: omp keeps credentials in **SQLite**
# (`auth_credentials`, `auth_credential_blocks` in `agent.db`), and a bind-
# mounted file cannot survive the rename SQLite does through its `-wal` and
# `-shm` sidecars.
#
# `~/.omp/agent` is the default profile's directory. omp resolves it through
# `getAgentDir()`, which moves to `~/.omp/profiles/<name>/agent` when an omp
# profile is active — so omh's `--profile` and omp's are two different words
# that happen to be spelled the same, and an omp profile is outside what this
# adapter carries.
= ["$HOME/.omp/agent/"]
# No `token`. There is no file whose existence proves an omp login: the
# credentials are rows in `agent.db`, and that database is created by boot noise
# — settings, model_perf, usage_history — on the first start. Pointing `token`
# at it would report a successful login for a session that never logged in,
# which is the exact false positive `auth::unfilled` exists to prevent, so the
# question is asked of the harness instead — see `[token-probe]` below.
= "run `/login` inside omp, then exit"
[]
= "omp usage --json"
# `omp usage` reports "usage limits for every authenticated account"; with no
# accounts its `reports` array is empty — verified by running it. `accountId`
# is the per-report field, read off the source rather than off a logged-in run,
# and it is the one claim here that `omh doctor` still has to settle.
= "accountId"
# How this harness names the things an agent does. Verified against
# `docs/tools/*.md` at v17.3.3: `bash` takes `command`, `read` takes `path`.
#
# No `search`: omp has `grep` and `glob` as separate tools and omh's vocabulary
# has one word for both, so a hook narrowing to `search` is dropped by name
# rather than silently matching half of what it asked for. The same reasoning,
# and the same omission, as the opencode adapter.
[]
= "edit"
= "read"
= "bash"
# omp prefers `AGENTS.md` and reads `CLAUDE.md` only as one more flavour of the
# same file, so there is nothing to put in `also` — unlike Claude Code, where
# both names are live and the bytes are identical either way.
[]
= "/work/AGENTS.md"
= "concat"
# The **personal** file, never the repo's: `rules::compose` already puts this
# project's own AGENTS.md into every session, so importing that one would
# deliver the same prose twice. omp's user-scope rules file is `RULES.md` in the
# agent directory. It also reads a user-scope `AGENTS.md` beside it; that one is
# left alone because omh imports one personal rules file per harness, not
# because importing it would duplicate anything.
= "$HOME/.omp/agent/RULES.md"
# `<agent-dir>/skills/<name>/SKILL.md`, one level deep and not recursive. From
# `packages/coding-agent/src/discovery/builtin.ts`, not from the prose: the docs
# name `.omp/skills` for a project and leave the user root to `getAgentDir()`.
[]
= "$HOME/.omp/agent/skills"
= "dir"
= "$HOME/.omp/agent/skills"
# User scope. omp also reads `.omp/mcp.json` and `.omp/.mcp.json` from a
# project, and neither is where omh mounts yours — the user file is the one
# that is omh's to own, the way `$HOME/.config/opencode/opencode.json` is.
#
# The document shape is Claude Code's: `{ "mcpServers": { name: { command, … } } }`,
# with `type` defaulting to `stdio` when absent. That is why this renders with
# `mcp-json` and needs no renderer of its own.
[]
= "$HOME/.omp/agent/mcp.json"
= "mcp-json"
= "$HOME/.omp/agent/mcp.json"
# No `verify`/`ready`, and this is the one place this adapter is weaker than
# `claude`. omp has no `mcp` subcommand: its listing is `/mcp`, a slash command
# inside the TUI, and reaching it head-lessly means starting an agent session —
# so the check would spend a model call to answer a question about a file.
#
# It was tried. `omp -p '/mcp list'` went in, `omh doctor` ran it, and the
# result was worse than nothing: the command wrote output that ran into the next
# check's line and swallowed it, so the run reported on six capabilities while
# claiming seven. A check that cannot fail cleanly is not a check.
#
# What that costs is exactly what `adapters.md` says it costs — this path is a
# claim no `doctor` run can settle, the same state the `claude` adapter's `mcp`
# binding was in for a year while pointing somewhere nothing reads. It is on
# record rather than papered over.
[]
= "$HOME/.omp/agent/commands"
= "dir"
= "$HOME/.omp/agent/commands"
# `~/.omp/agent/agents/*.md`, per `docs/task-agent-discovery.md`. The singular
# `agent` in the path is the config root; the plural is the directory.
[]
= "$HOME/.omp/agent/agents"
= "dir"
= "$HOME/.omp/agent/agents"
# Hooks are a **module**, not a configuration file — the second harness in a row
# to be so, which confirms what `OpencodePlugin`'s doc already suspected on one
# example: declarative hook config is the exception, not the rule.
#
# A hook module default-exports a factory that receives `pi` and registers
# handlers with `pi.on(...)`. Discovery scans `<scope>/hooks/pre/` and
# `<scope>/hooks/post/`, but pre/post is only where a file is *found*: the
# factories are loaded as extension modules and bind whatever moments they
# register, so one module under `pre/` carries all four. That last clause is a
# claim about oh-my-pi, and `omh doctor` is what settles it.
[]
= "$HOME/.omp/agent/hooks/pre/omh.ts"
= "omp-plugin"
# omh's moments, in omp's words. All four exist, as they do on claude —
# opencode is the one that maps three.
#
# Mapping `session-start` is not the same as being able to *speak* there. omp's
# session and turn events are observational: their return value is ignored, so
# only a `run` survives and an `inject` at `session-start` is dropped by name.
# `graph-orient` is dropped here for that reason, where opencode drops it for
# not having the moment at all. Closing that gap needs `pi.sendMessage`, which
# is on omp's superset API and is not mapped because no run has confirmed its
# signature — an unverified template that silently does nothing is worse than a
# hook dropped out loud.
[]
= "session_start"
= "turn_end"
= "tool_call"
= "tool_result"
# Where this harness keeps each field. Property names on `event.input` — the
# call's arguments — read in the renderer's own language the way opencode's
# are, not jq paths. Naming `event` itself would send the next person adding a
# field to `event.toolName`, which the renderer would emit as
# `event.input?.toolName` and bind to the empty string.
#
# `tool-file` is `path`, which is `read`'s parameter. It is **not** `edit`'s:
# omp's edit tool takes a single `input` string with the path embedded in
# `[PATH#TAG]` sections. A hook narrowing to `edit` *and* wanting the file is
# therefore dropped by name in the renderer rather than handed `""` — the
# schema has one `fields` map per harness and no way to say "this field exists
# on these tools and not those", so that one lives in `render::omp_plugin`.
[]
= "path"
= "command"
# Advisory text, at the one moment there is a channel for it: a `tool_result`
# handler returning `content` replaces what the model reads next, so appending
# to it reaches the model. The renderer is what restricts this to `after-tool`
# — `tool_call` can only block, and turning a nudge into a wall is the one
# translation omh refuses to make silently.
[]
= 'return { content: [...(event.content ?? []), { type: "text", text: {{text}} }] }'
# Blocking, with a reason the model is shown. `{ block: true, reason }` from a
# `tool_call` handler: the wrapper stops the call and `reason` becomes the
# thrown error text. A decision said out loud, the way `permissionDecision` is
# on Claude Code, rather than a throw the shell has to interpret.
[]
= 'return { block: true, reason: {{text}} }'