@agentdeploymentco/bishop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +800 -0
- package/dist/agent-dir.js +1 -0
- package/dist/agent-source.js +1 -0
- package/dist/agent-store.js +5 -0
- package/dist/attachments.js +2 -0
- package/dist/cli.js +30 -0
- package/dist/config.js +4 -0
- package/dist/db.js +52 -0
- package/dist/env.js +12 -0
- package/dist/errors.js +1 -0
- package/dist/exec.js +1 -0
- package/dist/files.js +2 -0
- package/dist/gc.js +2 -0
- package/dist/github/gh.js +3 -0
- package/dist/github/setup.js +15 -0
- package/dist/gmail/api.js +1 -0
- package/dist/gmail/auth.js +2 -0
- package/dist/gmail/authorize.js +1 -0
- package/dist/gmail/create.js +1 -0
- package/dist/gmail/destination.js +1 -0
- package/dist/gmail/fake.js +5 -0
- package/dist/gmail/files.js +1 -0
- package/dist/gmail/history.js +1 -0
- package/dist/gmail/interface.js +2 -0
- package/dist/gmail/labels.js +1 -0
- package/dist/gmail/loopback.js +1 -0
- package/dist/gmail/message.js +17 -0
- package/dist/gmail/oauth.js +3 -0
- package/dist/gmail/presence.js +5 -0
- package/dist/gmail/route.js +1 -0
- package/dist/gmail/setup.js +9 -0
- package/dist/harness/claude-options.js +1 -0
- package/dist/harness/claude.js +11 -0
- package/dist/harness/codex.js +10 -0
- package/dist/harness/create.js +1 -0
- package/dist/harness/fake.js +1 -0
- package/dist/harness/layers.js +1 -0
- package/dist/harness/principal.js +14 -0
- package/dist/harness/types.js +0 -0
- package/dist/interface/attachments.js +1 -0
- package/dist/interface/authorize.js +1 -0
- package/dist/interface/delivery.js +1 -0
- package/dist/interface/destination.js +1 -0
- package/dist/interface/history.js +1 -0
- package/dist/interface/state.js +1 -0
- package/dist/interface/types.js +0 -0
- package/dist/log.js +1 -0
- package/dist/retry.js +1 -0
- package/dist/schedule/cli.js +17 -0
- package/dist/schedule/cron.js +1 -0
- package/dist/schedule/presence.js +5 -0
- package/dist/schedule/prompt.js +2 -0
- package/dist/schedule/runner.js +5 -0
- package/dist/schedule/store.js +6 -0
- package/dist/sessions/queue.js +1 -0
- package/dist/sessions/store.js +27 -0
- package/dist/setup.js +48 -0
- package/dist/slack/agent-session.js +1 -0
- package/dist/slack/app.js +2 -0
- package/dist/slack/authorize.js +2 -0
- package/dist/slack/authorizer.js +3 -0
- package/dist/slack/bots.js +1 -0
- package/dist/slack/commands/run.js +1 -0
- package/dist/slack/commands/syntax.js +3 -0
- package/dist/slack/destination.js +1 -0
- package/dist/slack/directory.js +1 -0
- package/dist/slack/etiquette.js +1 -0
- package/dist/slack/files.js +1 -0
- package/dist/slack/format.js +4 -0
- package/dist/slack/handlers.js +1 -0
- package/dist/slack/history.js +1 -0
- package/dist/slack/inspect.js +2 -0
- package/dist/slack/interface.js +2 -0
- package/dist/slack/manifest.js +1 -0
- package/dist/slack/members.js +1 -0
- package/dist/slack/mentions.js +1 -0
- package/dist/slack/pending.js +1 -0
- package/dist/slack/phrases.js +1 -0
- package/dist/slack/presence.js +5 -0
- package/dist/slack/provision.js +19 -0
- package/dist/slack/route.js +1 -0
- package/dist/slack/say.js +1 -0
- package/dist/slack/status.js +1 -0
- package/dist/slack/tasks.js +1 -0
- package/dist/slack/thread.js +1 -0
- package/dist/slack/users.js +1 -0
- package/dist/slack/verbosity.js +1 -0
- package/dist/tools/files.js +2 -0
- package/dist/tools/schedules.js +8 -0
- package/dist/tools/server.js +1 -0
- package/dist/tools/types.js +1 -0
- package/dist/turn.js +1 -0
- package/dist/version.js +1 -0
- package/dist/worktrees.js +2 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
# Bishop
|
|
2
|
+
|
|
3
|
+
Bishop puts an agent where your team already works.
|
|
4
|
+
|
|
5
|
+
Point it at a directory that a coding agent already works in, and the agent becomes something your team can talk to: a Slack thread, an email thread, or both at once. Bishop handles the connection and the mapping between threads and agent sessions.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$ cd path/to/agent
|
|
9
|
+
$ bishop slack setup
|
|
10
|
+
$ bishop
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Bishop is an agent gateway: it sits between the places people work and the agents they work with. Today that is Slack and Gmail, one agent, in a single process.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node 24 or newer
|
|
18
|
+
- A directory the agent works in, meaning `claude` or `codex` runs there and does something useful
|
|
19
|
+
- At least one interface. For Slack: the [`slack` CLI](https://docs.slack.dev/tools/slack-cli/), logged in with `slack auth login`, unless you set the app up by hand (see [Without the Slack CLI](#without-the-slack-cli)), plus permission to install an app in your workspace. For email: a Google Workspace account, a Google Cloud project, and a Workspace user for the agent.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ npm install -g @agentdeploymentco/bishop
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That puts `bishop` on your PATH. Bishop always treats the current directory as the agent, so run it from the agent's directory. [In a container](#in-a-container) is the other way to run it.
|
|
28
|
+
|
|
29
|
+
### From source
|
|
30
|
+
|
|
31
|
+
The published package is minified and carries no source maps. If you work on Bishop, and so can read its repository, build it instead:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
$ git clone https://github.com/Agent-Deployment-Co/bishop.git
|
|
35
|
+
$ cd bishop
|
|
36
|
+
$ npm install
|
|
37
|
+
$ npm run build
|
|
38
|
+
$ npm link # optional, puts `bishop` on your PATH
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Without `npm link`, run it as `node /path/to/bishop/dist/cli.js`.
|
|
42
|
+
|
|
43
|
+
## Set up an agent
|
|
44
|
+
|
|
45
|
+
From the agent's directory:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ bishop slack setup
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It asks for an app name, a display name, and a description, defaulting the name to the directory name. Pass them as flags to skip the questions:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
$ bishop slack setup --name salesforce-guy \
|
|
55
|
+
--display-name "Salesforce Guy" \
|
|
56
|
+
--description "Answers Salesforce admin questions"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`--no-prompt` takes the defaults instead of asking. Bishop assumes it when there's no terminal, so this works in a script.
|
|
60
|
+
|
|
61
|
+
Setup creates the Slack app, installs it, and writes two files:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
.bishop/config.json configuration, commit this
|
|
65
|
+
.env credentials, gitignored for you
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It also adds `bishop.db` to `.gitignore`, which `bishop` creates on its first run to hold the thread-to-session mapping.
|
|
69
|
+
|
|
70
|
+
Then start it:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
$ bishop
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Invite the bot to a channel, or DM it directly.
|
|
77
|
+
|
|
78
|
+
### Without the Slack CLI
|
|
79
|
+
|
|
80
|
+
If you can't install the CLI, or somebody else owns app creation in your workspace, Bishop can adopt an app you set up by hand.
|
|
81
|
+
|
|
82
|
+
First get the manifest, so the scopes and events are right by construction:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
$ bishop slack manifest > manifest.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Create the app in Slack from that file (**Create New App**, then **From a manifest**), install it to the workspace, then collect two tokens from App Settings: the bot token (`xoxb-`) under OAuth & Permissions, and an app-level token (`xapp-`) with `connections:write` under Basic Information.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
$ bishop slack setup --manual
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
It asks for both tokens, checks them against Slack, and builds the config from what Slack reports. You don't have to type the app ID or name: `auth.test` returns them.
|
|
95
|
+
|
|
96
|
+
One thing it can't check is which events the app subscribes to. If the agent never answers anything, that's the first place to look.
|
|
97
|
+
|
|
98
|
+
### Email
|
|
99
|
+
|
|
100
|
+
Gmail is the second interface. It needs a Google Cloud project, and the agent needs its own Workspace user, say `salesforce-guy@example.com`. That address is what people write to, and its mailbox is what Bishop reads. Don't point this at a person's account.
|
|
101
|
+
|
|
102
|
+
Then create an OAuth client, once:
|
|
103
|
+
|
|
104
|
+
1. In the [Google Cloud console](https://console.cloud.google.com/), pick or create a project and **enable the Gmail API**.
|
|
105
|
+
2. Configure the OAuth consent screen with user type **Internal**. This is what lets you skip Google's app verification.
|
|
106
|
+
3. Create an OAuth client ID of type **Desktop app**.
|
|
107
|
+
4. Put both values in `.env`:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
BISHOP_GMAIL_CLIENT_ID=...
|
|
111
|
+
BISHOP_GMAIL_CLIENT_SECRET=...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then authorize the mailbox:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
$ bishop gmail setup
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A browser opens. Sign in **as the agent's Workspace user**, not as yourself, and approve. Bishop catches the redirect, writes `BISHOP_GMAIL_REFRESH_TOKEN` to `.env`, and tells you which mailbox it reached.
|
|
121
|
+
|
|
122
|
+
Re-running it later checks the credentials instead of authorizing again, so it doubles as a health check in a deploy script. It exits non-zero when the mailbox can't be reached.
|
|
123
|
+
|
|
124
|
+
**Set `gmail.allow` before pointing anyone at it.** Without it, every address that can reach the mailbox can use the agent, and a Workspace mailbox can be reached from anywhere.
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "gmail": { "allow": ["you@example.com", "@example.com"] } }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Moving it to a server
|
|
131
|
+
|
|
132
|
+
The refresh token isn't tied to the machine you authorized on. Copy three lines to the server's `.env` and start Bishop there:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
BISHOP_GMAIL_CLIENT_ID=...
|
|
136
|
+
BISHOP_GMAIL_CLIENT_SECRET=...
|
|
137
|
+
BISHOP_GMAIL_REFRESH_TOKEN=...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
To set it up on the server directly instead, run `bishop gmail setup` there. It notices it's over SSH, prints a URL instead of opening one, and asks you to paste back where the browser ended up. The browser lands on a `http://127.0.0.1` address that fails to load, which is expected: the code is in the address bar, and pasting the whole address is enough. `--no-browser` forces that mode anywhere.
|
|
141
|
+
|
|
142
|
+
#### When it stops working
|
|
143
|
+
|
|
144
|
+
Google revokes Gmail refresh tokens when the mailbox's password is reset. The agent goes quiet, the log says the token is no longer valid, and `bishop gmail setup` fixes it by authorizing again.
|
|
145
|
+
|
|
146
|
+
If that isn't acceptable, use domain-wide delegation instead. Nothing about it expires and no person is ever needed again, but it costs more to set up: a service account key, which most organizations now block by default, plus a Workspace super admin to grant the delegation. Set `BISHOP_GMAIL_USER` and `BISHOP_GMAIL_SERVICE_ACCOUNT` instead of the three OAuth variables, and run `bishop gmail setup` to check it. It prints the exact client ID and scope to paste into the admin console when the grant is missing.
|
|
147
|
+
|
|
148
|
+
Bishop runs whichever interfaces have credentials. Set up both and one process answers Slack and email at once, with separate threads and separate sessions.
|
|
149
|
+
|
|
150
|
+
### GitHub
|
|
151
|
+
|
|
152
|
+
An agent that opens pull requests, reads issues, or pushes branches needs a GitHub credential of its own. Without one it borrows whatever login happens to be on the machine, and in a container there is nothing to borrow.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
$ bishop github setup
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
It takes the token from the [`gh` CLI](https://github.com/cli/cli), offering to run `gh auth login` if you aren't logged in, verifies it against GitHub, and writes `GH_TOKEN` to `.env`. From there it reaches the agent's environment, and `gh` picks it up on its own.
|
|
159
|
+
|
|
160
|
+
The report names the account the token acts as and the scopes it carries, and prints the `gh auth refresh` for any scope the agent needs that's missing. Re-running it checks the token instead of issuing another, so it doubles as a health check in a deploy script. It exits non-zero when GitHub won't accept the token.
|
|
161
|
+
|
|
162
|
+
**A token from `gh auth login` acts as you, in every repository you can reach.** To limit the agent instead, create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens) scoped to the repositories it should touch, put it in `.env` as `GH_TOKEN`, and run setup to check it. GitHub reports no scopes for a fine-grained token, so setup says what it can't tell you rather than guessing.
|
|
163
|
+
|
|
164
|
+
git never reads `GH_TOKEN`, so `git push` still asks for a password until the credential helper is installed. Setup says so when it's missing:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
$ gh auth setup-git
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**`gh auth login` replaces whichever account the `gh` CLI is signed in as.** On a machine where you are signed in as yourself, `--temporary` runs the login in a `gh` configuration directory it creates and deletes, so your own login survives:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
$ bishop github setup --temporary
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The token still lands in `.env`. Because the temporary directory has no account in it, this always logs in rather than reusing one, and it needs a terminal. A `GH_TOKEN` already in `.env` is checked as usual and no login happens at all.
|
|
177
|
+
|
|
178
|
+
For a deployment whose secrets live somewhere other than a `.env` file, `--print` writes the credential to stdout as JSON and nothing to disk, with the report and any prompting on stderr:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
$ bishop github setup --print
|
|
182
|
+
{"GH_TOKEN":"gho_..."}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`--no-prompt` never offers the login, and fails with the command to run instead. `--temporary` and `--print` combine: log in as the agent, pipe the token into a secret store, leave nothing behind.
|
|
186
|
+
|
|
187
|
+
## Pointing at a different agent
|
|
188
|
+
|
|
189
|
+
By default Bishop treats the current directory as both its own home and the agent, so the files above land beside the agent. `--agent` separates the two:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
$ bishop --agent /srv/agents/salesforce-guy
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Bishop's config, credentials, and database stay in the directory you run from. The agent directory is only read, by the agent itself, so its `CLAUDE.md` and `.claude/settings.json` govern as always and nothing of Bishop's is written into it. **Bishop never updates a directory you point it at**, whether or not it's a git checkout: pull, branch, and edit it whenever you like. The exception to reading only is [worktree mode](#worktree-mode), where the agent creates worktrees under `.claude/worktrees/` in its own directory.
|
|
196
|
+
|
|
197
|
+
This is how you swap the agent underneath a running deployment. Point Bishop at a checkout, a worktree, or a release directory, and change which one it points at without touching any Slack configuration or losing thread history.
|
|
198
|
+
|
|
199
|
+
### Pointing at a repository
|
|
200
|
+
|
|
201
|
+
`--agent` also takes a git URL, which Bishop clones on startup:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
$ bishop --agent https://github.com/acme/salesforce-guy
|
|
205
|
+
$ bishop --agent https://github.com/acme/salesforce-guy/tree/staging
|
|
206
|
+
$ bishop --agent git@github.com:acme/salesforce-guy.git
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
GitLab's `/-/tree/<branch>` form works too, as does any URL git can clone. A bare `owner/repo` is treated as a local directory, since nothing distinguishes it from a relative path. Bishop uses whatever git credentials the machine already has, so a private repo works if `git clone` works there.
|
|
210
|
+
|
|
211
|
+
This is the other half of the split. Point at a directory and its state is yours: Bishop reads it, writes nothing into it, and you pull when you want to. The one thing it removes there is an old thread's worktree, see [Cleaning up after itself](#cleaning-up-after-itself). Point at a repository and Bishop owns the checkout instead, under `.bishop/agents/<owner>-<repo>`:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
.bishop/agents/acme-salesforce-guy/
|
|
215
|
+
├── mirror.git/ bare clone, the only thing a fetch touches
|
|
216
|
+
└── snapshots/
|
|
217
|
+
├── current -> b4c5d6e what the next new thread starts on
|
|
218
|
+
├── a1b2c3d/ an older commit, still in use by a thread
|
|
219
|
+
└── b4c5d6e/
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A snapshot is one commit, checked out and never rewritten. **A thread picks its snapshot on its first message and stays there for good**, so a conversation always sees one consistent set of files. Threads on the same commit share a snapshot, so a quiet week costs one directory.
|
|
223
|
+
|
|
224
|
+
`current` is for you, not for Bishop. Nothing reads it while a thread runs; it records what the next new thread would start on.
|
|
225
|
+
|
|
226
|
+
### Picking up a push without restarting
|
|
227
|
+
|
|
228
|
+
`--auto-update` checks the remote at the start of every new thread:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
$ bishop --agent https://github.com/acme/salesforce-guy --auto-update
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A new thread gets whatever is on the branch now. Threads already going keep their own snapshot, so nobody's conversation changes underneath them. Without the flag, Bishop checks once at startup and every thread that process handles starts on that commit.
|
|
235
|
+
|
|
236
|
+
Threads waking together share one fetch. If the fetch fails, new threads start on the snapshot Bishop already has and the reason is logged, so an expired credential doesn't take the agent offline.
|
|
237
|
+
|
|
238
|
+
It doesn't install anything. A commit that changes `package.json` gets a snapshot with no `node_modules`, and what to do about that is the agent's business. It also doesn't touch Bishop's own config, credentials, or Slack app, which are read from the directory you ran it from and still need a restart to change.
|
|
239
|
+
|
|
240
|
+
## Cleaning up after itself
|
|
241
|
+
|
|
242
|
+
A finished conversation leaves things behind: the snapshot it was pinned to, the worktree and branch it edited in, the files people shared in it, and its rows in Bishop's database. A schedule leaves a set every time it fires, which for an hourly one is 24 a day.
|
|
243
|
+
|
|
244
|
+
Bishop collects them itself, at startup and every six hours after, so a gateway left running for months doesn't fill a disk. Everything a thread left is kept for 30 days past its last turn:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"gc": {
|
|
249
|
+
"maxAgeDays": 30
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A snapshot a thread is still using is kept however old the commit is, and so is `current`. Nothing a live conversation is using is ever touched.
|
|
255
|
+
|
|
256
|
+
**It deletes what it collects.** A worktree goes with its branch, including work the agent committed there and nobody merged, and a thread that comes back after that starts fresh on the current files. If a thread's work matters, merge it before it ages out, or raise `maxAgeDays`.
|
|
257
|
+
|
|
258
|
+
**A collected thread stops being one Bishop is listening to.** In a Slack channel, mention the agent to pick it up again; a reply without a mention in a thread that old is ignored. DMs and email are unaffected.
|
|
259
|
+
|
|
260
|
+
To collect on your own terms, with Bishop stopped or from cron:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
$ bishop gc
|
|
264
|
+
$ bishop gc --max-age-days 7
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
It reads the database and the disk and nothing else, so it needs no credentials.
|
|
268
|
+
|
|
269
|
+
`bishop slack setup` and `bishop slack manifest` don't take `--agent`. They only deal with Bishop's own directory and the Slack app, and know nothing about any agent.
|
|
270
|
+
|
|
271
|
+
## Using it over email
|
|
272
|
+
|
|
273
|
+
Set up a Gmail mailbox for the agent (see [Email](#email)) and the agent has an address. Anyone on its allow list emails it, and the reply arrives in the same thread.
|
|
274
|
+
|
|
275
|
+
An email thread is an agent session, exactly as a Slack thread is. Reply to the agent's answer and it picks up where it left off.
|
|
276
|
+
|
|
277
|
+
Email works differently enough to be worth stating:
|
|
278
|
+
|
|
279
|
+
- **Nothing happens until the turn is over.** No streaming, no progress, no sign the agent is working. Then one plain-text reply arrives with everything it had to say. This makes a long turn fine, which it isn't in Slack.
|
|
280
|
+
- **The reply goes to everyone on the thread.** So Bishop refuses a thread with anyone on it who isn't on the allow list. If you're on the list and copied someone who isn't, it tells you why. If you're not on the list, it sends nothing at all.
|
|
281
|
+
- **Labels are the only progress you get.** Bishop marks each thread `Bishop/Working` while it runs, then `Bishop/Done`, `Bishop/Failed`, or `Bishop/Refused`. Open the mailbox to see what the agent is doing.
|
|
282
|
+
- **Attachments come through.** Bishop downloads what was attached and hands the agent the path, the same as in Slack.
|
|
283
|
+
- **No stop button.** Nothing in email can cancel a turn.
|
|
284
|
+
|
|
285
|
+
Bishop never answers automated mail: autoresponders, mailing lists, bounces, its own address, or anything Gmail marked spam. It also refuses mail whose sender Gmail's own verdict doesn't clear, without writing back, since the from address is the one thing known to be untrustworthy there. That check reads DMARC rather than a bare SPF or DKIM pass, and only from the topmost `Authentication-Results` header, because a sender can write that header themselves and only Gmail's own copy means anything.
|
|
286
|
+
|
|
287
|
+
Two rules are worth stating plainly, because both are about mail the agent must never send:
|
|
288
|
+
|
|
289
|
+
**Bishop only ever writes to an address on the allow list.** Someone not on it gets no reply, not even one explaining why. The thread is labelled `Bishop/Refused` and the reason goes in the log. Without this, pointing the agent at a mailbox that receives ordinary mail means it answers every stranger who writes in.
|
|
290
|
+
|
|
291
|
+
**Bishop never answers mail that arrived before you pointed it at the mailbox.** It records that moment the first time it connects and refuses anything older, whatever else happens to its polling state. Startup logs the cutoff as `answeringMailAfter`. Restarts still catch up on whatever arrived while it was down, because the cutoff is when it first attached, not when the process started. There is no setting that moves it earlier.
|
|
292
|
+
|
|
293
|
+
## Using it in Slack
|
|
294
|
+
|
|
295
|
+
**DM the bot** and every message reaches the agent.
|
|
296
|
+
|
|
297
|
+
**@-mention it in a channel** to start a thread. Bishop replies in that thread
|
|
298
|
+
and follows the conversation there. It ignores everything else in the channel, including messages in threads it isn't part of.
|
|
299
|
+
|
|
300
|
+
Each thread is its own agent session with its own history. Two threads are two independent conversations even with the same person, and a thread picks up where it left off, including after Bishop restarts.
|
|
301
|
+
|
|
302
|
+
**A turn shows its work.** Slack marks the thread as working for as long as the agent is, however long that is, which is also the stop button. Everything the agent writes lands as its own message as it writes it, and the tools it calls in between share a card that says what it is doing now. When the run of tool calls ends, the card becomes a line saying how many it used. The turn closes with the answer and a line saying how long it took, or how long it ran before you stopped it.
|
|
303
|
+
|
|
304
|
+
**Turn it down.** A thread showing every tool call is a thread that's hard to read when you already trust the agent. Send `` `! verbosity medium` `` to drop the tool card, or `` `! verbosity low` `` to leave only the answer. Send `` `! verbosity` `` to see where a thread stands, and `` `! verbosity default` `` to put it back. It applies to the turn that's running, so you can quiet one mid-flight. `slack.verbosity` in the config sets where every thread starts, and Slack always marks the thread as working whatever the level.
|
|
305
|
+
|
|
306
|
+
**Commands go to Bishop, not the agent.** Anything whose whole message is code opening with `! ` is a command: Bishop runs it, answers in the thread, and the agent is never told about either. `` `! help` `` lists them, and any of them takes `--help`. What follows the `!` has to look like a command name, so code and pasted output reach the agent as they always did.
|
|
307
|
+
|
|
308
|
+
**It doesn't answer every message in a busy thread.** In a thread where more than one other person is talking, the agent can leave a message alone when it wasn't for the agent, the way anyone else in the thread would. Slack still shows it thinking for a moment, the way somebody starts typing and then doesn't send. It always answers a message that @-mentions it, and always answers in a thread where you're the only other person talking, so a DM and a thread between you and the agent are unchanged.
|
|
309
|
+
|
|
310
|
+
**Write while it's working.** A message sent mid-turn gets an :hourglass_flowing_sand: and waits, rather than starting a second turn in the same conversation. When the turn ends, everything waiting goes to the agent as one prompt, with who wrote each one attached. Edit or delete a waiting message and the agent sees the change: what it gets is what the thread says at the moment it is sent.
|
|
311
|
+
|
|
312
|
+
**Share files with it.** Attach a file to a message, with or without a caption, and the agent gets it: Bishop downloads it and hands over the path. Screenshots included, so "look at this" and an image works. There is no restriction on type; what the agent can do with a `.parquet` is up to the agent. Files over 100MB reach it as a name and a note saying they were too large, which `files.maxMb` changes.
|
|
313
|
+
|
|
314
|
+
**It can open a file shared before it arrived.** Tag the agent into a thread that already has files in it and it is told what was shared, then downloads the ones it needs. Nothing is fetched on the way in, so tagging it into a thread with forty screenshots in it costs nothing.
|
|
315
|
+
|
|
316
|
+
**Mentions work both ways.** The agent reads mentions of other people as names rather than as Slack's user ids, and when it writes `@name` Bishop turns it into a real mention that notifies them. Other apps count: an agent can mention the bot beside it in a channel. A name that matches nobody, or more than one account, stays as plain text. `@here`, `@channel`, and `@everyone` are never converted, so an agent can't wake a workspace.
|
|
317
|
+
|
|
318
|
+
**It can talk to another agent, in a thread you started.** Pull two agents into the same thread and they can address each other, and the one you are talking to is told which messages came from another agent rather than a person. Neither can start the exchange: a bot's message is answered only in a thread that already has a session, so a bot can't DM the agent or open a thread with it. Apps that post under a name rather than an account, like an incoming webhook, are never answered at all. And an exchange between them is capped at three turns in a row by default, after which Bishop says it's holding off and waits for someone to say something. A turn another agent drew can't use Bishop's own tools either, so it can't leave a schedule behind. Set `slack.maxBotTurns` to change the cap, or to `0` to have the agent never answer another bot.
|
|
319
|
+
|
|
320
|
+
In a DM, Slack shows a stop button for as long as the turn runs. Pressing it cancels the agent mid-turn.
|
|
321
|
+
|
|
322
|
+
Slack caps messages near 4000 characters, so Bishop splits longer answers. Slack also doesn't render tables and handles nested lists badly, so agent output written for a terminal will sometimes look wrong in a thread.
|
|
323
|
+
|
|
324
|
+
## Scheduled prompts
|
|
325
|
+
|
|
326
|
+
Bishop can run a prompt on a clock and post the answer where you say, only when the agent has something to say.
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
bishop schedules add \
|
|
330
|
+
--name "CI check" \
|
|
331
|
+
--cron "0 9 * * 1-5" \
|
|
332
|
+
--to "#ops" \
|
|
333
|
+
--prompt "Check CI on main. If nothing is broken, answer <no-output>."
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`--to` takes `#channel` or `@person` for Slack, or an email address for Gmail, and never an id: Bishop resolves it once, when you add the schedule, and shows it back the same way. Adding a Slack destination needs three scopes (`channels:read`, `groups:read`, `im:write`) that an app installed before this existed won't have, so run `bishop slack setup` again if it says so.
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
bishop schedules list # everything scheduled here, in your own time zone
|
|
340
|
+
bishop schedules disable <id> # stop it firing, keep it
|
|
341
|
+
bishop schedules enable <id> # start it again, from now
|
|
342
|
+
bishop schedules rm <id> # remove it
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
What to expect:
|
|
346
|
+
|
|
347
|
+
- **Silence is the normal outcome.** Bishop posts nothing when the agent answers `<no-output>` or says nothing at all. `NOTHING TO REPORT` still works, so a prompt written before this doesn't have to change. Tell the agent in the prompt what is worth reporting; that criterion is yours, not Bishop's.
|
|
348
|
+
- **Nothing streams.** A scheduled turn is invisible while it runs, on Slack as much as on email, because Bishop can't unsay a report it turns out not to want. There is no way to stop one mid-turn.
|
|
349
|
+
- **Every firing is a fresh session.** The agent doesn't remember the last one, so tell it to write down anything it needs to carry over.
|
|
350
|
+
- **`--cron` is five fields**, read in `--tz` (defaults to this machine's zone), and no more often than every five minutes. `@daily` and friends work.
|
|
351
|
+
- **Nothing fires while Bishop is stopped, and nothing catches up.** A firing missed while it was down is skipped, and the log says so. A firing whose previous run is still going is skipped too.
|
|
352
|
+
- **A failure is never silent.** Bishop posts to the destination on the first failed run, records the reason in `schedules list`, and after five failures in a row turns the schedule off and says so. Re-enable it with `bishop schedules enable`.
|
|
353
|
+
- **A schedule stops when its author loses access.** Bishop re-checks the allow list at every firing, so removing someone stops what they scheduled.
|
|
354
|
+
|
|
355
|
+
Schedules live in `bishop.db`, not in a config file, so `bishop schedules list` is the only way to see what the agent will do tomorrow.
|
|
356
|
+
|
|
357
|
+
### Asking the agent for one
|
|
358
|
+
|
|
359
|
+
You can also just ask, in a thread or an email:
|
|
360
|
+
|
|
361
|
+
> @agent send me a report every morning at 9am in #ops, and stay quiet if there's nothing to say
|
|
362
|
+
|
|
363
|
+
The agent writes the schedule itself, and Bishop wakes up and delivers it. Leave the destination out and it posts back into the conversation you asked in.
|
|
364
|
+
|
|
365
|
+
Slack refuses a channel the app isn't in, Gmail refuses an address that isn't on `gmail.allow`, and you'll be told so in the thread rather than finding out at nine tomorrow. You can ask by email for something to land in Slack; it's the `#ops` that decides where it goes, not where you asked from.
|
|
366
|
+
|
|
367
|
+
Two limits the command line doesn't have, both because the agent is picking rather than you:
|
|
368
|
+
|
|
369
|
+
- **It won't start DMing someone who isn't on `slack.allow`**, and won't send mail at all unless `gmail.allow` is set. Posting in a channel is fine, since the app had to be invited there. Use `bishop schedules add` to aim somewhere the recipient hasn't opted into.
|
|
370
|
+
- **Twenty schedules per person.** Ask for a twenty-first and it'll tell you to remove one first.
|
|
371
|
+
|
|
372
|
+
What the agent can *see* is narrower. It lists and removes schedules you created, plus any that post into the conversation you're in, and nothing else, so asking it won't name private channels you aren't in. `bishop schedules list` still shows everything, with `by slack:U123…` naming who asked for each.
|
|
373
|
+
|
|
374
|
+
## Configuration
|
|
375
|
+
|
|
376
|
+
`.bishop/config.json` holds everything except credentials. Setup writes it, and you can edit it by hand.
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"slack": {
|
|
381
|
+
"workspace": "acme",
|
|
382
|
+
"app": {
|
|
383
|
+
"id": "A09XXXXXXXX",
|
|
384
|
+
"name": "salesforce-guy",
|
|
385
|
+
"displayName": "Salesforce Guy",
|
|
386
|
+
"description": "Answers Salesforce admin questions"
|
|
387
|
+
},
|
|
388
|
+
"allow": [
|
|
389
|
+
"hayes.davis@agentdeployment.co",
|
|
390
|
+
"revops",
|
|
391
|
+
"@sales-managers"
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
"gmail": {
|
|
395
|
+
"allow": ["hayes.davis@agentdeployment.co", "@agentdeployment.co"],
|
|
396
|
+
"pollSeconds": 20
|
|
397
|
+
},
|
|
398
|
+
"agent": {
|
|
399
|
+
"harness": "claude",
|
|
400
|
+
"model": "claude-sonnet-5",
|
|
401
|
+
"effort": "high"
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Every block is optional, and an interface runs when its credentials are set rather than when its block is here. `gmail` takes `allow`, `pollSeconds`, `labelPrefix`, and `markRead`; its `allow` is addresses and `@domain` entries, and an entry that is neither stops startup.
|
|
407
|
+
|
|
408
|
+
`slack` takes `allow`, `maxBotTurns`, and `verbosity`, plus the `workspace` and `app` fields setup writes.
|
|
409
|
+
|
|
410
|
+
`agent` takes `harness`, `model`, `effort`, `worktrees`, `options`, and `harnesses`. The last two are alternatives: `options` for a config that runs one harness, `harnesses` for one that switches.
|
|
411
|
+
|
|
412
|
+
Every key is checked at startup, so a typo fails with the path to it rather than being ignored. The one exception is `agent.options`, which is passed through to the harness unvalidated.
|
|
413
|
+
|
|
414
|
+
### Which agent runs
|
|
415
|
+
|
|
416
|
+
`agent.harness` picks the SDK. `claude` is the default and runs the Claude Agent SDK; `codex` runs OpenAI's Codex SDK.
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"agent": {
|
|
421
|
+
"harness": "codex"
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`--harness` overrides it for one run, and an unsupported name fails at startup naming the ones that exist:
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
$ bishop --harness codex
|
|
430
|
+
$ bishop --harness gemini
|
|
431
|
+
unknown harness "gemini"; supported: claude, codex
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Bishop manages no model credentials either way. Each harness authenticates however its own CLI does in that directory, so make `claude` or `codex` work there first. For Codex that means `codex login`, or `CODEX_API_KEY` in the environment or `.env`.
|
|
435
|
+
|
|
436
|
+
`OPENAI_API_KEY` is deliberately not read, because `codex` doesn't read it either. With only `OPENAI_API_KEY` set and no login, `codex` sends no credential at all and the request comes back `401 Missing bearer`. Picking it up here would invent a credential path Codex doesn't have, and on a machine with a working `codex login` it would override that login and move billing to the API key without saying so.
|
|
437
|
+
|
|
438
|
+
Bishop warns at startup when it can find neither a `CODEX_API_KEY` nor a login, since the first turn would otherwise fail on authentication with nothing to explain it. The startup check can't catch that itself: it stops before any model call, which is what makes it free. Otherwise the log names the credential it found.
|
|
439
|
+
|
|
440
|
+
A session id belongs to the harness that issued it, so changing this leaves running threads unable to resume. Bishop notices, logs it, and starts each thread a fresh session on its next message rather than failing the turn. Nothing else is lost.
|
|
441
|
+
|
|
442
|
+
Codex threads read differently in one way that shows: what its tool cards say is vaguer, because a command comes through as a raw shell line and nothing else, which Bishop won't put in a thread.
|
|
443
|
+
|
|
444
|
+
### Model and effort
|
|
445
|
+
|
|
446
|
+
Every harness has a model and a reasoning level, so both sit at the top of `agent` rather than inside `agent.options`:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"agent": {
|
|
451
|
+
"harness": "codex",
|
|
452
|
+
"model": "gpt-5.6-codex",
|
|
453
|
+
"effort": "high"
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Both are free strings, passed to the harness as written. The names belong to the harness and change with it, so Bishop doesn't check them: `claude` takes `claude-sonnet-5` and `low` through `max`, `codex` takes `gpt-5.6-codex` and `modelReasoningEffort`'s levels.
|
|
459
|
+
|
|
460
|
+
Nothing checks the value before the first turn, including the startup check, which stops before any model call. A name the harness doesn't know surfaces in a thread instead, differently by harness. Codex fails the turn with the API's own complaint about the field. Claude answers a bad model with the agent saying in prose that the model is wrong, and ignores an effort level it doesn't recognize. So an effort typo under Claude is silent: check the log line at startup, which names the model and effort Bishop passed.
|
|
461
|
+
|
|
462
|
+
`--model` and `--effort` override the config for one run:
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
$ bishop --harness claude --model claude-opus-5 --effort high
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Each harness maps them onto its own option, which is `model` and `effort` under Claude, `model` and `modelReasoningEffort` under Codex. Setting the same thing again in `agent.options` loses to these, and startup logs a warning naming the key it overrode. That way a flag never does nothing silently.
|
|
469
|
+
|
|
470
|
+
### Switching between harnesses
|
|
471
|
+
|
|
472
|
+
`agent.options` is written in one harness's vocabulary, so a config meant to run either one keeps a block per harness:
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"agent": {
|
|
477
|
+
"harness": "claude",
|
|
478
|
+
"worktrees": true,
|
|
479
|
+
"effort": "high",
|
|
480
|
+
|
|
481
|
+
"harnesses": {
|
|
482
|
+
"claude": {
|
|
483
|
+
"options": { "maxTurns": 30 }
|
|
484
|
+
},
|
|
485
|
+
"codex": {
|
|
486
|
+
"model": "gpt-5.6-codex",
|
|
487
|
+
"options": { "sandboxMode": "danger-full-access" }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`bishop` runs Claude with its block, and `bishop --harness codex` runs Codex with its own model and sandbox setting. Nothing written for one follows the other.
|
|
495
|
+
|
|
496
|
+
A block takes `model`, `effort`, and `options`, and nothing else. `model` and `effort` layer over the ones beside `harness`, so `effort` above applies to both harnesses and Codex adds a model. `options` doesn't layer: it replaces `agent.options` outright, since merging two objects written in different vocabularies would splice one harness's keys into the other's call.
|
|
497
|
+
|
|
498
|
+
That leaves `agent.options` as the options for whichever harness has no block of its own, which is what keeps a config that never switches harness exactly as it was. Set both for the same harness and startup warns that the block wins. Switch to a harness with no block and startup warns that it is being handed options meant for the other one, since the harness that doesn't own them ignores what it doesn't recognize and says nothing.
|
|
499
|
+
|
|
500
|
+
A harness name Bishop doesn't run fails at startup rather than being ignored, so a misspelled block can't quietly do nothing.
|
|
501
|
+
|
|
502
|
+
### Who can use the agent
|
|
503
|
+
|
|
504
|
+
`allow` takes usernames, emails, and group names in one list, and Bishop works out which is which. A leading `@` is optional. Groups expand to their members, and Bishop refreshes the list every few minutes so a membership change lands without a restart.
|
|
505
|
+
|
|
506
|
+
Every message is checked, not just the one that starts a thread, so someone who isn't allowed can't join a conversation another person started. They get a reply only they can see.
|
|
507
|
+
|
|
508
|
+
Another app is an entry like any other: list it by name and the agent will answer it, subject to the same limits on a bot-only exchange described in [Using it in Slack](#using-it-in-slack). This is the setting that decides which apps can reach the agent, and it matters more than it did: without a list, any app posting in a thread someone started can draw a turn, and what it posts is text nobody in the thread wrote. Set `slack.maxBotTurns` to `0` to keep every one of them out.
|
|
509
|
+
|
|
510
|
+
Leave `allow` out and anyone who can reach the bot can use the agent. Bishop warns at startup when that's the case.
|
|
511
|
+
|
|
512
|
+
An entry matching two different things, like a name that's both a person and a group, stops startup and says which candidates it found. An entry matching nothing logs a warning and Bishop keeps running, so a departed colleague's email doesn't take the agent down.
|
|
513
|
+
|
|
514
|
+
### Agent options
|
|
515
|
+
|
|
516
|
+
`agent.options` is passed to the agent harness. Under Claude that is the Agent SDK's own options object, not a settings file: anything from [Claude Code's settings reference](https://code.claude.com/docs/en/settings-reference) goes in the nested `settings` object, which is shaped like a `.claude/settings.json`. A settings key written at the top level is dropped by the SDK without a word, so put it a level down:
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"agent": {
|
|
521
|
+
"options": {
|
|
522
|
+
"maxTurns": 30,
|
|
523
|
+
"settings": {
|
|
524
|
+
"permissions": { "allow": ["Bash(python3:*)"] }
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
These layer over the agent's own settings files rather than replacing them, so a project that sets `model` in `.claude/settings.json` keeps it unless `agent.options` names `model` too. [`agent.model` and `agent.effort`](#model-and-effort) sit above `agent.options` in turn, and a config that switches harness puts its options in [a block per harness](#switching-between-harnesses) instead.
|
|
532
|
+
|
|
533
|
+
Bishop sets a few options itself and ignores them here: `cwd`, `resume`, `systemPrompt`, `abortController`, `sessionId`, `forkSession`, and `canUseTool`. Setting one logs a warning naming it. They decide which agent runs, which conversation it continues, and how Bishop tells the agent who it's talking to.
|
|
534
|
+
|
|
535
|
+
Bishop sets two defaults, both overridable. `permissionMode: "auto"`, because nothing in Slack can answer a permission prompt and an agent needing approval for a tool reports itself blocked instead of doing the work; override it in `agent.options` if you want something stricter. And `settings.autoMemoryEnabled: false`, because auto memory writes to `~/.claude/projects/<agent-directory>/memory`, outside the agent's own repository: it doesn't travel when you deploy the agent somewhere else, and a remote agent's directory changes with every commit, so the memory of the last one is orphaned. Turn it back on with:
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"agent": {
|
|
540
|
+
"options": {
|
|
541
|
+
"settings": { "autoMemoryEnabled": true }
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
That key belongs under `settings` and nowhere else. The harness ignores it at the top of `agent.options`, so Bishop warns at startup if you put it there.
|
|
548
|
+
|
|
549
|
+
`hooks` and `settings` merge instead of being replaced or ignored. Your hooks run alongside the one [worktree mode](#worktree-mode) installs, so setting `hooks` can't switch that off, and your `settings` object keeps Bishop's defaults for the keys it doesn't name. Pointing `settings` at a settings *file* instead opts out of both defaults, which Bishop warns about.
|
|
550
|
+
|
|
551
|
+
#### Under the Codex harness
|
|
552
|
+
|
|
553
|
+
`agent.options` takes Codex's own fields instead:
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
{
|
|
557
|
+
"agent": {
|
|
558
|
+
"harness": "codex",
|
|
559
|
+
"options": {
|
|
560
|
+
"sandboxMode": "danger-full-access",
|
|
561
|
+
"additionalDirectories": ["/srv/data"]
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Bishop's defaults are `approvalPolicy: "never"`, `sandboxMode: "workspace-write"`, `networkAccessEnabled: true`, and `skipGitRepoCheck: true`. Nothing in Slack can answer an approval prompt, so an agent that asks for one stalls the turn. The sandbox is the compensation: writes stay inside the working directory, and network access stays on because an agent that can't push, install, or fetch fails at something in most threads. Override any of them here.
|
|
568
|
+
|
|
569
|
+
Bishop sets `workingDirectory` and `developer_instructions` itself and ignores them here, for the same reasons as above.
|
|
570
|
+
|
|
571
|
+
`config` and `configOverrides` reach Codex's own `--config` mechanism, which is how anything in `config.toml` can be set per-agent. Bishop uses `config.developer_instructions` for the principal, so a `config` object you set is merged rather than replaced.
|
|
572
|
+
|
|
573
|
+
### Worktree mode
|
|
574
|
+
|
|
575
|
+
Threads share one directory, so two people directing a coding agent in two threads write the same files. Turn that off:
|
|
576
|
+
|
|
577
|
+
```json
|
|
578
|
+
{
|
|
579
|
+
"agent": {
|
|
580
|
+
"worktrees": true
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Each thread now works in its own git worktree, named `bishop-<team>-<channel>-<thread>`, and stays in it for every later message. It lives in `.claude/worktrees/` at the root of the repository the agent is in, which is the agent directory itself unless you pointed `--agent` at a git URL; there Bishop's own checkout layout puts it in `.bishop/agents/<slug>/mirror.git/.claude/worktrees/`. `git worktree list` always says.
|
|
586
|
+
|
|
587
|
+
How it gets there depends on the harness, because only one of them can be told to move itself:
|
|
588
|
+
|
|
589
|
+
- **Claude** creates the worktree the first time the agent decides to edit a file, so a thread that only asks questions never gets one. Bishop names it, tells the agent to enter it, and refuses edits in the shared checkout until it has. The branch is the worktree name prefixed with `worktree-`.
|
|
590
|
+
- **Codex** has no equivalent tool, so Bishop creates the worktree itself and starts the session inside it. Every thread gets one whether or not it edits anything, and the branch is the worktree name exactly. Nothing depends on the agent cooperating.
|
|
591
|
+
|
|
592
|
+
Either way the agent commits on that branch and says which one it is.
|
|
593
|
+
|
|
594
|
+
A worktree outlives the thread that made it, and Bishop collects it a month after the thread's last message, along with its branch. See [Cleaning up after itself](#cleaning-up-after-itself). To take one sooner: under Claude the harness locks each one while its session holds it, and the lock outlives the process, so removing one takes two commands:
|
|
595
|
+
|
|
596
|
+
```
|
|
597
|
+
$ git worktree list
|
|
598
|
+
$ git worktree unlock .claude/worktrees/bishop-T1-C1-1712345678-123456
|
|
599
|
+
$ git worktree remove .claude/worktrees/bishop-T1-C1-1712345678-123456
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Add `--force` to `remove` to discard uncommitted changes in it. Check `git -C <worktree> status` first: that's where a thread's unfinished work is.
|
|
603
|
+
|
|
604
|
+
A new worktree holds only tracked files, so `node_modules` and anything else your repository ignores is missing. Symlink what the build needs:
|
|
605
|
+
|
|
606
|
+
```json
|
|
607
|
+
{
|
|
608
|
+
"agent": {
|
|
609
|
+
"worktrees": true,
|
|
610
|
+
"options": {
|
|
611
|
+
"settings": {
|
|
612
|
+
"worktree": {
|
|
613
|
+
"symlinkDirectories": ["node_modules"],
|
|
614
|
+
"baseRef": "head"
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Bishop sets `baseRef` to `head` so a worktree branches from wherever the agent directory is now. Set it to `fresh` to branch from `origin/<default-branch>` instead. Everything else the harness offers here, including `sparsePaths` for a large monorepo, works the same way. That block is Claude's; under Codex, Bishop always branches from `HEAD` and there is nothing to configure.
|
|
623
|
+
|
|
624
|
+
Under Claude, Bishop refuses `Edit`, `Write`, and `NotebookEdit` in the shared directory until the agent has entered its worktree, and tells the agent why. Bash is not checked, since a command's target can't be read off its text. So this keeps threads out of each other's way; it isn't a sandbox, and an agent determined to write outside its worktree can. Under Codex the session starts in the worktree, so nothing has to be refused, and `sandboxMode` is what limits where it can write.
|
|
625
|
+
|
|
626
|
+
**When the agent directory isn't a git repository**, there's nothing to isolate. Bishop warns at startup and says so once in each thread, then edits the directory directly.
|
|
627
|
+
|
|
628
|
+
### Shared files
|
|
629
|
+
|
|
630
|
+
Bishop downloads what people attach and saves it under `.bishop/files`, one directory per thread, collected with everything else that thread left behind. The agent reads it from disk, so any file type works.
|
|
631
|
+
|
|
632
|
+
A file shared earlier in the conversation is named to the agent rather than downloaded, and the agent fetches the ones it wants with a tool. That keeps being tagged into a busy thread cheap, and it means the agent can go back for a file it was told about turns ago. A reference only works in the conversation the file was shared in.
|
|
633
|
+
|
|
634
|
+
```json
|
|
635
|
+
{
|
|
636
|
+
"files": {
|
|
637
|
+
"maxMb": 100
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
The default is 100. A file over the limit isn't refused: the agent is told its name and that it was too large, so it can say so. There is no restriction on type and won't be.
|
|
643
|
+
|
|
644
|
+
**Slack needs the `files:read` scope**, which is new. An app set up before this has to be reinstalled to grant it, and until then Bishop warns at startup and the agent sees a note in place of each file. Run `bishop slack manifest` for the current manifest, or reinstall from App Settings. Gmail needs nothing new.
|
|
645
|
+
|
|
646
|
+
### Credentials
|
|
647
|
+
|
|
648
|
+
`.env` holds the Slack tokens, written by setup:
|
|
649
|
+
|
|
650
|
+
```
|
|
651
|
+
BISHOP_SLACK_BOT_TOKEN=xoxb-...
|
|
652
|
+
BISHOP_SLACK_APP_TOKEN=xapp-...
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Add `ANTHROPIC_API_KEY` if the agent should use a specific key. Without one, the agent authenticates however `claude` does on that machine, usually an existing login. Bishop logs which of the two it used at startup.
|
|
656
|
+
|
|
657
|
+
Anything else in `.env` reaches the agent's environment too, which is how `GH_TOKEN` from [`bishop github setup`](#github) gets there.
|
|
658
|
+
|
|
659
|
+
## Running it for real
|
|
660
|
+
|
|
661
|
+
Bishop is one process that logs JSON to stdout and errors to stderr, so any process supervisor works. It needs only outbound access to Slack, since it uses Socket Mode. No public URL, no inbound firewall rule.
|
|
662
|
+
|
|
663
|
+
```
|
|
664
|
+
$ BISHOP_LOG_LEVEL=info bishop
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
`BISHOP_LOG_LEVEL=debug` adds tool activity and every routing decision. Pipe through `npx pino-pretty` to read it by eye.
|
|
668
|
+
|
|
669
|
+
At startup Bishop checks that the agent can launch, resolves the allow list, and connects to Slack, failing with a specific message if any of those doesn't work.
|
|
670
|
+
|
|
671
|
+
### As a systemd service
|
|
672
|
+
|
|
673
|
+
A unit file keeps Bishop running across crashes and reboots on a Linux host:
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
[Unit]
|
|
677
|
+
Description=Bishop gateway for <agent>
|
|
678
|
+
After=network-online.target
|
|
679
|
+
Wants=network-online.target
|
|
680
|
+
|
|
681
|
+
[Service]
|
|
682
|
+
Type=simple
|
|
683
|
+
User=<user>
|
|
684
|
+
WorkingDirectory=/path/to/agent
|
|
685
|
+
Environment=BISHOP_LOG_LEVEL=info
|
|
686
|
+
ExecStart=/path/to/node /path/to/bishop/dist/cli.js --agent /path/to/agent
|
|
687
|
+
Restart=on-failure
|
|
688
|
+
RestartSec=5
|
|
689
|
+
|
|
690
|
+
[Install]
|
|
691
|
+
WantedBy=multi-user.target
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Point `ExecStart` at an absolute path to `node`, not the bare command. systemd doesn't source `.bashrc` or `.profile`, so a version installed through nvm isn't on `PATH` for the service even though it is for you in a login shell. `which node` finds the real path; symlink it somewhere stable like `~/.local/bin` so the unit survives a node upgrade without an edit. Installed from npm, the script it runs is `<npm root -g>/@agentdeploymentco/bishop/dist/cli.js`.
|
|
695
|
+
|
|
696
|
+
```
|
|
697
|
+
$ sudo systemctl enable --now bishop.service
|
|
698
|
+
$ journalctl -u bishop -f
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Editing the unit file needs no sudo, applying it always does.** `chown` the unit to the service's own user (`sudo chown <user> /etc/systemd/system/bishop.service`) so an edit is a normal file write, then scope `sudoers` to exactly the two commands a change needs: `daemon-reload` and `restart bishop.service`. That is the smallest grant that lets a deployment change its own `ExecStart` without a root shell.
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
<user> ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload, /usr/bin/systemctl restart bishop.service
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Switching `--agent` from a local path to a git URL with `--auto-update`, replace the flag, don't append it.** `ExecStart` is a backslash-continued multi-line value, and the old `--agent /path` line loses its trailing `\` when a new one gets pasted below it. systemd then ends the directive at that line, and the new `--agent` line below becomes a bare, invalid line in `[Service]`. `systemctl status` catches half of this ("unit file changed on disk, run daemon-reload") but not the syntax error, so read the file back after editing rather than trusting the diff you meant to make.
|
|
708
|
+
|
|
709
|
+
**A restart kills whichever turn asked for it, when Bishop is asked to reconfigure itself.** An agent that edits its own unit file and runs `systemctl restart` takes down the process running that same turn. It comes back clean, since session and thread state live in Bishop's own working directory rather than the agent's, so the conversation resumes on the next message. But the response that triggered the restart cuts off mid-stream, which reads as a crash to whoever is waiting on it unless they're told to expect it. Some harnesses also refuse this outright: Claude Code's own permission model blocks a session from editing a file outside its project directory or restarting the service that's running it, so a human ends up applying the unit change and the restart by hand either way.
|
|
710
|
+
|
|
711
|
+
**A fresh `--auto-update` snapshot has no git identity.** Each commit gets its own directory under `.bishop/agents/<slug>/snapshots/`, cloned straight from the bare mirror, and none of them inherit a `user.name`/`user.email` set locally in an old checkout. Set it globally for the service's user once (`git config --global user.name ...`) rather than per snapshot, or every commit the agent makes fails with "Author identity unknown."
|
|
712
|
+
|
|
713
|
+
### In a container
|
|
714
|
+
|
|
715
|
+
`ghcr.io/agent-deployment-co/bishop` runs Bishop on Debian with Node 24, for `linux/amd64` and `linux/arm64`. The image is the agent's shell as well as Bishop's, so it carries git, `gh`, ripgrep, jq, curl, ssh, and python3 alongside both harness SDKs. `uv` and `uvx` are there too, with CPython 3.14 already installed and on `PATH` as `python3.14`, since Debian's `python3` is externally managed and won't let anything install into it.
|
|
716
|
+
|
|
717
|
+
Two directories to persist. `/data` is the working directory, holding everything Bishop writes:
|
|
718
|
+
|
|
719
|
+
```
|
|
720
|
+
/data/.bishop/config.json configuration
|
|
721
|
+
/data/.bishop/agents/ cloned agents, when --agent is a git URL
|
|
722
|
+
/data/bishop.db the thread-to-session mapping
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`/home/node` is the second, holding what the harnesses write: their logins, and the session transcripts they resume from. Bishop resumes a thread by handing back the session id it recorded in `bishop.db`, so a home directory that dies with the container leaves every existing thread asking for a transcript the harness no longer has. Mount it or lose every conversation on the next deploy.
|
|
726
|
+
|
|
727
|
+
Credentials go in the environment rather than a `.env` file:
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
$ docker run -d --name bishop \
|
|
731
|
+
-v bishop-data:/data -v bishop-home:/home/node \
|
|
732
|
+
-e BISHOP_SLACK_BOT_TOKEN -e BISHOP_SLACK_APP_TOKEN \
|
|
733
|
+
-e ANTHROPIC_API_KEY \
|
|
734
|
+
-e GIT_CONFIG_COUNT=2 \
|
|
735
|
+
-e GIT_CONFIG_KEY_0=user.name -e GIT_CONFIG_VALUE_0="Agent" \
|
|
736
|
+
-e GIT_CONFIG_KEY_1=user.email -e GIT_CONFIG_VALUE_1="agent@example.com" \
|
|
737
|
+
ghcr.io/agent-deployment-co/bishop \
|
|
738
|
+
--agent https://github.com/you/your-agent.git --auto-update
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
The entrypoint is `bishop`, so arguments after the image are its flags, and any other command works the same way: `docker run --rm -v bishop-data:/data ghcr.io/agent-deployment-co/bishop gc`.
|
|
742
|
+
|
|
743
|
+
Set a git identity, as above, or the agent can't commit what it does in a worktree. `GIT_CONFIG_COUNT` reaches every snapshot and worktree Bishop creates, which a `git config` run in one directory doesn't.
|
|
744
|
+
|
|
745
|
+
Bishop runs as uid 1000 and needs to write both volumes. On Kubernetes that means a `fsGroup` in the pod's security context, or volumes already owned by 1000.
|
|
746
|
+
|
|
747
|
+
Tags: `latest` and `sha-<commit>` from `main`, plus `<version>` and `<major>.<minor>` from a `v*` git tag. `bishop --version` reads `package.json`, so a release tag has to bump it in the same commit or the image reports a version its tag disagrees with.
|
|
748
|
+
|
|
749
|
+
Setup commands that need a browser or a terminal (`bishop slack setup`, `bishop gmail setup`) are meant to be run on a laptop. Carry the tokens they produce to the container as environment variables, and the `.bishop/config.json` they write into the volume.
|
|
750
|
+
|
|
751
|
+
## Troubleshooting
|
|
752
|
+
|
|
753
|
+
**"--agent directory doesn't exist"** or **"is not a directory"** means the path is wrong. Bishop checks it before anything else, because the harness reports a bad directory as a native binary failing to launch.
|
|
754
|
+
|
|
755
|
+
**"The Claude agent couldn't start"** means the harness didn't launch. Check that
|
|
756
|
+
`claude` works in that directory first. Note that a wrong `ANTHROPIC_API_KEY` passes this check and fails on the first real message, since only a live call tests a key.
|
|
757
|
+
|
|
758
|
+
**The agent says it's blocked on permissions.** Something needs approval that
|
|
759
|
+
`auto` mode declined. Add an allow rule under `agent.options.settings.permissions.allow`.
|
|
760
|
+
|
|
761
|
+
**The bot doesn't answer in a channel.** It only replies to mentions and to
|
|
762
|
+
threads it's already in. Confirm it's in the channel and that you mentioned it.
|
|
763
|
+
|
|
764
|
+
**The bot doesn't answer at all** and the log shows a refusal. The sender isn't
|
|
765
|
+
on the `allow` list.
|
|
766
|
+
|
|
767
|
+
**The agent never answers, and nothing appears in the log.** If you set the app up by hand, check its Event Subscriptions. Bishop can't see them, and an app subscribed to nothing looks identical to an idle one.
|
|
768
|
+
|
|
769
|
+
**Setup says the app kept a `(local)` suffix.** The `slack` CLI's login expires
|
|
770
|
+
after 12 hours, which stops Bishop from correcting the app name. Run `slack auth login` again, or rename the app in Slack.
|
|
771
|
+
|
|
772
|
+
**You can't find what a thread changed.** In worktree mode the work is on that thread's branch, not in the agent directory. `git worktree list` shows every one, and `git branch --list 'worktree-bishop-*'` shows the branches.
|
|
773
|
+
|
|
774
|
+
**A build or test fails inside a worktree.** A fresh worktree has only tracked files, so `node_modules` and anything else the repository ignores isn't there. Add it to `agent.options.settings.worktree.symlinkDirectories`.
|
|
775
|
+
|
|
776
|
+
**The agent says it can't edit a file.** In worktree mode Bishop refuses writes in the shared directory. The agent is told to call `EnterWorktree` first, and normally does it unprompted. If the agent directory isn't a git repository, that can't work, and Bishop says so in the thread.
|
|
777
|
+
|
|
778
|
+
**Sessions get confused.** Delete `bishop.db` to make every thread start fresh.
|
|
779
|
+
It holds only the thread-to-session mapping.
|
|
780
|
+
|
|
781
|
+
## What this version doesn't do
|
|
782
|
+
|
|
783
|
+
- One agent per directory, one process. No agent registry.
|
|
784
|
+
- Slack and Gmail only. No Teams.
|
|
785
|
+
- No sandboxing between sessions. [Worktree mode](#worktree-mode) keeps concurrent threads out of each other's files, but it's a convention with a guardrail rather than a sandbox, and it needs a git repository. Without it, two threads can write the same file at once.
|
|
786
|
+
- Files come in but don't go out. The agent can read what you attach; it can't attach anything to its answer.
|
|
787
|
+
- No approval flow. Permission decisions are the harness's, and Slack has no way to ask a person.
|
|
788
|
+
- No way to turn Bishop's own tools off. Bishop listens on `127.0.0.1` on an ephemeral port to serve them, authenticated with a credential minted per turn and destroyed when the turn ends. Nothing is reachable from off the machine, but anything running on it can reach the port and will be refused.
|
|
789
|
+
|
|
790
|
+
Bishop notices neither a revoked Slack app nor a dead socket, so a supervised process can look healthy while receiving nothing. Restart it if the agent goes quiet.
|
|
791
|
+
|
|
792
|
+
## How setup gets the tokens
|
|
793
|
+
|
|
794
|
+
Worth knowing if you're wondering why the `slack` CLI is a requirement.
|
|
795
|
+
|
|
796
|
+
Socket Mode needs an app-level (`xapp-`) token, and no Slack API will mint one. The `slack` CLI does, and hands it to a project it runs. So setup writes a throwaway Slack project, runs it once to collect both tokens, and stops it. The CLI creates that app as a dev app and appends ` (local)` to its name, so setup pushes the intended manifest over it afterward. That changes no permissions and needs no reinstall.
|
|
797
|
+
|
|
798
|
+
## License
|
|
799
|
+
|
|
800
|
+
Not yet chosen, and the published package says so: `license` is `UNLICENSED`, which grants nothing. Nobody outside the company should build on it until there are real terms.
|