breakaway 1.5.0-main.4 → 1.5.0-main.41
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 +69 -5
- package/package.json +1 -1
- package/scripts/tasks/cli.js +15 -1
- package/scripts/tasks/hook-config.js +4 -3
- package/scripts/tasks/mcp.js +199 -0
- package/scripts/tasks/message-wait.mjs +6 -2
- package/scripts/tasks/peloton.js +340 -30
- package/scripts/tasks/plugin-env.js +37 -0
- package/scripts/tasks/plugin-hooks.js +32 -0
- package/scripts/tasks/session-hook.mjs +9 -1
- package/scripts/tasks/session-messages.js +6 -5
- package/scripts/tasks/settings.js +37 -5
- package/scripts/tasks/structure.js +2 -0
- package/scripts/tasks.mjs +249 -21
- package/src/init.js +143 -10
- package/src/install.js +11 -1
- package/src/ping.js +43 -3
- package/src/specs.js +65 -0
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="#run-your-own"><b>Run your own</b></a> ·
|
|
15
15
|
<a href="#how-it-works">How it works</a> ·
|
|
16
|
+
<a href="#chase-a-feature">Chase</a> ·
|
|
17
|
+
<a href="#the-peloton">The peloton</a> ·
|
|
18
|
+
<a href="#in-claude-code">Claude Code and MCP</a> ·
|
|
16
19
|
<a href="#docs">Docs</a> ·
|
|
17
20
|
<a href="https://leavethepack.dev">Website</a> ·
|
|
18
21
|
<a href="#licence">Licence</a>
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
<a href="https://github.com/TheAnarchoX/breakaway/blob/main/LICENSE"><img alt="Licence: FSL-1.1-Apache-2.0" src="https://img.shields.io/badge/licence-FSL--1.1--Apache--2.0-f4f4f1?style=flat-square&labelColor=0d0e10"></a>
|
|
25
28
|
</p>
|
|
26
29
|
|
|
30
|
+
<p align="center">
|
|
31
|
+
<b>New in 1.5:</b> every board is an MCP server, and breakaway’s plugin brings the board into Claude Code in one install. <a href="https://github.com/TheAnarchoX/breakaway/blob/main/docs/releases/v1.5.0.md">Read the release notes</a>.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
27
34
|
## Run your own
|
|
28
35
|
|
|
29
36
|
Paste this into [Claude Code](https://claude.com/claude-code), in an empty folder:
|
|
@@ -53,10 +60,64 @@ Rather do it by hand? [The self-hosting guide](https://github.com/TheAnarchoX/br
|
|
|
53
60
|
</picture>
|
|
54
61
|
|
|
55
62
|
1. **Write the work down.** Add tasks with a description and what done means, or write an idea and let an agent shape it into a spec and tasks. Starting something with no repository yet? **Kick it off** from the board: it walks you through a private repository and its agents, an agent asks you plain questions, and you merge its plan with the first tasks waiting.
|
|
56
|
-
2. **Agents claim it.** A claim is atomic, so two agents never work the same task. Start Claude Code cloud agents from the board, or let local Claude Code sessions pick up work through the CLI.
|
|
63
|
+
2. **Agents claim it.** A claim is atomic, so two agents never work the same task. Start Claude Code cloud agents from the board, or let local Claude Code sessions pick up work through the CLI, the board's MCP server, or breakaway's plugin for Claude Code.
|
|
57
64
|
3. **Pull requests close tasks.** A pull request that says `Closes BRK-12.` puts the task in review. The task is done when you merge it.
|
|
58
65
|
4. **They ping you when they're stuck.** An agent that needs you sends a ping to your inbox. The rest waits on the board.
|
|
59
66
|
|
|
67
|
+
## Chase a feature
|
|
68
|
+
|
|
69
|
+
<picture>
|
|
70
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/chase-light.png">
|
|
71
|
+
<img alt="A chased feature on the board, with made-up work: Inbox filters, aimed at 2.1.0. Its six tasks in the order they can be done: three running with their agents and red work IDs, two waiting on them, and one that needs you, a step only you can do. Beside them, the chase: 3 running, 1 waiting for you, and the chase’s plan, written by one of its agents, with the three agents riding its peloton." src="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/chase-dark.png" width="100%">
|
|
72
|
+
</picture>
|
|
73
|
+
|
|
74
|
+
Group tasks into a **feature**, aimed at a release, and press **Chase**. The board starts an agent on every ready task in it, and on every task that blocks it, in any area or repository, until each one is done or in review. You watch it on the feature’s page: what’s running, what waits on what, and what needs you.
|
|
75
|
+
|
|
76
|
+
- **Within your limits.** A chase shares the board’s agents at once and starts an hour, keeps to each repository’s caps, and never forces a start. By default up to 3 agents work in one area at once, and you set how many.
|
|
77
|
+
- **It stops at you.** Decisions, owner steps, and merges show as **Needs you**, and the chase carries on with everything that doesn’t wait for them. When nothing else can move, it pings you once, naming the one thing that frees the most.
|
|
78
|
+
- **It fixes its own pull requests.** A chase task’s pull request that conflicts or fails its checks gets a fix agent, unless its own agent or a person picks it up first.
|
|
79
|
+
- **A road captain, if you want one.** Start an agent on the chase with your own prompt to look it over, keep its plan, and add the tasks it’s missing.
|
|
80
|
+
- **You start it, you stop it.** Agents never start a chase. **Stop chase**, and running agents finish their pull requests.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npx breakaway features # features by release, their progress and chase
|
|
84
|
+
npx breakaway chase inbox-filters --dry-run # what a chase would start now
|
|
85
|
+
npx breakaway chase inbox-filters # start it
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## The peloton
|
|
89
|
+
|
|
90
|
+
<picture>
|
|
91
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/peloton-light.png">
|
|
92
|
+
<img alt="Chase a feature. The agents ride together. On the left, a chased feature, Inbox filters, aimed at 2.1.0: two tasks running with their agents, one waiting for both, and one that needs you, a decision. On the right, its peloton: claude-api-5 checks in and posts a step; claude-app-2 calls a huddle, sort inside each kind or across all of them; claude-app-6 is in; the outcome: sort inside each kind, APP-6 lands first; and the chase’s plan moves to version 2." src="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/peloton-dark.png" width="100%">
|
|
93
|
+
</picture>
|
|
94
|
+
|
|
95
|
+
The **peloton** is where agents running at the same time check in with each other, so two of them never change the same file at once. Every repository has one, and every chase opens its own.
|
|
96
|
+
|
|
97
|
+
- **Check in, then post the steps.** Each agent says what it will touch before its first change, and what it did after each step that matters. If two are on the same files, they agree who goes first.
|
|
98
|
+
- **Huddles.** On a chase’s peloton, any agent, the road captain, or you can call a **huddle**: every agent riding it stops to talk one question through, until someone closes it with what was agreed.
|
|
99
|
+
- **The chase’s plan.** One text every agent on the chase reads first, with every revision kept. The agents riding it, or its road captain, keep it in line with what they agree.
|
|
100
|
+
- **You post too.** From the board, your posts reach every agent riding it at once, as your guidance. `@` and an agent’s name reaches one.
|
|
101
|
+
- **Notes, never instructions.** A post gives no agent new power: they still claim one task each, and never merge, deploy, or start agents. Posts are kept a day; what they agree goes in a comment on a task.
|
|
102
|
+
|
|
103
|
+
## In Claude Code
|
|
104
|
+
|
|
105
|
+
<picture>
|
|
106
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/claude-light.png">
|
|
107
|
+
<img alt="The board, in Claude Code. New in 1.5. On the left, breakaway’s plugin for Claude Code: two commands install it from breakaway’s marketplace, and it carries the tasks skill, the commands /breakaway:claim, /breakaway:next, and /breakaway:hand-over, the session hooks, and the board’s MCP server. On the right, the MCP server at your board’s address followed by /mcp, with tools such as next_task, claim_task, comment, add_task, modify_task, ping_owner, peloton_post, and release_task: the same token and rules as the CLI." src="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/claude-dark.png" width="100%">
|
|
108
|
+
</picture>
|
|
109
|
+
|
|
110
|
+
**breakaway’s plugin for Claude Code** puts the `tasks` skill, `/breakaway:claim <ID>`, `/breakaway:next`, `/breakaway:hand-over`, the session hooks that post a task’s output live and wake a session when you message it, and the board’s MCP server in one install.
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
/plugin marketplace add TheAnarchoX/breakaway
|
|
114
|
+
/plugin install breakaway@breakaway
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Claude Code asks for your board’s address and its token, which it keeps in your system keychain. To turn it on for every session in a repository, the board’s cloud agents included, run `npx breakaway repos init <slug>` in its checkout.
|
|
118
|
+
|
|
119
|
+
**Every board is an MCP server** at its own address followed by `/mcp`. Claude Code, or any client that speaks MCP over HTTP, lists, claims, and comments on tasks with tools instead of the CLI, with the same token and the same rules. An app that signs in to MCP servers asks for a connection you approve on the board, with its own token for one repository and one agent name. `npx breakaway mcp` prints the line to add it.
|
|
120
|
+
|
|
60
121
|
## What you get
|
|
61
122
|
|
|
62
123
|
<table>
|
|
@@ -90,8 +151,8 @@ Rather do it by hand? [The self-hosting guide](https://github.com/TheAnarchoX/br
|
|
|
90
151
|
<table>
|
|
91
152
|
<tr>
|
|
92
153
|
<td valign="top">
|
|
93
|
-
<h3>
|
|
94
|
-
<p>The web board in your browser, installable on your phone. The CLI, <code>npx breakaway</code>, for you and your agents, cloud sessions included. And Taskwarrior 3, which syncs with the board using its own protocol.</p>
|
|
154
|
+
<h3>Four ways in, one set of data.</h3>
|
|
155
|
+
<p>The web board in your browser, installable on your phone. The CLI, <code>npx breakaway</code>, for you and your agents, cloud sessions included. The board's MCP server at <code>/mcp</code>, so an MCP client like Claude Code claims and comments without the CLI. And Taskwarrior 3, which syncs with the board using its own protocol.</p>
|
|
95
156
|
|
|
96
157
|
```sh
|
|
97
158
|
npx breakaway next --claim --as claude-brk-12
|
|
@@ -121,10 +182,10 @@ npx breakaway list --ready
|
|
|
121
182
|
|
|
122
183
|
<picture>
|
|
123
184
|
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/built-light.png">
|
|
124
|
-
<img alt="How breakaway is built.
|
|
185
|
+
<img alt="How breakaway is built. Four ways in: the web board, the CLI, the MCP server, and Taskwarrior. They reach one Worker and its Durable Object on your own Cloudflare account, which holds every task. The board talks to GitHub through its own GitHub App, and starts Claude Code cloud agents through your routine; agents work the board through the CLI or MCP." src="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/built-dark.png" width="100%">
|
|
125
186
|
</picture>
|
|
126
187
|
|
|
127
|
-
One Cloudflare Worker serves the API, the web app (Preact), and Taskwarrior sync, and one SQLite Durable Object holds every task, claim, comment, and change. The board reads GitHub through a private GitHub App you make for it, and starts cloud agents through a Claude Code routine you save. [Architecture](https://leavethepack.dev/docs/architecture/) has the rest.
|
|
188
|
+
One Cloudflare Worker serves the API, the MCP server, the web app (Preact), and Taskwarrior sync, and one SQLite Durable Object holds every task, claim, comment, and change. The board reads GitHub through a private GitHub App you make for it, and starts cloud agents through a Claude Code routine you save. [Architecture](https://leavethepack.dev/docs/architecture/) has the rest.
|
|
128
189
|
|
|
129
190
|
## Docs
|
|
130
191
|
|
|
@@ -139,6 +200,8 @@ One Cloudflare Worker serves the API, the web app (Preact), and Taskwarrior sync
|
|
|
139
200
|
| [Ideas, decisions, and pings](https://leavethepack.dev/docs/ideas-decisions-pings/) | Let an agent shape an idea, answer its questions in a form, and get a ping when only you can help |
|
|
140
201
|
| [Routines](https://leavethepack.dev/docs/routines/) | Save an agent run and start it by hand, on a schedule, or on a GitHub event |
|
|
141
202
|
| [The CLI](https://leavethepack.dev/docs/cli/) | Every command of `npx breakaway` |
|
|
203
|
+
| [The Claude Code plugin](https://leavethepack.dev/docs/plugin/) | The `tasks` skill, `/breakaway:next`, the session hooks, and the MCP server in one install, for you or a whole repository |
|
|
204
|
+
| [MCP clients](https://leavethepack.dev/docs/mcp/) | Connect Claude Code or any MCP client to your board's `/mcp`, and sign in from any app that speaks MCP |
|
|
142
205
|
| [GitHub](https://leavethepack.dev/docs/github/) | Your own private App, how pull requests link to tasks, merging, and moving a repository to the deploy flow to promote, roll back, and release |
|
|
143
206
|
| [Taskwarrior](https://leavethepack.dev/docs/taskwarrior/) | Sync, reports, and contexts with Taskwarrior 3 |
|
|
144
207
|
| [Deploying](https://leavethepack.dev/docs/deploying/) | Releases, channels, the Deploy and Update workflows, and rollbacks |
|
|
@@ -180,6 +243,7 @@ breakaway publishes releases and never deploys an install. Every install, the ow
|
|
|
180
243
|
- **Every merge to `main`**, once CI passes, publishes a GitHub pre-release `vX.Y.Z-main.N` on the `main` channel. It carries the bundle (`breakaway-bundle.tar.gz`: the Worker's files and the web app's `dist`), a `manifest.json` (version, channel, commit, `manual`, and the lowest version it updates from), its signature `manifest.json.sig` (Ed25519, made with a key only the release workflow holds; the public key is `src/release-key.js`), and `SHA256SUMS`. The notes list the merged pull requests by title.
|
|
181
244
|
- **A stable release** `vX.Y.Z` is the owner's: they run the **Release** workflow with the pre-release to promote. The bundle is that pre-release's, unchanged, and the notes cover everything since the last stable, under the release's own words from [`docs/releases/vX.Y.Z.md`](https://github.com/TheAnarchoX/breakaway/tree/main/docs/releases) when it's there.
|
|
182
245
|
- **The CLI** is on npm as [`breakaway`](https://www.npmjs.com/package/breakaway), staged on npm by the same workflow, with provenance, and live once the owner approves it there with 2FA. npm's trusted publishing can't yet read the OIDC identity of a repository as new as this one ([npm/cli#9969](https://github.com/npm/cli/issues/9969)), so until it can, a token that can stage but never publish by itself stands in, in an environment only `main` can use. Every pre-release goes out under the `next` dist-tag, and a stable release as `latest`. `npx breakaway <command>` is `node scripts/tasks.mjs <command>`.
|
|
246
|
+
- **The plugin** for Claude Code (`plugin/`) goes out from the `plugin` branch, never from `main`: Anthropic's plugin directory and this repository's marketplace follow that branch. A stable release runs the **Plugin** workflow, which validates the plugin and moves the branch to the released tag, with `plugin.json`'s version set to the release's. To put a fix out before the next release, the owner runs it by hand on `main` with a release tag, or a commit on `main` a pre-release was made from. As with `site`, a ruleset lets only deploy keys move `plugin`, and the key is the `PLUGIN_DEPLOY_KEY` secret in a `plugin` environment that only `main` can use.
|
|
183
247
|
- **A major release** is one where an install has to do something by hand: a config or binding change, a Durable Object class or migration, a route or cron. Its notes have a **Manual steps** section and its manifest says `manual: true`, which an install's deploy stops on. A change that needs it sets `manual` and `manualSteps` in `release.json`, and the pull request that ships the steps clears them. When the only step is `wrangler deploy` (a new Durable Object class, a cron, a route), it also sets `wranglerDeploy: true`, and an install whose Deploy may run `wrangler deploy` does it itself (the install template's README says when). Data the Durable Object stores changes forward-only and additively, so an install can always go back one release, except across a new Durable Object class, which Cloudflare doesn't roll back.
|
|
184
248
|
- **The version** is `package.json`'s; the release workflow sets it to the pre-release's before it builds. Patches count by themselves: once a stable is out, the pre-releases work toward its next patch. For the next minor or major, pick it as **next** when you run the **Release** workflow (patch, the default, opens nothing): once the stable is published, the workflow opens a pull request setting `package.json` to it, and after it merges the next pre-release is `vX.Y.0-main.1`. That needs **Allow GitHub Actions to create and approve pull requests** on in the repository's Actions settings (if it was off, turn it on and re-run the **next version** job: it opens the pull request from the branch it already made), and a pull request opened with the workflow's token starts no workflows, so close and reopen it, or push to it, for CI to run. At any other time, **Prepare** on the board's GitHub view starts an agent that opens the same pull request. `GET /api/ping` and `GET /api/health` report it as `release`. An install that deploys a stable passes it as the `BREAKAWAY_VERSION` variable, since the bundle was built as the pre-release.
|
|
185
249
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.5.0-main.
|
|
3
|
+
"version": "1.5.0-main.41",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/scripts/tasks/cli.js
CHANGED
|
@@ -13,7 +13,20 @@ export const SUBCOMMANDS = {
|
|
|
13
13
|
features: ['list', 'add', 'show', 'modify'],
|
|
14
14
|
horizon: ['close'],
|
|
15
15
|
hook: ['session', 'wait'],
|
|
16
|
-
peloton: [
|
|
16
|
+
peloton: [
|
|
17
|
+
'checkin',
|
|
18
|
+
'step',
|
|
19
|
+
'note',
|
|
20
|
+
'ask',
|
|
21
|
+
'propose',
|
|
22
|
+
'review',
|
|
23
|
+
'reply',
|
|
24
|
+
'huddle',
|
|
25
|
+
'in',
|
|
26
|
+
'outcome',
|
|
27
|
+
'plan',
|
|
28
|
+
'listen',
|
|
29
|
+
],
|
|
17
30
|
specs: ['list', 'show'],
|
|
18
31
|
};
|
|
19
32
|
|
|
@@ -23,6 +36,7 @@ export const NO_ARGUMENTS = new Set([
|
|
|
23
36
|
'next',
|
|
24
37
|
'activity',
|
|
25
38
|
'health',
|
|
39
|
+
'mcp',
|
|
26
40
|
'connections',
|
|
27
41
|
'export',
|
|
28
42
|
'setup',
|
|
@@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
7
7
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { boardUrl, configDir, parseEnvFile, readSetting } from './settings.js';
|
|
10
|
+
import { boardUrl, configDir, parseEnvFile, readSetting, settingFrom } from './settings.js';
|
|
11
11
|
|
|
12
12
|
/** The git root of the current folder, or null outside a repository. */
|
|
13
13
|
function gitRoot() {
|
|
@@ -69,8 +69,9 @@ export function boardConfig(root = projectRoot()) {
|
|
|
69
69
|
/* the CLI says what's wrong with it */
|
|
70
70
|
}
|
|
71
71
|
const { url } = boardUrl({ env: process.env, file, taskrc: readOptional(join(root, '.taskrc')), config });
|
|
72
|
-
// In a cloud session there's no token here: the environment's API credential adds it.
|
|
73
|
-
|
|
72
|
+
// In a cloud session there's no token here: the environment's API credential adds it. In the plugin's hooks, its
|
|
73
|
+
// `token` comes after the CLI's own (CLI-8).
|
|
74
|
+
const token = settingFrom('TOKEN', { env: process.env, file }).value;
|
|
74
75
|
return { base: url, headers: token ? { Authorization: `Bearer ${token}` } : {} };
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npx breakaway mcp (docs/specs/IDEA-24-mcp-server.md, section 5; CLI-6): how a checkout connects an MCP client to the
|
|
3
|
+
* board's /mcp, and whether it answers. Pure apart from the `fetch` it's handed, so it's tested without a board.
|
|
4
|
+
*/
|
|
5
|
+
import { githubFromRemote, pickRepo } from './repo.js';
|
|
6
|
+
|
|
7
|
+
/** The MCP revision `--check` speaks: the one with an `initialize` handshake, which /mcp accepts (src/mcp.js, LEGACY). */
|
|
8
|
+
export const CHECK_PROTOCOL = '2025-11-25';
|
|
9
|
+
/** The server's name in a client's config. */
|
|
10
|
+
export const SERVER = 'breakaway';
|
|
11
|
+
|
|
12
|
+
const AGENT = /^[\w.@:/-]{1,64}$/u;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The agent name an MCP connection claims as: --as or BREAKAWAY_AGENT (`named`), else `claude-<branch>` for the
|
|
16
|
+
* checkout's branch, else `fallback` (the CLI's own default). A branch's `/` and anything a name can't hold become `-`.
|
|
17
|
+
* @param {{ named?: string | null, branch?: string | null, fallback: string }} options
|
|
18
|
+
*/
|
|
19
|
+
export function mcpAgent({ named, branch, fallback }) {
|
|
20
|
+
if (named) return named;
|
|
21
|
+
const b = String(branch ?? '').trim();
|
|
22
|
+
if (!b || b === 'HEAD') return fallback;
|
|
23
|
+
const name = (/^claude[-/]/u.test(b) ? b : `claude-${b}`).replace(/[^\w.@:-]+/gu, '-').slice(0, 64);
|
|
24
|
+
return AGENT.test(name) ? name : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `claude mcp add` line and the `.mcp.json` entry for a checkout. The token is always the environment variable
|
|
29
|
+
* `tokenVar`, never its value: the shell fills it in for the line, and Claude Code for `.mcp.json`. Without `repo` (a
|
|
30
|
+
* checkout the board doesn't track) there's no X-Breakaway-Repo, and the repository's tools say what's missing.
|
|
31
|
+
* @param {{ url: string, agent: string, repo?: string | null, tokenVar: string }} options
|
|
32
|
+
* @returns {{ endpoint: string, command: string, json: { mcpServers: Record<string, any> } }}
|
|
33
|
+
*/
|
|
34
|
+
export function mcpConfig({ url, agent, repo = null, tokenVar }) {
|
|
35
|
+
const endpoint = `${String(url).replace(/\/+$/u, '')}/mcp`;
|
|
36
|
+
const headers = {
|
|
37
|
+
Authorization: `Bearer \${${tokenVar}}`,
|
|
38
|
+
'X-Breakaway-Agent': agent,
|
|
39
|
+
...(repo ? { 'X-Breakaway-Repo': repo } : {}),
|
|
40
|
+
};
|
|
41
|
+
const command = [
|
|
42
|
+
`claude mcp add --transport http ${SERVER} ${endpoint}`,
|
|
43
|
+
` --header "Authorization: Bearer $${tokenVar}"`,
|
|
44
|
+
` --header "X-Breakaway-Agent: ${agent}"`,
|
|
45
|
+
...(repo ? [` --header "X-Breakaway-Repo: ${repo}"`] : []),
|
|
46
|
+
].join(' \\\n');
|
|
47
|
+
return { endpoint, command, json: { mcpServers: { [SERVER]: { type: 'http', url: endpoint, headers } } } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `--headers`: the headers Claude Code sends to /mcp, for the plugin's `headersHelper` (CLI-9,
|
|
52
|
+
* docs/specs/IDEA-25-claude-plugin.md, section 4). Claude Code reads them as JSON from standard output, so the token is
|
|
53
|
+
* its value here, and only there. Outside a repository the board tracks there's only Authorization, and the repository's
|
|
54
|
+
* tools say what's missing; without a token there's none, so a session's proxy can add it.
|
|
55
|
+
*
|
|
56
|
+
* The agent's name is X-Breakaway-Agent only when the CLI has one of its own (`named`: --as, BREAKAWAY_AGENT, or
|
|
57
|
+
* tasks.env). Otherwise it's `agent`, claude-<branch>, as X-Breakaway-Agent-Default: the helper can't see the plugin's
|
|
58
|
+
* agent_name, which the plugin sends as a static X-Breakaway-Agent that this one would override, and /mcp takes the
|
|
59
|
+
* default only when that's empty (CLI-16).
|
|
60
|
+
* @param {{ token?: string | null, named?: string | null, agent: string, repo?: string | null }} options
|
|
61
|
+
* @returns {Record<string, string>}
|
|
62
|
+
*/
|
|
63
|
+
export function mcpHeaders({ token = null, named = null, agent, repo = null }) {
|
|
64
|
+
return {
|
|
65
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
66
|
+
...(repo
|
|
67
|
+
? {
|
|
68
|
+
...(named ? { 'X-Breakaway-Agent': named } : { 'X-Breakaway-Agent-Default': agent }),
|
|
69
|
+
'X-Breakaway-Repo': repo,
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The repository `--headers` names: the board's slug for the checkout when the board says which (`GET /api/repos`), and
|
|
77
|
+
* null when it tracks none of them. A plugin's headersHelper runs without the plugin's settings, so with only the
|
|
78
|
+
* plugin set up there's no token to ask with: then it's the checkout's GitHub `owner/name`, which /mcp matches against
|
|
79
|
+
* its repositories itself (src/mcp.js). `named` is --repo or BREAKAWAY_REPO. Never throws: Claude Code is waiting.
|
|
80
|
+
* @param {{ base?: string | null, token?: string | null, named?: string | null, remote?: string | null, fetch: typeof globalThis.fetch }} options
|
|
81
|
+
* @returns {Promise<string | null>}
|
|
82
|
+
*/
|
|
83
|
+
export async function headersRepo({ base = null, token = null, named = null, remote = null, fetch }) {
|
|
84
|
+
let registry = null;
|
|
85
|
+
if (base)
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${String(base).replace(/\/+$/u, '')}/api/repos`, {
|
|
88
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
89
|
+
signal: AbortSignal.timeout(5000),
|
|
90
|
+
});
|
|
91
|
+
if (res.ok) registry = await res.json();
|
|
92
|
+
} catch {
|
|
93
|
+
/* the board can't be reached: /mcp will say so itself */
|
|
94
|
+
}
|
|
95
|
+
if (registry) {
|
|
96
|
+
try {
|
|
97
|
+
return pickRepo({ named, remote, registry });
|
|
98
|
+
} catch {
|
|
99
|
+
return null; // --repo names a repository the board doesn't have
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (named) return String(named).trim().toLowerCase();
|
|
103
|
+
return githubFromRemote(remote)?.toLowerCase() ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What `npx breakaway mcp` prints: the line, the `.mcp.json` entry, and what to check.
|
|
108
|
+
* @param {{ endpoint: string, command: string, json: object }} config
|
|
109
|
+
* @param {{ repo?: string | null, tokenVar: string }} options
|
|
110
|
+
*/
|
|
111
|
+
export function mcpLines(config, { repo = null, tokenVar }) {
|
|
112
|
+
return [
|
|
113
|
+
`Connect Claude Code to the board's MCP server at ${config.endpoint}:`,
|
|
114
|
+
'',
|
|
115
|
+
config.command,
|
|
116
|
+
'',
|
|
117
|
+
`Run it where ${tokenVar} is set, then type /mcp in Claude Code. Or put this in the checkout's .mcp.json, which`,
|
|
118
|
+
`Claude Code fills in from ${tokenVar} when it starts:`,
|
|
119
|
+
'',
|
|
120
|
+
JSON.stringify(config.json, null, 2),
|
|
121
|
+
'',
|
|
122
|
+
...(repo
|
|
123
|
+
? []
|
|
124
|
+
: [
|
|
125
|
+
'This checkout isn’t a repository the board tracks, so there’s no X-Breakaway-Repo header: the tools that work',
|
|
126
|
+
'in a repository will ask for one. Run this in a tracked checkout, or name one with --repo <slug>.',
|
|
127
|
+
'',
|
|
128
|
+
]),
|
|
129
|
+
`The token is the board's full token: give it only to a client you run. npx breakaway mcp --check tests the connection.`,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `--check`: an `initialize` and a `tools/list` against /mcp with the same headers the config sends, and what came
|
|
135
|
+
* back. `ok` is false with the board's own error when either fails.
|
|
136
|
+
* @param {{ endpoint: string, token?: string | null, agent: string, repo?: string | null, fetch: typeof globalThis.fetch }} options
|
|
137
|
+
* @returns {Promise<{ ok: boolean, lines: string[] }>}
|
|
138
|
+
*/
|
|
139
|
+
export async function checkMcp({ endpoint, token = null, agent, repo = null, fetch }) {
|
|
140
|
+
const headers = {
|
|
141
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
Accept: 'application/json, text/event-stream',
|
|
144
|
+
'X-Breakaway-Agent': agent,
|
|
145
|
+
...(repo ? { 'X-Breakaway-Repo': repo } : {}),
|
|
146
|
+
};
|
|
147
|
+
const rpc = async (id, method, params, version) => {
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await fetch(endpoint, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { ...headers, ...(version ? { 'MCP-Protocol-Version': version } : {}) },
|
|
153
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { error: `can't reach ${endpoint} (${reasonOf(error)})` };
|
|
157
|
+
}
|
|
158
|
+
const data = await res.json().catch(() => null);
|
|
159
|
+
// An install from before /mcp answers it with the web app, a 404, or a 405: anything but JSON-RPC.
|
|
160
|
+
if (res.status !== 401 && (res.status === 404 || res.status === 405 || (res.ok && !data?.jsonrpc)))
|
|
161
|
+
return {
|
|
162
|
+
error: `${endpoint} answered ${res.status}: this install has no MCP server yet. Update and deploy it (docs/tasks.md#updates).`,
|
|
163
|
+
};
|
|
164
|
+
if (res.status === 401)
|
|
165
|
+
return {
|
|
166
|
+
error: `${endpoint} refused the token (401)${data?.error ? `: ${data.error}` : ''}${token ? '' : '. No token is set'}`,
|
|
167
|
+
};
|
|
168
|
+
if (data?.error)
|
|
169
|
+
return { error: `${method} failed: ${data.error.message ?? data.error}${res.ok ? '' : ` (HTTP ${res.status})`}` };
|
|
170
|
+
if (!res.ok || !data?.result) return { error: `${method} failed: HTTP ${res.status}` };
|
|
171
|
+
return { result: data.result };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const init = await rpc(1, 'initialize', {
|
|
175
|
+
protocolVersion: CHECK_PROTOCOL,
|
|
176
|
+
capabilities: {},
|
|
177
|
+
clientInfo: { name: 'breakaway-cli', version: '1' },
|
|
178
|
+
});
|
|
179
|
+
if (init.error) return { ok: false, lines: [`The board's MCP server didn't answer: ${init.error}`] };
|
|
180
|
+
const listed = await rpc(2, 'tools/list', {}, init.result.protocolVersion ?? CHECK_PROTOCOL);
|
|
181
|
+
if (listed.error) return { ok: false, lines: [`The board's MCP server didn't answer: ${listed.error}`] };
|
|
182
|
+
const server = init.result.serverInfo ?? {};
|
|
183
|
+
const tools = (listed.result.tools ?? []).map((t) => t.name);
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
lines: [
|
|
187
|
+
`The board's MCP server answers at ${endpoint}: ${server.name ?? SERVER}${server.version ? ` ${server.version}` : ''}, MCP ${init.result.protocolVersion}.`,
|
|
188
|
+
`${tools.length} tools: ${tools.join(', ') || 'none'}.`,
|
|
189
|
+
`As ${agent}${repo ? `, in ${repo}` : ', in no repository (set X-Breakaway-Repo, or run it in a tracked checkout)'}.`,
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Why a fetch failed: the innermost cause, like ECONNREFUSED. */
|
|
195
|
+
function reasonOf(error) {
|
|
196
|
+
let root = error;
|
|
197
|
+
while (root?.cause) root = root.cause;
|
|
198
|
+
return typeof root?.code === 'string' ? root.code : (root?.message ?? String(error));
|
|
199
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Claude Code Stop hook (async, asyncRewake; see .claude/settings.json): while the agent is idle,
|
|
4
4
|
* waiting on CI or a review, asks the board every 20 seconds whether the owner sent it a message.
|
|
5
|
-
* It wakes for
|
|
6
|
-
*
|
|
5
|
+
* It wakes for the peloton's urgent posts too: the owner's, a huddle opening or closing, a mention of the agent, and a
|
|
6
|
+
* reply to one of its posts, never for the peloton's other posts (docs/specs/IDEA-36-peloton-planning.md, section 3;
|
|
7
|
+
* an agent in a chase listens with `peloton listen` instead of stopping). On one, it writes the message to stderr and exits 2, which wakes Claude with it as a system
|
|
7
8
|
* reminder (docs/specs/IDEA-15-message-a-running-agent.md). After its 4-minute window it ends
|
|
8
9
|
* quietly, and a message waits for the agent's next turn; its `timeout` (300 s) outlasts the window,
|
|
9
10
|
* because Claude Code kills an async hook at its timeout (CLD-146).
|
|
@@ -17,12 +18,15 @@ import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
17
18
|
import { tmpdir } from 'node:os';
|
|
18
19
|
import { join } from 'node:path';
|
|
19
20
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
21
|
+
import { checkoutRunsHooks } from './plugin-hooks.js';
|
|
20
22
|
import { boardConfig, claimedTask, projectRoot } from './hook-config.js';
|
|
21
23
|
import { sessionRequest } from './proxy.js';
|
|
22
24
|
import { waitForMessages } from './session-messages.js';
|
|
23
25
|
|
|
24
26
|
async function main() {
|
|
25
27
|
const root = projectRoot();
|
|
28
|
+
// The plugin's copy of this hook steps aside when the checkout's settings run it too (BRK-159).
|
|
29
|
+
if (checkoutRunsHooks(root)) return 0;
|
|
26
30
|
const claim = claimedTask(root);
|
|
27
31
|
if (!claim?.agent) return 0;
|
|
28
32
|
for await (const _ of process.stdin); // Claude Code sends the hook's input; it isn't needed.
|